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 ?
+
+ : }
+ : null}
+
+ ) : null
+}
+
+
+export default StreamPreview
diff --git a/src/components/StreamPreview/styles.js b/src/components/StreamPreview/styles.js
new file mode 100644
index 0000000..8c01875
--- /dev/null
+++ b/src/components/StreamPreview/styles.js
@@ -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;
+`
\ No newline at end of file
diff --git a/src/components/Svg/Chevron.js b/src/components/Svg/Chevron.js
index a731043..571655b 100644
--- a/src/components/Svg/Chevron.js
+++ b/src/components/Svg/Chevron.js
@@ -1,8 +1,9 @@
import { h } from 'preact'
+import { Svg } from './base'
import { svgPropTypes } from './proptypes'
const Chevron = ({ colour = 'inherit', size, ...rest }) => (
-
+
)
Chevron.propTypes = {
diff --git a/src/components/Svg/Cross.js b/src/components/Svg/Cross.js
index c0a6026..5860d7f 100644
--- a/src/components/Svg/Cross.js
+++ b/src/components/Svg/Cross.js
@@ -1,9 +1,10 @@
import { h } from 'preact'
+import { Svg } from './base'
import { svgPropTypes } from './proptypes'
const Cross = ({ colour = 'inherit', size, ...rest }) => (
-
)
Cross.propTypes = {
diff --git a/src/components/Svg/Play.js b/src/components/Svg/Play.js
index 410606a..00b25de 100644
--- a/src/components/Svg/Play.js
+++ b/src/components/Svg/Play.js
@@ -1,15 +1,16 @@
import { h } from 'preact'
import { number, string } from 'prop-types'
+import { Svg } from './base'
const Play = ({ size = '24', colour = 'inherit', ...rest }) => (
-
+
-
+
)
Play.propTypes = {
diff --git a/src/components/Svg/base.js b/src/components/Svg/base.js
new file mode 100644
index 0000000..348e108
--- /dev/null
+++ b/src/components/Svg/base.js
@@ -0,0 +1,7 @@
+import styled from 'styled-components'
+
+export const Svg = styled.svg`
+ ${props => props.onClick ? `
+ cursor: pointer;
+ ` : ''}
+`
\ No newline at end of file
diff --git a/src/data/strings.js b/src/data/strings.js
index ab26e2a..d537afd 100644
--- a/src/data/strings.js
+++ b/src/data/strings.js
@@ -2,7 +2,8 @@ export default {
en: {
program: 'Program',
pastStream: 'Previous Episodes',
- nowPlaying: 'Now playing',
+ nowPlaying: 'Currently streaming',
+ startingSoon: 'Starting soon',
noStreams: 'No upcoming streams, check back soon.',
underscoreTagline: ['LEAVE THE', 'SURVEILLANCE ECONOMY', '— TOGETHER.'],
streamDateFuture: 'Going live at: ',
@@ -21,6 +22,7 @@ export default {
nextStream: 'Next stream',
episodes: 'episodes',
today: 'today',
- tomorrow: 'tomorrow'
+ tomorrow: 'tomorrow',
+ eventDetails: 'Event details',
},
}
diff --git a/src/hooks/data.js b/src/hooks/data.js
index 2e30087..bf3ac8b 100644
--- a/src/hooks/data.js
+++ b/src/hooks/data.js
@@ -1,9 +1,12 @@
+import { h, render } from 'preact'
import { useEffect, useState } from 'preact/hooks'
import axios from 'axios'
import ICAL from 'ical.js'
import config from '../data/config'
-import { useSeriesStore } from '../store/index'
+import { useSeriesStore, useStreamStore } from '../store/index'
import 'regenerator-runtime/runtime'
+import { useInterval } from './timerHooks'
+import { secondsToMilliseconds } from 'date-fns'
export const useEventCalendar = () => {
const [data, setData] = useState([])
@@ -112,6 +115,7 @@ export const useEventApi = () => {
setData(responseData)
+ console.log({ data: responseData })
setLoading(false)
}
}
@@ -121,4 +125,31 @@ export const useEventApi = () => {
}, [])
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))
}
\ No newline at end of file
diff --git a/src/layouts/Page/index.js b/src/layouts/Page/index.js
index 80f51cb..ea1ea45 100644
--- a/src/layouts/Page/index.js
+++ b/src/layouts/Page/index.js
@@ -8,7 +8,8 @@ import Header, { NavigationModal as MenuModal } from '../../components/Header'
import { capitaliseFirstLetter } from '../../helpers/string'
import { defaultTheme } from '../../assets/theme'
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 { setTheme } = useTheme(store => store)
diff --git a/src/pages/SeriesPage/styles.js b/src/pages/SeriesPage/styles.js
index 50971b5..4efe53e 100644
--- a/src/pages/SeriesPage/styles.js
+++ b/src/pages/SeriesPage/styles.js
@@ -14,6 +14,7 @@ import { H1, H2, Span, Label } from '../../components/Text'
import Link from '../../components/Link'
import Button from '../../components/Button'
import { slugify } from '../../helpers/string'
+import { ButtonsRows } from '../../components/EpisodeCard'
export const TrailerContainer = styled.div`
height: 22em;
@@ -143,6 +144,8 @@ export const EpisodeCard = ({
image,
description,
beginsOn,
+ endsOn,
+ url,
hasPassed,
videoUrl,
onClickButton,
@@ -180,15 +183,7 @@ export const EpisodeCard = ({
{hasPassed ? (
) : (
-
-
-
+
)}
)
diff --git a/src/store/index.js b/src/store/index.js
index bba022a..0c89986 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -4,6 +4,8 @@ import { defaultTheme } from '../assets/theme'
export const useSeriesStore = create((set, get) => ({
series: {},
episodes: [],
+
+ // Methods
setSeries: series => set({ series }),
setEpisodes: () => {
if (get().series) {
@@ -16,11 +18,28 @@ export const useSeriesStore = create((set, get) => ({
export const [useTheme] = create(set => ({
theme: defaultTheme,
+
+ // Methods
setTheme: (theme) => set({ theme }),
setDefaultTheme: () => set({ theme: defaultTheme })
}))
export const [useUiStore] = create((set, get) => ({
mobileMenuOpen: false,
+ streamPreviewMinimized: false,
+ streamActive: false,
+
+ // Methods
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 }),
}))
\ No newline at end of file