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 { useState, useEffect } from 'preact/hooks'
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 config from './src/data/config'
import Info from './src/components/Info'
import { useEventCalendar, useEventApi } from './src/hooks/data'
import { useTimeout } from './src/hooks/timerHooks'
export default () => {
export default (props) => {
console.log({ props })
const [currentVideo, setCurrentVideo] = useState(null)
const [streamIsLive, setStreamIsLive] = useState(false)
const [infoActive, setInfoActive] = useState(false)
@ -60,7 +61,7 @@ export default () => {
) : (
<Info
data={calData}
loading={loading || !minLoadTimePassed}
loading={false}
infoActive={infoActive}
currentVideo={currentVideo}
setInfoActive={setInfoActive}

View File

@ -1,5 +1,42 @@
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')

View File

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

View File

@ -15,17 +15,16 @@ import {
PositionedCross as CrossSvg,
Row,
ActionButton as Button,
Img,
Trailer,
} from './styles'
import intro from '../../data/intro.md'
import credits from '../../data/credits.md'
import { colours } from '../../assets/theme'
import config from '../../data/config'
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 [embedURL, setEmbedUrl] = useState('')
@ -36,26 +35,26 @@ const Info = ({ data, loading, infoActive, setInfoActive, currentVideo }) => {
setEmbedUrl('')
}
const pastStreams =
data && data.length
? data.filter(feeditem => isPast(new Date(feeditem.end)))
: []
// 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 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 =
@ -71,79 +70,75 @@ const Info = ({ data, loading, infoActive, setInfoActive, currentVideo }) => {
}
return (
<InfoLayout loading={loading}>
{embedURL ? (
<InfoLayout>
{/* {embedURL ? (
<VideoEmbed onClose={deactivateEmbed} url={embedURL} />
) : null}
{infoActive && (
<CrossSvg size="64" onClick={() => setInfoActive(false)} />
)}
{!loading && (
<Fragment>
<InfoContent>
<H1>The Para-Real:</H1>
<H1>Finding the Future in Unexpected Places</H1>
<Markdown withLinebreaks>{intro}</Markdown>
) : null} */}
<Fragment>
<InfoContent>
<H1>The Para-Real:</H1>
<H1>Finding the Future in Unexpected Places</H1>
<Markdown withLinebreaks>{intro}</Markdown>
<Trailer imgSrc={trailerThumb} onClick={onClickTrailerButton} />
<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>
)}
<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}
{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>
{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>
)
}

View File

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

View File

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

View File

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