Finished off Series-page,

This commit is contained in:
Sunda 2021-10-12 14:45:52 +02:00
parent a3bea12645
commit 84ea28cd7d
20 changed files with 355 additions and 700 deletions

102
app.js
View File

@ -1,72 +1,46 @@
import { h } from 'preact' import { h } from 'preact'
import { useState, useEffect } from 'preact/hooks'
import { isWithinInterval, subHours, addHours } from 'date-fns'
import { zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz'
import Video from './src/components/Video' import { H1 } from './src/components/Text'
import Info from './src/components/Info' import Page from './src/layouts/Page'
import { useEventCalendar, useEventApi } from './src/hooks/data'
import { useTimeout } from './src/hooks/timerHooks'
export default (props) => { export default () => (
<Page>
<H1>LANGING PAGE</H1>
</Page >
)
// console.log({ props })
const [currentVideo, setCurrentVideo] = useState(null)
const [streamIsLive, setStreamIsLive] = useState(false)
const [infoActive, setInfoActive] = useState(false)
const [minLoadTimePassed, setMinTimeUp] = useState(false)
const { data: calData, loading } = useEventCalendar()
const { data: eventsData, loading: eventsLoading } = useEventApi()
useTimeout(() => {
setMinTimeUp(true)
}, 1500)
useEffect(() => { // useEffect(() => {
if (calData && calData.length) { // if (calData && calData.length) {
calData.forEach((stream, index) => { // calData.forEach((stream, index) => {
const utcStartDate = zonedTimeToUtc( // const utcStartDate = zonedTimeToUtc(
new Date(stream.start), // new Date(stream.start),
'Europe/Berlin' // 'Europe/Berlin'
) // )
const utcEndDate = zonedTimeToUtc(new Date(stream.end), 'Europe/Berlin') // const utcEndDate = zonedTimeToUtc(new Date(stream.end), 'Europe/Berlin')
const { timeZone } = Intl.DateTimeFormat().resolvedOptions() // const { timeZone } = Intl.DateTimeFormat().resolvedOptions()
// const zonedStartDate = utcToZonedTime(utcStartDate, timeZone)
// const zonedEndDate = utcToZonedTime(utcEndDate, timeZone)
// if (
// isWithinInterval(new Date(), {
// start: subHours(zonedStartDate, 1),
// end: addHours(zonedEndDate, 1),
// })
// ) {
// setCurrentVideo(stream)
// }
// if (
// isWithinInterval(new Date(), {
// start: zonedStartDate,
// end: zonedEndDate,
// })
// ) {
// setStreamIsLive(true)
// }
// })
// }
// }, [calData, eventsData, eventsLoading])
const zonedStartDate = utcToZonedTime(utcStartDate, timeZone)
const zonedEndDate = utcToZonedTime(utcEndDate, timeZone)
if (
isWithinInterval(new Date(), {
start: subHours(zonedStartDate, 1),
end: addHours(zonedEndDate, 1),
})
) {
setCurrentVideo(stream)
}
if (
isWithinInterval(new Date(), {
start: zonedStartDate,
end: zonedEndDate,
})
) {
setStreamIsLive(true)
}
})
}
}, [calData, eventsData, eventsLoading])
return (
<div>
{currentVideo && !infoActive && minLoadTimePassed ? (
<Video video={currentVideo} setInfoActive={setInfoActive} />
) : (
<Info
data={calData}
loading={false}
infoActive={infoActive}
currentVideo={currentVideo}
setInfoActive={setInfoActive}
/>
)}
</div>
)
}

View File

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>The Para-Real: Finding the Future in Unexpected Places</title> <title>Underscore Streams</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />

View File

@ -25,6 +25,7 @@
"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-helmet": "^6.1.0",
"react-router-dom": "^5.3.0", "react-router-dom": "^5.3.0",
"striptags": "^3.2.0", "striptags": "^3.2.0",
"styled-components": "^5.2.1", "styled-components": "^5.2.1",

View File

@ -26,8 +26,13 @@ export const textSizes = {
hg: 200, hg: 200,
} }
export const defaultTheme = {
background: colours.rose, foreground: colours.midnight, highlight: colours.highlight
}
export default { export default {
colours, colours,
textSizes, textSizes,
ui, ui,
defaultTheme
} }

View File

