Merge remote-tracking branch 'origin/refactor-components' into develop

This commit is contained in:
Hossein Mehrabi 2023-08-22 03:51:07 +03:30
commit 1f33c01035
No known key found for this signature in database
GPG Key ID: 45C04964191AFAA1
19 changed files with 241 additions and 194 deletions

View File

@ -7,12 +7,11 @@ import { Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import ReactPlayer from 'react-player'
import { LPE } from '../../types/lpe.types'
import { convertSecToMinAndSec } from '@/utils/string.utils'
export const RenderEpisodeBlock = ({
block,
}: {
block: LPE.Podcast.Document['transcription'][0]
block: Extract<LPE.Post.ContentBlock, LPE.Post.TextBlock>
hide?: boolean
}) => {
const isYoutubeRegex =
@ -25,22 +24,12 @@ export const RenderEpisodeBlock = ({
<ReactPlayer url={youtubeLink[0]} />
) : (
<TranscriptionItem>
<TranscriptionData>
{block?.start && (
<Typography variant="body2" component={'p'}>
{convertSecToMinAndSec(Number(block.start))}
</Typography>
)}
<Typography variant="body2" component={'p'}>
{block.speaker}
</Typography>
</TranscriptionData>
<Transcript
variant="body2"
component={'p'}
genericFontFamily="sans-serif"
className={extractClassFromFirstTag(block.html) || ''}
id={extractIdFromFirstTag(block.html) || `p-${block.start}`}
id={extractIdFromFirstTag(block.html) || `p-${block.id}`}
dangerouslySetInnerHTML={{ __html: extractInnerHtml(block.html) }}
/>
</TranscriptionItem>
@ -58,10 +47,3 @@ const Transcript = styled(Typography)`
line-height: var(--lsd-h6-lineHeight);
}
`
const TranscriptionData = styled.div`
display: flex;
align-items: center;
gap: 8px;
margin-bottom: var(--lsd-body2-lineHeight);
`

View File

@ -10,7 +10,8 @@ type Props = {
const EpisodeBlocks = ({ data }: Props) => {
const [showMore, setShowMore] = useState(false)
const blocks = data?.transcription
const blocks = data?.content as LPE.Post.TextBlock[]
return blocks?.length && showMore ? (
<>

View File

@ -15,17 +15,20 @@ export default function EpisodeBody({ episode, relatedEpisodes }: Props) {
const youtube = episode?.channels.find(
(channel) => channel?.name === LPE.Podcast.ChannelNames.Youtube,
)
const simplecast = episode?.channels.find(
(channel) => channel?.name === LPE.Podcast.ChannelNames.Simplecast,
)
const channel = youtube ?? simplecast ?? null
const state = useHookstate(playerState)
const duration = Math.round(state.value.duration / 60)
return (
<EpisodeContainer>
<EpisodeHeader
{...episode}
url={youtube?.url as string}
duration={duration}
/>
{!!channel && (
<EpisodeHeader {...episode} channel={channel} duration={duration} />
)}
<EpisodeTranscript episode={episode} />
<EpisodeFooter episode={episode} relatedEpisodes={relatedEpisodes} />
</EpisodeContainer>

View File

@ -13,10 +13,10 @@ type Props = {
const EpisodeFooter = ({ episode, relatedEpisodes }: Props) => {
const footnotes = useMemo(() => {
return (
episode.credits &&
episode.credits
.filter((b) => b.footnotes.length)
.map((b) => b.footnotes)
episode.content &&
episode.content
.filter((b) => (b as LPE.Post.TextBlock).footnotes.length)
.map((b) => (b as LPE.Post.TextBlock).footnotes)
.flat()
)
}, [episode])

View File

@ -3,6 +3,7 @@ import styled from '@emotion/styled'
import { useState } from 'react'
import { LPE } from '@/types/lpe.types'
import { PostCard } from '@/components/PostCard'
import { Grid, GridItem } from '@/components/Grid/Grid'
type props = {
podcastSlug: string
@ -25,12 +26,14 @@ const RelatedEpisodes = ({ podcastSlug, relatedEpisodes }: props) => {
<Typography>More Episodes</Typography>
<EpisodeCards>
{relatedEpisodes.slice(0, showIndex).map((episode, idx: number) => (
<PostCardContainer className="w-8" key={'related-episode' + idx}>
<PostCard
key={'related-episode' + idx}
contentType={LPE.PostTypes.Podcast}
data={{
authors: episode.authors,
date: episode.publishedAt ? new Date(episode.publishedAt) : null,
date: episode.publishedAt
? new Date(episode.publishedAt)
: null,
slug: episode.show?.slug
? `${episode.show?.slug}/${episode.slug}`
: `${podcastSlug}/${episode.slug}`,
@ -45,6 +48,7 @@ const RelatedEpisodes = ({ podcastSlug, relatedEpisodes }: props) => {
},
}}
/>
</PostCardContainer>
))}
</EpisodeCards>
{relatedEpisodes?.length > 4 && (
@ -64,11 +68,8 @@ const Container = styled.div`
flex-direction: column;
`
const EpisodeCards = styled.div`
display: flex;
flex-direction: row;
const EpisodeCards = styled(Grid)`
gap: 0 16px;
flex-wrap: wrap;
`
const ShowButton = styled(Button)`
@ -76,4 +77,8 @@ const ShowButton = styled(Button)`
margin-top: 24px;
`
const PostCardContainer = styled(GridItem)`
padding-top: 24px;
`
export default RelatedEpisodes

View File

@ -8,24 +8,31 @@ import EpisodeStats from '../Episode.Stats'
import EpisodePlayer from './Episode.Player'
export type EpisodeHeaderProps = LPE.Podcast.Document & {
url: string
channel: LPE.Podcast.Channel
duration: number
}
const EpisodeHeader = ({
url,
channel,
title,
description,
publishedAt,
tags,
show,
channels,
duration,
coverImage,
}: EpisodeHeaderProps) => {
const date = new Date(publishedAt)
return (
<EpisodeHeaderContainer>
<EpisodePlayer url={url} />
<EpisodePlayer
title={title}
showTitle={show?.title ?? ''}
channel={channel}
coverImage={coverImage}
/>
<EpisodeStats date={date} duration={duration} />
<EpisodeTitle variant="h1" genericFontFamily="serif" component="h1">
{title}

View File

@ -3,33 +3,41 @@ import ReactPlayer from 'react-player'
import { useHookstate } from '@hookstate/core'
import { playerState } from '@/components/GlobalAudioPlayer/globalAudioPlayer.state'
import { useEffect, useRef } from 'react'
import { extractUUIDFromEpisode } from '@/utils/string.utils'
import { getAudioSourceFromEpisode } from '@/utils/data.utils'
import { episodeState } from '@/components/GlobalAudioPlayer/episode.state'
import SimplecastPlayer from './Episode.SimplecastPlayer'
import { LPE } from '@/types/lpe.types'
export type EpisodePlayerProps = {
url: string
channel: LPE.Podcast.Channel
coverImage: LPE.Podcast.Document['coverImage']
title: string
showTitle: string
}
const EpisodePlayer = ({ url }: EpisodePlayerProps) => {
const EpisodePlayer = ({
channel,
coverImage,
title,
showTitle,
}: EpisodePlayerProps) => {
const state = useHookstate(playerState)
const epState = useHookstate(episodeState)
const isYoutubeRegex =
/(?:\/(?:embed|v|e)\/|\/watch\?v=|\/v\/|https?:\/\/(?:www\.)?youtu\.be\/)([^?&]+)/
const isSimplecast = channel?.name === LPE.Podcast.ChannelNames.Simplecast
const youtubeLink = url.match(isYoutubeRegex) ?? []
const url =
channel?.name === LPE.Podcast.ChannelNames.Youtube
? channel.url
: (
channel as Extract<
LPE.Podcast.Channel,
{ name: typeof LPE.Podcast.ChannelNames.Simplecast }
>
).data.audioFileUrl
const playerContainerRef = useRef<HTMLDivElement>(null)
const playerRef = useRef<ReactPlayer>(null)
const isSimplecastRegex =
/^https?:\/\/([a-zA-Z0-9-]+\.)*simplecast\.com\/[^?\s]+(\?[\s\S]*)?$/
const isSimplecast = isSimplecastRegex.test(url)
const simplecastLink = url.match(isSimplecastRegex) ?? []
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
@ -52,40 +60,19 @@ const EpisodePlayer = ({ url }: EpisodePlayerProps) => {
}, [])
useEffect(() => {
const episodeId = extractUUIDFromEpisode(simplecastLink[0] ?? '')
if (isSimplecast) {
const getAudioSource = async () => {
const response = await getAudioSourceFromEpisode(episodeId as string)
epState.set({
episodeId: episodeId as string,
title: response.title,
podcast: response.podcast.title,
url: response.ad_free_audio_file_url,
thumbnail: response.image_url,
episodeId: 'aaa',
title: title,
podcast: showTitle,
url: url,
coverImage: coverImage ?? null,
})
state.set((prev) => ({
...prev,
url: response.ad_free_audio_file_url,
url: url,
}))
}
getAudioSource()
} else {
state.set((prev) => ({
...prev,
url,
}))
const thumbnail = `https://img.youtube.com/vi/${youtubeLink[1]}/0.jpg`
epState.set((prev) => ({
...prev,
thumbnail: thumbnail,
}))
}
}, [])
}, [url, title, showTitle, coverImage])
useEffect(() => {
if (!state.value.isEnabled) {
@ -122,8 +109,9 @@ const EpisodePlayer = ({ url }: EpisodePlayerProps) => {
{isSimplecast && (
<SimplecastPlayer
playerRef={playerRef}
title={epState.value.title as string}
thumbnail={epState.value.thumbnail as string}
coverImage={
epState.value.coverImage as LPE.Podcast.Document['coverImage']
}
handlePlay={handlePlay}
handlePause={handlePause}
/>

View File

@ -2,6 +2,7 @@ import { playerState } from '@/components/GlobalAudioPlayer/globalAudioPlayer.st
import { PauseIcon } from '@/components/Icons/PauseIcon'
import { PlayIcon } from '@/components/Icons/PlayIcon'
import { VolumeIcon } from '@/components/Icons/VolumeIcon'
import { LPE } from '@/types/lpe.types'
import { convertSecToMinAndSec } from '@/utils/string.utils'
import { Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
@ -11,16 +12,14 @@ import { useState } from 'react'
export type SimplecastPlayerProps = {
playerRef: React.RefObject<any>
title: string
thumbnail: string
coverImage: LPE.Podcast.Document['coverImage']
handlePlay: (state: any) => void
handlePause: (state: any) => void
}
const SimplecastPlayer = ({
playerRef,
title,
thumbnail,
coverImage,
handlePlay,
handlePause,
}: SimplecastPlayerProps) => {
@ -52,9 +51,9 @@ const SimplecastPlayer = ({
return (
<Container>
{thumbnail && (
{coverImage && (
<ImageContainer>
<Thumbnail src={thumbnail} alt={title} fill priority />
<Thumbnail src={coverImage.url} alt={coverImage.alt} fill priority />
</ImageContainer>
)}
<Controls>

View File

@ -183,10 +183,10 @@ export default function GlobalAudioPlayer() {
onProgress={handleProgress}
/>
<RightMenu>
{!!epState.value.thumbnail && (
{!!epState.value.coverImage && (
<Image
src={epState.value.thumbnail}
alt={epState.value.thumbnail}
src={epState.value.coverImage.url}
alt={epState.value.coverImage.alt}
width={48}
height={48}
/>

View File

@ -1,3 +1,4 @@
import { LPE } from '@/types/lpe.types'
import { hookstate } from '@hookstate/core'
export type EpisodeState = {
@ -5,7 +6,7 @@ export type EpisodeState = {
title: string
podcast: string
url: string
thumbnail: string
coverImage: LPE.Post.ImageBlock | null
}
export const defaultEpisodeState: EpisodeState = {
@ -13,7 +14,7 @@ export const defaultEpisodeState: EpisodeState = {
title: '',
podcast: '',
url: '',
thumbnail: '',
coverImage: null,
}
export const episodeState =

View File

@ -1,12 +1,14 @@
import styled from '@emotion/styled'
import { LPE } from '../../types/lpe.types'
import { PostCard } from '@/components/PostCard/PostCard'
import { Grid, GridItem } from '../Grid/Grid'
interface Props {
header?: React.ReactNode
episodes: LPE.Podcast.Document[]
show?: LPE.Podcast.Show
divider?: boolean
isFeatured?: boolean
}
export default function EpisodesList({
@ -14,13 +16,18 @@ export default function EpisodesList({
episodes,
show,
divider = false,
isFeatured = false,
}: Props) {
return (
<EpisodeListContainer>
{header}
<EpisodesContainer>
{episodes.slice(0, 2).map((episode) => (
<PostCardContainer key={episode.id} divider={divider}>
{episodes.map((episode) => (
<PostCardContainer
key={episode.id}
divider={divider}
className={isFeatured ? 'w-8' : 'w-4'}
>
<PostCard
contentType={LPE.PostTypes.Podcast}
data={{
@ -55,13 +62,12 @@ const EpisodeListContainer = styled.div`
padding-top: 16px;
`
const EpisodesContainer = styled.div`
display: flex;
gap: 16px;
const EpisodesContainer = styled(Grid)`
gap: 36px 16px;
`
const PostCardContainer = styled.div<{ divider: boolean }>`
padding-top: 24px;
const PostCardContainer = styled(GridItem)<{ divider: boolean }>`
padding-block: 24px;
border-top: ${({ divider }) =>
divider ? '1px solid rgb(var(--lsd-border-primary))' : 'none'};
`

View File

@ -23,7 +23,7 @@ export default function PodcastShowCard({
<Container {...props}>
<LogosCircleIcon width={73} height={73} />
<ShowData>
<Typography variant="h3">{show.title}</Typography>
<Title variant="h3">{show.title}</Title>
<PodcastHost show={show} />
<Description variant="body2">{show.description}</Description>
</ShowData>
@ -35,7 +35,10 @@ const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 68px;
`
const Title = styled(Typography)`
margin-bottom: 16px;
`
const ShowData = styled.div`

View File

@ -4,6 +4,7 @@ import { Button, Typography } from '@acid-info/lsd-react'
import Link from 'next/link'
import PodcastHost from './Podcast.Host'
import Image from 'next/image'
import { Grid, GridItem } from '../Grid/Grid'
interface Props {
shows: LPE.Podcast.Show[]
@ -14,7 +15,8 @@ export default function PodcastsLists({ shows }: Props) {
<PodcastListsContainer>
{shows &&
shows.map((show) => (
<PodcastCard key={show.id}>
<ShowCardContainer key={show.id} className="w-8">
<ShowCard>
<Image
src={show.logo.url}
width={56}
@ -33,23 +35,28 @@ export default function PodcastsLists({ shows }: Props) {
<Link href={`/podcasts/${show.slug}`}>
<Button>Go to the show page</Button>
</Link>
</PodcastCard>
</ShowCard>
</ShowCardContainer>
))}
</PodcastListsContainer>
)
}
const PodcastListsContainer = styled.div`
display: flex;
const PodcastListsContainer = styled(Grid)`
gap: 16px;
@media (max-width: 768px) {
flex-direction: column;
}
`
const PodcastCard = styled.div`
const ShowCardContainer = styled(GridItem)``
const ShowCard = styled.div`
display: flex;
flex-direction: column;
padding: 24px;
border: 1px solid rgb(var(--lsd-text-primary));
width: 50%;
`
const Row = styled.div`

View File

@ -22,11 +22,7 @@ const EpisodeContainer = (props: Props) => {
)
}
const EpisodeBodyContainer = styled(GridItem)`
@media (min-width: 768px) and (max-width: 1200px) {
grid-column: span 10 !important;
}
`
const EpisodeBodyContainer = styled(GridItem)``
const EpisodeGrid = styled(Grid)`
width: 100%;

View File

@ -2,10 +2,11 @@ import { Grid, GridItem } from '@/components/Grid/Grid'
import styled from '@emotion/styled'
import { LPE } from '../types/lpe.types'
import EpisodesList from '@/components/Podcasts/Episodes.List'
import { Typography } from '@acid-info/lsd-react'
import { Button, Typography } from '@acid-info/lsd-react'
import PodcastShowCard from '@/components/Podcasts/PodcastShowCard'
import { PodcastType } from '@/components/PostCard/PostCard'
import PodcastSection from '@/components/Podcasts/Podcast.Section'
import { api } from '@/services/api.service'
import { useState } from 'react'
interface Props {
show: LPE.Podcast.Show
@ -15,30 +16,58 @@ interface Props {
const PodcastShowContainer = (props: Props) => {
const { show, latestEpisodes, highlightedEpisodes } = props
const [latest, setLatest] = useState(latestEpisodes)
const [page, setPage] = useState(2)
const [showSeeMore, setShowSeeMore] = useState(true)
const handleLoadMore = async () => {
const response = await api.getLatestEpisodes({
page: page,
limit: 9,
showSlug: show.slug,
})
if (response.data.length === 0) {
setShowSeeMore(false)
return
}
setLatest((prev) => [...prev, ...response.data])
setPage((prev) => prev + 1)
}
return (
<>
<PodcastsGrid>
<PodcastsBodyContainer className={'w-16'}>
<PodcastShowCard show={show} />
<PodcastSection>
<EpisodesList
header={<Typography variant="body2">All episodes</Typography>}
episodes={highlightedEpisodes}
show={show}
isFeatured={true}
/>
</PodcastSection>
<EpisodesList episodes={latestEpisodes} divider={true} show={show} />
<EpisodesList episodes={latest} divider={true} show={show} />
</PodcastsBodyContainer>
</PodcastsGrid>
{showSeeMore && (
<SeeMoreButton onClick={handleLoadMore}>
See more episodes
</SeeMoreButton>
)}
</>
)
}
const PodcastsBodyContainer = styled(GridItem)`
@media (min-width: 768px) and (max-width: 1200px) {
grid-column: span 10 !important;
}
const PodcastsBodyContainer = styled(GridItem)``
const SeeMoreButton = styled(Button)`
display: block;
width: 236px;
height: 40px;
margin: 24px auto;
`
const PodcastsGrid = styled(Grid)`

View File

@ -24,7 +24,12 @@ const PodcastsContainer = (props: Props) => {
<PodcastSection>
<EpisodesList
header={<EpisodeListHeader>Latest Episodes</EpisodeListHeader>}
episodes={highlightedEpisodes}
episodes={highlightedEpisodes.slice(0, 2)}
isFeatured={true}
/>
<EpisodesList
episodes={highlightedEpisodes.slice(2)}
divider={true}
/>
</PodcastSection>
@ -57,11 +62,7 @@ const PodcastsContainer = (props: Props) => {
)
}
const PodcastsBodyContainer = styled(GridItem)`
@media (min-width: 768px) and (max-width: 1200px) {
grid-column: span 10 !important;
}
`
const PodcastsBodyContainer = styled(GridItem)``
const PodcastsGrid = styled(Grid)`
width: 100%;

View File

@ -75,6 +75,7 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
const { data: relatedArticles } = await unbodyApi.getRelatedArticles({
id: data.id,
})
const { data: articlesFromSameAuthors } =
await unbodyApi.getArticlesFromSameAuthors(
slug as string,

View File

@ -42,16 +42,25 @@ const EpisodePage = ({ episode, relatedEpisodes, errors }: EpisodeProps) => {
}
export async function getStaticPaths() {
const { data } = await unbodyApi.getPodcastShows({ populateEpisodes: true })
const paths = data.flatMap((show) => {
return (
show?.episodes &&
show.episodes.map((episode) => {
return {
paths: [
{
params: {
showSlug: `hasing-it-out`,
epSlug: `test-podcast-highlighted`,
showSlug: show.slug,
epSlug: episode.slug,
},
},
],
fallback: true,
}
})
)
})
return {
paths: paths,
fallback: false,
}
}

View File

@ -47,10 +47,19 @@ const PodcastShowPage = ({
}
export async function getStaticPaths() {
// TODO : dynamic paths
const { data } = await unbodyApi.getPodcastShows({ populateEpisodes: false })
const paths = data.map((show) => {
return {
paths: [{ params: { showSlug: `hashing-it-out` } }],
fallback: true,
params: {
showSlug: show.slug,
},
}
})
return {
paths: paths,
fallback: false,
}
}
@ -75,7 +84,7 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
await unbodyApi.getLatestEpisodes({
showSlug: showSlug as string,
page: 1,
limit: 12,
limit: 9,
})
// TODO : error handling
@ -83,7 +92,7 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
await unbodyApi.getHighlightedEpisodes({
showSlug: showSlug as string,
page: 1,
limit: 9,
limit: 2,
})
return {