Merge pull request #90 from acid-info/refactor-podcasts

Refactor podcasts & episode components
This commit is contained in:
jeangovil 2023-08-25 13:08:53 +03:30 committed by GitHub
commit 1c4b35b3ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 580 additions and 210 deletions

View File

@ -1,12 +1,12 @@
import {
extractClassFromFirstTag,
extractIdFromFirstTag,
extractInnerHtml,
} from '@/utils/html.utils'
import { Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import ReactPlayer from 'react-player'
import { LPE } from '../../types/lpe.types'
import { parseText, parseTimestamp } from '@/utils/string.utils'
export const RenderEpisodeBlock = ({
block,
@ -24,19 +24,24 @@ export const RenderEpisodeBlock = ({
<ReactPlayer url={youtubeLink[0]} />
) : (
<TranscriptionItem>
<Typography variant="body2">{parseTimestamp(block.text)}</Typography>
<Transcript
variant="body2"
component={'p'}
genericFontFamily="sans-serif"
className={extractClassFromFirstTag(block.html) || ''}
id={extractIdFromFirstTag(block.html) || `p-${block.id}`}
dangerouslySetInnerHTML={{ __html: extractInnerHtml(block.html) }}
/>
>
{parseText(block.text)}
</Transcript>
</TranscriptionItem>
)
}
const TranscriptionItem = styled.div`
display: flex;
flex-direction: column;
gap: var(--lsd-body2-lineHeight);
margin-bottom: calc(var(--lsd-body2-lineHeight) * 2);
`

View File

@ -5,6 +5,7 @@ import EpisodeHeader from './Header/Episode.Header'
import EpisodeTranscript from './Episode.Transcript'
import { playerState } from '../GlobalAudioPlayer/globalAudioPlayer.state'
import { useHookstate } from '@hookstate/core'
import { uiConfigs } from '@/configs/ui.configs'
interface Props {
episode: LPE.Podcast.Document
@ -39,9 +40,9 @@ const EpisodeContainer = styled.article`
display: flex;
position: relative;
flex-direction: column;
gap: 16px;
max-width: 700px;
max-width: 696px;
@media (min-width: 768px) and (max-width: 1200px) {
@media (max-width: 768px) {
margin-top: ${uiConfigs.navbarRenderedHeight - 16}px;
}
`

View File

@ -17,17 +17,19 @@ const EpisodeCredits = ({
open={open}
onChange={() => setOpen((prev) => !prev)}
>
{credits?.map((credit, idx) => (
<Reference key={idx}>
<Typography
component="p"
variant="body3"
id={credit.id.replace('#', '')}
>
{credit.text}
</Typography>
</Reference>
))}
<Credits>
{credits?.map((credit, idx) => (
<Credit key={idx}>
<Typography
component="p"
variant="body3"
id={credit.id.replace('#', '')}
>
{credit.text}
</Typography>
</Credit>
))}
</Credits>
</Collapse>
</Container>
) : null
@ -41,10 +43,18 @@ const Container = styled.div`
}
`
const Reference = styled.div`
const Credits = styled.div`
display: flex;
padding: 8px 14px;
gap: 8px;
flex-direction: column;
justify-content: center;
gap: var(--lsd-body3-lineHeight);
padding: 12px 14px;
`
const Credit = styled.div`
display: flex;
flex-direction: flex;
align-items: center;
`
export default EpisodeCredits

View File

@ -17,20 +17,22 @@ const EpisodeFootnotes = ({
open={open}
onChange={() => setOpen((prev) => !prev)}
>
{footnotes.map((footnote, idx) => (
<Reference key={idx}>
<Typography
component="a"
variant="body3"
href={`#${footnote.refId}`}
target="_blank"
id={footnote.id.replace('#', '')}
>
{footnote.refValue}
</Typography>
<P dangerouslySetInnerHTML={{ __html: footnote.valueHTML }} />
</Reference>
))}
<Footnotes>
{footnotes.map((footnote, idx) => (
<Footnote key={idx}>
<Typography
component="a"
variant="body3"
href={`#${footnote.refId}`}
target="_blank"
id={footnote.id.replace('#', '')}
>
{footnote.refValue}
</Typography>
<P dangerouslySetInnerHTML={{ __html: footnote.valueHTML }} />
</Footnote>
))}
</Footnotes>
</Collapse>
</Container>
) : null
@ -44,11 +46,18 @@ const Container = styled.div`
}
`
const Reference = styled.div`
const Footnotes = styled.div`
display: flex;
align-items: center;
padding: 8px 14px;
gap: 8px;
flex-direction: column;
justify-content: center;
gap: var(--lsd-body3-lineHeight);
padding: 12px 14px;
`
const Footnote = styled.div`
display: flex;
flex-direction: flex;
align-items: baseline;
`
const P = styled.p`

View File

@ -1,9 +1,8 @@
import { LPE } from '@/types/lpe.types'
import { Button, Typography } from '@acid-info/lsd-react'
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'
import { PostsGrid } from '../../PostsGrid'
type props = {
podcastSlug: string
@ -24,18 +23,25 @@ const RelatedEpisodes = ({ podcastSlug, relatedEpisodes }: props) => {
return (
<Container>
<Typography>More Episodes</Typography>
<EpisodeCards>
{relatedEpisodes.slice(0, showIndex).map((episode, idx: number) => (
<PostCardContainer className="w-8" key={'related-episode' + idx}>
<PostCard
size="xsmall"
displayPodcastShow={false}
contentType={LPE.PostTypes.Podcast}
data={{ ...PostCard.toData(episode), tags: [] }}
/>
</PostCardContainer>
))}
</EpisodeCards>
<PostsGrid
displayPodcastShow={false}
posts={relatedEpisodes.slice(0, showIndex)}
pattern={[{ cols: 2, size: 'xsmall' }]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [{ cols: 1, size: 'small' }],
},
{
breakpoint: 'sm',
pattern: [{ cols: 1, size: 'small' }],
},
{
breakpoint: 'md',
pattern: [{ cols: 2, size: 'small' }],
},
]}
/>
{relatedEpisodes?.length > 4 && (
<ShowButton onClick={toggleShowMore}>
{showMore ? 'Show less' : 'Show more'}
@ -53,17 +59,9 @@ const Container = styled.div`
flex-direction: column;
`
const EpisodeCards = styled(Grid)`
gap: 0 16px;
`
const ShowButton = styled(Button)`
height: 40px;
margin-top: 24px;
`
const PostCardContainer = styled(GridItem)`
padding-top: 24px;
`
export default RelatedEpisodes

View File

@ -16,21 +16,21 @@ const renderChannel = (channel: LPE.Podcast.Channel) => {
return (
<Channel href={channel.url} target="_blank">
<SpotifyIcon width={16} height={16} />
<Typography variant="body2">Spotify</Typography>
<ChannelName variant="body2">Spotify</ChannelName>
</Channel>
)
case LPE.Podcast.ChannelNames.ApplePodcasts:
return (
<Channel href={channel.url} target="_blank">
<ApplePodcastsIcon width={16} height={16} />
<Typography variant="body2">Apple Podcasts</Typography>
<ChannelName variant="body2">Apple Podcasts</ChannelName>
</Channel>
)
case LPE.Podcast.ChannelNames.GooglePodcasts:
return (
<Channel href={channel.url} target="_blank">
<GooglePodcastsIcon width={16} height={16} />
<Typography variant="body2">Google Podcasts</Typography>
<ChannelName variant="body2">Google Podcasts</ChannelName>
</Channel>
)
default:
@ -53,7 +53,7 @@ const EpisodeChannelContainer = styled.header`
margin-top: 32px;
@media (max-width: 768px) {
padding-top: 32px;
margin-top: 24px;
}
`
@ -64,4 +64,11 @@ const Channel = styled(Link)`
text-decoration: none;
`
const ChannelName = styled(Typography)`
@media (max-width: 768px) {
font-size: var(--lsd-body3-fontSize) !important;
line-height: var(--lsd-body3-lineHeight) !important;
}
`
export default EpisodeChannels

View File

@ -2,10 +2,11 @@ import { Tags } from '@/components/Tags'
import { Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import { LPE } from '../../../types/lpe.types'
import { LogosCircleIcon } from '@/components/Icons/LogosCircleIcon'
import EpisodeChannels from './Episode.Channels'
import EpisodeStats from '../Episode.Stats'
import EpisodePlayer from './Episode.Player'
import Image from 'next/image'
import Link from 'next/link'
export type EpisodeHeaderProps = LPE.Podcast.Document & {
channel: LPE.Podcast.Channel
@ -37,10 +38,19 @@ const EpisodeHeader = ({
<EpisodeTitle variant="h1" genericFontFamily="serif" component="h1">
{title}
</EpisodeTitle>
<PodcastName>
<LogosCircleIcon width={24} height={24} />
Network State Podcast
</PodcastName>
{show && (
<CustomLink href={`/podcasts/${show.slug}`}>
<Show>
<Image
src={show.logo.url}
alt={show.logo.alt}
width={24}
height={24}
/>
<Typography variant="body2">{show?.title}</Typography>
</Show>
</CustomLink>
)}
{tags && <Tags tags={tags} />}
{channels && <EpisodeChannels channels={channels} />}
{description && (
@ -59,10 +69,6 @@ const EpisodeHeader = ({
const EpisodeHeaderContainer = styled.header`
display: flex;
flex-direction: column;
@media (max-width: 768px) {
padding-top: 32px;
}
`
const CustomTypography = styled(Typography)`
@ -75,6 +81,8 @@ const EpisodeTitle = styled(Typography)`
margin-bottom: 16px;
@media (max-width: 768px) {
margin-bottom: 8px;
font-size: var(--lsd-h4-fontSize) !important;
line-height: var(--lsd-h4-lineHeight) !important;
}
`
@ -82,35 +90,22 @@ const EpisodeSubtitle = styled(CustomTypography)`
margin-top: 32px;
@media (max-width: 768px) {
font-size: var(--lsd-subtitle1-fontSize);
font-size: var(--lsd-subtitle1-fontSize) !important;
line-height: var(--lsd-subtitle1-lineHeight) !important;
margin-top: 24px;
}
`
const PodcastName = styled.div`
const Show = styled.div`
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
text-decoration: none;
`
// 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%;
}
const CustomLink = styled(Link)`
text-decoration: none;
`
export default EpisodeHeader

View File

@ -2,10 +2,11 @@ import styled from '@emotion/styled'
import ReactPlayer from 'react-player'
import { useHookstate } from '@hookstate/core'
import { playerState } from '@/components/GlobalAudioPlayer/globalAudioPlayer.state'
import { useEffect, useRef } from 'react'
import { useEffect, useRef, useState } from 'react'
import { episodeState } from '@/components/GlobalAudioPlayer/episode.state'
import SimplecastPlayer from './Episode.SimplecastPlayer'
import { LPE } from '@/types/lpe.types'
import { useRouter } from 'next/router'
export type EpisodePlayerProps = {
channel: LPE.Podcast.Channel
@ -20,9 +21,14 @@ const EpisodePlayer = ({
title,
showTitle,
}: EpisodePlayerProps) => {
const router = useRouter()
const state = useHookstate(playerState)
const epState = useHookstate(episodeState)
const playerContainerRef = useRef<HTMLDivElement>(null)
const playerRef = useRef<ReactPlayer>(null)
const isSimplecast = channel?.name === LPE.Podcast.ChannelNames.Simplecast
const url =
@ -35,16 +41,32 @@ const EpisodePlayer = ({
>
).data.audioFileUrl
const playerContainerRef = useRef<HTMLDivElement>(null)
const playerRef = useRef<ReactPlayer>(null)
const keepPlaying =
state.value.url !== url && state.value.isEnabled && state.value.playing
const [keepGlobalPlay, setKeepGlobalPlay] = useState(keepPlaying)
useEffect(() => {
if (keepPlaying) {
setKeepGlobalPlay(true)
playerRef.current?.seekTo(0)
}
}, [keepPlaying])
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
state.set((prev) => ({
...prev,
isEnabled: false,
}))
if (keepPlaying) {
state.set((prev) => ({
...prev,
isEnabled: true,
}))
} else {
state.set((prev) => ({
...prev,
isEnabled: false,
}))
}
} else {
state.set((prev) => ({
...prev,
@ -57,22 +79,32 @@ const EpisodePlayer = ({
return () => {
observer.disconnect()
}
}, [keepGlobalPlay])
useEffect(() => {
const handleLeave = () => {
if (state.value.playing) {
state.set((prev) => ({
...prev,
isEnabled: true,
}))
}
}
router.events.on('routeChangeStart', handleLeave)
return () => {
router.events.off('routeChangeStart', handleLeave)
}
}, [])
useEffect(() => {
epState.set({
episodeId: 'aaa',
title: title,
podcast: showTitle,
url: url,
url: url as string,
coverImage: coverImage ?? null,
})
state.set((prev) => ({
...prev,
url: url,
}))
}, [url, title, showTitle, coverImage])
}, [title, showTitle, coverImage])
useEffect(() => {
if (!state.value.isEnabled) {
@ -80,6 +112,21 @@ const EpisodePlayer = ({
}
}, [state.value.isEnabled])
useEffect(() => {
if (channel?.name === LPE.Podcast.ChannelNames.Youtube) {
window.addEventListener('message', function (event) {
if (event.origin == 'https://www.youtube.com') {
const data = JSON.parse(event?.data)
const volume = data?.info?.volume
if (typeof volume !== 'undefined') {
state.set((prev) => ({ ...prev, volume: volume / 100 }))
}
}
})
}
}, [])
const handleProgress = (newState: {
playedSeconds: number
played: number
@ -96,7 +143,18 @@ const EpisodePlayer = ({
}
const handlePlay = () => {
state.set((prev) => ({ ...prev, playing: true }))
if (keepGlobalPlay) {
setKeepGlobalPlay(false)
state.set((prev) => ({
...prev,
isEnabled: false,
played: 0,
url: url,
playing: true,
}))
} else {
state.set((prev) => ({ ...prev, url: url, playing: true }))
}
}
const handlePause = () => state.set((prev) => ({ ...prev, playing: false }))
@ -108,6 +166,9 @@ const EpisodePlayer = ({
<>
{isSimplecast && (
<SimplecastPlayer
playing={keepGlobalPlay ? false : state.value.playing}
playedSeconds={keepGlobalPlay ? 0 : state.value.playedSeconds}
played={keepGlobalPlay ? 0 : state.value.played}
playerRef={playerRef}
coverImage={
epState.value.coverImage as LPE.Podcast.Document['coverImage']
@ -120,15 +181,22 @@ const EpisodePlayer = ({
<ReactPlayer
forceAudio={isSimplecast ? true : false}
ref={playerRef}
url={state.value.url as string}
playing={state.value.playing}
url={url as string}
playing={keepGlobalPlay ? false : state.value.playing}
controls={isSimplecast ? false : true}
volume={state.value.volume}
muted={state.value.isEnabled ? true : false}
muted={
state.value.isEnabled ? true : state.value.muted ? true : false
}
onProgress={handleProgress}
onPlay={handlePlay}
onPause={handlePause}
onDuration={handleDuration}
config={{
youtube: {
playerVars: { enablejsapi: 1 },
},
}}
/>
</PlayerContainer>
</>
@ -139,7 +207,7 @@ const PlayerContainer = styled.div<{ isAudio: boolean }>`
margin-bottom: ${(props) => (props.isAudio ? '0' : '32px')};
position: relative;
padding-bottom: ${(props) => (props.isAudio ? '0' : '56.25%')};
padding-top: 30px;
padding-top: 32px;
height: 0;
overflow: hidden;
@ -152,6 +220,10 @@ const PlayerContainer = styled.div<{ isAudio: boolean }>`
width: 100%;
height: 100%;
}
@media (max-width: 768px) {
padding-top: 24px;
}
`
export default EpisodePlayer

View File

@ -1,18 +1,15 @@
import { playerState } from '@/components/GlobalAudioPlayer/globalAudioPlayer.state'
import { PauseIcon } from '@/components/Icons/PauseIcon'
import { PlayIcon } from '@/components/Icons/PlayIcon'
import { LPE } from '@/types/lpe.types'
import { convertSecToMinAndSec } from '@/utils/string.utils'
import { Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import { useHookstate } from '@hookstate/core'
import Image from 'next/image'
import { useState } from 'react'
import { VolumeIcon } from '@/components/Icons/VolumeIcon'
import { LpeAudioPlayerControls } from '@/components/LpePlayer/Controls/Controls'
import { ResponsiveImage } from '@/components/ResponsiveImage/ResponsiveImage'
export type SimplecastPlayerProps = {
playing: boolean
played: number
playedSeconds: number
playerRef: React.RefObject<any>
coverImage: LPE.Podcast.Document['coverImage']
handlePlay: () => void
@ -20,6 +17,9 @@ export type SimplecastPlayerProps = {
}
const SimplecastPlayer = ({
playing,
played,
playedSeconds,
playerRef,
coverImage,
handlePlay,
@ -63,20 +63,24 @@ const SimplecastPlayer = ({
<Controls>
<LpeAudioPlayerControls
duration={state.value.duration}
playedSeconds={state.value.playedSeconds}
playing={state.value.playing}
played={state.value.played}
playedSeconds={playedSeconds}
playing={playing}
played={played}
onPause={handlePause}
onPlay={handlePlay}
muted={state.value.muted}
onVolumeToggle={() =>
state.set((prev) => ({ ...prev, muted: !prev.muted }))
state.set((prev) => ({
...prev,
muted: !prev.muted,
}))
}
timeTrackProps={{
onValueChange: handleSeekChange,
onMouseUp: handleSeekMouseUp,
onMouseDown: handleSeekMouseDown,
}}
allowFullScreen={true}
color={'white'}
/>
</Controls>

View File

@ -102,7 +102,10 @@ export default function GlobalAudioPlayer() {
<LpeAudioPlayer
controlProps={{
onVolumeToggle: () =>
state.set((prev) => ({ ...prev, muted: !prev.muted })),
state.set((prev) => ({
...prev,
muted: !prev.muted,
})),
duration: state.value.duration,
played: state.value.played,
muted: state.value.muted,
@ -126,25 +129,24 @@ export default function GlobalAudioPlayer() {
url={state.value.url as string}
width="100%"
height="100%"
pip={state.value.pip}
playing={state.value.playing}
controls={state.value.controls}
light={state.value.light}
loop={state.value.loop}
playbackRate={state.value.playbackRate}
volume={state.value.volume}
muted={state.value.isEnabled ? false : true}
onReady={() => console.log('onReady')}
onStart={() => console.log('onStart')}
muted={state.value.muted ? true : state.value.isEnabled ? false : true}
onPlay={handlePlay}
onPause={handlePause}
onBuffer={() => console.log('onBuffer')}
onPlaybackRateChange={handleOnPlaybackRateChange}
onSeek={(e) => console.log('onSeek', e)}
onEnded={handleEnded}
onError={(e) => console.log('onError', e)}
onDuration={handleDuration}
onProgress={handleProgress}
// onReady={() => console.log('onReady')}
// onStart={() => console.log('onStart')}
// onBuffer={() => console.log('onBuffer')}
// onSeek={(e) => console.log('onSeek', e)}
// onError={(e) => console.log('onError', e)}
/>
<RightMenu>
{!!epState.value.coverImage && (

View File

@ -2,7 +2,6 @@ import { LPE } from '@/types/lpe.types'
import { hookstate } from '@hookstate/core'
export type EpisodeState = {
episodeId: string
title: string
podcast: string
url: string
@ -10,7 +9,6 @@ export type EpisodeState = {
}
export const defaultEpisodeState: EpisodeState = {
episodeId: '',
title: '',
podcast: '',
url: '',

View File

@ -0,0 +1,27 @@
import { LsdIcon } from '@acid-info/lsd-react'
export const FullscreenIcon = LsdIcon(
(props) => (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clip-path="url(#clip0_2418_8608)">
<path
d="M4 18C4 19.1 4.9 20 6 20H10V18H6V14H4V18ZM20 6C20 4.9 19.1 4 18 4H14V6H18V10H20V6ZM6 6H10V4H6C4.9 4 4 4.9 4 6V10H6V6ZM20 18V14H18V18H14V20H18C19.1 20 20 19.1 20 18Z"
fill={props.fill || 'white'}
/>
</g>
<defs>
<clipPath id="clip0_2418_8608">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
),
{ filled: true },
)

View File

@ -0,0 +1 @@
export * from './FullscreenIcon'

View File

@ -3,16 +3,20 @@ import { LsdIcon } from '@acid-info/lsd-react'
export const MuteIcon = LsdIcon(
(props) => (
<svg
width="12"
height="12"
viewBox="0 0 12 12"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M9 6C8.99986 5.44142 8.84377 4.89396 8.54931 4.41929C8.25485 3.94462 7.83372 3.56159 7.33333 3.31333V4.78667L8.96667 6.42C8.98667 6.28667 9 6.14667 9 6ZM10.6667 6C10.6667 6.62667 10.5333 7.21333 10.3067 7.76L11.3133 8.76667C11.7663 7.91496 12.0022 6.96466 12 6C12 3.14667 10.0067 0.76 7.33333 0.153333V1.52667C9.26 2.1 10.6667 3.88667 10.6667 6ZM0.846667 0L0 0.846667L3.15333 4H0V8H2.66667L6 11.3333V6.84667L8.83333 9.68C8.38667 10.0267 7.88667 10.3 7.33333 10.4667V11.84C8.23551 11.6331 9.07751 11.2201 9.79333 10.6333L11.1533 12L12 11.1533L6 5.15333L0.846667 0ZM6 0.666667L4.60667 2.06L6 3.45333V0.666667Z"
fill="black"
/>
<g id="volume_off-16px">
<path
id="Vector"
d="M11 8C10.9999 7.44142 10.8438 6.89396 10.5493 6.41929C10.2549 5.94462 9.83372 5.56159 9.33333 5.31333V6.78667L10.9667 8.42C10.9867 8.28667 11 8.14667 11 8ZM12.6667 8C12.6667 8.62667 12.5333 9.21333 12.3067 9.76L13.3133 10.7667C13.7663 9.91496 14.0022 8.96466 14 8C14 5.14667 12.0067 2.76 9.33333 2.15333V3.52667C11.26 4.1 12.6667 5.88667 12.6667 8ZM2.84667 2L2 2.84667L5.15333 6H2V10H4.66667L8 13.3333V8.84667L10.8333 11.68C10.3867 12.0267 9.88667 12.3 9.33333 12.4667V13.84C10.2355 13.6331 11.0775 13.2201 11.7933 12.6333L13.1533 14L14 13.1533L8 7.15333L2.84667 2ZM8 2.66667L6.60667 4.06L8 5.45333V2.66667Z"
fill={props.fill || 'white'}
/>
</g>
</svg>
),
{ filled: true },

View File

@ -3,16 +3,24 @@ import { LsdIcon } from '@acid-info/lsd-react'
export const VolumeIcon = LsdIcon(
(props) => (
<svg
width="12"
height="12"
viewBox="0 0 12 12"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M0 3.84667V7.84667H2.66667L6 11.18V0.513333L2.66667 3.84667H0ZM9 5.84667C9 4.66667 8.32 3.65333 7.33333 3.16V8.52667C8.32 8.04 9 7.02667 9 5.84667ZM7.33333 0V1.37333C9.26 1.94667 10.6667 3.73333 10.6667 5.84667C10.6667 7.96 9.26 9.74667 7.33333 10.32V11.6933C10.0067 11.0867 12 8.7 12 5.84667C12 2.99333 10.0067 0.606667 7.33333 0V0Z"
fill={props.fill}
/>
<g clip-path="url(#clip0_2418_8612)">
<path
d="M3 9.00001V15H7L12 20V4.00001L7 9.00001H3ZM16.5 12C16.5 10.23 15.48 8.71001 14 7.97001V16.02C15.48 15.29 16.5 13.77 16.5 12ZM14 3.23001V5.29001C16.89 6.15001 19 8.83001 19 12C19 15.17 16.89 17.85 14 18.71V20.77C18.01 19.86 21 16.28 21 12C21 7.72001 18.01 4.14001 14 3.23001Z"
fill={props.fill || 'white'}
/>
</g>
<defs>
<clipPath id="clip0_2418_8612">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
),
{ filled: true },

View File

@ -21,7 +21,7 @@ export interface LpeAudioPlayerControlsProps {
onPause: () => void
onPlay: () => void
onVolumeToggle: () => void
allowFullScreen?: boolean
color?: string
timeTrackProps: ControlsTimeTrackProps
@ -38,6 +38,7 @@ export const LpeAudioPlayerControls = (props: LpeAudioPlayerControlsProps) => {
onPlay,
color = 'rgba(var(--lsd-surface-secondary), 1)',
onVolumeToggle,
allowFullScreen = false,
timeTrackProps: { onValueChange, onMouseDown, onMouseUp },
} = props
@ -46,17 +47,27 @@ export const LpeAudioPlayerControls = (props: LpeAudioPlayerControlsProps) => {
<Buttons>
<Row>
<PlayPause onClick={playing ? onPause : onPlay}>
{playing ? <PauseIcon fill={color} /> : <PlayIcon fill={color} />}
{playing ? (
<PauseIcon width={24} height={24} fill={color} />
) : (
<PlayIcon width={24} height={24} fill={color} />
)}
</PlayPause>
<TimeContainer>
<TimeContainer color={color}>
<Time variant="body3">{convertSecToMinAndSec(playedSeconds)}</Time>
<Typography variant="body3">/</Typography>
<Time variant="body3">{convertSecToMinAndSec(duration)}</Time>
</TimeContainer>
</Row>
<VolumeContainer onClick={onVolumeToggle}>
{muted ? <MuteIcon fill={color} /> : <VolumeIcon fill={color} />}
</VolumeContainer>
<Row>
<VolumeContainer onClick={onVolumeToggle}>
{muted ? (
<MuteIcon width={24} height={24} fill={color} />
) : (
<VolumeIcon width={24} height={24} fill={color} />
)}
</VolumeContainer>
</Row>
</Buttons>
<Seek className={styles.audioPlayer}>
<TimeTrack
@ -71,7 +82,11 @@ export const LpeAudioPlayerControls = (props: LpeAudioPlayerControlsProps) => {
</Container>
)
}
const Container = styled.div``
const Container = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
`
const Buttons = styled.div`
display: flex;
@ -99,17 +114,22 @@ const PlayPause = styled.button`
justify-content: center;
border: none;
background: none;
margin-right: 8px;
padding: 0;
`
const Row = styled.div`
display: flex;
align-items: center;
white-space: pre-wrap;
gap: 8px;
`
const TimeContainer = styled(Row)`
const TimeContainer = styled(Row)<{ color: string }>`
gap: 8px;
span {
color: ${({ color }) => color || 'black'};
}
`
const Time = styled(Typography)`

View File

@ -1,10 +1,3 @@
import { PauseIcon } from '@/components/Icons/PauseIcon'
import { PlayIcon } from '@/components/Icons/PlayIcon'
import { convertSecToMinAndSec } from '@/utils/string.utils'
import { Typography } from '@acid-info/lsd-react'
import { MuteIcon } from '@/components/Icons/MuteIcon'
import { VolumeIcon } from '@/components/Icons/VolumeIcon'
import styles from '@/components/GlobalAudioPlayer/GlobalAudioPlayer.module.css'
import React from 'react'
import styled from '@emotion/styled'
import {

View File

@ -0,0 +1,45 @@
import { uiConfigs } from '@/configs/ui.configs'
import { Button, Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import Link from 'next/link'
const NotFound = () => {
return (
<Container>
<Title genericFontFamily="serif" variant="h3">
Page not found
</Title>
<Description variant="body1">
Sorry, the page you are looking for doesnt exist or has been moved.
<br />
Try searching our site.
</Description>
<Link href="/search">
<SearchButton size="large">Go to search</SearchButton>
</Link>
</Container>
)
}
export default NotFound
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin-top: calc(120px + ${uiConfigs.navbarRenderedHeight}px);
`
const Title = styled(Typography)`
margin-bottom: 16px;
`
const Description = styled(Typography)`
margin-bottom: 48px;
max-width: 510px;
text-align: center;
`
const SearchButton = styled(Button)`
width: fit-content;
`

View File

@ -0,0 +1 @@
export { default as NotFound } from './NotFound'

View File

@ -13,4 +13,9 @@ export default function PodcastSection({ children, marginTop = 140 }: Props) {
const Section = styled.div<{ marginTop: number }>`
margin-top: ${(props) => props.marginTop}px;
border-top: 1px solid rgb(var(--lsd-border-primary));
@media (max-width: 768px) {
margin-top: 80px;
margin-bottom: 80px;
}
`

View File

@ -21,7 +21,7 @@ export default function PodcastShowCard({
}: PodcastShowCardProps) {
return (
<Container {...props}>
<LogosCircleIcon width={73} height={73} />
<LogosCircleIcon width={74} height={74} />
<ShowData>
<Title variant="h3">{show.title}</Title>
<PodcastHost show={show} />
@ -53,4 +53,13 @@ const ShowData = styled.div`
const Description = styled(Typography)`
margin-top: 16px;
@media (min-width: 768px) and (max-width: 1200px) {
margin-top: 12px;
}
@media (max-width: 768px) {
text-align: center;
margin-top: 8px;
}
`

View File

@ -1,8 +1,7 @@
import styled from '@emotion/styled'
import { LPE } from '../../types/lpe.types'
import { Button, Typography } from '@acid-info/lsd-react'
import { ArrowDownIcon, Badge, 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'
@ -17,27 +16,46 @@ export default function PodcastsLists({ shows }: Props) {
shows.map((show) => (
<ShowCardContainer key={show.id} className="w-8">
<ShowCard>
<Image
src={show.logo.url}
width={56}
height={56}
alt={show.logo.alt}
/>
<Typography variant="h3">{show.title}</Typography>
<Row>
<PodcastHost show={show} />
<Typography variant="body2"></Typography>
<Typography variant="body2">
{show.numberOfEpisodes} EP
</Typography>
</Row>
<Description
variant="body2"
dangerouslySetInnerHTML={{ __html: show.description }}
/>
<Link href={`/podcasts/${show.slug}`}>
<Button>Go to the show page</Button>
</Link>
<Top>
<ShowInfoContainer>
<Image
src={show.logo.url}
width={38}
height={38}
alt={show.logo.alt}
/>
<ShowInfo>
<Typography variant="body2">{show.title}</Typography>
<Typography variant="body3">
{show.numberOfEpisodes} EP
</Typography>
</ShowInfo>
</ShowInfoContainer>
<ShowButtonLink href={`/podcasts/${show.slug}`}>
<ShowButton>
<ShowButtonText variant="body3">
Podcast page
</ShowButtonText>
<ArrowDownIcon />
</ShowButton>
</ShowButtonLink>
</Top>
<Bottom>
<Description
dangerouslySetInnerHTML={{ __html: show.description }}
/>
{/* @ts-ignore */}
{shows?.tags && (
<BadgeContainer>
{/* @ts-ignore */}
{show.tags.map((tag) => (
<Badge key={tag} size="small">
{tag}
</Badge>
))}
</BadgeContainer>
)}
</Bottom>
</ShowCard>
</ShowCardContainer>
))}
@ -50,6 +68,7 @@ const PodcastListsContainer = styled(Grid)`
@media (max-width: 768px) {
flex-direction: column;
gap: 24px;
}
`
@ -62,12 +81,81 @@ const ShowCard = styled.div`
border: 1px solid rgb(var(--lsd-text-primary));
`
const ShowInfoContainer = styled.div`
display: flex;
gap: 8px;
`
const ShowInfo = styled.div`
display: flex;
flex-direction: column;
gap: 2px;
`
const Row = styled.div`
display: flex;
gap: 8px;
margin-bottom: 16px;
`
const Description = styled(Typography)`
margin-bottom: 16px;
const Top = styled(Row)`
justify-content: space-between;
`
const Bottom = styled.div`
margin-top: 88px;
@media (max-width: 768px) {
margin-top: 64px;
}
`
const Description = styled(Typography)`
font-size: var(--lsd-h6-fontSize);
@media (max-width: 768px) {
font-size: var(--lsd-body1-fontSize);
}
`
const ShowButtonLink = styled(Link)`
text-decoration: none;
`
const ShowButton = styled(Button)`
display: flex;
align-items: center;
padding: 6px 10px 6px 12px;
width: 122px;
height: 28px;
gap: 12px;
> span {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
svg {
transform: rotate(-90deg);
}
@media (max-width: 768px) {
width: 28px;
height: 28px;
padding: 7px;
}
`
const ShowButtonText = styled(Typography)`
@media (max-width: 768px) {
display: none;
}
`
const BadgeContainer = styled.div`
margin-top: 24px;
display: flex;
gap: 8px;
`

View File

@ -13,11 +13,11 @@ const EpisodeContainer = (props: Props) => {
return (
<EpisodeGrid>
<Gap className={'w-4'} />
<GridItem className={'w-4'} />
<EpisodeBodyContainer className={'w-8'}>
<EpisodeBody episode={episode} relatedEpisodes={relatedEpisodes} />
</EpisodeBodyContainer>
<Gap className={'w-4'} />
<GridItem className={'w-4'} />
</EpisodeGrid>
)
}
@ -26,14 +26,10 @@ const EpisodeBodyContainer = styled(GridItem)``
const EpisodeGrid = styled(Grid)`
width: 100%;
margin-top: -47px; // offset for uiConfig.postSectionMargin
@media (min-width: 768px) and (max-width: 1200px) {
}
`
const Gap = styled(GridItem)`
@media (max-width: 550px) {
display: none;
}
`
export default EpisodeContainer

View File

@ -2,6 +2,7 @@ import { Grid, GridItem } from '@/components/Grid/Grid'
import EpisodesList from '@/components/Podcasts/Episodes.List'
import PodcastSection from '@/components/Podcasts/Podcast.Section'
import PodcastShowCard from '@/components/Podcasts/PodcastShowCard'
import { uiConfigs } from '@/configs/ui.configs'
import { Button, Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import { useRecentEpisodes } from '../queries/useRecentEpisodes.query'
@ -25,9 +26,9 @@ const PodcastShowContainer = (props: Props) => {
return (
<>
<PodcastsGrid>
<PodcastsBodyContainer className={'w-16'}>
<PodcastsBodyContainer>
<PodcastShowCard show={show} />
<PodcastSection>
<PodcastSection marginTop={64}>
<EpisodesList
shows={[show]}
displayShow={false}
@ -93,7 +94,18 @@ const PodcastShowContainer = (props: Props) => {
)
}
const PodcastsBodyContainer = styled(GridItem)``
const PodcastsGrid = styled(Grid)`
width: 100%;
margin-top: -15px; // offset for postSectionMargin
@media (max-width: 768px) {
margin-top: ${uiConfigs.navbarRenderedHeight + 48}px;
}
`
const PodcastsBodyContainer = styled(GridItem)`
grid-column: span 16;
`
const SeeMoreButton = styled(Button)`
display: block;
@ -102,10 +114,4 @@ const SeeMoreButton = styled(Button)`
margin: 24px auto;
`
const PodcastsGrid = styled(Grid)`
width: 100%;
@media (min-width: 768px) and (max-width: 1200px) {
}
`
export default PodcastShowContainer

View File

@ -1,10 +1,11 @@
import { Grid, GridItem } from '@/components/Grid/Grid'
import { LogosCircleIcon } from '@/components/Icons/LogosCircleIcon'
import EpisodesList from '@/components/Podcasts/Episodes.List'
import PodcastSection from '@/components/Podcasts/Podcast.Section'
import PodcastsLists from '@/components/Podcasts/Podcasts.Lists'
import { uiConfigs } from '@/configs/ui.configs'
import { Button, Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import Image from 'next/image'
import Link from 'next/link'
import { LPE } from '../types/lpe.types'
@ -71,7 +72,12 @@ const PodcastsContainer = (props: Props) => {
header={
<EpisodeListHeader>
<Show>
<LogosCircleIcon width={38} height={38} />
<Image
src={show.logo.url}
alt={show.logo.alt}
width={38}
height={38}
/>
<PodcastInfo>
<Typography variant="body1">{show.title}</Typography>
<Typography variant="body3">
@ -111,7 +117,8 @@ const PodcastsBodyContainer = styled(GridItem)``
const PodcastsGrid = styled(Grid)`
width: 100%;
@media (min-width: 768px) and (max-width: 1200px) {
@media (max-width: 768px) {
margin-top: ${uiConfigs.navbarRenderedHeight + 80}px;
}
`

View File

@ -0,0 +1,25 @@
import { Main } from '@/components/Main'
import { NavBarProps } from '@/components/NavBar/NavBar'
import { PropsWithChildren, useMemo } from 'react'
import { MainProps } from '../../components/Main/Main'
import { AppBar } from '../../components/NavBar'
interface Props {
navbarProps?: NavBarProps
mainProps?: Partial<MainProps>
}
export default function DefaultLayout(props: PropsWithChildren<Props>) {
const { mainProps = {}, navbarProps = {} } = props
const navbarDefaultState = useMemo(
() => navbarProps.defaultState ?? { showTitle: true },
[navbarProps],
)
return (
<>
<AppBar {...navbarProps} defaultState={navbarDefaultState} />
<Main {...mainProps}>{props.children}</Main>
</>
)
}

View File

@ -0,0 +1 @@
export { default as DefaultLayout } from './NotFound.layout'

24
src/pages/404.tsx Normal file
View File

@ -0,0 +1,24 @@
import { NotFound } from '@/components/NotFound'
import { SEO } from '@/components/SEO'
import NotFoundLayout from '@/layouts/NotFoundLayout/NotFound.layout'
import { ReactNode } from 'react'
const NotFoundPage = () => {
return (
<>
<SEO
title={'Not Found'}
description={'Description'}
imageUrl={undefined}
tags={[]}
/>
<NotFound />
</>
)
}
NotFoundPage.getLayout = function getLayout(page: ReactNode) {
return <NotFoundLayout>{page}</NotFoundLayout>
}
export default NotFoundPage

View File

@ -65,3 +65,12 @@ export function convertToIframe(url: string) {
return `<iframe height="200px" width="100%" frameborder="no" scrolling="no" seamless src="${url}"></iframe>`
}
export function parseText(text: string) {
return text.replace(/^\d{2}:\d{2}\s|\[\d+\]/g, '')
}
export function parseTimestamp(text: string) {
const time = text.match(/^\d{2}:\d{2}/g)
return time ? time[0] : ''
}