feat: implement episode API

This commit is contained in:
jinhojang6 2023-08-18 13:31:10 +09:00
parent 028aa01682
commit 9439f26c60
9 changed files with 114 additions and 90 deletions

View File

@ -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>
) )
} }

View File

@ -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} />
</> </>
) )
} }

View File

@ -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>
) )
} }

View File

@ -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

View File

@ -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}

View File

@ -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>

View File

@ -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>
) )

View File

@ -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,
}, },
} }
} }

View File

@ -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,