mirror of
https://github.com/acid-info/logos-press-engine.git
synced 2025-02-22 14:18:14 +00:00
Merge pull request #90 from acid-info/refactor-podcasts
Refactor podcasts & episode components
This commit is contained in:
commit
1c4b35b3ca
@ -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);
|
||||
`
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
`
|
||||
|
@ -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
|
||||
|
@ -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`
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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 && (
|
||||
|
@ -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: '',
|
||||
|
27
src/components/Icons/FullscreenIcon/FullscreenIcon.tsx
Normal file
27
src/components/Icons/FullscreenIcon/FullscreenIcon.tsx
Normal 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 },
|
||||
)
|
1
src/components/Icons/FullscreenIcon/index.ts
Normal file
1
src/components/Icons/FullscreenIcon/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './FullscreenIcon'
|
@ -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 },
|
||||
|
@ -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 },
|
||||
|
@ -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)`
|
||||
|
@ -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 {
|
||||
|
45
src/components/NotFound/NotFound.tsx
Normal file
45
src/components/NotFound/NotFound.tsx
Normal 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 doesn’t 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;
|
||||
`
|
1
src/components/NotFound/index.ts
Normal file
1
src/components/NotFound/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as NotFound } from './NotFound'
|
@ -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;
|
||||
}
|
||||
`
|
||||
|
@ -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;
|
||||
}
|
||||
`
|
||||
|
@ -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;
|
||||
`
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
`
|
||||
|
||||
|
25
src/layouts/NotFoundLayout/NotFound.layout.tsx
Normal file
25
src/layouts/NotFoundLayout/NotFound.layout.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
1
src/layouts/NotFoundLayout/index.ts
Normal file
1
src/layouts/NotFoundLayout/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as DefaultLayout } from './NotFound.layout'
|
24
src/pages/404.tsx
Normal file
24
src/pages/404.tsx
Normal 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
|
@ -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] : ''
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user