refac to put data handling on server

This commit is contained in:
sunda 2021-10-20 16:33:06 +02:00
parent 6d2090eea9
commit be6aa96e1b
10 changed files with 140 additions and 154 deletions

View File

@ -1,16 +1,19 @@
import { h, render } from 'preact' import { h, render } from 'preact'
import { useState } from 'preact/hooks' import { useState } from 'preact/hooks'
import { ThemeProvider } from 'styled-components'
import { BrowserRouter, Route, Switch } from 'react-router-dom' import { BrowserRouter, Route, Switch } from 'react-router-dom'
import Main from './app' import Main from './app'
import SeriesPage from './src/pages/SeriesPage' import SeriesPage from './src/pages/SeriesPage'
import { useEventApi, useEventCalendar } from './src/hooks/data' import { useEventApi, useEventCalendar } from './src/hooks/data'
import { useTheme } from './src/store'
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 FourOhFour from './src/pages/404'
import Series from './src/pages/Series' import Series from './src/pages/Series'
const App = () => { const App = () => {
const { theme } = useTheme((store) => store)
const { data: calData, calLoading } = useEventCalendar() const { data: calData, calLoading } = useEventCalendar()
const { data: seriesDataArray, loading: eventsLoading } = useEventApi() const { data: seriesDataArray, loading: eventsLoading } = useEventApi()
const [minLoadTimePassed, setMinTimeUp] = useState(false) const [minLoadTimePassed, setMinTimeUp] = useState(false)
@ -22,23 +25,26 @@ const App = () => {
const seriesData = Object.values(seriesDataArray) const seriesData = Object.values(seriesDataArray)
return calLoading || eventsLoading || !minLoadTimePassed ? ( return (
<LoaderLayout /> <ThemeProvider theme={theme}>
) : ( {calLoading || eventsLoading || !minLoadTimePassed ? (
<BrowserRouter> <LoaderLayout />
<Switch> ) : (
<Route exact path="/" component={Main} /> <BrowserRouter>
<Route exact path="/series" component={Series} /> <Switch>
{seriesData.length ? seriesData.map(series => ( <Route exact path="/" component={Main} />
<Route exact path={`/series/${series.slug}`}> <Route exact path="/series" component={Series} />
<SeriesPage data={series} /> {seriesData.length ? seriesData.map(series => (
</Route>)) : null} <Route exact path={`/series/${series.slug}`}>
<Route path="*"> <SeriesPage data={series} />
<FourOhFour /> </Route>)) : null}
</Route> <Route path="*">
</Switch> <FourOhFour />
</BrowserRouter> </Route>
) </Switch>
</BrowserRouter>
)}
</ThemeProvider>)
} }

View File

@ -1,66 +1,80 @@
import { h } from 'preact' import { h } from 'preact'
import { Link as ReactLink } from 'react-router-dom' import { Link as ReactLink } from 'react-router-dom'
import { RightBox, StyledRow as Row, Modal } from './styles' import { RightBox, StyledRow as Row, Modal, PositionedCross as CrossSvg } from './styles'
import { ImageLogo } from '../Logo' import { ImageLogo } from '../Logo'
import { useWindowSize } from '../../hooks/dom' import { useWindowSize } from '../../hooks/dom'
import Link from '../Link' import Link from '../Link'
import { Span } from '../Text' import { Span } from '../Text'
import CrossSvg from '../Svg/Cross'
import navigation from '../../data/navigation' import navigation from '../../data/navigation'
import { colours, screenSizes, textSizes } from '../../assets/theme' import { colours, screenSizes, textSizes } from '../../assets/theme'
import { useToggle } from '../../hooks/utility' import { useTheme, useUiStore } from '../../store'
const Navigation = ({ theme = {}, lang = 'en', headerTheme }) => navigation[lang].map(navItem => ( const Navigation = ({ theme = {}, lang = 'en', miniHeader, toggleMobileMenu }) => navigation[lang].map(navItem => (
<Link <Link
navLink navLink
to={navItem.to} to={navItem.to}
href={navItem.href} href={navItem.href}
onClick={toggleMobileMenu}
textProps={{ textProps={{
size: textSizes.xl, size: textSizes.xl,
colour: headerTheme.foreground || theme.foreground || colours.rose colour: miniHeader ? theme.background : theme.foreground || colours.rose
}}> }}>
{navItem.label} {navItem.label}
</Link> </Link>
)) ))
const NavigationModal = ({ theme = {}, lang = 'en', headerTheme, toggleMenuOpen, ...rest }) => ( export const NavigationModal = ({ theme = {}, lang = 'en', headerTheme, ...rest }) => {
<Modal theme={theme} {...rest}> const { toggleMobileMenu } = useUiStore(store => store)
<ReactLink to="/">
<ImageLogo />
</ReactLink>
<CrossSvg size={32} colour={theme.foreground} onClick={toggleMenuOpen} />
<div>
<Navigation theme={theme} lang={lang} headerTheme={theme} {...rest} />
</div>
</Modal>
)
const FullHeader = ({ theme = {}, headerTheme = {}, lang = 'en', miniHeader, isMobile, toggleMenuOpen, ...rest }) => ( return (
<Row theme={headerTheme} align="center" justify="space-between" miniHeader={miniHeader} {...rest}> <Modal {...rest}>
<ReactLink to="/">
<ImageLogo />
</ReactLink>
<CrossSvg size={32} colour={theme.foreground} onClick={toggleMobileMenu} />
<div>
<Navigation theme={theme} lang={lang} headerTheme={theme} toggleMobileMenu={toggleMobileMenu} {...rest} />
</div>
</Modal>
)
}
const FullHeader = ({ theme = {}, lang = 'en', miniHeader, isMobile, ...rest }) => (
<Row align="center" justify="space-between" miniHeader={miniHeader} {...rest}>
{!miniHeader ? <ReactLink to="/"> {!miniHeader ? <ReactLink to="/">
<ImageLogo /> <ImageLogo />
</ReactLink> : null} </ReactLink> : null}
{!isMobile ? ( {!isMobile ? (
<RightBox as="nav"> <RightBox as="nav">
<Navigation theme={theme} lang={lang} headerTheme={headerTheme} {...rest} /> <Navigation theme={theme} lang={lang} miniHeader={miniHeader} {...rest} />
</RightBox> </RightBox>
) : <Span onClick={toggleMenuOpen} size={textSizes.xl} colour={headerTheme.foreground || theme.foreground || colours.rose} fontFamily="Lunchtype24" ) : <MobileMenuToggle miniHeader={miniHeader} />}
>Menu</Span>}
</Row> </Row>
) )
export const MobileMenuToggle = ({ miniHeader, ...props }) => {
const { toggleMobileMenu } = useUiStore(store => store)
const { theme } = useTheme(store => store)
return (
<Span
onClick={toggleMobileMenu}
size={textSizes.xl}
colour={miniHeader ? theme.background : theme.foreground || colours.rose}
fontFamily="Lunchtype24"
{...props}
> Menu</Span>)
}
const Header = ({ miniHeader, theme, ...rest }) => { const Header = ({ miniHeader, theme, ...rest }) => {
const headerTheme = { foreground: theme.background, background: 'transparent', }
const { width: screenWidth } = useWindowSize() const { width: screenWidth } = useWindowSize()
const [menuOpen, toggleMenuOpen] = useToggle(false) const { menuOpen } = useUiStore(store => store)
const isMobile = screenWidth < screenSizes.lg const isMobile = screenWidth < screenSizes.lg
if (menuOpen) return <NavigationModal toggleMenuOpen={toggleMenuOpen} theme={theme} headerTheme={headerTheme} {...rest} /> return <FullHeader menuOpen={menuOpen} miniHeader={miniHeader} isMobile={isMobile} theme={theme} {...rest} />
return <FullHeader toggleMenuOpen={toggleMenuOpen} menuOpen={menuOpen} miniHeader={miniHeader} isMobile={isMobile} theme={theme} headerTheme={headerTheme} {...rest} />
} }
export default Header export default Header

View File

@ -7,7 +7,7 @@ import CrossSvg from '../Svg/Cross'
export const StyledRow = styled(Row)` export const StyledRow = styled(Row)`
pointer-events: all; pointer-events: all;
height: 100px; height: 100px;
background-color: ${({ theme }) => theme.background}; background-color: transparent;
color: ${({ theme }) => theme.foreground}; color: ${({ theme }) => theme.foreground};
position: absolute; position: absolute;
left: ${({ miniHeader }) => miniHeader ? 'initial' : '0'}; left: ${({ miniHeader }) => miniHeader ? 'initial' : '0'};
@ -71,6 +71,7 @@ export const Modal = styled.div`
` `
export const PositionedCross = styled(CrossSvg)` export const PositionedCross = styled(CrossSvg)`
position: fixed; position: fixed;
right: 1em; right: 2.5em;
top: 1em; top: 2em;
cursor: pointer;
` `

View File

@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'
import { colours } from '../../assets/theme' import { colours } from '../../assets/theme'
export const RRLink = styled(Link)` export const RRLink = styled(Link)`
color: ${props => { console.log({ col: props.$colour }); return props.$colour || colours.highlight }}; color: ${props => props.$colour || colours.highlight};
text-decoration: none; text-decoration: none;
${props => props.$navLink ? ` ${props => props.$navLink ? `
font-family: Lunchtype24; font-family: Lunchtype24;

View File

@ -99,7 +99,7 @@ export const useEventCalendar = () => {
export const useEventApi = () => { export const useEventApi = () => {
const [series, setSeries] = useSeriesStore(store => [store.series, store.setSeries]) const [series, episodes, setSeries, setEpisodes] = useSeriesStore(store => [store.series, store.episodes, store.setSeries, store.setEpisodes])
const [loading, setLoading] = useState(!!series.length) const [loading, setLoading] = useState(!!series.length)
@ -112,6 +112,9 @@ export const useEventApi = () => {
) )
setSeries(responseData) setSeries(responseData)
// setEpisodes()
// console.log({ episodes })
setLoading(false) setLoading(false)
} }

View File

@ -99,6 +99,10 @@ export const LoaderWrapper = styled.div`
export const Content = styled.div` export const Content = styled.div`
padding-top: 80px; padding-top: 80px;
@media screen and (max-width: ${screenSizes.md}px) {
padding-top: 100px;
}
img.img-logo { img.img-logo {
max-height: 80px; max-height: 80px;
mix-blend-mode: exclusion; mix-blend-mode: exclusion;
@ -122,6 +126,10 @@ export const PositionedLink = styled(Link)`
max-height: 80px; max-height: 80px;
mix-blend-mode: exclusion; mix-blend-mode: exclusion;
padding: 0.5em 2em 0; padding: 0.5em 2em 0;
@media screen and (max-width: ${screenSizes.md}px) {
padding-left: 1em
}
} }
` `

View File

@ -1,27 +1,42 @@
import { Fragment, h } from 'preact' import { Fragment, h } from 'preact'
import PropTypes, { oneOfType, shape, string } from 'prop-types' import PropTypes, { oneOfType, shape, string } from 'prop-types'
import { useEffect } from 'preact/hooks'
import SEO from '../../components/Seo' import SEO from '../../components/Seo'
import Header from '../../components/Header' import Header, { NavigationModal as MenuModal } from '../../components/Header'
import { capitaliseFirstLetter } from '../../helpers/string' import { capitaliseFirstLetter } from '../../helpers/string'
import { defaultTheme } from '../../assets/theme' import { defaultTheme } from '../../assets/theme'
import { ThemedBlock } from './styles' import { ThemedBlock } from './styles'
import { useTheme, useUiStore } from '../../store'
const Page = ({ children, title = '', description, metaImg, backTo, noindex, withHeader = true, theme = defaultTheme }) => ( const Page = ({ children, title = '', description, metaImg, backTo, noindex, withHeader = true, theme = defaultTheme }) => {
<Fragment> const { setTheme } = useTheme(store => store)
<SEO const { mobileMenuOpen: menuOpen, toggleMenuOpen } = useUiStore(store => store)
title={title.toLowerCase() === 'index' ? title : capitaliseFirstLetter(title)}
description={description}
metaImg={metaImg} useEffect(() => {
noindex={noindex} if (theme) {
/> setTheme(theme)
{withHeader ? <Header theme={theme} /> : null} }
<ThemedBlock theme={theme} withHeader={withHeader}> }, [])
{children}
</ThemedBlock> return (
</Fragment> <Fragment>
) <SEO
title={title.toLowerCase() === 'index' ? title : capitaliseFirstLetter(title)}
description={description}
metaImg={metaImg}
noindex={noindex}
/>
{withHeader ? <Header theme={theme} /> : null}
{menuOpen ? <MenuModal theme={theme} toggleMenuOpen={toggleMenuOpen} /> : null}
<ThemedBlock theme={theme} withHeader={withHeader}>
{children}
</ThemedBlock>
</Fragment>
)
}
Page.propTypes = { Page.propTypes = {
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,

View File

@ -1,5 +1,6 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
import { h, Fragment } from 'preact' import { h, Fragment } from 'preact'
import { useEffect } from 'preact/hooks'
import striptags from 'striptags' import striptags from 'striptags'
import { H1 } from '../../components/Text' import { H1 } from '../../components/Text'
@ -18,8 +19,11 @@ import {
import Page from '../../layouts/Page' import Page from '../../layouts/Page'
import { splitArray } from '../../helpers/utils' import { splitArray } from '../../helpers/utils'
import { defaultTheme } from '../../assets/theme'
const SeriesPage = ({ data }) => { const SeriesPage = ({ data }) => {
const theme = data.theme || defaultTheme
const credits = data.credits ? ` const credits = data.credits ? `
## Credits ## Credits
${data.credits} ${data.credits}
@ -43,12 +47,12 @@ const SeriesPage = ({ data }) => {
return ( return (
<Page title={data.title} theme={data.theme} withHeader={false}> <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={theme}>
<Fragment> <Fragment>
<InfoContent> <InfoContent>
<H1>{data.title}:</H1> <H1>{data.title}:</H1>
<H1>{data.subtitle}</H1> <H1>{data.subtitle}</H1>
{data.description ? <Markdown withLinebreaks theme={data.theme}>{data.description}</Markdown> : null} {data.description ? <Markdown withLinebreaks theme={theme}>{data.description}</Markdown> : null}
{data.trailer ? ( {data.trailer ? (
<TrailerContainer> <TrailerContainer>
@ -75,7 +79,7 @@ const SeriesPage = ({ data }) => {
<Title>{translations.en.program}:</Title> <Title>{translations.en.program}:</Title>
{data.episodes.future.map(feeditem => ( {data.episodes.future.map(feeditem => (
<EpisodeCard <EpisodeCard
theme={data.theme} theme={theme}
key={feeditem.start} key={feeditem.start}
tzShort={tzShort} tzShort={tzShort}
{...feeditem} {...feeditem}
@ -88,7 +92,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} theme={theme}
key={feeditem.beginsOn} key={feeditem.beginsOn}
hasPassed hasPassed
onClickButton={() => null} // todo: fix this onClickButton={() => null} // todo: fix this
@ -99,7 +103,7 @@ const SeriesPage = ({ data }) => {
) : null} ) : null}
{credits ? <InfoContent> {credits ? <InfoContent>
<Title>Credits</Title> <Title>Credits</Title>
<Markdown theme={data.theme}>{credits}</Markdown> <Markdown theme={theme}>{credits}</Markdown>
</InfoContent> : null} </InfoContent> : null}
</Fragment> </Fragment>

View File

@ -1,35 +0,0 @@
export const getMetadataByKey = (episode, key, firstItem = true) => {
const filteredItems = episode.metadata.length ? episode.metadata.filter(
meta => meta.key.includes(key)
) : null
if (filteredItems) {
return firstItem ? filteredItems[0].value : filteredItems
}
return null
}
export const getPostByKey = (posts, key) => {
const filteredPostItems = posts.elements.length ? posts.elements.filter(post => post.title === key) : []
return filteredPostItems.length ? filteredPostItems[0].body : null
}
export const getResourcesByKey = (resources, key, lookup = 'title') => {
const filteredResources = resources.elements.length ? resources.elements.filter(resource => resource[lookup].includes(key)) : []
if (!filteredResources.length) return null
return filteredResources
}
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

@ -1,56 +1,26 @@
import create from 'zustand' import create from 'zustand'
import striptags from 'striptags' import { defaultTheme } from '../assets/theme'
import { isFuture, isPast } from 'date-fns'
import { slugify } from '../helpers/string'
import { getMetadataByKey, getPostByKey, getResourcesByKey, getPeertubeIDfromUrl, getEpisodeCode } from './helpers'
import { colours, defaultTheme } from '../assets/theme'
export const [useSeriesStore] = create(set => ({ export const useSeriesStore = create((set, get) => ({
series: [], series: [],
episodes: [],
setSeries: seriesArray => { setSeries: series => set({ series }),
const allSeries = seriesArray.map(({ name, organizedEvents, posts, resources, banner, summary, members }) => { setEpisodes: () => {
if (get().series) {
set({
const allEpisodes = organizedEvents.elements.length ? organizedEvents.elements.map(ep => ({ episodes: get().series.map(series => series.episodes)
title: ep.title, })
beginsOn: ep.beginsOn, } else set({})
endsOn: ep.endsOn,
description: ep.description,
media: ep.media,
image: ep.picture ? ep.picture.url : null,
peertubeId: getPeertubeIDfromUrl(getMetadataByKey(ep, 'peertube:url')),
code: getEpisodeCode(ep)
})) : []
const trailer = getResourcesByKey(resources, 'SERIES_TRAILER')?.[0]?.resourceUrl ?? null
const theme = striptags(getPostByKey(posts, 'THEME'))
const orgLinks = getResourcesByKey(resources, 'ORG_LINK:')?.map(link => link.title)
console.log({ orgLinks })
const series = {
title: name,
subtitle: striptags(summary),
description: getPostByKey(posts, 'SERIES_INFO'),
posts: posts.elements,
resources: resources.elements,
image: banner ? banner.url : '',
episodes: {
future: allEpisodes.filter(ep => isFuture(new Date(ep.endsOn))).sort((a, b) => new Date(a.beginsOn) - new Date(b.beginsOn)),
past: allEpisodes.filter(ep => isPast(new Date(ep.endsOn))).sort((a, b) => new Date(a.beginsOn) - new Date(b.beginsOn))
},
slug: slugify(name),
credits: getPostByKey(posts, 'SERIES_CREDITS'),
trailer,
links: getResourcesByKey(resources, 'SERIES_LINK') || [],
theme: theme ? JSON.parse(theme) : defaultTheme,
hosts: members.elements.filter(({ actor }) => actor.name !== 'streamappbot')
}
return series
})
set({ series: allSeries })
} }
})) }))
export const [useTheme] = create(set => ({
theme: defaultTheme,
setTheme: (theme) => set({ theme }),
setDefaultTheme: () => set({ theme: defaultTheme })
}))
export const [useUiStore] = create((set, get) => ({
mobileMenuOpen: false,
toggleMobileMenu: () => set({ mobileMenuOpen: !get().mobileMenuOpen }),
}))