@ -3,11 +3,11 @@ import { colours } from '../../assets/theme'
const Button = styled.button` const Button = styled.button`
background-color: transparent; background-color: transparent;
border: 1px solid ${colours.midnightDarker}; border: 1px solid ${colours.rose};
padding: 0.3em 1em; padding: 0.3em 1em;
font-family: Karla; font-family: Karla;
font-weight: inherit; font-weight: inherit;
color: ${colours.midnightDarker}; color: ${colours.rose};
opacity: 1; opacity: 1;
text-decoration: none; text-decoration: none;
font-size: 24px; font-size: 24px;
@ -21,8 +21,8 @@ const Button = styled.button`
} }
:hover { :hover {
background-color: ${colours.midnightDarker}; background-color: ${colours.rose};
color: ${colours.roseLight}; color: ${colours.midnightDarker};
svg { svg {
path { path {

View File

@ -1,4 +0,0 @@
export const sortData = data =>
Object.values(data)
.filter(feedItem => feedItem.type === 'VEVENT')
.sort((a, b) => new Date(a.start) - new Date(b.start))

View File

@ -1,146 +0,0 @@
/* eslint-disable react/prop-types */
import { h, Fragment } from 'preact'
import { useState, useEffect } from 'preact/hooks'
import { isFuture, isPast } from 'date-fns'
import { H1 } from '../Text'
import Markdown from '../Markdown'
import translations from '../../data/strings'
import InfoLayout from '../InfoLayout'
import VideoEmbed from '../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 Info = ({ 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('')
}
// 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>
{/* {embedURL ? (
<VideoEmbed onClose={deactivateEmbed} url={embedURL} />
) : 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} />
<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 Info

View File

@ -1,240 +0,0 @@
import { h, Fragment } from 'preact'
import { zonedTimeToUtc, utcToZonedTime, format } from 'date-fns-tz'
import styled from 'styled-components'
import { colours, textSizes } from '../../assets/theme'
import config from '../../data/config'
import Logo from '../Logo'
import translations from '../../data/strings'
import CrossSvg from '../Svg/Cross'
import PlaySvg from '../Svg/Play'
import { P, H1, H2, Span, Label } from '../Text'
import Link from '../Link'
import { bool, instanceOf, string } from 'prop-types'
import Markdown from '../Markdown'
import Button from '../Button'
export const Wrapper = styled.div`
height: 100vh;
width: 100vw;
padding: 2em;
display: flex;
justify-content: space-between;
flex-direction: column;
background: url(https://images.unsplash.com/photo-1579762715118-a6f1d4b934f1?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=3031&q=80)
${colours.rose};
box-sizing: border-box;
background-size: cover;
background-position-y: 50%;
background-position-x: 20vw;
background-blend-mode: soft-light;
p,
h1,
h2 {
color: ${colours.midnightDarker};
}
`
export const TrailerContainer = styled.div`
background: url(${props => props.imgSrc});
/* width: 100%; */
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;
/* width: 32px; */
/* height: 32px; */
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,
}

View File

@ -1,54 +0,0 @@
import { h } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks'
import { bool, string } from 'prop-types'
import { H1, H2, Label, Span } from '../Text'
import {
Wrapper,
TaglineContainer,
LoaderWrapper,
Content,
Hero,
FadeBottom,
} from './styles'
import translations from '../../data/strings'
import { colours } from '../../assets/theme'
import Loader from '../Loader'
import { useTimeout } from '../../hooks/timerHooks'
import { NdcLogo, RFLogo } from '../Logo'
const InfoLayout = ({ title, subtitle, image, children }) => (
<Wrapper>
<Content>
{children}
</Content>
<Hero image={image}>
<div>
<H1>{title}</H1>
<H1
css={`
max-width: 50%;
`}
>
{subtitle}
</H1>
</div>
<TaglineContainer>
<a href="https://newdesigncongress.org/">
<NdcLogo active colour={colours.offwhite} />
</a>
<Label size="24">{'//'}</Label>
<a href="https://reclaimfutures.org/">
<RFLogo active colour={colours.offwhite} />
</a>
</TaglineContainer>
<FadeBottom />
</Hero>
</Wrapper>
)
InfoLayout.propTypes = {
loading: bool,
}
export default InfoLayout

View File

@ -0,0 +1,81 @@
import { h } from 'preact'
import { bool, string } from 'prop-types'
import Helmet from 'react-helmet'
const siteTitle = 'Underscore Streams'
function SEO({ description, title, metaImg: imgSrc, noindex }) {
return (
<Helmet
htmlAttributes={{
lang: 'en',
}}
title={title}
titleTemplate={`%s \\ ${siteTitle}`}
meta={[
{
name: 'description',
content: description,
},
{
property: 'og:title',
content: title ? `${title} \\\\ ${siteTitle}` : siteTitle,
},
{
property: 'og:url',
content: 'https://looseantenna.fm',
},
{
property: 'og:description',
content: description,
},
{
property: 'og:image',
content: imgSrc,
},
{
property: 'og:type',
content: 'website',
},
{
name: 'twitter:card',
content: 'summary',
},
{
name: 'twitter:title',
content: title,
},
{
name: 'twitter:image',
content: `${imgSrc}`,
},
{
name: 'twitter:description',
content: description,
},
{
name: 'apple-mobile-web-app-capable',
content: 'yes',
},
(noindex ? {
name: 'robots',
content: 'noindex',
} : {}),
].concat()}
/>
)
}
SEO.defaultProps = {
meta: [],
description: '',
}
SEO.propTypes = {
description: string,
metaImg: string,
title: string.isRequired,
noindex: bool,
}
export default SEO

View File

@ -1,44 +1,29 @@
import { Fragment, h } from 'preact' import { Fragment, h } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks'
import { string } from 'prop-types' import { string } from 'prop-types'
import { VideoWrapper, Iframe, StyledCrossSvg as CrossSvg } from './styles' import { Iframe } from './styles'
const Video = ({ url, onClose }) => { const VideoEmbed = ({ url, ...rest }) => {
const overlayTimeout = useRef(null) console.log({ url })
const [overlayActive, setOverlayActiveState] = useState(false) const id = url.split('/').pop()
const src = `https://tv.undersco.re/videos/embed/${id}?title=0&warningTitle=0&peertubeLink=0`
const activateOverlay = () => {
clearTimeout(overlayTimeout.current)
overlayTimeout.current = null
setOverlayActiveState(true)
overlayTimeout.current = setTimeout(
() => setOverlayActiveState(false),
1500
)
}
return ( return (
<Fragment> <Fragment>
<VideoWrapper $active={overlayActive}>
<CrossSvg $active={overlayActive} onClick={onClose} size="64" />
</VideoWrapper>
<Iframe <Iframe
onMouseMove={activateOverlay}
width="560" width="560"
height="315" height="315"
src={url} src={src}
frameborder="0" frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen allowfullscreen
sandbox="allow-same-origin allow-scripts allow-popups"
{...rest}
/> />
</Fragment> </Fragment>
) )
} }
Video.propTypes = { VideoEmbed.propTypes = {
url: string, url: string,
} }
export default Video export default VideoEmbed

View File

@ -1,43 +1,6 @@
import styled from 'styled-components' import styled from 'styled-components'
import { colours } from '../../assets/theme'
import CrossSvg from '../Svg/Cross'
export const VideoWrapper = styled.div`
width: 100vw;
height: 100vh;
z-index: 2;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
pointer-events: none;
/* background-color: ${props => (props.$active ? 'red' : 'green')}; */
`
export const Iframe = styled.iframe` export const Iframe = styled.iframe`
z-index: 1; width: 100%;
width: 100vw; height: 100%;
height: 100vh;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
`
export const StyledCrossSvg = styled(CrossSvg)`
pointer-events: all;
position: fixed;
right: 2.5em;
top: 2em;
width: 64px;
height: 64px;
cursor: pointer;
stroke: ${colours.midnightDarker};
z-index: 12;
opacity: ${props => (props.$active ? '1' : '0.2')};
&:hover {
opacity: 1;
}
` `

View File

@ -17,3 +17,14 @@ export const slugify = (title) => {
return str return str
} }
export const capitaliseFirstLetter = word =>
word ? `${word?.charAt(0).toUpperCase()}${word?.slice(1)}` : '';
export const camelise = str => {
return str
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => {
return index === 0 ? letter.toLowerCase() : letter.toUpperCase();
})
.replace(/\s+/g, '');
};

View File

