feat: add single podcast and podcasts route
This commit is contained in:
parent
297c49e06a
commit
fe4a3da6d7
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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' },
|
|
@ -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
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue