series grid page

This commit is contained in:
Sunda 2021-10-15 15:37:54 +02:00
parent 84ea28cd7d
commit b2fad77434
36 changed files with 623 additions and 171 deletions

View File

@ -7,6 +7,8 @@ import SeriesPage from './src/pages/SeriesPage'
import { useEventApi, useEventCalendar } from './src/hooks/data' import { useEventApi, useEventCalendar } from './src/hooks/data'
import { useTimeout } from './src/hooks/timerHooks' import { useTimeout } from './src/hooks/timerHooks'
import LoaderLayout from './src/pages/LoaderLayout' import LoaderLayout from './src/pages/LoaderLayout'
import FourOhFour from './src/pages/404'
import Series from './src/pages/Series'
const App = () => { const App = () => {
const { data: calData, calLoading } = useEventCalendar() const { data: calData, calLoading } = useEventCalendar()
@ -26,10 +28,14 @@ const App = () => {
<BrowserRouter> <BrowserRouter>
<Switch> <Switch>
<Route exact path="/" component={Main} /> <Route exact path="/" component={Main} />
<Route exact path="/series" component={Series} />
{seriesData.length ? seriesData.map(series => ( {seriesData.length ? seriesData.map(series => (
<Route exact path={`/series/${series.slug}`}> <Route exact path={`/series/${series.slug}`}>
<SeriesPage data={series} /> <SeriesPage data={series} />
</Route>)) : null} </Route>)) : null}
<Route path="*">
<FourOhFour />
</Route>
</Switch> </Switch>
</BrowserRouter> </BrowserRouter>
) )

View File

@ -1,2 +1,22 @@
@import 'reset'; @import 'reset';
@import 'fontface'; @import 'fontface';
body {
background-color: #112b39;
overflow-x: hidden;
scrollbar-color: #feb9b3;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 5px;
height: 5px;
}
&::-webkit-scrollbar-track {
background-color: #f6f4f5;
}
&::-webkit-scrollbar-thumb {
background-color: #feb9b3;
border-radius: 0px;
}
}

View File

