feat: implement episode API
This commit is contained in:
parent
028aa01682
commit
9439f26c60
|
@ -7,11 +7,12 @@ import { playerState } from '../GlobalAudioPlayer/globalAudioPlayer.state'
|
||||||
import { useHookstate } from '@hookstate/core'
|
import { useHookstate } from '@hookstate/core'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: LPE.Podcast.Document
|
episode: LPE.Podcast.Document
|
||||||
|
relatedEpisodes: LPE.Podcast.Document[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EpisodeBody({ data }: Props) {
|
export default function EpisodeBody({ episode, relatedEpisodes }: Props) {
|
||||||
const youtube = data?.channels.find(
|
const youtube = episode?.channels.find(
|
||||||
(channel) => channel?.name === LPE.Podcast.ChannelNames.Youtube,
|
(channel) => channel?.name === LPE.Podcast.ChannelNames.Youtube,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,12 +22,12 @@ export default function EpisodeBody({ data }: Props) {
|
||||||
return (
|
return (
|
||||||
<EpisodeContainer>
|
<EpisodeContainer>
|
||||||
<EpisodeHeader
|
<EpisodeHeader
|
||||||
{...data}
|
{...episode}
|
||||||
url={youtube?.url as string}
|
url={youtube?.url as string}
|
||||||
duration={duration}
|
duration={duration}
|
||||||
/>
|
/>
|
||||||
<EpisodeTranscript data={data} />
|
<EpisodeTranscript episode={episode} />
|
||||||
<EpisodeFooter data={data} />
|
<EpisodeFooter episode={episode} relatedEpisodes={relatedEpisodes} />
|
||||||
</EpisodeContainer>
|
</EpisodeContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,14 @@ import EpisodeBlocks from './Episode.Blocks'
|
||||||
import { Typography } from '@acid-info/lsd-react'
|
import { Typography } from '@acid-info/lsd-react'
|
||||||
import EpisodeDivider from './Episode.Divider'
|
import EpisodeDivider from './Episode.Divider'
|
||||||
|
|
||||||
const EpisodeTranscript = ({ data }: { data: LPE.Podcast.Document }) => {
|
const EpisodeTranscript = ({ episode }: { episode: LPE.Podcast.Document }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<EpisodeDivider />
|
<EpisodeDivider />
|
||||||
<Title component="h6" variant="h6">
|
<Title component="h6" variant="h6">
|
||||||
Transcript
|
Transcript
|
||||||
</Title>
|
</Title>
|
||||||
<EpisodeBlocks data={data} />
|
<EpisodeBlocks data={episode} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,67 +5,30 @@ import RelatedEpisodes from './Episode.RelatedEpisodes'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import EpisodeFootnotes from './Episode.Footnotes'
|
import EpisodeFootnotes from './Episode.Footnotes'
|
||||||
|
|
||||||
const TEMP_MORE_EPISODES = [
|
type Props = {
|
||||||
{
|
episode: LPE.Podcast.Document
|
||||||
id: 1,
|
relatedEpisodes: LPE.Podcast.Document[]
|
||||||
thumbnail:
|
}
|
||||||
'https://images.cdn.unbody.io/00f8908f-9dff-456e-9640-13defd9ae433/image/a04e5542-d027-44d5-b914-bd4cadf17d25_image1.png',
|
|
||||||
publishedAt: '2023-07-11T20:30:00.000Z',
|
|
||||||
title: 'Title 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
thumbnail:
|
|
||||||
'https://images.cdn.unbody.io/00f8908f-9dff-456e-9640-13defd9ae433/image/a04e5542-d027-44d5-b914-bd4cadf17d25_image1.png',
|
|
||||||
publishedAt: '2023-07-12T20:30:00.000Z',
|
|
||||||
title: 'Title 2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
thumbnail:
|
|
||||||
'https://images.cdn.unbody.io/00f8908f-9dff-456e-9640-13defd9ae433/image/a04e5542-d027-44d5-b914-bd4cadf17d25_image1.png',
|
|
||||||
publishedAt: '2023-07-13T20:30:00.000Z',
|
|
||||||
title: 'Title 3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
thumbnail:
|
|
||||||
'https://images.cdn.unbody.io/00f8908f-9dff-456e-9640-13defd9ae433/image/a04e5542-d027-44d5-b914-bd4cadf17d25_image1.png',
|
|
||||||
publishedAt: '2023-07-14T20:30:00.000Z',
|
|
||||||
title: 'Title 4',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
thumbnail:
|
|
||||||
'https://images.cdn.unbody.io/00f8908f-9dff-456e-9640-13defd9ae433/image/a04e5542-d027-44d5-b914-bd4cadf17d25_image1.png',
|
|
||||||
publishedAt: '2023-07-14T20:30:00.000Z',
|
|
||||||
title: 'Title 5',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
thumbnail:
|
|
||||||
'https://images.cdn.unbody.io/00f8908f-9dff-456e-9640-13defd9ae433/image/a04e5542-d027-44d5-b914-bd4cadf17d25_image1.png',
|
|
||||||
publishedAt: '2023-07-14T20:30:00.000Z',
|
|
||||||
title: 'Title 6',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const EpisodeFooter = ({ data }: { data: LPE.Podcast.Document }) => {
|
const EpisodeFooter = ({ episode, relatedEpisodes }: Props) => {
|
||||||
const footnotes = useMemo(() => {
|
const footnotes = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
data.credits &&
|
episode.credits &&
|
||||||
data.credits
|
episode.credits
|
||||||
.filter((b) => b.footnotes.length)
|
.filter((b) => b.footnotes.length)
|
||||||
.map((b) => b.footnotes)
|
.map((b) => b.footnotes)
|
||||||
.flat()
|
.flat()
|
||||||
)
|
)
|
||||||
}, [data])
|
}, [episode])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EpisodeFooterContainer>
|
<EpisodeFooterContainer>
|
||||||
{footnotes?.length && <EpisodeFootnotes footnotes={footnotes} />}
|
{footnotes?.length && <EpisodeFootnotes footnotes={footnotes} />}
|
||||||
{data?.credits?.length && <EpisodeCredits credits={data.credits} />}
|
{episode?.credits?.length && <EpisodeCredits credits={episode.credits} />}
|
||||||
<RelatedEpisodes relatedEpisodes={TEMP_MORE_EPISODES} />
|
<RelatedEpisodes
|
||||||
|
podcastSlug={episode.show?.slug as string}
|
||||||
|
relatedEpisodes={relatedEpisodes}
|
||||||
|
/>
|
||||||
</EpisodeFooterContainer>
|
</EpisodeFooterContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,29 @@
|
||||||
|
import { LPE } from '@/types/lpe.types'
|
||||||
import { Typography } from '@acid-info/lsd-react'
|
import { Typography } from '@acid-info/lsd-react'
|
||||||
import styled from '@emotion/styled'
|
import styled from '@emotion/styled'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
thumbnail: string
|
coverImage: LPE.Image.Document
|
||||||
title: string
|
title: string
|
||||||
publishedAt: string
|
publishedAt: string
|
||||||
|
slug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const MoreEpisodesCard = ({ thumbnail, title, publishedAt }: Props) => {
|
const MoreEpisodesCard = ({ coverImage, title, publishedAt, slug }: Props) => {
|
||||||
const date = new Date(publishedAt)
|
const date = new Date(publishedAt)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<ImageContainer>
|
{coverImage?.url && (
|
||||||
<Image src={thumbnail} fill alt={thumbnail} />
|
<CustomLink href={`/podcasts/${slug}`}>
|
||||||
</ImageContainer>
|
<ImageContainer>
|
||||||
|
<Image src={coverImage.url} fill alt={coverImage.alt} />
|
||||||
|
</ImageContainer>
|
||||||
|
</CustomLink>
|
||||||
|
)}
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
<Typography variant="body3" genericFontFamily="sans-serif">
|
<Typography variant="body3" genericFontFamily="sans-serif">
|
||||||
PODCAST
|
PODCAST
|
||||||
|
@ -29,9 +38,11 @@ const MoreEpisodesCard = ({ thumbnail, title, publishedAt }: Props) => {
|
||||||
})}
|
})}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Row>
|
</Row>
|
||||||
<Typography variant="h6" genericFontFamily="serif">
|
<CustomLink href={`/podcasts/${slug}`}>
|
||||||
{title}
|
<Typography variant="h6" genericFontFamily="serif">
|
||||||
</Typography>
|
{title}
|
||||||
|
</Typography>
|
||||||
|
</CustomLink>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -58,4 +69,8 @@ const Row = styled.div`
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const CustomLink = styled(Link)`
|
||||||
|
text-decoration: none;
|
||||||
|
`
|
||||||
|
|
||||||
export default MoreEpisodesCard
|
export default MoreEpisodesCard
|
||||||
|
|
|
@ -2,8 +2,14 @@ import { Button, Typography } from '@acid-info/lsd-react'
|
||||||
import styled from '@emotion/styled'
|
import styled from '@emotion/styled'
|
||||||
import MoreEpisodesCard from './Episode.MoreEpisodesCard'
|
import MoreEpisodesCard from './Episode.MoreEpisodesCard'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { LPE } from '@/types/lpe.types'
|
||||||
|
|
||||||
const RelatedEpisodes = ({ relatedEpisodes }: any) => {
|
type props = {
|
||||||
|
podcastSlug: string
|
||||||
|
relatedEpisodes: LPE.Podcast.Document[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const RelatedEpisodes = ({ podcastSlug, relatedEpisodes }: props) => {
|
||||||
const [showMore, setShowMore] = useState(false)
|
const [showMore, setShowMore] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -14,9 +20,10 @@ const RelatedEpisodes = ({ relatedEpisodes }: any) => {
|
||||||
? relatedEpisodes.map((episode: any, idx: number) => (
|
? relatedEpisodes.map((episode: any, idx: number) => (
|
||||||
<MoreEpisodesCard
|
<MoreEpisodesCard
|
||||||
key={'related-episode' + idx}
|
key={'related-episode' + idx}
|
||||||
thumbnail={episode.thumbnail}
|
coverImage={episode.coverImage}
|
||||||
title={episode.title}
|
title={episode.title}
|
||||||
publishedAt={episode.publishedAt}
|
publishedAt={episode.publishedAt}
|
||||||
|
slug={`${podcastSlug}/${episode.slug}`}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: relatedEpisodes && !showMore
|
: relatedEpisodes && !showMore
|
||||||
|
@ -25,9 +32,10 @@ const RelatedEpisodes = ({ relatedEpisodes }: any) => {
|
||||||
.map((episode: any, idx: number) => (
|
.map((episode: any, idx: number) => (
|
||||||
<MoreEpisodesCard
|
<MoreEpisodesCard
|
||||||
key={'related-episode' + idx}
|
key={'related-episode' + idx}
|
||||||
thumbnail={episode.thumbnail}
|
coverImage={episode.coverImage}
|
||||||
title={episode.title}
|
title={episode.title}
|
||||||
publishedAt={episode.publishedAt}
|
publishedAt={episode.publishedAt}
|
||||||
|
slug={`${podcastSlug}/${episode.slug}`}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: null}
|
: null}
|
||||||
|
|
|
@ -4,17 +4,18 @@ import styled from '@emotion/styled'
|
||||||
import { LPE } from '../types/lpe.types'
|
import { LPE } from '../types/lpe.types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: LPE.Podcast.Document
|
episode: LPE.Podcast.Document
|
||||||
|
relatedEpisodes: LPE.Podcast.Document[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const EpisodeContainer = (props: Props) => {
|
const EpisodeContainer = (props: Props) => {
|
||||||
const { data } = props
|
const { episode, relatedEpisodes } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EpisodeGrid>
|
<EpisodeGrid>
|
||||||
<Gap className={'w-4'} />
|
<Gap className={'w-4'} />
|
||||||
<EpisodeBodyContainer className={'w-8'}>
|
<EpisodeBodyContainer className={'w-8'}>
|
||||||
<EpisodeBody data={data} />
|
<EpisodeBody episode={episode} relatedEpisodes={relatedEpisodes} />
|
||||||
</EpisodeBodyContainer>
|
</EpisodeBodyContainer>
|
||||||
<Gap className={'w-4'} />
|
<Gap className={'w-4'} />
|
||||||
</EpisodeGrid>
|
</EpisodeGrid>
|
||||||
|
|
|
@ -20,14 +20,16 @@ const PodcastShowContainer = (props: Props) => {
|
||||||
<PodcastsGrid>
|
<PodcastsGrid>
|
||||||
<PodcastsBodyContainer className={'w-16'}>
|
<PodcastsBodyContainer className={'w-16'}>
|
||||||
<PodcastShowCard show={show} />
|
<PodcastShowCard show={show} />
|
||||||
|
|
||||||
<PodcastSection>
|
<PodcastSection>
|
||||||
<EpisodesList
|
<EpisodesList
|
||||||
header={<Typography variant="body2">All episodes</Typography>}
|
header={<Typography variant="body2">All episodes</Typography>}
|
||||||
episodes={highlightedEpisodes}
|
episodes={highlightedEpisodes}
|
||||||
|
show={show}
|
||||||
/>
|
/>
|
||||||
</PodcastSection>
|
</PodcastSection>
|
||||||
|
|
||||||
<EpisodesList episodes={latestEpisodes} divider={true} />
|
<EpisodesList episodes={latestEpisodes} divider={true} show={show} />
|
||||||
</PodcastsBodyContainer>
|
</PodcastsBodyContainer>
|
||||||
</PodcastsGrid>
|
</PodcastsGrid>
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,67 +7,98 @@ import { LPE } from '../../../types/lpe.types'
|
||||||
import EpisodeLayout from '@/layouts/EpisodeLayout/Episode.layout'
|
import EpisodeLayout from '@/layouts/EpisodeLayout/Episode.layout'
|
||||||
import { EpisodeProvider } from '@/context/episode.context'
|
import { EpisodeProvider } from '@/context/episode.context'
|
||||||
|
|
||||||
import TEMP_DATA from '../episode-temp-data.json'
|
import unbodyApi from '@/services/unbody/unbody.service'
|
||||||
|
|
||||||
type EpisodeProps = {
|
type EpisodeProps = {
|
||||||
data: LPE.Podcast.Document
|
episode: LPE.Podcast.Document
|
||||||
|
relatedEpisodes: LPE.Podcast.Document[]
|
||||||
errors: string | null
|
errors: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const EpisodePage = ({ data, errors }: EpisodeProps) => {
|
const EpisodePage = ({ episode, relatedEpisodes, errors }: EpisodeProps) => {
|
||||||
const {
|
const {
|
||||||
query: { showSlug, epSlug },
|
query: { showSlug, epSlug },
|
||||||
} = useRouter()
|
} = useRouter()
|
||||||
|
|
||||||
if (!data) return null
|
if (!episode) return null
|
||||||
if (errors) return <div>{errors}</div>
|
if (errors) return <div>{errors}</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SEO
|
<SEO
|
||||||
title={data.title}
|
title={episode.title}
|
||||||
description={data.description}
|
description={episode.description}
|
||||||
image={data.coverImage}
|
image={episode.coverImage}
|
||||||
imageUrl={undefined}
|
imageUrl={undefined}
|
||||||
pagePath={`/podcasts/${showSlug}/${epSlug}`}
|
pagePath={`/podcasts/${showSlug}/${epSlug}`}
|
||||||
tags={[...data.tags, ...data.authors.map((author) => author.name)]}
|
tags={[
|
||||||
|
...episode.tags,
|
||||||
|
...episode.authors.map((author) => author.name),
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
<EpisodeContainer data={data} />
|
<EpisodeContainer episode={episode} relatedEpisodes={relatedEpisodes} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
// TODO : dynamic paths
|
|
||||||
return {
|
return {
|
||||||
paths: [{ params: { showSlug: `hasing-it-out`, epSlug: `test` } }],
|
paths: [
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
showSlug: `hasing-it-out`,
|
||||||
|
epSlug: `test-podcast-highlighted`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
fallback: true,
|
fallback: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||||
const { epSlug } = params!
|
const { showSlug, epSlug } = params!
|
||||||
|
|
||||||
if (!epSlug) {
|
if (!epSlug || !showSlug) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
props: { why: 'no slug' },
|
props: { why: 'no slug' },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, errors } = TEMP_DATA
|
// TODO : error handling
|
||||||
|
const { data: episodeData, errors: episodeErros } =
|
||||||
|
await unbodyApi.getPodcastEpisode({
|
||||||
|
showSlug: showSlug as string,
|
||||||
|
slug: epSlug as string,
|
||||||
|
textBlocks: true,
|
||||||
|
})
|
||||||
|
|
||||||
if (!data) {
|
// TODO : error handlings
|
||||||
|
const { data: relatedEpisodesData, errors: relatedEpisodesErros } =
|
||||||
|
await unbodyApi.getRelatedEpisodes({
|
||||||
|
showSlug: showSlug as string,
|
||||||
|
id: episodeData?.id as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!episodeData) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
props: { why: 'no article' },
|
props: { why: 'no article' },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO : handle undefined values in JSON
|
||||||
|
const episode = JSON.parse(JSON.stringify(episodeData).replace(/null/g, '""'))
|
||||||
|
|
||||||
|
// TODO : handle undefined values in JSON
|
||||||
|
const relatedEpisodes = JSON.parse(
|
||||||
|
JSON.stringify(relatedEpisodesData).replace(/null/g, '""'),
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
data: data,
|
episode,
|
||||||
error: JSON.stringify(errors),
|
relatedEpisodes,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,11 +64,13 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO : error handling
|
||||||
const { data: showData, errors: podcastShowDataErrors } =
|
const { data: showData, errors: podcastShowDataErrors } =
|
||||||
await unbodyApi.getPodcastShow({
|
await unbodyApi.getPodcastShow({
|
||||||
showSlug: showSlug as string,
|
showSlug: showSlug as string,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO : error handling
|
||||||
const { data: latestEpisodesData, errors: latestEpisodesErros } =
|
const { data: latestEpisodesData, errors: latestEpisodesErros } =
|
||||||
await unbodyApi.getLatestEpisodes({
|
await unbodyApi.getLatestEpisodes({
|
||||||
showSlug: showSlug as string,
|
showSlug: showSlug as string,
|
||||||
|
@ -76,6 +78,7 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||||
limit: 12,
|
limit: 12,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO : error handling
|
||||||
const { data: highlightedEpisodesData, errors: highlightedEpisodesErrors } =
|
const { data: highlightedEpisodesData, errors: highlightedEpisodesErrors } =
|
||||||
await unbodyApi.getHighlightedEpisodes({
|
await unbodyApi.getHighlightedEpisodes({
|
||||||
showSlug: showSlug as string,
|
showSlug: showSlug as string,
|
||||||
|
|
Loading…
Reference in New Issue