feat: add single podcast and podcasts route

This commit is contained in:
jinhojang6 2023-08-16 20:55:27 +09:00
parent 297c49e06a
commit fe4a3da6d7
9 changed files with 183 additions and 16 deletions

View File

@ -3,7 +3,6 @@ import {
extractIdFromFirstTag,
extractInnerHtml,
} from '@/utils/html.utils'
import { HeadingElementsRef } from '@/utils/ui.utils'
import { Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import ReactPlayer from 'react-player'

View File

@ -3,6 +3,8 @@ import { LPE } from '../../types/lpe.types'
import EpisodeFooter from './Footer/Episode.Footer'
import EpisodeHeader from './Header/Episode.Header'
import EpisodeTranscript from './Episode.Transcript'
import { calcReadingTime } from '@/utils/string.utils'
import { extractContentFromHTML } from '@/utils/html.utils'
interface Props {
data: LPE.Podcast.Document
@ -13,9 +15,18 @@ export default function EpisodeBody({ data }: Props) {
(channel) => channel?.name === LPE.Podcast.ChannelNames.Youtube,
)
const trascriptionString = (data.transcription || [])
.map((block) => extractContentFromHTML(block.html))
.join(' ')
const readingTime = calcReadingTime(trascriptionString)
return (
<EpisodeContainer>
<EpisodeHeader {...data} url={youtube?.url as string} />
<EpisodeHeader
{...data}
url={youtube?.url as string}
readingTime={readingTime}
/>
<EpisodeTranscript data={data} />
<EpisodeFooter data={data} />
</EpisodeContainer>

View File

@ -6,7 +6,10 @@ import ReactPlayer from 'react-player'
import { default as Stats } from '@/components/Article/Article.Stats'
import { LogosCircleIcon } from '@/components/Icons/LogosCircleIcon'
export type EpisodeHeaderProps = LPE.Podcast.Document & { url: string }
export type EpisodeHeaderProps = LPE.Podcast.Document & {
url: string
readingTime: number
}
const EpisodeHeader = ({
title,
@ -14,14 +17,21 @@ const EpisodeHeader = ({
publishedAt,
tags,
url,
readingTime,
}: EpisodeHeaderProps) => {
const date = new Date(publishedAt)
return (
<EpisodeHeaderContainer>
<PlayerContainer>
<ReactPlayer url={url} forceVideo={true} controls={true} />
<ReactPlayer
url={url}
forceVideo={true}
controls={true}
onProgress={(data) => console.log(data)}
/>
</PlayerContainer>
<Stats date={date} readingLength={6} />
<Stats date={date} readingLength={readingTime} />
<EpisodeTitle variant="h1" genericFontFamily="serif" component="h1">
{title}
</EpisodeTitle>
@ -29,6 +39,7 @@ const EpisodeHeader = ({
<LogosCircleIcon width={24} height={24} />
Network State Podcast
</PodcastName>
{tags && <Tags tags={tags} />}
{description && (
<EpisodeSubtitle
variant="h6"
@ -38,7 +49,6 @@ const EpisodeHeader = ({
{description}
</EpisodeSubtitle>
)}
{tags && <Tags tags={tags} />}
</EpisodeHeaderContainer>
)
}
@ -46,6 +56,10 @@ const EpisodeHeader = ({
const EpisodeHeaderContainer = styled.header`
display: flex;
flex-direction: column;
@media (max-width: 768px) {
padding-top: 32px;
}
`
const CustomTypography = styled(Typography)`
@ -62,7 +76,7 @@ const EpisodeTitle = styled(Typography)`
`
const EpisodeSubtitle = styled(CustomTypography)`
margin-bottom: 16px;
margin-top: 32px;
@media (max-width: 768px) {
font-size: var(--lsd-subtitle1-fontSize);
@ -76,8 +90,24 @@ const PodcastName = styled.div`
margin-bottom: 20px;
`
// 16:9 responsive aspect ratio
const PlayerContainer = styled.div`
margin-bottom: 32px;
position: relative;
padding-bottom: 56.25%;
padding-top: 30px;
height: 0;
overflow: hidden;
iframe,
object,
embed {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
`
export default EpisodeHeader

View File

@ -10,7 +10,7 @@ export const VolumeIcon = LsdIcon(
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clip-path="url(#clip0_228_17463)">
<g clipPath="url(#clip0_228_17463)">
<path
d="M2 5.99901V9.99901H4.66667L8 13.3323V2.66568L4.66667 5.99901H2ZM11 7.99901C11 6.81901 10.32 5.80568 9.33333 5.31234V10.679C10.32 10.1923 11 9.17901 11 7.99901ZM9.33333 2.15234V3.52568C11.26 4.09901 12.6667 5.88568 12.6667 7.99901C12.6667 10.1123 11.26 11.899 9.33333 12.4723V13.8457C12.0067 13.239 14 10.8523 14 7.99901C14 5.14568 12.0067 2.75901 9.33333 2.15234Z"
fill="black"

View File

@ -3,11 +3,11 @@ import EpisodeContainer from '@/containers/EpisodeContainer'
import { GetStaticPropsContext } from 'next'
import { useRouter } from 'next/router'
import { ReactNode } from 'react'
import { LPE } from '../../types/lpe.types'
import { LPE } from '../../../types/lpe.types'
import EpisodeLayout from '@/layouts/EpisodeLayout/Episode.layout'
import { EpisodeProvider } from '@/context/episode.context'
import TEMP_DATA from './episode-temp-data.json'
import TEMP_DATA from '../episode-temp-data.json'
type EpisodeProps = {
data: LPE.Podcast.Document
@ -16,7 +16,7 @@ type EpisodeProps = {
const EpisodePage = ({ data, errors }: EpisodeProps) => {
const {
query: { slug },
query: { showSlug, epSlug },
} = useRouter()
if (!data) return null
@ -29,7 +29,7 @@ const EpisodePage = ({ data, errors }: EpisodeProps) => {
description={data.description}
image={data.coverImage}
imageUrl={undefined}
pagePath={`/episode/${slug}`}
pagePath={`/podcasts/${showSlug}/${epSlug}`}
tags={[...data.tags, ...data.authors.map((author) => author.name)]}
/>
<EpisodeContainer data={data} />
@ -38,16 +38,17 @@ const EpisodePage = ({ data, errors }: EpisodeProps) => {
}
export async function getStaticPaths() {
// TODO : dynamic paths
return {
paths: [{ params: { slug: `test` } }],
paths: [{ params: { showSlug: `hasing-it-out`, epSlug: `test` } }],
fallback: true,
}
}
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
const { slug } = params!
const { epSlug } = params!
if (!slug) {
if (!epSlug) {
return {
notFound: true,
props: { why: 'no slug' },

View File

@ -0,0 +1,66 @@
import { SEO } from '@/components/SEO'
import { GetStaticPropsContext } from 'next'
import { useRouter } from 'next/router'
import { ReactNode } from 'react'
import { LPE } from '../../../types/lpe.types'
import EpisodeLayout from '@/layouts/EpisodeLayout/Episode.layout'
type PodcastShowProps = {
data: LPE.Podcast.Document
errors: string | null
}
const PodcastShowPage = ({ data, errors }: PodcastShowProps) => {
const {
query: { showSlug },
} = useRouter()
if (!data) return null
if (errors) return <div>{errors}</div>
return (
<>
<SEO
title={data.title}
description={data.description}
image={data.coverImage}
imageUrl={undefined}
pagePath={`/podcasts/${showSlug}`}
tags={[...data.tags]}
/>
<div style={{ marginTop: '200px' }}>Single Podcasts Page WIP</div>
</>
)
}
export async function getStaticPaths() {
// TODO : dynamic paths
return {
paths: [{ params: { showSlug: `hashing-it-out` } }],
fallback: true,
}
}
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
const { showSlug } = params!
if (!showSlug) {
return {
notFound: true,
props: { why: 'no slug' },
}
}
return {
props: {
data: { tags: ['Social', 'Political'] },
error: null,
},
}
}
PodcastShowPage.getLayout = function getLayout(page: ReactNode) {
return <EpisodeLayout>{page}</EpisodeLayout>
}
export default PodcastShowPage

View File

@ -12,7 +12,7 @@
"description": "Here we can have a summary of the podcast episode. Here we can have a summary of the podcast episode. Here we can have a summary of the podcast episode. Here we can have a summary of the podcast episode.Here we can have a summary of the podcast episode.Here we can have a summary of the podcast episode.Here we can have a summary of the podcast episode.Here we can have a summary of the podcast episode.Here we can have a summary of the podcast episode.Here we can have a summary of the podcast episode.",
"publishedAt": "2023-07-11T20:30:00.000Z",
"episodeNumber": 1,
"tags": [],
"tags": ["Tools", "Cyber Punk", "Docs"],
"credits": [
{
"id": "6bd84ceb-1f22-41f4-a229-306356306da7",
@ -230,6 +230,10 @@
}
],
"channels": [
{
"name": "youtube",
"url": "https://www.youtube.com/watch?v=vmx_oOb2On0"
},
{
"name": "youtube",
"url": "https://www.youtube.com/watch?v=vmx_oOb2On0"

View File

@ -0,0 +1,45 @@
import { SEO } from '@/components/SEO'
import { GetStaticPropsContext } from 'next'
import { useRouter } from 'next/router'
import { ReactNode } from 'react'
import { LPE } from '../../types/lpe.types'
import EpisodeLayout from '@/layouts/EpisodeLayout/Episode.layout'
type PodcastsProps = {
data: LPE.Podcast.Document
errors: string | null
}
const PodcastShowPage = ({ data, errors }: PodcastsProps) => {
if (!data) return null
if (errors) return <div>{errors}</div>
return (
<>
<SEO
title={data.title}
description={data.description}
image={data.coverImage}
imageUrl={undefined}
pagePath={`/podcasts`}
tags={[...data.tags]}
/>
<div style={{ marginTop: '200px' }}>Podcasts Page WIP</div>
</>
)
}
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
return {
props: {
data: { tags: ['Social', 'Political'] },
error: null,
},
}
}
PodcastShowPage.getLayout = function getLayout(page: ReactNode) {
return <EpisodeLayout>{page}</EpisodeLayout>
}
export default PodcastShowPage

View File

@ -30,3 +30,14 @@ export const isAuthorsParagraph = (html: string) => {
// so if the email is in the first 50% of the text, it's probably the author line so we want to exclude it
return matches.join('').length / html.length > 0.5
}
export function extractContentFromHTML(htmlString: string) {
const regex = /<[^>]+>([^<]+)<\/[^>]+>/
const match = regex.exec(htmlString)
if (match && match.length > 1) {
return match[1]
} else {
return null
}
}