@ -0,0 +1,53 @@
import { h } from 'preact'
import { bool } from 'prop-types'
import { H1, Label } from '../../components/Text'
import {
Wrapper,
TaglineContainer,
Content,
Hero,
FadeBottom,
} from './styles'
import { colours } from '../../assets/theme'
import { NdcLogo, RFLogo } from '../../components/Logo'
const InfoLayout = ({ title, subtitle, image, children, theme }) => {
console.log({ theme })
return (
<Wrapper theme={theme}>
<Content>
{children}
</Content>
<Hero image={image}>
<div>
<H1>{title}</H1>
<H1
css={`
max-width: 50%;
`}
>
{subtitle}
</H1>
</div>
<TaglineContainer>
<a href="https://newdesigncongress.org/">
<NdcLogo active colour={colours.offwhite} />
</a>
<Label size="24">{'//'}</Label>
<a href="https://reclaimfutures.org/">
<RFLogo active colour={colours.offwhite} />
</a>
</TaglineContainer>
<FadeBottom />
</Hero>
</Wrapper>
)
}
InfoLayout.propTypes = {
loading: bool,
}
export default InfoLayout

View File

@ -1,7 +1,7 @@
import styled from 'styled-components' import styled from 'styled-components'
import { colours } from '../../assets/theme' import { colours } from '../../assets/theme'
import Logo from '../Logo' import Logo from '../../components/Logo'
const heroWidth = 'calc(100vw - 600px - 4em)' const heroWidth = 'calc(100vw - 600px - 4em)'
@ -10,16 +10,10 @@ export const Wrapper = styled.div`
width: 100vw; width: 100vw;
padding: 2em; padding: 2em;
display: flex; display: flex;
background-color: ${colours.midnight};
box-sizing: border-box; box-sizing: border-box;
position: fixed; position: fixed;
overflow-y: scroll; overflow-y: scroll;
p,
h1,
h2 {
color: ${colours.rose};
}
@media screen and (max-width: 1200px) { @media screen and (max-width: 1200px) {
padding: 1.5em; padding: 1.5em;
@ -27,6 +21,30 @@ export const Wrapper = styled.div`
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
padding: 1em; padding: 1em;
} }
background-color: ${props => props.theme.background};
p,
h1,
h2,
h3 {
color: ${props => props.theme.foreground};
}
a {
color: ${props => props.theme.highlight};
}
button {
color: ${props => props.theme.foreground};
border-color: ${props => props.theme.foreground};
&:hover {
border-color: ${props => props.theme.background};
color: ${props => props.theme.background};
background-color: ${props => props.theme.foreground};
}
}
` `
export const Top = styled.div` export const Top = styled.div`

35
src/layouts/Page/index.js Normal file
View File

@ -0,0 +1,35 @@
import { Fragment, h } from 'preact'
import PropTypes, { oneOfType, shape, string } from 'prop-types'
import SEO from '../../components/Seo'
// import Header from '../../molecules/Header'
import { capitaliseFirstLetter } from '../../helpers/string'
const Page = ({ children, title = '', description, metaImg, backTo, noindex }) => (
<Fragment>
<SEO
title={title.toLowerCase() === 'index' ? title : capitaliseFirstLetter(title)}
description={description}
metaImg={metaImg}
noindex={noindex}
/>
{/* <Header /> */}
{children}
</Fragment>
)
Page.propTypes = {
children: PropTypes.node.isRequired,
title: oneOfType([
string,
shape({
en: string,
fr: string,
}),
]),
description: string,
metaImg: string,
}
export default Page

View File

