refactor for dynamic routes

This commit is contained in:
Sunda 2021-10-11 15:50:24 +02:00
parent fe85d93700
commit 7c63f7538c
13 changed files with 716 additions and 109 deletions

9
app.js
View File

@ -1,15 +1,16 @@
import { h } from 'preact' import { h } from 'preact'
import { useState, useEffect } from 'preact/hooks' import { useState, useEffect } from 'preact/hooks'
import { isWithinInterval, subHours, addHours } from 'date-fns' import { isWithinInterval, subHours, addHours } from 'date-fns'
import { zonedTimeToUtc, utcToZonedTime, format } from 'date-fns-tz' import { zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz'
import Video from './src/components/Video' import Video from './src/components/Video'
import config from './src/data/config'
import Info from './src/components/Info' import Info from './src/components/Info'
import { useEventCalendar, useEventApi } from './src/hooks/data' import { useEventCalendar, useEventApi } from './src/hooks/data'
import { useTimeout } from './src/hooks/timerHooks' import { useTimeout } from './src/hooks/timerHooks'
export default () => { export default (props) => {
console.log({ props })
const [currentVideo, setCurrentVideo] = useState(null) const [currentVideo, setCurrentVideo] = useState(null)
const [streamIsLive, setStreamIsLive] = useState(false) const [streamIsLive, setStreamIsLive] = useState(false)
const [infoActive, setInfoActive] = useState(false) const [infoActive, setInfoActive] = useState(false)
@ -60,7 +61,7 @@ export default () => {
) : ( ) : (
<Info <Info
data={calData} data={calData}
loading={loading || !minLoadTimePassed} loading={false}
infoActive={infoActive} infoActive={infoActive}
currentVideo={currentVideo} currentVideo={currentVideo}
setInfoActive={setInfoActive} setInfoActive={setInfoActive}

View File

@ -1,5 +1,42 @@
import { h, render } from 'preact' import { h, render } from 'preact'
import App from './app' import { useState } from 'preact/hooks'
import { BrowserRouter, Route, Switch } from 'react-router-dom'
import Main from './app'
import SeriesPage from './src/pages/SeriesPage'
import { slugify } from './src/helpers/string'
import { useEventApi, useEventCalendar } from './src/hooks/data'
import { useTimeout } from './src/hooks/timerHooks'
import LoaderLayout from './src/pages/LoaderLayout'
const App = () => {
const { data: calData, calLoading } = useEventCalendar()
const { data: seriesDataArray, loading: eventsLoading } = useEventApi()
const [minLoadTimePassed, setMinTimeUp] = useState(false)
useTimeout(() => {
setMinTimeUp(true)
}, 1500)
const seriesData = Object.values(seriesDataArray)
console.log(seriesData)
return calLoading || eventsLoading || !minLoadTimePassed ? (
<LoaderLayout />
) : (
<BrowserRouter>
<Switch>
<Route exact path="/" component={Main} />
{seriesData.length ? seriesData.map(series => (
<Route exact path={`/series/${slugify(series.name)}`}>
<SeriesPage data={series} />
</Route>)) : null}
</Switch>
</BrowserRouter>
)
}
const appEl = document.getElementById('app') const appEl = document.getElementById('app')

View File

@ -19,11 +19,14 @@
"axios": "^0.21.1", "axios": "^0.21.1",
"date-fns": "^2.19.0", "date-fns": "^2.19.0",
"date-fns-tz": "^1.1.4", "date-fns-tz": "^1.1.4",
"dotenv": "^10.0.0",
"ical": "^0.8.0", "ical": "^0.8.0",
"ical.js": "^1.4.0", "ical.js": "^1.4.0",
"markdown-to-jsx": "^7.1.2", "markdown-to-jsx": "^7.1.2",
"preact": "^10.5.12", "preact": "^10.5.12",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react-router-dom": "^5.3.0",
"striptags": "^3.2.0",
"styled-components": "^5.2.1" "styled-components": "^5.2.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -15,17 +15,16 @@ import {
PositionedCross as CrossSvg, PositionedCross as CrossSvg,
Row, Row,
ActionButton as Button, ActionButton as Button,
Img,
Trailer, Trailer,
} from './styles' } from './styles'
import intro from '../../data/intro.md' import intro from '../../data/intro.md'
import credits from '../../data/credits.md' import credits from '../../data/credits.md'
import { colours } from '../../assets/theme'
import config from '../../data/config' import config from '../../data/config'
import trailerThumb from '../../assets/img/main_thumb.png' import trailerThumb from '../../assets/img/main_thumb.png'
const Info = ({ data, loading, infoActive, setInfoActive, currentVideo }) => { const Info = ({ data }) => {
const trailerUrl = `https://www.youtube-nocookie.com/embed/${config.seriesTrailerId}?autoplay=1&vq=hd1080` const trailerUrl = `https://www.youtube-nocookie.com/embed/${config.seriesTrailerId}?autoplay=1&vq=hd1080`
const [embedURL, setEmbedUrl] = useState('') const [embedURL, setEmbedUrl] = useState('')
@ -36,26 +35,26 @@ const Info = ({ data, loading, infoActive, setInfoActive, currentVideo }) => {
setEmbedUrl('') setEmbedUrl('')
} }
const pastStreams = // const pastStreams =
data && data.length // data && data.length
? data.filter(feeditem => isPast(new Date(feeditem.end))) // ? data.filter(feeditem => isPast(new Date(feeditem.end)))
: [] // : []
const futureStreams = // const futureStreams =
data && data.length // data && data.length
? data // ? data
.filter( // .filter(
feeditem => // feeditem =>
feeditem.id !== (currentVideo && currentVideo.id) && // feeditem.id !== (currentVideo && currentVideo.id) &&
isFuture(new Date(feeditem.start)) // isFuture(new Date(feeditem.start))
) // )
.sort( // .sort(
(a, b) => // (a, b) =>
// Turn your strings into dates, and then subtract them // // Turn your strings into dates, and then subtract them
// to get a value that is either negative, positive, or zero. // // to get a value that is either negative, positive, or zero.
new Date(a.start) - new Date(b.start) // new Date(a.start) - new Date(b.start)
) // )
: [] // : []
const dateString = `${new Date()}` const dateString = `${new Date()}`
let tzShort = let tzShort =
@ -71,14 +70,10 @@ const Info = ({ data, loading, infoActive, setInfoActive, currentVideo }) => {
} }
return ( return (
<InfoLayout loading={loading}> <InfoLayout>
{embedURL ? ( {/* {embedURL ? (
<VideoEmbed onClose={deactivateEmbed} url={embedURL} /> <VideoEmbed onClose={deactivateEmbed} url={embedURL} />
) : null} ) : null} */}
{infoActive && (
<CrossSvg size="64" onClick={() => setInfoActive(false)} />
)}
{!loading && (
<Fragment> <Fragment>
<InfoContent> <InfoContent>
<H1>The Para-Real:</H1> <H1>The Para-Real:</H1>
@ -104,7 +99,7 @@ const Info = ({ data, loading, infoActive, setInfoActive, currentVideo }) => {
</a> </a>
</Row> </Row>
</InfoContent> </InfoContent>
{currentVideo && ( {/* {currentVideo && (
<Fragment> <Fragment>
<Title>{translations.en.nowPlaying}:</Title> <Title>{translations.en.nowPlaying}:</Title>
<VideoCard {...currentVideo} /> <VideoCard {...currentVideo} />
@ -138,12 +133,12 @@ const Info = ({ data, loading, infoActive, setInfoActive, currentVideo }) => {
/> />
))} ))}
</Fragment> </Fragment>
) : null} ) : null} */}
<InfoContent> {/* <InfoContent>
<Markdown>{credits}</Markdown> <Markdown>{credits}</Markdown>
</InfoContent> </InfoContent> */}
</Fragment> </Fragment>
)}
</InfoLayout> </InfoLayout>
) )
} }

View File

@ -17,7 +17,7 @@ import Loader from '../Loader'
import { useTimeout } from '../../hooks/timerHooks' import { useTimeout } from '../../hooks/timerHooks'
import { NdcLogo, RFLogo } from '../Logo' import { NdcLogo, RFLogo } from '../Logo'
const InfoLayout = ({ children, loading }) => ( const InfoLayout = ({ title, subtitle, children, loading }) => (
<Wrapper> <Wrapper>
<Content> <Content>
{loading ? ( {loading ? (
@ -30,15 +30,13 @@ const InfoLayout = ({ children, loading }) => (
</Content> </Content>
<Hero> <Hero>
<div> <div>
<H1>The</H1> <H1>{title}</H1>
<H1>Para-</H1>
<H1>Real</H1>
<H1 <H1
css={` css={`
max-width: 50%; max-width: 50%;
`} `}
> >
Finding the Future in Unexpected Places {subtitle}
</H1> </H1>
</div> </div>
<TaglineContainer> <TaglineContainer>

View File

@ -39,10 +39,8 @@ export const Top = styled.div`
const gradientColourLight = '#F8E5E2' const gradientColourLight = '#F8E5E2'
const gradientColourDark = colours.midnightDarker const gradientColourDark = colours.midnightDarker
const getGradient = (direction, lightDark) => const getGradient = (direction, lightDark) =>
`linear-gradient(to ${direction}, ${ `linear-gradient(to ${direction}, ${lightDark === 'dark' ? gradientColourDark : gradientColourLight
lightDark === 'dark' ? gradientColourDark : gradientColourLight }ee 0%,${lightDark === 'dark' ? gradientColourDark : gradientColourLight
}ee 0%,${
lightDark === 'dark' ? gradientColourDark : gradientColourLight
}00 100%);` }00 100%);`
// prettier-ignore // prettier-ignore
@ -78,7 +76,7 @@ export const Hero = styled.div`
} }
h1:not(:last-of-type) { h1:not(:last-of-type) {
font-size: 30vh; font-size: 15vw;
@media screen and (max-height: 600px) { @media screen and (max-height: 600px) {
font-size: 20vh; font-size: 20vh;

View File

@ -4,16 +4,18 @@ import { useInterval, useTimeout } from '../../hooks/timerHooks'
import { colours } from '../../assets/theme' import { colours } from '../../assets/theme'
import { H1 } from '../Text' import { H1 } from '../Text'
const defaultLoader = [':..', '.:.', '..:', '...']
// const symbols = ['⌏', '⌎', '⌌', '⌍'] // const symbols = ['⌏', '⌎', '⌌', '⌍']
const Loader = ({ const Loader = ({
active = true, active = true,
offset = 0, offset = 0,
animation = [':..', '.:.', '..:', '...'], animation = defaultLoader,
colour = colours.rose,
}) => { }) => {
const [text, setText] = useState('.') const [text, setText] = useState('.')
const arrayPosition = useRef(offset) const arrayPosition = useRef(offset)
const rate = 350 const rate = 300
useInterval( useInterval(
() => { () => {
@ -29,7 +31,7 @@ const Loader = ({
) )
return ( return (
<H1 as="span" colour={colours.midnightDarker}> <H1 as="span" colour={colour}>
{text} {text}
</H1> </H1>
) )

View File

@ -0,0 +1,11 @@
/* eslint-disable indent */
/* NOT WORKING YET */
export default {
PEERTUBE_ROOT: process.env.PEERTUBE_ROOT,
EVENTS_API_URL: process.env.EVENTS_API_URL,
SERIES_TRAILER_ID: process.env.SERIES_TRAILER_ID,
CALENDAR_ID: process.env.CALENDAR_ID,
CHAT_GUILD_ID: process.env.CHAT_GUILD_ID,
CHAT_CHANNEL_ID: process.env.CHAT_CHANNEL_ID,
CHAT_CSS: process.env.CHAT_CSS,
}

19
src/helpers/string.js Normal file
View File

@ -0,0 +1,19 @@
export const slugify = (title) => {
let str = title.replace(/^\s+|\s+$/g, '') // trim
str = str.toLowerCase()
// remove accents, swap ñ for n, etc
const from = 'àáäâèéëêìíïîòóöôùúüûñç·/_,:;'
const to = 'aaaaeeeeiiiioooouuuunc------'
for (let i = 0, l = from.length; i < l; i++) {
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
}
str = str
.replace(/[^a-z0-9 -]/g, '') // remove invalid chars
.replace(/\s+/g, '-') // collapse whitespace and replace by -
.replace(/-+/g, '-') // collapse dashes
return str
}

View File

@ -0,0 +1,30 @@
import { h } from 'preact'
import translations from '../../data/strings'
import { H1 } from '../../components/Text'
import {
Wrapper,
LoaderWrapper,
Hero,
PositionedLogo as Logo,
TaglineContainer,
} from './styles'
import Loader from '../../components/Loader'
const LoaderLayout = () => (
<Wrapper>
<Logo active />
<LoaderWrapper>
<Loader />
</LoaderWrapper>
<Hero />
<TaglineContainer>
{translations &&
translations.en.underscoreTagline.map(line => (
<H1 key={line}>{line}</H1>
))}
</TaglineContainer>
</Wrapper>
)
export default LoaderLayout

View File

@ -0,0 +1,151 @@
import styled from 'styled-components'
import { colours } from '../../assets/theme'
import bg from '../../assets/img/hero/1lg.png'
// import { H1 } from '../../components/Text'
import Logo from '../../components/Logo'
const heroWidth = '66vw'
export const Wrapper = styled.div`
height: 100vh;
width: 100vw;
padding: 2em;
display: flex;
background-color: ${colours.midnightDarker};
box-sizing: border-box;
position: fixed;
overflow-y: scroll;
p,
h1,
h2 {
color: ${colours.midnightDarker};
}
@media screen and (max-width: 1200px) {
padding: 1.5em;
}
@media screen and (max-width: 800px) {
padding: 1em;
}
`
export const Top = styled.div`
width: 50%;
`
const gradientColourLight = '#F8E5E2'
const gradientColourDark = colours.midnightDarker
const getGradient = (direction, lightDark) =>
`linear-gradient(to ${direction}, ${lightDark === 'dark' ? gradientColourDark : gradientColourLight
}ee 0%,${lightDark === 'dark' ? gradientColourDark : gradientColourLight
}00 100%);`
// prettier-ignore
export const Fade = styled.div`
width: 100%;
background-color: linear;
position: fixed;
padding: 2em 0 1em 2em;
top: 0;
left: 0;
background: ${getGradient('bottom')};
`
export const Hero = styled.div`
width: ${heroWidth};
height: 100vh;
background: url(${bg});
background-size: cover;
background-position-x: right;
background-position-y: 60%;
position: fixed;
padding: 0;
right: 0;
top: 0;
padding: 2em 2em 8px 2em;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
pointer-events: none;
/*
h1:not(:last-of-type) {
font-size: 30vh;
@media screen and (max-height: 600px) {
font-size: 20vh;
}
@media screen and (max-width: 1200px) {
font-size: 20vh;
}
} */
@media screen and (max-width: 1000px) {
display: none;
}
`
export const LoaderWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
position: fixed;
top: 0;
width: 33vw;
@media screen and (max-width: 1000px) {
width: 100vw;
}
`
export const Content = styled.div`
/* margin-bottom: 3em; */
`
export const PositionedLogo = styled(Logo)`
position: fixed;
top: 2em;
left: 3em;
`
export const FadeBottom = styled.div`
background: ${getGradient('top', 'dark')};
width: ${heroWidth};
position: fixed;
bottom: 0;
padding-bottom: 0.5em;
right: 0;
/* left: 0; */
pointer-events: none;
min-height: 75px;
`
export const TaglineContainer = styled.div`
width: 100%;
bottom: 0em;
padding-bottom: 0;
right: 1em;
display: flex;
justify-content: flex-end;
flex-direction: column;
align-items: flex-end;
position: fixed;
z-index: 2;
h1 {
margin-bottom: 0.2em;
}
@media screen and (max-width: 1000px) {
h1 {
color: ${colours.rose};
font-size: 24px;
}
}
`

View File

@ -0,0 +1,149 @@
/* eslint-disable react/prop-types */
import { h, Fragment } from 'preact'
import { useState, useEffect } from 'preact/hooks'
import { isFuture, isPast } from 'date-fns'
import striptags from 'striptags'
import { H1 } from '../../components/Text'
import Markdown from '../../components/Markdown'
// import translations from '../../data/strings'
import InfoLayout from '../../components/InfoLayout'
import VideoEmbed from '../../components/VideoEmbed'
import {
VideoCard,
Title,
InfoContent,
PositionedCross as CrossSvg,
Row,
ActionButton as Button,
Trailer,
} from './styles'
import intro from '../../data/intro.md'
// import credits from '../../data/credits.md'
import config from '../../data/config'
import trailerThumb from '../../assets/img/main_thumb.png'
const SeriesPage = ({ data }) => {
const trailerUrl = `https://www.youtube-nocookie.com/embed/${config.seriesTrailerId}?autoplay=1&vq=hd1080`
const [embedURL, setEmbedUrl] = useState('')
const onClickTrailerButton = () => {
setEmbedUrl(trailerUrl)
}
const deactivateEmbed = () => {
setEmbedUrl('')
}
console.log({ data })
// const pastStreams =
// data && data.length
// ? data.filter(feeditem => isPast(new Date(feeditem.end)))
// : []
// const futureStreams =
// data && data.length
// ? data
// .filter(
// feeditem =>
// feeditem.id !== (currentVideo && currentVideo.id) &&
// isFuture(new Date(feeditem.start))
// )
// .sort(
// (a, b) =>
// // Turn your strings into dates, and then subtract them
// // to get a value that is either negative, positive, or zero.
// new Date(a.start) - new Date(b.start)
// )
// : []
const dateString = `${new Date()}`
let tzShort =
// Works for the majority of modern browsers
dateString.match(/\(([^\)]+)\)$/) ||
// IE outputs date strings in a different format:
dateString.match(/([A-Z]+) [\d]{4}$/)
if (tzShort) {
// Old Firefox uses the long timezone name (e.g., "Central
// Daylight Time" instead of "CDT")
tzShort = tzShort[1].match(/[A-Z]/g).join('')
}
return (
<InfoLayout title={data.name} subtitle={striptags(data.summary)}>
{embedURL ? (
<VideoEmbed onClose={deactivateEmbed} url={embedURL} />
) : null}
<Fragment>
<InfoContent>
<H1>{data.name}:</H1>
<H1>{data.summary}</H1>
<Markdown withLinebreaks>{intro}</Markdown>
<Trailer imgSrc={trailerThumb} onClick={onClickTrailerButton} />
<Row>
<a
href="https://discord.gg/Xu9D3qVana"
target="_blank"
rel="noopener noreferrer"
>
<Button>Join the Discord</Button>
</a>
<a
href="https://ndc.substack.com/subscribe"
target="_blank"
rel="noopener noreferrer"
>
<Button>Subscribe to the mailing list</Button>
</a>
</Row>
</InfoContent>
{/* {currentVideo && (
<Fragment>
<Title>{translations.en.nowPlaying}:</Title>
<VideoCard {...currentVideo} />
</Fragment>
)}
{futureStreams.length ? (
<Fragment>
<Title>{translations.en.nextStream}:</Title>
{futureStreams.map(feeditem => (
<VideoCard
key={feeditem.start}
tzShort={tzShort}
{...feeditem}
/>
))}
</Fragment>
) : null}
{pastStreams.length ? (
<Fragment>
<Title>{translations.en.pastStream}:</Title>
{pastStreams.map(feeditem => (
<VideoCard
key={feeditem.start}
hasPassed
onClickButton={() =>
setEmbedUrl(`${config.peertube_root}${feeditem.embedPath}`)
}
{...feeditem}
/>
))}
</Fragment>
) : null} */}
{/* <InfoContent>
<Markdown>{credits}</Markdown>
</InfoContent> */}
</Fragment>
</InfoLayout>
)
}
export default SeriesPage

View File

@ -0,0 +1,213 @@
import { h, Fragment } from 'preact'
import { zonedTimeToUtc, utcToZonedTime, format } from 'date-fns-tz'
import { bool, instanceOf, string } from 'prop-types'
import styled from 'styled-components'
import { colours, textSizes } from '../../assets/theme'
import config from '../../data/config'
import Logo from '../../components/Logo'
import translations from '../../data/strings'
import CrossSvg from '../../components/Svg/Cross'
import PlaySvg from '../../components/Svg/Play'
import { P, H1, H2, Span, Label } from '../../components/Text'
import { Link } from '../../components/Link'
import Button from '../../components/Button'
export const TrailerContainer = styled.div`
background: url(${props => props.imgSrc});
height: 20em;
background-size: cover;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid ${colours.midnightDarker};
cursor: pointer;
margin-bottom: 16px;
div {
padding: 1em 2em;
background-color: #ffffffba;
display: flex;
flex-direction: row;
align-items: center;
}
label {
color: ${colours.midnightDarker};
margin-left: 8px;
font-size: 20px;
}
:hover div {
background-color: #ffffff;
}
`
export const Trailer = props => (
<TrailerContainer {...props}>
<div>
<PlaySvg colour={colours.midnightDarker} size="20" />
<Label>{translations.en.watchTrailer}</Label>
</div>
</TrailerContainer>
)
export const ActionButton = styled(Button)`
font-size: 18px;
`
export const Row = styled.div`
display: flex;
flex-direction: row;
margin-bottom: 32px;
a {
display: block;
width: 50%;
&:not(:last-of-type) {
margin-right: 16px;
}
}
`
export const InfoContent = styled.div`
max-width: 600px;
margin: 0 0 0em 2px;
padding-bottom: 1em;
h1 {
display: none;
&:last-of-type {
margin-bottom: 32px;
}
@media screen and (max-width: 1000px) {
display: block;
}
}
`
export const PositionedLogo = styled(Logo)`
margin-bottom: 64px;
`
export const TaglineContainer = styled.div`
h1 {
margin-top: 32px;
}
`
export const Title = styled(H1)`
margin: 0.3em 0;
`
export const PositionedCross = styled(CrossSvg)`
position: fixed;
right: 2.5em;
top: 2em;
cursor: pointer;
stroke: ${colours.midnightDarker};
z-index: 5;
&:hover {
opacity: 0.5;
}
`
export const VCWrapper = styled.div`
max-width: 600px;
margin: 0 0 6em 2px;
button {
margin-top: 16px;
}
p {
margin-left: 2px;
}
`
const VCImg = styled.img`
width: 100%;
margin-bottom: 8px;
`
const ItemTitleWrapper = styled.div`
margin-bottom: 0.3em;
`
const DateLabel = styled(Label)`
margin: 1em 0;
display: block;
`
const LinkBlock = styled(Link)`
display: block;
width: 100%;
`
const renderTitles = titles =>
titles.split('\\n').map(title => <H2 key={title}>{title}</H2>)
export const VideoCard = ({
title,
description,
start,
previewPath,
hasPassed,
videoUrl,
onClickButton,
tzShort,
}) => {
const startDate = new Date(start)
const utcDate = zonedTimeToUtc(startDate, 'Europe/Berlin')
const { timeZone } = Intl.DateTimeFormat().resolvedOptions()
const zonedDate = utcToZonedTime(utcDate, timeZone)
return (
<VCWrapper>
<DateLabel colour={colours.midnight} size={textSizes.lg}>
{`${hasPassed ? translations.en.streamDatePast : ''}`}
<Span bold colour={colours.midnight}>
{hasPassed
? format(zonedDate, 'dd/MM/yy')
: `${format(zonedDate, 'do LLLL y // HH:mm')} ${tzShort}`}
</Span>
</DateLabel>
{videoUrl && hasPassed ? (
<LinkBlock href={videoUrl}>
<ItemTitleWrapper>{renderTitles(title)}</ItemTitleWrapper>
<VCImg src={`${config.peertube_root}${previewPath}`} alt="" />
</LinkBlock>
) : (
<Fragment>
<ItemTitleWrapper>{renderTitles(title)}</ItemTitleWrapper>
<VCImg src={`${config.peertube_root}${previewPath}`} alt="" />
</Fragment>
)}
<P>{description}</P>
{hasPassed ? (
<Button onClick={onClickButton}>{translations.en.watchEpisode}</Button>
) : (
<a
href={
hasPassed
? videoUrl
: `webcal://cloud.undersco.re/remote.php/dav/public-calendars/${config.calendarId}/?export`
}
>
<Button>{translations.en.subEvent}</Button>
</a>
)}
</VCWrapper>
)
}
VideoCard.propTypes = {
title: string,
description: string,
start: instanceOf(Date),
previewPath: string,
hasPassed: bool,
videoUrl: string,
}