diff --git a/index.js b/index.js index 5f45bc8..2e597a9 100644 --- a/index.js +++ b/index.js @@ -1,27 +1,56 @@ -import { h, render } from 'preact' -import { useState } from 'preact/hooks' +import { Fragment, h, render } from 'preact' +import { useEffect, useState } from 'preact/hooks' import { ThemeProvider } from 'styled-components' 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 SeriesPage from './src/pages/SeriesPage' -import { useEventApi } from './src/hooks/data' -import { useTheme } from './src/store' +import { useEventApi, usePeertubeApi } from './src/hooks/data' +import { useStreamStore, useTheme } from './src/store' import { useTimeout } from './src/hooks/timerHooks' import LoaderLayout from './src/pages/LoaderLayout' import FourOhFour from './src/pages/404' import Series from './src/pages/Series' import Program from './src/pages/Program' +import StreamPreview from './src/components/StreamPreview' const App = () => { const { theme } = useTheme((store) => store) const { data, loading: eventsLoading } = useEventApi() const [minLoadTimePassed, setMinTimeUp] = useState(false) + const { setCurrentStream, currentStream, streamIsLive } = useStreamStore(store => store) + usePeertubeApi(data.episodes) useTimeout(() => { setMinTimeUp(true) }, 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 }) const seriesData = data.series ? Object.values(data.series) : [] @@ -32,20 +61,23 @@ const App = () => { {!seriesData.length || eventsLoading || !minLoadTimePassed ? ( ) : ( - - - - - - {seriesData.length ? seriesData.map(series => ( - - - )) : null} - - - - - + + + + + + + {seriesData.length ? seriesData.map(series => ( + + + )) : null} + + + + + + + )} ) } diff --git a/package.json b/package.json index 5aefdaf..998eaa7 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "axios": "^0.21.1", "date-fns": "^2.19.0", "date-fns-tz": "^1.1.4", + "datebook": "^7.0.7", "dotenv": "^10.0.0", "ical": "^0.8.0", "ical.js": "^1.4.0", diff --git a/src/assets/theme/index.js b/src/assets/theme/index.js index 28fb8ed..aaea9ca 100644 --- a/src/assets/theme/index.js +++ b/src/assets/theme/index.js @@ -31,6 +31,7 @@ export const screenSizes = { sm: 800, md: 1000, lg: 1500, + xl: 1700, } export const defaultTheme = { diff --git a/src/components/EpisodeCard/index.js b/src/components/EpisodeCard/index.js index 4ec8b3b..3d4b86d 100644 --- a/src/components/EpisodeCard/index.js +++ b/src/components/EpisodeCard/index.js @@ -1,26 +1,37 @@ import { h } from 'preact' +import { ICalendar } from 'datebook' import { format } from 'date-fns' +import striptags from 'striptags' import Link from '../Link' import { H2, H3, Label } from '../Text' import strings from '../../data/strings' import { andList } from '../../helpers/string' -import { colours } from '../../assets/theme' -import { Img, Left, Right, Center, Title, Row, Column, StyledButton as Button } from './styles' +import { colours, screenSizes } from '../../assets/theme' +import { Img, Left, Right, Center, Title, ButtonRow, StyledButton as Button } from './styles' 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 series = seriesId ? allSeries.filter(({ id }) => id === seriesId)[0] : {} 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 ( - + - - + + + +
{title} @@ -34,9 +45,28 @@ const EpisodeCard = ({ image, title, seriesId, beginsOn, id, ...rest }) => { - + ) } +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 ( + + + + + + ) +} + export default EpisodeCard diff --git a/src/components/EpisodeCard/styles.js b/src/components/EpisodeCard/styles.js index efb3a99..f9ebefd 100644 --- a/src/components/EpisodeCard/styles.js +++ b/src/components/EpisodeCard/styles.js @@ -1,43 +1,92 @@ import styled from 'styled-components' -import { colours } from '../../assets/theme' +import { colours, screenSizes } from '../../assets/theme' 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' -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)` margin-right: 1em; + width: 20vw; + +@media screen and (max-width: ${screenSizes.md}px) { + width: 80vw; + margin-right: 0em; + } ` export const Center = styled(FlexColumn)` - max-width: 60%; -` +@media screen and (max-width: ${screenSizes.md}px) { + order: 2; +} + ` 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; -` + ` export const Right = styled.div` flex: 1; text-align: right; + + @media screen and (max-width: ${screenSizes.md}px) { + position: relative; + top: 1em; + order: 1; + } ` export const StyledButton = styled(Button)` /* width: max-content; */ margin-top: 0.5em; padding: 0.3em 2em; + + @media screen and (max-width: ${screenSizes.md}px) { + margin: 0.5em 0; + } ` export const Img = styled.div` background: url(${({ src }) => src}); - width: 25vw; /* height: 215px; */ + width: 20vw; padding-bottom: calc((9 / 16) * 100%); background-size: cover; position: relative; background-position: center; + + @media screen and (max-width: ${screenSizes.md}px) { + width: 80vw; + } ` \ No newline at end of file diff --git a/src/components/Flex/index.js b/src/components/Flex/index.js index 0f352d7..6fd0853 100644 --- a/src/components/Flex/index.js +++ b/src/components/Flex/index.js @@ -1,6 +1,16 @@ import { bool, number, oneOf } from 'prop-types' 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` display: flex; flex-direction: ${props => (props.reverse ? 'row-reverse' : 'row')}; @@ -28,4 +38,6 @@ const propTypes = { } Row.propTypes = propTypes -Column.propTypes = propTypes \ No newline at end of file +Column.propTypes = propTypes + +export default Flexbox \ No newline at end of file diff --git a/src/components/StreamPreview/helpers.js b/src/components/StreamPreview/helpers.js new file mode 100644 index 0000000..9ef0d36 --- /dev/null +++ b/src/components/StreamPreview/helpers.js @@ -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 +} \ No newline at end of file diff --git a/src/components/StreamPreview/index.js b/src/components/StreamPreview/index.js new file mode 100644 index 0000000..76f5b4a --- /dev/null +++ b/src/components/StreamPreview/index.js @@ -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 ? ( + + + + {isMinimized ? : } + + {!isMinimized ? + + {isLive ? +