@ -1,42 +1,25 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
import { h, Fragment } from 'preact' import { h, Fragment } from 'preact'
import { useState, useEffect } from 'preact/hooks'
import { isFuture, isPast } from 'date-fns'
import striptags from 'striptags' import striptags from 'striptags'
import { H1 } from '../../components/Text' import { H1 } from '../../components/Text'
import Markdown from '../../components/Markdown' import Markdown from '../../components/Markdown'
import translations from '../../data/strings' import translations from '../../data/strings'
import InfoLayout from '../../components/InfoLayout' import InfoLayout from '../../layouts/InfoLayout'
import VideoEmbed from '../../components/VideoEmbed' import VideoEmbed from '../../components/VideoEmbed'
import { import {
EpisodeCard, EpisodeCard,
Title, Title,
InfoContent, InfoContent,
PositionedCross as CrossSvg,
Row, Row,
ActionButton as Button, ActionButton as Button,
Trailer, TrailerContainer,
} from './styles' } from './styles'
import intro from '../../data/intro.md'
// import credits from '../../data/credits.md'
import config from '../../data/config' import config from '../../data/config'
import trailerThumb from '../../assets/img/main_thumb.png' import Page from '../../layouts/Page'
const SeriesPage = ({ data }) => { 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({ past: data.episodes.past, future: data.episodes.future })
const credits = ` const credits = `
## Credits ## Credits
@ -57,48 +40,40 @@ const SeriesPage = ({ data }) => {
} }
return ( return (
<InfoLayout title={data.title} subtitle={striptags(data.subtitle)} image={data.image}> <Page title={data.title}>
{embedURL ? ( <InfoLayout title={data.title} subtitle={data.subtitle} image={data.image} theme={data.theme}>
<VideoEmbed onClose={deactivateEmbed} url={embedURL} />
) : null}
<Fragment> <Fragment>
<InfoContent> <InfoContent>
<H1>{data.title}:</H1> <H1>{data.title}:</H1>
<H1>{data.subtitle}</H1> <H1>{data.subtitle}</H1>
<Markdown withLinebreaks>{data.description}</Markdown> <Markdown withLinebreaks>{data.description}</Markdown>
<Trailer imgSrc={trailerThumb} onClick={onClickTrailerButton} /> {data.trailer ? (
<TrailerContainer>
<VideoEmbed url={data.trailer} />
</TrailerContainer>
) : null}
<Row> {data.links.length ?
<Row wrap>
{data.links.map(link => (
<a <a
href="https://discord.gg/Xu9D3qVana" href={link.resourceUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<Button>Join the Discord</Button> <Button>{link.summary}</Button>
</a> </a>
<a ))}
href="https://ndc.substack.com/subscribe" </Row> : null
target="_blank" }
rel="noopener noreferrer"
>
<Button>Subscribe to the mailing list</Button>
</a>
</Row>
</InfoContent> </InfoContent>
{/* {currentVideo && (
<Fragment>
<Title>{translations.en.nowPlaying}:</Title>
<EpisodeCard {...currentVideo} />
</Fragment>
)}
*/}
{data.episodes.future.length ? ( {data.episodes.future.length ? (
<Fragment> <Fragment>
<Title>{translations.en.nextStream}:</Title> <Title>{translations.en.nextStream}:</Title>
{data.episodes.future.map(feeditem => ( {data.episodes.future.map(feeditem => (
<EpisodeCard <EpisodeCard
theme={data.theme}
key={feeditem.start} key={feeditem.start}
tzShort={tzShort} tzShort={tzShort}
{...feeditem} {...feeditem}
@ -111,6 +86,7 @@ const SeriesPage = ({ data }) => {
<Title>Past streams:</Title> <Title>Past streams:</Title>
{data.episodes.past.map(feeditem => ( {data.episodes.past.map(feeditem => (
<EpisodeCard <EpisodeCard
theme={data.theme}
key={feeditem.beginsOn} key={feeditem.beginsOn}
hasPassed hasPassed
onClickButton={() => onClickButton={() =>
@ -128,6 +104,7 @@ const SeriesPage = ({ data }) => {
</Fragment> </Fragment>
</InfoLayout> </InfoLayout>
</Page>
) )
} }

View File

@ -16,14 +16,8 @@ import { Link } from '../../components/Link'
import Button from '../../components/Button' import Button from '../../components/Button'
export const TrailerContainer = styled.div` export const TrailerContainer = styled.div`
background: url(${props => props.imgSrc}); height: 22em;
height: 20em;
background-size: cover;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid ${colours.midnightDarker}; border: 1px solid ${colours.midnightDarker};
cursor: pointer;
margin-bottom: 16px; margin-bottom: 16px;
div { div {
@ -35,7 +29,6 @@ export const TrailerContainer = styled.div`
} }
label { label {
color: ${colours.midnightDarker};
margin-left: 8px; margin-left: 8px;
font-size: 20px; font-size: 20px;
} }
@ -45,14 +38,6 @@ export const TrailerContainer = styled.div`
} }
` `
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)` export const ActionButton = styled(Button)`
font-size: 18px; font-size: 18px;
@ -62,6 +47,7 @@ export const Row = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-bottom: 32px; margin-bottom: 32px;
flex-wrap: ${props => props.wrap ? 'wrap' : 'nowrap'};
a { a {
display: block; display: block;
@ -135,7 +121,7 @@ const VCImg = styled.img`
` `
const ItemTitleWrapper = styled.div` const ItemTitleWrapper = styled.div`
margin-bottom: 0.3em; margin-bottom: 1em;
` `
const DateLabel = styled(Label)` const DateLabel = styled(Label)`
@ -159,19 +145,19 @@ export const EpisodeCard = ({
videoUrl, videoUrl,
onClickButton, onClickButton,
tzShort, tzShort,
image image,
theme,
}) => { }) => {
const startDate = new Date(beginsOn) const startDate = new Date(beginsOn)
console.log({ startDate })
const utcDate = zonedTimeToUtc(startDate, 'Europe/Berlin') const utcDate = zonedTimeToUtc(startDate, 'Europe/Berlin')
const { timeZone } = Intl.DateTimeFormat().resolvedOptions() const { timeZone } = Intl.DateTimeFormat().resolvedOptions()
const zonedDate = utcToZonedTime(utcDate, timeZone) const zonedDate = utcToZonedTime(utcDate, timeZone)
return ( return (
<VCWrapper> <VCWrapper>
<DateLabel size={textSizes.lg}> <DateLabel size={textSizes.lg} colour={theme.foreground}>
{`${hasPassed ? translations.en.streamDatePast : ''}`} {`${hasPassed ? translations.en.streamDatePast : ''}`}
<Span bold colour={colours.rose}> <Span bold colour={theme.foreground}>
{hasPassed {hasPassed
? format(zonedDate, 'dd/MM/yy') ? format(zonedDate, 'dd/MM/yy')
: `${format(zonedDate, 'do LLLL y // HH:mm')} ${tzShort}`} : `${format(zonedDate, 'do LLLL y // HH:mm')} ${tzShort}`}

View File

@ -15,9 +15,12 @@ export const getPostByKey = (posts, key) => {
return filteredPostItems.length ? filteredPostItems[0].body : null return filteredPostItems.length ? filteredPostItems[0].body : null
} }
export const getResourcesByKey = (resources, key) => { export const getResourcesByKey = (resources, key, lookup = 'title') => {
const filteredResources = resources.elements.length ? resources.elements.filter(resource => resource.title === key) : [] const filteredResources = resources.elements.length ? resources.elements.filter(resource => resource[lookup] === key) : []
return filteredResources.length ? filteredResources[0].resourceUrl : null
if (!filteredResources.length) return null
return filteredResources
} }
export const getPeertubeIDfromUrl = (string) => string && string.includes('https://tv.undersco.re') ? string.split('/').pop() : string export const getPeertubeIDfromUrl = (string) => string && string.includes('https://tv.undersco.re') ? string.split('/').pop() : string

View File

@ -3,6 +3,7 @@ import striptags from 'striptags'
import { isFuture, isPast } from 'date-fns' import { isFuture, isPast } from 'date-fns'
import { slugify } from '../helpers/string' import { slugify } from '../helpers/string'
import { getMetadataByKey, getPostByKey, getResourcesByKey, getPeertubeIDfromUrl } from './helpers' import { getMetadataByKey, getPostByKey, getResourcesByKey, getPeertubeIDfromUrl } from './helpers'
import { colours, defaultTheme } from '../assets/theme'
export const [useSeriesStore] = create(set => ({ export const [useSeriesStore] = create(set => ({
series: [], series: [],
@ -20,6 +21,10 @@ export const [useSeriesStore] = create(set => ({
peertubeId: getPeertubeIDfromUrl(getMetadataByKey(ep, 'mz:live:peertube:url')), peertubeId: getPeertubeIDfromUrl(getMetadataByKey(ep, 'mz:live:peertube:url')),
})) : [] })) : []
const trailer = getResourcesByKey(resources, 'SERIES_TRAILER')?.[0]?.resourceUrl ?? null
const theme = striptags(getPostByKey(posts, 'THEME'))
const series = { const series = {
title: name, title: name,
subtitle: striptags(summary), subtitle: striptags(summary),
@ -33,7 +38,9 @@ export const [useSeriesStore] = create(set => ({
}, },
slug: slugify(name), slug: slugify(name),
credits: getPostByKey(posts, 'SERIES_CREDITS'), credits: getPostByKey(posts, 'SERIES_CREDITS'),
trailerId: getPeertubeIDfromUrl(getResourcesByKey(resources, 'SERIES_TRAILER')) trailer,
links: getResourcesByKey(resources, 'LINK') || [],
theme: theme ? JSON.parse(theme) : defaultTheme
} }
return series return series
}) })