added cal event generation, lots of styling
This commit is contained in:
parent
b932d96d80
commit
e6e05d90ef
40
index.js
40
index.js
@ -1,27 +1,56 @@
|
|||||||
import { h, render } from 'preact'
|
import { Fragment, h, render } from 'preact'
|
||||||
import { useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import { ThemeProvider } from 'styled-components'
|
import { ThemeProvider } from 'styled-components'
|
||||||
import { BrowserRouter, Route, Switch } from 'react-router-dom'
|
import { BrowserRouter, Route, Switch } from 'react-router-dom'
|
||||||
|
import { isWithinInterval, subHours, addHours } from 'date-fns'
|
||||||
|
import { zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz'
|
||||||
|
|
||||||
import Main from './app'
|
import Main from './app'
|
||||||
import SeriesPage from './src/pages/SeriesPage'
|
import SeriesPage from './src/pages/SeriesPage'
|
||||||
import { useEventApi } from './src/hooks/data'
|
import { useEventApi, usePeertubeApi } from './src/hooks/data'
|
||||||
import { useTheme } from './src/store'
|
import { useStreamStore, 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'
|
||||||
import Program from './src/pages/Program'
|
import Program from './src/pages/Program'
|
||||||
|
import StreamPreview from './src/components/StreamPreview'
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const { theme } = useTheme((store) => store)
|
const { theme } = useTheme((store) => store)
|
||||||
const { data, loading: eventsLoading } = useEventApi()
|
const { data, loading: eventsLoading } = useEventApi()
|
||||||
const [minLoadTimePassed, setMinTimeUp] = useState(false)
|
const [minLoadTimePassed, setMinTimeUp] = useState(false)
|
||||||
|
const { setCurrentStream, currentStream, streamIsLive } = useStreamStore(store => store)
|
||||||
|
usePeertubeApi(data.episodes)
|
||||||
|
|
||||||
useTimeout(() => {
|
useTimeout(() => {
|
||||||
setMinTimeUp(true)
|
setMinTimeUp(true)
|
||||||
}, 1500)
|
}, 1500)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Array.isArray(data.episodes)) {
|
||||||
|
data.episodes.forEach(stream => {
|
||||||
|
const utcStartDate = zonedTimeToUtc(
|
||||||
|
new Date(stream.beginsOn),
|
||||||
|
'Europe/Berlin'
|
||||||
|
)
|
||||||
|
const utcEndDate = zonedTimeToUtc(new Date(stream.endsOn), 'Europe/Berlin')
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
setCurrentStream(stream)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [eventsLoading])
|
||||||
|
|
||||||
// console.log({ episodes: data.episodes, series: data.series })
|
// console.log({ episodes: data.episodes, series: data.series })
|
||||||
|
|
||||||
const seriesData = data.series ? Object.values(data.series) : []
|
const seriesData = data.series ? Object.values(data.series) : []
|
||||||
@ -32,6 +61,7 @@ const App = () => {
|
|||||||
{!seriesData.length || eventsLoading || !minLoadTimePassed ? (
|
{!seriesData.length || eventsLoading || !minLoadTimePassed ? (
|
||||||
<LoaderLayout />
|
<LoaderLayout />
|
||||||
) : (
|
) : (
|
||||||
|
<Fragment>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/" component={Main} />
|
<Route exact path="/" component={Main} />
|
||||||
@ -46,6 +76,8 @@ const App = () => {
|
|||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
<StreamPreview stream={currentStream} isLive={streamIsLive} />
|
||||||
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
</ThemeProvider>)
|
</ThemeProvider>)
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"date-fns": "^2.19.0",
|
"date-fns": "^2.19.0",
|
||||||
"date-fns-tz": "^1.1.4",
|
"date-fns-tz": "^1.1.4",
|
||||||
|
"datebook": "^7.0.7",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"ical": "^0.8.0",
|
"ical": "^0.8.0",
|
||||||
"ical.js": "^1.4.0",
|
"ical.js": "^1.4.0",
|
||||||
|
@ -31,6 +31,7 @@ export const screenSizes = {
|
|||||||
sm: 800,
|
sm: 800,
|
||||||
md: 1000,
|
md: 1000,
|
||||||
lg: 1500,
|
lg: 1500,
|
||||||
|
xl: 1700,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultTheme = {
|
export const defaultTheme = {
|
||||||
|
@ -1,26 +1,37 @@
|
|||||||
import { h } from 'preact'
|
import { h } from 'preact'
|
||||||
|
import { ICalendar } from 'datebook'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
|
import striptags from 'striptags'
|
||||||
import Link from '../Link'
|
import Link from '../Link'
|
||||||
import { H2, H3, Label } from '../Text'
|
import { H2, H3, Label } from '../Text'
|
||||||
import strings from '../../data/strings'
|
import strings from '../../data/strings'
|
||||||
import { andList } from '../../helpers/string'
|
import { andList } from '../../helpers/string'
|
||||||
import { colours } from '../../assets/theme'
|
import { colours, screenSizes } from '../../assets/theme'
|
||||||
import { Img, Left, Right, Center, Title, Row, Column, StyledButton as Button } from './styles'
|
import { Img, Left, Right, Center, Title, ButtonRow, StyledButton as Button } from './styles'
|
||||||
import { useEventApi } from '../../hooks/data'
|
import { useEventApi } from '../../hooks/data'
|
||||||
|
import { useWindowSize } from '../../hooks/dom'
|
||||||
|
import Flex from '../Flex'
|
||||||
|
|
||||||
const EpisodeCard = ({ image, title, seriesId, beginsOn, id, ...rest }) => {
|
|
||||||
|
|
||||||
|
const EpisodeCard = ({ image, title, seriesId, beginsOn, endsOn, id, url, description, ...rest }) => {
|
||||||
const { data: { series: allSeries } } = useEventApi()
|
const { data: { series: allSeries } } = useEventApi()
|
||||||
|
|
||||||
const series = seriesId ? allSeries.filter(({ id }) => id === seriesId)[0] : {}
|
const series = seriesId ? allSeries.filter(({ id }) => id === seriesId)[0] : {}
|
||||||
const hosts = series.hosts ? series.hosts.map(host => host.actor.name) : null
|
const hosts = series.hosts ? series.hosts.map(host => host.actor.name) : null
|
||||||
const startTime = format(new Date(beginsOn), 'ha')
|
const startTime = format(new Date(beginsOn), 'h:mma')
|
||||||
|
|
||||||
|
const { width: screenWidth } = useWindowSize()
|
||||||
|
const isMobile = screenWidth < screenSizes.md
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row align="stretch" {...rest}>
|
<Flex align="stretch" direction={isMobile ? 'column' : 'row'} {...rest}>
|
||||||
<Left>
|
<Left>
|
||||||
<Link to={`/series/${series.slug}#${id}`}><Img src={image} /></Link>
|
<Link to={`/series/${series.slug}#${id}`}>
|
||||||
<Button>Play now</Button>
|
<Img src={image} />
|
||||||
|
</Link>
|
||||||
|
<ButtonsRows title={title} description={description} beginsOn={beginsOn} endsOn={endsOn} url={url} />
|
||||||
</Left>
|
</Left>
|
||||||
<Center justify="space-betweeen">
|
<Center justify="space-betweeen">
|
||||||
<Title size={24} colour={colours.rose} weight="500">{title}</Title>
|
<Title size={24} colour={colours.rose} weight="500">{title}</Title>
|
||||||
@ -34,9 +45,28 @@ const EpisodeCard = ({ image, title, seriesId, beginsOn, id, ...rest }) => {
|
|||||||
<Right>
|
<Right>
|
||||||
<Label size={24} colour={colours.rose} weight="600">{startTime}</Label>
|
<Label size={24} colour={colours.rose} weight="600">{startTime}</Label>
|
||||||
</Right>
|
</Right>
|
||||||
</Row>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ButtonsRows = ({ title, description, beginsOn, endsOn, url }) => {
|
||||||
|
const icalendar = new ICalendar({
|
||||||
|
title,
|
||||||
|
location: 'https://stream.undersco.re/',
|
||||||
|
description: description ? striptags(description) : '',
|
||||||
|
start: new Date(beginsOn),
|
||||||
|
end: new Date(endsOn),
|
||||||
|
})
|
||||||
|
|
||||||
|
const dlIcal = () => icalendar.download()
|
||||||
|
return (
|
||||||
|
<ButtonRow justify="space-between">
|
||||||
|
<Button onClick={dlIcal}>{strings.en.subEvent}</Button>
|
||||||
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||||
|
<Button>{strings.en.eventDetails}</Button>
|
||||||
|
</a>
|
||||||
|
</ButtonRow>)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export default EpisodeCard
|
export default EpisodeCard
|
||||||
|
@ -1,43 +1,92 @@
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { colours } from '../../assets/theme'
|
import { colours, screenSizes } from '../../assets/theme'
|
||||||
import { Label, H2 } from '../Text'
|
import { Label, H2 } from '../Text'
|
||||||
import { Row as FlexRow, Column as FlexColumn } from '../Flex'
|
import Flexbox, { Row as FlexRow, Column as FlexColumn } from '../Flex'
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
|
|
||||||
export const Row = styled(FlexRow)`
|
export const ButtonRow = styled(Flexbox)`
|
||||||
|
width: 100%;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
button, a{
|
||||||
|
font-size: 16px;
|
||||||
|
width: 49%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: ${screenSizes.md}px) and (max-width: ${screenSizes.lg}px) {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
button, a {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
`
|
|
||||||
export const Column = styled(FlexColumn)`
|
|
||||||
`
|
`
|
||||||
|
|
||||||
export const Left = styled(FlexColumn)`
|
export const Left = styled(FlexColumn)`
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
|
width: 20vw;
|
||||||
|
|
||||||
|
@media screen and (max-width: ${screenSizes.md}px) {
|
||||||
|
width: 80vw;
|
||||||
|
margin-right: 0em;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const Center = styled(FlexColumn)`
|
export const Center = styled(FlexColumn)`
|
||||||
max-width: 60%;
|
@media screen and (max-width: ${screenSizes.md}px) {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const Title = styled(H2)`
|
export const Title = styled(H2)`
|
||||||
|
max-width: 60%;
|
||||||
|
|
||||||
|
@media screen and (max-width: ${screenSizes.md}px) {
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: ${screenSizes.sm}px) {
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const Right = styled.div`
|
export const Right = styled.div`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
|
@media screen and (max-width: ${screenSizes.md}px) {
|
||||||
|
position: relative;
|
||||||
|
top: 1em;
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
export const StyledButton = styled(Button)`
|
export const StyledButton = styled(Button)`
|
||||||
/* width: max-content; */
|
/* width: max-content; */
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
padding: 0.3em 2em;
|
padding: 0.3em 2em;
|
||||||
|
|
||||||
|
@media screen and (max-width: ${screenSizes.md}px) {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const Img = styled.div`
|
export const Img = styled.div`
|
||||||
background: url(${({ src }) => src});
|
background: url(${({ src }) => src});
|
||||||
width: 25vw;
|
|
||||||
/* height: 215px; */
|
/* height: 215px; */
|
||||||
|
width: 20vw;
|
||||||
padding-bottom: calc((9 / 16) * 100%);
|
padding-bottom: calc((9 / 16) * 100%);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
|
||||||
|
@media screen and (max-width: ${screenSizes.md}px) {
|
||||||
|
width: 80vw;
|
||||||
|
}
|
||||||
`
|
`
|
@ -1,6 +1,16 @@
|
|||||||
import { bool, number, oneOf } from 'prop-types'
|
import { bool, number, oneOf } from 'prop-types'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const Flexbox = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: ${props => (props.direction || 'row')};
|
||||||
|
justify-content: ${props => props.justify || 'flex-start'};
|
||||||
|
align-items: ${props => props.align || 'flex-start'};
|
||||||
|
${props => props.flex ? `
|
||||||
|
flex: ${props.flex};
|
||||||
|
` : ''}
|
||||||
|
`
|
||||||
|
|
||||||
export const Row = styled.div`
|
export const Row = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: ${props => (props.reverse ? 'row-reverse' : 'row')};
|
flex-direction: ${props => (props.reverse ? 'row-reverse' : 'row')};
|
||||||
@ -29,3 +39,5 @@ const propTypes = {
|
|||||||
|
|
||||||
Row.propTypes = propTypes
|
Row.propTypes = propTypes
|
||||||
Column.propTypes = propTypes
|
Column.propTypes = propTypes
|
||||||
|
|
||||||
|
export default Flexbox
|
8
src/components/StreamPreview/helpers.js
Normal file
8
src/components/StreamPreview/helpers.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import strings from '../../data/strings'
|
||||||
|
|
||||||
|
export const getLabel = (stream, isLive, isMinimized) => {
|
||||||
|
const currentLanguage = 'en'
|
||||||
|
const prefix = isLive ? strings[currentLanguage].nowPlaying : strings[currentLanguage].startingSoon
|
||||||
|
if (isMinimized) return `${prefix}: ${stream.title}`
|
||||||
|
return prefix
|
||||||
|
}
|
65
src/components/StreamPreview/index.js
Normal file
65
src/components/StreamPreview/index.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { Fragment, h } from 'preact'
|
||||||
|
import { useEffect, useRef } from 'preact/hooks'
|
||||||
|
import { PeerTubePlayer } from '@peertube/embed-api'
|
||||||
|
import { string } from 'prop-types'
|
||||||
|
import Link from '../Link'
|
||||||
|
import { Label } from '../Text'
|
||||||
|
import strings from '../../data/strings'
|
||||||
|
import { Box, Img, Iframe } from './styles'
|
||||||
|
import { colours, textSizes } from '../../assets/theme'
|
||||||
|
import { Row } from '../Flex'
|
||||||
|
import CrossSvg from '../Svg/Cross'
|
||||||
|
import { useUiStore } from '../../store'
|
||||||
|
import { getLabel } from './helpers'
|
||||||
|
import Chevron from '../Svg/Chevron'
|
||||||
|
// import { useEventApi } from '../../hooks/data'
|
||||||
|
|
||||||
|
const StreamPreview = ({ stream, isLive, ...rest }) => {
|
||||||
|
const currentLanguage = 'en'
|
||||||
|
const videoiFrame = useRef(null)
|
||||||
|
const ptVideo = useRef(null)
|
||||||
|
const { isMinimized, toggleMinimized } = useUiStore(store => ({ isMinimized: store.streamPreviewMinimized, toggleMinimized: store.toggleStreamPreviewMinimized }))
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const setupAndPlayVideo = async () => {
|
||||||
|
const player = new PeerTubePlayer(videoiFrame.current)
|
||||||
|
await player.ready
|
||||||
|
|
||||||
|
ptVideo.current = player
|
||||||
|
player.setVolume(0)
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
if (isLive) {
|
||||||
|
setupAndPlayVideo()
|
||||||
|
}
|
||||||
|
}, [isLive, isMinimized])
|
||||||
|
|
||||||
|
return stream ? (
|
||||||
|
<Box isMinimized={isMinimized}>
|
||||||
|
<Row justify="space-between">
|
||||||
|
<Label colour={colours.midnightDarker} size={textSizes.lg}>{getLabel(stream, isLive, isMinimized)}</Label>
|
||||||
|
{isMinimized ? <Chevron colour={colours.midnightDarker} size={14} onClick={toggleMinimized} /> : <CrossSvg colour={colours.midnightDarker} size={16} onClick={toggleMinimized} />}
|
||||||
|
</Row>
|
||||||
|
{!isMinimized ?
|
||||||
|
<Fragment>
|
||||||
|
{isLive ?
|
||||||
|
<Iframe
|
||||||
|
width="560"
|
||||||
|
height="315"
|
||||||
|
sandbox="allow-same-origin allow-scripts allow-popups"
|
||||||
|
autoplay="autoplay"
|
||||||
|
muted="muted"
|
||||||
|
src={`https://tv.undersco.re${stream.embedPath}?api=1&controls=false`}
|
||||||
|
frameBorder="0"
|
||||||
|
allowFullScreen
|
||||||
|
ref={videoiFrame}
|
||||||
|
/>
|
||||||
|
: <Img src={stream.image} />}
|
||||||
|
</Fragment> : null}
|
||||||
|
</Box>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default StreamPreview
|
57
src/components/StreamPreview/styles.js
Normal file
57
src/components/StreamPreview/styles.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import styled from 'styled-components'
|
||||||
|
import { colours, screenSizes } from '../../assets/theme'
|
||||||
|
|
||||||
|
export const Box = styled.div`
|
||||||
|
position: fixed;
|
||||||
|
bottom: ${props => props.isMinimized ? 0 : '2em'};
|
||||||
|
right: ${props => props.isMinimized ? 0 : '2em'};
|
||||||
|
background-color: ${colours.white};
|
||||||
|
padding: ${props => props.isMinimized ? '0.2em 0.2em' : '0.5em 0.5em'};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: ${props => props.isMinimized ? 0 : '0.5em'};
|
||||||
|
margin-right: ${props => props.isMinimized ? '0.5em' : 0};
|
||||||
|
font-size: ${props => props.isMinimized ? '15' : '21'}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: ${screenSizes.xs}px) {
|
||||||
|
bottom: 0em;
|
||||||
|
right: 0em;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export const Img = styled.div`
|
||||||
|
background: url(${({ src }) => src});
|
||||||
|
width: 16vw;
|
||||||
|
padding-bottom: calc((9 / 16) * 100%);
|
||||||
|
background-size: cover;
|
||||||
|
position: relative;
|
||||||
|
background-position: center;
|
||||||
|
|
||||||
|
@media screen and (max-width: ${screenSizes.xl}px) {
|
||||||
|
width: 20vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: ${screenSizes.lg}px) {
|
||||||
|
width: 25vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: ${screenSizes.md}px) {
|
||||||
|
width: 33vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: ${screenSizes.sm}px) {
|
||||||
|
width: 50vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: ${screenSizes.xs}px) {
|
||||||
|
width: 60vw;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Iframe = styled.iframe`
|
||||||
|
width: 20vw;
|
||||||
|
height: 11.2vw;
|
||||||
|
`
|
@ -1,8 +1,9 @@
|
|||||||
import { h } from 'preact'
|
import { h } from 'preact'
|
||||||
|
import { Svg } from './base'
|
||||||
import { svgPropTypes } from './proptypes'
|
import { svgPropTypes } from './proptypes'
|
||||||
|
|
||||||
const Chevron = ({ colour = 'inherit', size, ...rest }) => (
|
const Chevron = ({ colour = 'inherit', size, ...rest }) => (
|
||||||
<svg
|
<Svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
height={size}
|
height={size}
|
||||||
@ -15,7 +16,7 @@ const Chevron = ({ colour = 'inherit', size, ...rest }) => (
|
|||||||
d="M49.68 19.005l.002-.003 46.733 46.733-14.849 14.85-31.889-31.89-30.889 30.89L3.94 64.735 49.675 19l.005.005z"
|
d="M49.68 19.005l.002-.003 46.733 46.733-14.849 14.85-31.889-31.89-30.889 30.89L3.94 64.735 49.675 19l.005.005z"
|
||||||
clipRule="evenodd"
|
clipRule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</Svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
Chevron.propTypes = {
|
Chevron.propTypes = {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { h } from 'preact'
|
import { h } from 'preact'
|
||||||
|
import { Svg } from './base'
|
||||||
|
|
||||||
import { svgPropTypes } from './proptypes'
|
import { svgPropTypes } from './proptypes'
|
||||||
|
|
||||||
const Cross = ({ colour = 'inherit', size, ...rest }) => (
|
const Cross = ({ colour = 'inherit', size, ...rest }) => (
|
||||||
<svg viewBox="0 0 100 100" height={size} {...rest}>
|
<Svg viewBox="0 0 100 100" height={size} {...rest}>
|
||||||
<path
|
<path
|
||||||
stroke={colour}
|
stroke={colour}
|
||||||
strokeLinecap="none"
|
strokeLinecap="none"
|
||||||
@ -11,7 +12,7 @@ const Cross = ({ colour = 'inherit', size, ...rest }) => (
|
|||||||
strokeWidth="18"
|
strokeWidth="18"
|
||||||
d="M11.354 11.757l77.637 77.636m-77.627 0L89 11.757"
|
d="M11.354 11.757l77.637 77.636m-77.627 0L89 11.757"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</Svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
Cross.propTypes = {
|
Cross.propTypes = {
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { h } from 'preact'
|
import { h } from 'preact'
|
||||||
import { number, string } from 'prop-types'
|
import { number, string } from 'prop-types'
|
||||||
|
import { Svg } from './base'
|
||||||
|
|
||||||
const Play = ({ size = '24', colour = 'inherit', ...rest }) => (
|
const Play = ({ size = '24', colour = 'inherit', ...rest }) => (
|
||||||
<svg viewBox="0 0 24 24" height={size} width={size} {...rest}>
|
<Svg viewBox="0 0 24 24" height={size} width={size} {...rest}>
|
||||||
<path
|
<path
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
stroke={colour}
|
stroke={colour}
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
d="M2.12436,1.73205 C2.12436,0.96225 2.95769,0.481125 3.62436,0.866025 L21.6244,11.2583 C22.291,11.6432 22.291,12.6055 21.6244,12.9904 L3.62436,23.3827 C2.95769,23.7676 2.12436,23.2865 2.12436,22.5167 L2.12436,1.73205 Z"
|
d="M2.12436,1.73205 C2.12436,0.96225 2.95769,0.481125 3.62436,0.866025 L21.6244,11.2583 C22.291,11.6432 22.291,12.6055 21.6244,12.9904 L3.62436,23.3827 C2.95769,23.7676 2.12436,23.2865 2.12436,22.5167 L2.12436,1.73205 Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</Svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
Play.propTypes = {
|
Play.propTypes = {
|
||||||
|
7
src/components/Svg/base.js
Normal file
7
src/components/Svg/base.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
export const Svg = styled.svg`
|
||||||
|
${props => props.onClick ? `
|
||||||
|
cursor: pointer;
|
||||||
|
` : ''}
|
||||||
|
`
|
@ -2,7 +2,8 @@ export default {
|
|||||||
en: {
|
en: {
|
||||||
program: 'Program',
|
program: 'Program',
|
||||||
pastStream: 'Previous Episodes',
|
pastStream: 'Previous Episodes',
|
||||||
nowPlaying: 'Now playing',
|
nowPlaying: 'Currently streaming',
|
||||||
|
startingSoon: 'Starting soon',
|
||||||
noStreams: 'No upcoming streams, check back soon.',
|
noStreams: 'No upcoming streams, check back soon.',
|
||||||
underscoreTagline: ['LEAVE THE', 'SURVEILLANCE ECONOMY', '— TOGETHER.'],
|
underscoreTagline: ['LEAVE THE', 'SURVEILLANCE ECONOMY', '— TOGETHER.'],
|
||||||
streamDateFuture: 'Going live at: ',
|
streamDateFuture: 'Going live at: ',
|
||||||
@ -21,6 +22,7 @@ export default {
|
|||||||
nextStream: 'Next stream',
|
nextStream: 'Next stream',
|
||||||
episodes: 'episodes',
|
episodes: 'episodes',
|
||||||
today: 'today',
|
today: 'today',
|
||||||
tomorrow: 'tomorrow'
|
tomorrow: 'tomorrow',
|
||||||
|
eventDetails: 'Event details',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
import { h, render } from 'preact'
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import axios from 'axios'
|
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, useStreamStore } from '../store/index'
|
||||||
import 'regenerator-runtime/runtime'
|
import 'regenerator-runtime/runtime'
|
||||||
|
import { useInterval } from './timerHooks'
|
||||||
|
import { secondsToMilliseconds } from 'date-fns'
|
||||||
|
|
||||||
export const useEventCalendar = () => {
|
export const useEventCalendar = () => {
|
||||||
const [data, setData] = useState([])
|
const [data, setData] = useState([])
|
||||||
@ -112,6 +115,7 @@ export const useEventApi = () => {
|
|||||||
|
|
||||||
|
|
||||||
setData(responseData)
|
setData(responseData)
|
||||||
|
console.log({ data: responseData })
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,3 +126,30 @@ export const useEventApi = () => {
|
|||||||
|
|
||||||
return { loading, data }
|
return { loading, data }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const usePeertubeApi = async () => {
|
||||||
|
const { currentStream, setCurrentStream, setStreamIsLive, streamIsLive } = useStreamStore(store => store)
|
||||||
|
|
||||||
|
if (!currentStream) return
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
if (!currentStream.peertubeId) return
|
||||||
|
const { peertubeId } = currentStream
|
||||||
|
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { state, embedPath }
|
||||||
|
} = await axios.get(`https://tv.undersco.re/api/v1/videos/${peertubeId}`)
|
||||||
|
|
||||||
|
setStreamIsLive(state.id === 1)
|
||||||
|
setCurrentStream({ ...currentStream, embedPath })
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
fetchData()
|
||||||
|
}, streamIsLive ? secondsToMilliseconds(15) : secondsToMilliseconds(1))
|
||||||
|
}
|
@ -8,7 +8,8 @@ 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'
|
import { useStreamStore, useTheme, useUiStore } from '../../store'
|
||||||
|
import StreamPreview from '../../components/StreamPreview'
|
||||||
|
|
||||||
const Page = ({ children, title = '', description, metaImg, backTo, noindex, withHeader = true, theme = defaultTheme }) => {
|
const Page = ({ children, title = '', description, metaImg, backTo, noindex, withHeader = true, theme = defaultTheme }) => {
|
||||||
const { setTheme } = useTheme(store => store)
|
const { setTheme } = useTheme(store => store)
|
||||||
|
@ -14,6 +14,7 @@ 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'
|
||||||
import { slugify } from '../../helpers/string'
|
import { slugify } from '../../helpers/string'
|
||||||
|
import { ButtonsRows } from '../../components/EpisodeCard'
|
||||||
|
|
||||||
export const TrailerContainer = styled.div`
|
export const TrailerContainer = styled.div`
|
||||||
height: 22em;
|
height: 22em;
|
||||||
@ -143,6 +144,8 @@ export const EpisodeCard = ({
|
|||||||
image,
|
image,
|
||||||
description,
|
description,
|
||||||
beginsOn,
|
beginsOn,
|
||||||
|
endsOn,
|
||||||
|
url,
|
||||||
hasPassed,
|
hasPassed,
|
||||||
videoUrl,
|
videoUrl,
|
||||||
onClickButton,
|
onClickButton,
|
||||||
@ -180,15 +183,7 @@ export const EpisodeCard = ({
|
|||||||
{hasPassed ? (
|
{hasPassed ? (
|
||||||
<Button onClick={onClickButton}>{translations.en.watchEpisode}</Button>
|
<Button onClick={onClickButton}>{translations.en.watchEpisode}</Button>
|
||||||
) : (
|
) : (
|
||||||
<a
|
<ButtonsRows title={title} description={description} beginsOn={beginsOn} endsOn={endsOn} url={url} />
|
||||||
href={
|
|
||||||
hasPassed
|
|
||||||
? videoUrl
|
|
||||||
: `webcal://cloud.undersco.re/remote.php/dav/public-calendars/${config.calendarId}/?export`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button>{translations.en.subEvent}</Button>
|
|
||||||
</a>
|
|
||||||
)}
|
)}
|
||||||
</VCWrapper>
|
</VCWrapper>
|
||||||
)
|
)
|
||||||
|
@ -4,6 +4,8 @@ import { defaultTheme } from '../assets/theme'
|
|||||||
export const useSeriesStore = create((set, get) => ({
|
export const useSeriesStore = create((set, get) => ({
|
||||||
series: {},
|
series: {},
|
||||||
episodes: [],
|
episodes: [],
|
||||||
|
|
||||||
|
// Methods
|
||||||
setSeries: series => set({ series }),
|
setSeries: series => set({ series }),
|
||||||
setEpisodes: () => {
|
setEpisodes: () => {
|
||||||
if (get().series) {
|
if (get().series) {
|
||||||
@ -16,11 +18,28 @@ export const useSeriesStore = create((set, get) => ({
|
|||||||
|
|
||||||
export const [useTheme] = create(set => ({
|
export const [useTheme] = create(set => ({
|
||||||
theme: defaultTheme,
|
theme: defaultTheme,
|
||||||
|
|
||||||
|
// Methods
|
||||||
setTheme: (theme) => set({ theme }),
|
setTheme: (theme) => set({ theme }),
|
||||||
setDefaultTheme: () => set({ theme: defaultTheme })
|
setDefaultTheme: () => set({ theme: defaultTheme })
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const [useUiStore] = create((set, get) => ({
|
export const [useUiStore] = create((set, get) => ({
|
||||||
mobileMenuOpen: false,
|
mobileMenuOpen: false,
|
||||||
|
streamPreviewMinimized: false,
|
||||||
|
streamActive: false,
|
||||||
|
|
||||||
|
// Methods
|
||||||
toggleMobileMenu: () => set({ mobileMenuOpen: !get().mobileMenuOpen }),
|
toggleMobileMenu: () => set({ mobileMenuOpen: !get().mobileMenuOpen }),
|
||||||
|
toggleStreamPreviewMinimized: () => set({ streamPreviewMinimized: !get().streamPreviewMinimized }),
|
||||||
|
toggleStreamActive: () => set({ streamActive: !get().streamActive }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const [useStreamStore] = create((set) => ({
|
||||||
|
currentStream: null,
|
||||||
|
streamIsLive: false,
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
setCurrentStream: (currentStream) => set({ currentStream }),
|
||||||
|
setStreamIsLive: (streamIsLive) => set({ streamIsLive }),
|
||||||
}))
|
}))
|
Loading…
Reference in New Issue
Block a user