@ -21,13 +21,13 @@ export const textSizes = {
sm: 14, sm: 14,
md: 16, md: 16,
lg: 21, lg: 21,
xl: 32, xl: 28,
xxl: 92, xxl: 92,
hg: 200, hg: 200,
} }
export const defaultTheme = { export const defaultTheme = {
background: colours.rose, foreground: colours.midnight, highlight: colours.highlight background: colours.midnightDarker, foreground: colours.rose, highlight: colours.highlight
} }
export default { export default {

View File

@ -0,0 +1,15 @@
import styled from 'styled-components'
export const Row = styled.div`
display: flex;
flex-direction: ${props => (props.reverse ? 'row-reverse' : 'row')};
justify-content: ${props => props.justify || 'flex-start'};
align-items: ${props => props.align || 'flex-start'};
`
export const Column = styled.div`
display: flex;
flex-direction: ${props => (props.reverse ? 'column-reverse' : 'column')};
justify-content: ${props => props.justify || 'flex-start'};
align-items: ${props => props.align || 'flex-start'};
`

View File

@ -0,0 +1,36 @@
import { h } from 'preact'
import { Link as ReactLink } from 'react-router-dom'
import { RightBox, StyledRow as Row } from './styles'
import { ImageLogo } from '../Logo'
import Link from '../Link'
import navigation from '../../data/navigation'
import { colours, textSizes } from '../../assets/theme'
const Navigation = ({ theme = {}, lang = 'en' }) => (
<RightBox as="nav">
{navigation[lang].map(navItem => <Link navLink to={navItem.to} href={navItem.href} textProps={{
size: textSizes.xl, colour: theme.foreground || colours.rose
}}>{navItem.label}</Link>)}
</RightBox>
)
const BigHeader = ({ theme = {}, lang = 'en', ...rest }) => (
<Row theme={theme} align="center" justify="space-between" {...rest}>
<ReactLink to="/">
<ImageLogo />
</ReactLink>
<Navigation theme={theme} lang={lang} {...rest} />
</Row>
)
const MiniHeader = ({ theme = {}, lang = 'en', ...rest }) => (
<Row theme={theme} align="center" justify="space-between" miniHeader {...rest}>
<Navigation theme={theme} lang={lang} {...rest} />
</Row>
)
const Header = ({ miniHeader, ...rest }) => miniHeader ? <MiniHeader {...rest} /> : <BigHeader {...rest} />
export default Header

View File

@ -0,0 +1,30 @@
import styled from 'styled-components'
import { Row } from '../Flex'
export const StyledRow = styled(Row)`
pointer-events: all;
height: 100px;
background-color: ${({ theme }) => theme.background};
color: ${({ theme }) => theme.foreground};
position: absolute;
left: ${({ miniHeader }) => miniHeader ? 'initial' : '0'};
top: 0;
right: 0;
z-index: 1;
img {
max-height: 80px;
mix-blend-mode: exclusion;
padding: 1em;
}
a:hover {
opacity: 0.5;
}
`
export const RightBox = styled(Row)`
a {
margin-right: 4em;
}
`

View File

@ -1,6 +1,23 @@
import styled from 'styled-components' import { h } from 'preact'
import { useRouteMatch } from 'react-router-dom'
import { A } from '../Text'
import { RRLink } from './styles'
const Link = ({ children, to, activeOnlyWhenExact = true, href, textProps: { colour, size } = {}, navLink, ...rest }) => {
const match = useRouteMatch({
path: to,
exact: activeOnlyWhenExact
})
console.log({ colour })
return href ? <A href={href} colour={colour} size={size} fontFamily={navLink ? 'Lunchtype24' : null} {...rest}>{children}</A> : (
<A as="span" colour={colour} size={size} {...rest}>
<RRLink to={to} $navLink={navLink} $colour={colour} $selected={match && navLink}>{children}</RRLink>
</A>
)
}
export const Link = styled.a`
text-decoration: none;
`
export default Link export default Link

View File

@ -0,0 +1,32 @@
import styled from 'styled-components'
import { Link } from 'react-router-dom'
import { colours } from '../../assets/theme'
export const RRLink = styled(Link)`
color: ${props => { console.log({ col: props.$colour }); return props.$colour || colours.highlight }};
text-decoration: none;
${props => props.$navLink ? `
font-family: Lunchtype24;
` : ''};
position: relative;
${props => props.$selected ? `
&::before, &::after {
color: ${colours.highlight};
font-size: 21px;
position: absolute;
top: 50%;
font-family: 'Karla';
transform: translateY(-50%);
}
&::before {
content: '< ';
left: -1em;
}
&::after {
content: ' >';
right: -1em;
}
` : ''};
`

View File

@ -3,6 +3,8 @@ import { bool, string } from 'prop-types'
import styled from 'styled-components' import styled from 'styled-components'
import { colours } from '../../assets/theme' import { colours } from '../../assets/theme'
import LogoBird from '../../../static/logo.png'
const Logo = ({ colour = colours.offwhite, ...rest }) => ( const Logo = ({ colour = colours.offwhite, ...rest }) => (
<LogoSvg <LogoSvg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -56,4 +58,10 @@ export const RFLogo = ({ colour = colours.offwhite, ...rest }) => (
</LogoSvg> </LogoSvg>
) )
const Img = styled.img`
mix-blend-mode: exclusion;
`
export const ImageLogo = (props) => <Img className="img-logo" src={LogoBird} alt={props.alt} {...props} />
export default Logo export default Logo

View File

@ -4,8 +4,8 @@ import MarkdownRenderer from 'markdown-to-jsx'
import { MarkdownWrapper } from './styles' import { MarkdownWrapper } from './styles'
import { P, A, H1, H2, H3, Span } from '../Text' import { P, A, H1, H2, H3, Span } from '../Text'
const Markdown = ({ children, withLinebreaks, options, ...rest }) => ( const Markdown = ({ children, withLinebreaks, options, theme, ...rest }) => (
<MarkdownWrapper $withLinebreaks={withLinebreaks}> <MarkdownWrapper $withLinebreaks={withLinebreaks} theme={theme}>
<MarkdownRenderer <MarkdownRenderer
options={{ options={{
overrides: { overrides: {

View File

@ -5,7 +5,7 @@ import { P } from '../Text'
export const MarkdownWrapper = styled.span` export const MarkdownWrapper = styled.span`
a { a {
color: ${colours.highlight}; color: ${({ theme }) => theme && theme.highlight || colours.highlight};
} }
img { img {

View File

@ -0,0 +1,40 @@
import { formatDistanceToNow } from 'date-fns'
import { h } from 'preact'
import Link from '../Link'
import { H2, H3, Label } from '../Text'
import strings from '../../data/strings'
import { Img, LabelBlock, Wrapper } from './styles'
import { andList } from '../../helpers/string'
import { colours } from '../../assets/theme'
const SeriesCard = ({ series: { image, episodes: allEpisodes, title, subtitle, hosts: hostsArray, slug, }, isPast, ...rest }) => {
const episodes = allEpisodes[isPast ? 'past' : 'future']
const hosts = hostsArray.map(({ actor }) => actor.name)
// return episodes.length ? (
return (
<Link to={`series/${slug}`}>
<Wrapper {...rest}>
<Img src={image}>
<LabelBlock
$position="top"
>
{episodes.length} {strings.en.episodes}
</LabelBlock>
<LabelBlock
$position="bottom"
>
{isPast ? strings.en.lastStream : strings.en.nextStream} {episodes && episodes.length && formatDistanceToNow(new Date(episodes[0].endsOn), { addSuffix: true })}
</LabelBlock>
</Img>
<H2 size={32} colour={colours.rose}>{title}</H2>
<H3 size={21} colour={colours.rose}>{subtitle}</H3>
{hosts.length ? <Label size={16} colour={colours.rose}> {andList(hosts)}</Label> : null}
</Wrapper>
</Link>
)
// ) : null
}
export default SeriesCard

View File

@ -0,0 +1,43 @@
import styled from 'styled-components'
import { colours } from '../../assets/theme'
import { Label } from '../Text'
export const Wrapper = styled.div`
display: flex;
flex-direction: column;
margin-right: 3em;
width: min-content;
h2 {
margin-bottom: 0.1em;
}
h3 {
margin-bottom: 0.5em;
}
&:hover div {
filter: invert()
}
`
export const Img = styled.div`
background: url(${({ src }) => src});
width: 380px;
height: 215px;
background-size: cover;
position: relative;
background-position: center;
margin: 0 0 1em 0;
`
export const LabelBlock = styled(Label).attrs(props => ({
colour: props.colour || colours.midnightDarker
}))`
position: absolute;
background-color: ${colours.white};
right: 0;
${({ $position }) => $position === 'top' ?
'top: 0;' : 'bottom: 0;'
};
padding: 4px
`

View File

@ -78,11 +78,24 @@ export const H1 = ({ children, size, ...rest }) => (
</Text> </Text>
) )
export const H2 = ({ children, ...rest }) => ( export const H2 = ({ children, size, ...rest }) => (
<Text <Text
tag="h2" tag="h2"
weight="700" weight="700"
$size="25" $size={size || '25'}
lineHeight="1"
fontFamily="Lunchtype24"
{...rest}
>
{children}
</Text>
)
export const H3 = ({ children, size, ...rest }) => (
<Text
tag="h3"
weight="700"
$size={size || '21'}
lineHeight="1" lineHeight="1"
fontFamily="Lunchtype24" fontFamily="Lunchtype24"
{...rest} {...rest}
@ -103,8 +116,8 @@ export const P = ({ children, size, ...rest }) => (
{children} {children}
</Text> </Text>
) )
export const Span = ({ children, ...rest }) => ( export const Span = ({ children, colour, size, weight, ...rest }) => (
<Text tag="span" colour="inherit" $size="inherit" weight="inherit" {...rest}> <Text tag="span" colour={colour || 'inherit'} $size={size || 'inherit'} weight={weight || 'inherit'} {...rest}>
{children} {children}
</Text> </Text>
) )
@ -120,8 +133,8 @@ export const Label = ({ children, size, ...rest }) => (
{children} {children}
</Text> </Text>
) )
export const A = ({ children, ...rest }) => ( export const A = ({ children, colour, fontFamily, ...rest }) => (
<Text tag="a" colour={colours.highlight} {...rest}> <Text tag="a" colour={colour || colours.highlight} fontFamily={fontFamily} {...rest}>
{children} {children}
</Text> </Text>
) )

View File

@ -3,18 +3,18 @@ import { colours } from '../../assets/theme'
export const TextBase = styled.span` export const TextBase = styled.span`
${({ ${({
$size, $size,
$fontStyle, $fontStyle,
weight, weight,
colour, colour,
align, align,
lineHeight, lineHeight,
opacity = 1, opacity = 1,
$fontFamily: fontFamily, $fontFamily: fontFamily,
selectable, selectable,
underline, underline,
$sizeUnit, $sizeUnit,
}) => css` }) => css`
font-family: ${fontFamily}; font-family: ${fontFamily};
font-weight: ${weight}; font-weight: ${weight};
text-align: ${align}; text-align: ${align};

View File

@ -1,17 +0,0 @@
## Credits
A [NEW DESIGN CONGRESS](https://newdesigncongress.org) project
Series operated by **[RECLAIMFUTURES](https://reclaimfutures.org)**
Infrastructure by **[UNDERSCO.RE](https://undersco.re)**
Stream Design by **[BENJAMIN JONES](mailto:benjamin@newdesigncongress.org)**
Host & Research by **[CADE DIEHM](https://shiba.computer)**
Stream Backgrounds by **[IGNATIUS GILFEDDER](https://ignatius.design)**
Music \*\*\*\*by: [♥ GOJII ♥](https://gojii.bandcamp.com/), [ABELARD](https://abelard.bandcamp.com/), [Frog of Earth](https://frogoftheearth.bandcamp.com/) & [ .](https://spatialmanufactureltd.bandcamp.com/)\*\*
Simulcasted at [UNDERSCORE TV](https://stream.undersco.re) and [Twitch](https://twitch.tv/newdesigncongress)

View File

@ -1,3 +0,0 @@
[NEW DESIGN CONGRESS](https://newdesigncongress.org) x [RECLAIMFUTURES](https://reclaimfutures.org) present _The Para-Real: Finding the Future in Unexpected Places,_ a livestream series about subcultures building livelihoods in spite of platform exploitation. Over 12 episodes streamed weekly, we meet filmmakers who have never met their actors, artists building their own networks of value, documentarians exploring digital identity, and members of resilient subcultures. All of these people share a commonality: they have an innate understanding of the _Para-Real,_ and have seized upon it to better their surroundings.
Between the digital realm and our physical world, _The Para-Real_ is a third space, equally material but poorly understood. The _Para-Real_ is where class politics, economics and the outcomes of hardware and infrastructure design collide. It manifests as the desire for play that turns young Minecraft players into network administrators, the moments where digital security meets physical safety, the creation of mutually-supportive artist-driven marketplaces or the tension inherent in Virtual Realitys land-grab against the living room. The _Para-Real_ is the embodiment of the observation, _We shape our tools, and thereafter our tools shape us._ _The future is not a Zoom call_. The digital systems we are confined to today merge protocol with platform to prey on isolation and extract value from labour. That we grapple with this incarnation of the digital realm indicates a dominant cartel in decline. In its place is a vacuum. We must resist the immature groupthink of the 90s vision of what the Internet can be. The _Para-Real_ is once again contested space.

16
src/data/navigation.js Normal file
View File

@ -0,0 +1,16 @@
export default {
en: [
{
label: 'Program guide',
to: '/program'
},
{
label: 'Series',
to: '/series'
},
{
label: 'Archive',
href: 'https://tv.undersco.re/'
}
]
}

View File

@ -1,6 +1,6 @@
export default { export default {
en: { en: {
nextStream: 'Program', program: 'Program',
pastStream: 'Previous Episodes', pastStream: 'Previous Episodes',
nowPlaying: 'Now playing', nowPlaying: 'Now playing',
noStreams: 'No upcoming streams, check back soon.', noStreams: 'No upcoming streams, check back soon.',
@ -11,5 +11,14 @@ export default {
watchEpisode: 'Watch Episode', watchEpisode: 'Watch Episode',
watchTrailer: 'Watch the trailer', watchTrailer: 'Watch the trailer',
joinStream: 'Click to join stream', joinStream: 'Click to join stream',
fourohfour: '404',
pageNotFound: 'Page not found',
goHome: 'Return to Home',
series: 'Series',
currentSeries: 'Current Series',
pastSeries: 'Past Series',
lastStream: 'Last stream',
nextStream: 'Next stream',
episodes: 'episodes',
}, },
} }

View File

@ -28,3 +28,37 @@ export const camelise = str => {
}) })
.replace(/\s+/g, ''); .replace(/\s+/g, '');
}; };
/**
*
* Takes an array of strings and returns a string list separated by
* commas and an 'and' connector before the last item.
* @param {Array} list - List of strings || 2d array of strings. [0] = string to display, [1] = string to inject into before/after.
* @param {string} [connector] - Optional connector to join the last item, defaults to '&'.
* @param {Object} [wrapEachItem] - Optional object to wrap each item with a string.
* @param {string} [wrapEachItem.before] - Optional string to prefix each item.
* @param {string} [wrapEachItem.after] - Optional string to suffix each item.
* @param {string} [replace] - String to be replaced by second part of list item array if supplied.
*
*/
export const andList = (list, connector = '&', wrapEachItem, replace = '{#}') => {
if (!Array.isArray(list)) return list;
const wrapItem = (item) => {
if (wrapEachItem) {
if (Array.isArray(item)) {
return `${wrapEachItem.before.replace(replace, item[1])}${item[0]}${wrapEachItem.after.replace(replace, item[1])}`;
}
return `${wrapEachItem.before}${item}${wrapEachItem.after}`;
}
return item;
};
return list.map((item, index) => {
if (index + 1 === list.length && index !== 0) {
return ` ${connector} ${wrapItem(item)}`;
}
return list.length === 1 || index === list.length - 2 ? wrapItem(item) : `${wrapItem(item)}, `;
}).join('');
};

5
src/helpers/utils.js Normal file
View File

@ -0,0 +1,5 @@
export const splitArray = (arr, num = 2) => arr.reduce((result, value, index, array) => {
if (index % num === 0)
result.push(array.slice(index, index + num))
return result
}, [])

View File

@ -3,6 +3,7 @@ import axios from 'axios'
import ICAL from 'ical.js' import ICAL from 'ical.js'
import config from '../data/config' import config from '../data/config'
import { useSeriesStore } from '../store/index' import { useSeriesStore } from '../store/index'
import 'regenerator-runtime/runtime'
export const useEventCalendar = () => { export const useEventCalendar = () => {
const [data, setData] = useState([]) const [data, setData] = useState([])
@ -99,7 +100,7 @@ export const useEventCalendar = () => {
export const useEventApi = () => { export const useEventApi = () => {
const [series, setSeries] = useSeriesStore(store => [store.series, store.setSeries]) const [series, setSeries] = useSeriesStore(store => [store.series, store.setSeries])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(!!series.length)
async function fetchData() { async function fetchData() {

View File

@ -1,53 +1,40 @@
import { h } from 'preact' import { h } from 'preact'
import { bool } from 'prop-types' import { bool } from 'prop-types'
import { Link } from 'react-router-dom'
import { H1, Label } from '../../components/Text' import { H1 } from '../../components/Text'
import { import {
Wrapper, Wrapper,
TaglineContainer,
Content, Content,
Hero, Hero,
FadeBottom, FadeTop,
PositionedLink,
} from './styles' } from './styles'
import { colours } from '../../assets/theme' import Header from '../../components/Header'
import { NdcLogo, RFLogo } from '../../components/Logo' import { ImageLogo } from '../../components/Logo'
const InfoLayout = ({ title, subtitle, image, children, theme }) => { const InfoLayout = ({ title, subtitle, image, children, theme }) => (
console.log({ theme }) <Wrapper theme={theme}>
return ( <PositionedLink to="/" theme={theme}>
<Wrapper theme={theme}> <ImageLogo />
<Content> </PositionedLink>
{children} <Content>
</Content> {children}
<Hero image={image}> </Content>
<div> <Hero image={image}>
<H1>{title}</H1> <Header theme={{ foreground: theme.background, background: 'transparent', }} miniHeader />
<H1 <H1>{title}</H1>
css={` <H1
css={`
max-width: 50%; max-width: 50%;
`} `}
> >
{subtitle} {subtitle}
</H1> </H1>
</div> <FadeTop colour={theme.foreground} />
<TaglineContainer> </Hero>
<a href="https://newdesigncongress.org/"> </Wrapper>
<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 export default InfoLayout

View File

@ -1,7 +1,7 @@
import { Link } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import { colours } from '../../assets/theme' import { colours } from '../../assets/theme'
import { ImageLogo as Logo } from '../../components/Logo'
import Logo from '../../components/Logo'
const heroWidth = 'calc(100vw - 600px - 4em)' const heroWidth = 'calc(100vw - 600px - 4em)'
@ -29,9 +29,9 @@ export const Wrapper = styled.div`
h3 { h3 {
color: ${props => props.theme.foreground}; color: ${props => props.theme.foreground};
} }
a { /* a {
color: ${props => props.theme.highlight}; color: ${props => props.theme.highlight};
} } */
button { button {
color: ${props => props.theme.foreground}; color: ${props => props.theme.foreground};
@ -51,23 +51,8 @@ export const Top = styled.div`
width: 50%; width: 50%;
` `
const gradientColourLight = '#F8E5E2' const getGradient = (direction, colour) =>
const gradientColourDark = colours.midnightDarker `linear-gradient(to ${direction}, ${colour}ee 0%,${colour}00 100%);`
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` export const Hero = styled.div`
width: ${heroWidth}; width: ${heroWidth};
@ -83,16 +68,17 @@ export const Hero = styled.div`
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: flex-end;
pointer-events: none; pointer-events: none;
h1, h1,
h2 { h2 {
color: ${colours.offwhite}; color: ${colours.offwhite};
} }
h1:not(:last-of-type) { h1{
margin-bottom: 0.2em;
&:not(:last-of-type) {
font-size: 12vw; font-size: 12vw;
margin-bottom: 0.5em;
@media screen and (max-height: 600px) { @media screen and (max-height: 600px) {
font-size: 20vh; font-size: 20vh;
@ -100,7 +86,7 @@ export const Hero = styled.div`
@media screen and (max-width: 1200px) { @media screen and (max-width: 1200px) {
font-size: 20vh; font-size: 20vh;
} }
} }}
@media screen and (max-width: 1000px) { @media screen and (max-width: 1000px) {
display: none; display: none;
@ -118,27 +104,43 @@ export const LoaderWrapper = styled.div`
` `
export const Content = styled.div` export const Content = styled.div`
/* margin-bottom: 3em; */ padding-top: 80px;
img.img-logo {
max-height: 80px;
mix-blend-mode: exclusion;
/* padding: 1em; */
}
` `
export const PositionedLogo = styled(Logo)`` export const PositionedLink = styled(Link)`
position: absolute;
z-index: 2;
top: 0;
left: 0;
background-color: ${({ theme }) => theme.background};
export const FadeBottom = styled.div` &:hover img {
background: ${getGradient('top', 'dark')}; filter: invert(1);
mix-blend-mode: normal;
}
img {
max-height: 80px;
mix-blend-mode: exclusion;
padding: 0.5em 2em 0;
}
`
export const FadeTop = styled.div`
background: ${({ colour }) => colour ? getGradient('bottom', colour) : colours.rose};
width: ${heroWidth}; width: ${heroWidth};
position: fixed; position: fixed;
bottom: 0em; top: 0em;
padding-bottom: 0.5em; padding-bottom: 0.5em;
right: 0; right: 0;
/* left: 0; */
pointer-events: none; pointer-events: none;
min-height: 75px; min-height: 75px;
/* @media screen and (max-width: 800px) {
h1 {
font-size: 24px;
}
} */
` `
export const TaglineContainer = styled.div` export const TaglineContainer = styled.div`

View File

@ -3,10 +3,12 @@ import PropTypes, { oneOfType, shape, string } from 'prop-types'
import SEO from '../../components/Seo' import SEO from '../../components/Seo'
// import Header from '../../molecules/Header' import Header from '../../components/Header'
import { capitaliseFirstLetter } from '../../helpers/string' import { capitaliseFirstLetter } from '../../helpers/string'
import { defaultTheme } from '../../assets/theme'
import { ThemedBlock } from './styles'
const Page = ({ children, title = '', description, metaImg, backTo, noindex }) => ( const Page = ({ children, title = '', description, metaImg, backTo, noindex, withHeader = true, theme = defaultTheme }) => (
<Fragment> <Fragment>
<SEO <SEO
title={title.toLowerCase() === 'index' ? title : capitaliseFirstLetter(title)} title={title.toLowerCase() === 'index' ? title : capitaliseFirstLetter(title)}
@ -14,8 +16,10 @@ const Page = ({ children, title = '', description, metaImg, backTo, noindex }) =
metaImg={metaImg} metaImg={metaImg}
noindex={noindex} noindex={noindex}
/> />
{/* <Header /> */} {withHeader ? <Header theme={theme} /> : null}
{children} <ThemedBlock theme={theme} withHeader={withHeader}>
{children}
</ThemedBlock>
</Fragment> </Fragment>
) )

View File

@ -0,0 +1,12 @@
import styled from 'styled-components'
// import { defaultTheme } from '../../assets/theme'
export const ThemedBlock = styled.div`
color: ${({ theme }) => theme.foreground};
background-color: ${({ theme }) => theme.background};
padding-top: ${({ withHeader }) => withHeader ? '100px' : 0};
min-height: 100vh;
height: 100%;
min-width: 100vw;
box-sizing: border-box;
`

23
src/pages/404/index.js Normal file
View File

@ -0,0 +1,23 @@
import { h } from 'preact'
import translations from '../../data/strings'
import { H2 } from '../../components/Text'
import {
Wrapper,
Title,
StyledLink as Link
} from './styles'
import { textSizes } from '../../assets/theme'
import Page from '../../layouts/Page'
const FourOhFour = () => (
<Page title={translations.en.pageNotFound} noindex>
<Wrapper>
<Title size={textSizes.hg}>{translations.en.fourohfour}</Title>
<H2>{translations.en.pageNotFound}</H2>
<Link to="/">{translations.en.goHome}</Link>
</Wrapper>
</Page>
)
export default FourOhFour

33
src/pages/404/styles.js Normal file
View File

@ -0,0 +1,33 @@
import styled from 'styled-components'
import { colours } from '../../assets/theme'
import { H1 } from '../../components/Text'
import Link from '../../components/Link'
export const Wrapper = styled.div`
height: calc(100vh - 100px);
width: 100vw;
padding: 2em;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
background-color: ${colours.midnightDarker};
box-sizing: border-box;
@media screen and (max-width: 1200px) {
padding: 1.5em;
}
@media screen and (max-width: 800px) {
padding: 1em;
}
`
export const Title = styled(H1)`
margin-bottom: 0.2em
`
export const StyledLink = styled(Link)`
margin-top: 1em;
`

View File

@ -4,14 +4,14 @@ import bg from '../../assets/img/hero/1lg.png'
// import { H1 } from '../../components/Text' // import { H1 } from '../../components/Text'
import Logo from '../../components/Logo' import { ImageLogo as Logo } from '../../components/Logo'
const heroWidth = '66vw' const heroWidth = 'calc(100vw - 600px - 4em)'
export const Wrapper = styled.div` export const Wrapper = styled.div`
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
padding: 2em; /* padding: 2em; */
display: flex; display: flex;
background-color: ${colours.midnightDarker}; background-color: ${colours.midnightDarker};
box-sizing: border-box; box-sizing: border-box;
@ -25,10 +25,10 @@ export const Wrapper = styled.div`
} }
@media screen and (max-width: 1200px) { @media screen and (max-width: 1200px) {
padding: 1.5em; /* padding: 1.5em; */
} }
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
padding: 1em; /* padding: 1em; */
} }
` `
@ -96,7 +96,7 @@ export const LoaderWrapper = styled.div`
height: 100vh; height: 100vh;
position: fixed; position: fixed;
top: 0; top: 0;
width: 33vw; width: calc(600px + 4em);
@media screen and (max-width: 1000px) { @media screen and (max-width: 1000px) {
width: 100vw; width: 100vw;
@ -108,9 +108,10 @@ export const Content = styled.div`
` `
export const PositionedLogo = styled(Logo)` export const PositionedLogo = styled(Logo)`
position: fixed; /* position: fixed; */
top: 2em; max-height: 80px;
left: 3em; mix-blend-mode: exclusion;
padding: 1em;
` `
export const FadeBottom = styled.div` export const FadeBottom = styled.div`

53
src/pages/Series/index.js Normal file
View File

@ -0,0 +1,53 @@
/* eslint-disable react/prop-types */
import { isWithinInterval } from 'date-fns'
import { h } from 'preact'
import { H1, H2 } from '../../components/Text'
import strings from '../../data/strings'
import { useEventApi } from '../../hooks/data'
import { Content, SeriesGrid, SeriesRow } from './styles'
import Page from '../../layouts/Page'
import SeriesCard from '../../components/SeriesCard'
import { colours } from '../../assets/theme'
const Series = () => {
const { data: seriesDataArray } = useEventApi()
const pastSeries = []
const currentSeries = seriesDataArray.filter(series => {
// const seriesInTheLastMonth = series.episodes.past.filter(episode => {
// })
if (series.episodes.future.length) {
return true
}
pastSeries.push(series)
return false
})
return (
<Page title={strings.en.series}>
<Content>
<SeriesGrid>
<H1 colour={colours.rose}>{strings.en.currentSeries}</H1>
<SeriesRow>
{currentSeries.map(series => (
<SeriesCard series={series} />
))}
</SeriesRow>
<H1 colour={colours.rose}>{strings.en.pastSeries}</H1>
<SeriesRow>
{pastSeries.map(series => (
<SeriesCard series={series} isPast />
))}
</SeriesRow>
</SeriesGrid>
</Content>
</Page>
)
}
export default Series

View File

@ -0,0 +1,14 @@
import styled from 'styled-components'
import { Row } from '../../components/Flex'
export const Content = styled.div`
padding-top: 64px;
`
export const SeriesGrid = styled.div`
margin-left: 32px;
`
export const SeriesRow = styled(Row)`
margin: 3em 0 6em 0;
`

View File

@ -18,6 +18,9 @@ import {
import config from '../../data/config' import config from '../../data/config'
import Page from '../../layouts/Page' import Page from '../../layouts/Page'
import { splitArray } from '../../helpers/utils'
import Header from '../../components/Header'
import theme from '../../assets/theme'
const SeriesPage = ({ data }) => { const SeriesPage = ({ data }) => {
@ -39,14 +42,17 @@ const SeriesPage = ({ data }) => {
tzShort = tzShort[1].match(/[A-Z]/g).join('') tzShort = tzShort[1].match(/[A-Z]/g).join('')
} }
const links = data.links.length ? splitArray(data.links, 2) : null
return ( return (
<Page title={data.title}> <Page title={data.title} theme={data.theme} withHeader={false}>
<InfoLayout title={data.title} subtitle={data.subtitle} image={data.image} theme={data.theme}> <InfoLayout title={data.title} subtitle={data.subtitle} image={data.image} theme={data.theme}>
<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 theme={data.theme}>{data.description}</Markdown>
{data.trailer ? ( {data.trailer ? (
<TrailerContainer> <TrailerContainer>
@ -54,9 +60,9 @@ const SeriesPage = ({ data }) => {
</TrailerContainer> </TrailerContainer>
) : null} ) : null}
{data.links.length ? {links ?
<Row wrap> links.map(linkRow => <Row>
{data.links.map(link => ( {linkRow.map(link => (
<a <a
href={link.resourceUrl} href={link.resourceUrl}
target="_blank" target="_blank"
@ -65,12 +71,12 @@ const SeriesPage = ({ data }) => {
<Button>{link.summary}</Button> <Button>{link.summary}</Button>
</a> </a>
))} ))}
</Row> : null </Row>) : null
} }
</InfoContent> </InfoContent>
{data.episodes.future.length ? ( {data.episodes.future.length ? (
<Fragment> <Fragment>
<Title>{translations.en.nextStream}:</Title> <Title>{translations.en.program}:</Title>
{data.episodes.future.map(feeditem => ( {data.episodes.future.map(feeditem => (
<EpisodeCard <EpisodeCard
theme={data.theme} theme={data.theme}
@ -89,18 +95,16 @@ const SeriesPage = ({ data }) => {
theme={data.theme} theme={data.theme}
key={feeditem.beginsOn} key={feeditem.beginsOn}
hasPassed hasPassed
onClickButton={() => onClickButton={() => null} // todo: fix this
setEmbedUrl(`${config.peertube_root}${feeditem.embedPath}`)
}
{...feeditem} {...feeditem}
/> />
))} ))}
</Fragment> </Fragment>
) : null} ) : null}
<InfoContent> {data.credits ? <InfoContent>
<Title>Credits</Title> <Title>Credits</Title>
<Markdown>{credits}</Markdown> <Markdown theme={data.theme}>{credits}</Markdown>
</InfoContent> </InfoContent> : null}
</Fragment> </Fragment>
</InfoLayout> </InfoLayout>

View File

@ -9,10 +9,9 @@ import Markdown from '../../components/Markdown'
import Logo from '../../components/Logo' import Logo from '../../components/Logo'
import translations from '../../data/strings' import translations from '../../data/strings'
import CrossSvg from '../../components/Svg/Cross' import CrossSvg from '../../components/Svg/Cross'
import PlaySvg from '../../components/Svg/Play'
import { P, H1, H2, Span, Label } from '../../components/Text' import { H1, H2, Span, Label } from '../../components/Text'
import { Link } from '../../components/Link' 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`
@ -104,7 +103,8 @@ export const PositionedCross = styled(CrossSvg)`
export const VCWrapper = styled.div` export const VCWrapper = styled.div`
max-width: 600px; max-width: 600px;
margin: 0 0 6em 2px; margin: 0 0 0 2px;
padding-bottom: 3em;
button { button {
margin-top: 16px; margin-top: 16px;
@ -139,13 +139,15 @@ const renderTitles = titles =>
export const EpisodeCard = ({ export const EpisodeCard = ({
title, title,
image,
description, description,
beginsOn, beginsOn,
hasPassed, hasPassed,
videoUrl, videoUrl,
onClickButton, onClickButton,
tzShort, tzShort,
image,
theme, theme,
}) => { }) => {
const startDate = new Date(beginsOn) const startDate = new Date(beginsOn)
@ -174,7 +176,7 @@ export const EpisodeCard = ({
<VCImg src={image} alt="" /> <VCImg src={image} alt="" />
</Fragment> </Fragment>
)} )}
<Markdown withLinebreaks>{description}</Markdown> <Markdown theme={theme}>{description}</Markdown>
{hasPassed ? ( {hasPassed ? (
<Button onClick={onClickButton}>{translations.en.watchEpisode}</Button> <Button onClick={onClickButton}>{translations.en.watchEpisode}</Button>
) : ( ) : (
@ -193,13 +195,10 @@ export const EpisodeCard = ({
} }
EpisodeCard.propTypes = { EpisodeCard.propTypes = {
title: string,
description: string, description: string,
beginsOn: instanceOf(Date), beginsOn: instanceOf(Date),
onClickButton: func, onClickButton: func,
tzShort: string, tzShort: string,
image: string,
previewPath: string,
hasPassed: bool, hasPassed: bool,
videoUrl: string, videoUrl: string,
} }

View File

@ -1,10 +1,10 @@
export const getMetadataByKey = (episode, key) => { export const getMetadataByKey = (episode, key, firstItem = true) => {
const filteredItems = episode.metadata.length ? episode.metadata.filter( const filteredItems = episode.metadata.length ? episode.metadata.filter(
meta => meta.key === key meta => meta.key.includes(key)
) : null ) : null
if (filteredItems) { if (filteredItems) {
return filteredItems[0].value return firstItem ? filteredItems[0].value : filteredItems
} }
return null return null
@ -16,7 +16,7 @@ export const getPostByKey = (posts, key) => {
} }
export const getResourcesByKey = (resources, key, lookup = 'title') => { export const getResourcesByKey = (resources, key, lookup = 'title') => {
const filteredResources = resources.elements.length ? resources.elements.filter(resource => resource[lookup] === key) : [] const filteredResources = resources.elements.length ? resources.elements.filter(resource => resource[lookup].includes(key)) : []
if (!filteredResources.length) return null if (!filteredResources.length) return null
@ -24,3 +24,12 @@ export const getResourcesByKey = (resources, key, lookup = 'title') => {
} }
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
export const getEpisodeCode = (episode) => {
const metaItems = getMetadataByKey(episode, 'mz:plain:', false)
if (metaItems && metaItems.length) {
// TODO
}
return 'TODO'
}

View File

@ -2,14 +2,15 @@ import create from 'zustand'
import striptags from 'striptags' 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, getEpisodeCode } from './helpers'
import { colours, defaultTheme } from '../assets/theme' import { colours, defaultTheme } from '../assets/theme'
export const [useSeriesStore] = create(set => ({ export const [useSeriesStore] = create(set => ({
series: [], series: [],
setSeries: seriesArray => { setSeries: seriesArray => {
const allSeries = seriesArray.map(({ name, organizedEvents, posts, resources, banner, summary }) => { const allSeries = seriesArray.map(({ name, organizedEvents, posts, resources, banner, summary, members }) => {
const allEpisodes = organizedEvents.elements.length ? organizedEvents.elements.map(ep => ({ const allEpisodes = organizedEvents.elements.length ? organizedEvents.elements.map(ep => ({
title: ep.title, title: ep.title,
@ -18,11 +19,15 @@ export const [useSeriesStore] = create(set => ({
description: ep.description, description: ep.description,
media: ep.media, media: ep.media,
image: ep.picture ? ep.picture.url : null, image: ep.picture ? ep.picture.url : null,
peertubeId: getPeertubeIDfromUrl(getMetadataByKey(ep, 'mz:live:peertube:url')), peertubeId: getPeertubeIDfromUrl(getMetadataByKey(ep, 'peertube:url')),
code: getEpisodeCode(ep)
})) : [] })) : []
const trailer = getResourcesByKey(resources, 'SERIES_TRAILER')?.[0]?.resourceUrl ?? null const trailer = getResourcesByKey(resources, 'SERIES_TRAILER')?.[0]?.resourceUrl ?? null
const theme = striptags(getPostByKey(posts, 'THEME')) const theme = striptags(getPostByKey(posts, 'THEME'))
const orgLinks = getResourcesByKey(resources, 'ORG_LINK:')?.map(link => link.title)
console.log({ orgLinks })
const series = { const series = {
@ -39,8 +44,9 @@ export const [useSeriesStore] = create(set => ({
slug: slugify(name), slug: slugify(name),
credits: getPostByKey(posts, 'SERIES_CREDITS'), credits: getPostByKey(posts, 'SERIES_CREDITS'),
trailer, trailer,
links: getResourcesByKey(resources, 'LINK') || [], links: getResourcesByKey(resources, 'SERIES_LINK') || [],
theme: theme ? JSON.parse(theme) : defaultTheme theme: theme ? JSON.parse(theme) : defaultTheme,
hosts: members.elements.filter(({ actor }) => actor.name !== 'streamappbot')
} }
return series return series
}) })

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB