diff --git a/app.js b/app.js index 6f1169f..10f95af 100644 --- a/app.js +++ b/app.js @@ -1,14 +1,13 @@ import { h } from 'preact' -import axios from 'axios' -// eslint-disable-next-line import/no-extraneous-dependencies +import { useState, useEffect } from 'preact/hooks' import 'regenerator-runtime/runtime' -import { useEffect, useState } from 'preact/hooks' +import axios from 'axios' import Video from './src/components/Video' import config from './src/data/config' import Info from './src/components/Info' -import { useFetch } from './src/assets/hooks/calendar' -import { P } from './src/components/Text' +import { useCalendar } from './src/hooks/data' +import { useTimeout } from './src/hooks/timerHooks' // const appStates = [ // 'noStream', @@ -19,13 +18,58 @@ import { P } from './src/components/Text' export default () => { const [isPlaying, setIsPlaying] = useState(false) const [videoUrl, setVideoUrl] = useState(null) - // const [feedData, setFeedData] = useState([]) + const [feedData, setFeedData] = useState([]) + const [minLoadTimePassed, setMinTimeUp] = useState(false) + const { data, loading } = useCalendar() - const { data: feedData, loading } = useFetch(`${config.calendar}`) + useTimeout(() => { + setMinTimeUp(true) + }, 1500) + + useEffect(() => { + if (data && data.length) { + data.forEach(async (calItem, index) => { + if (calItem.url) { + const id = calItem.url.val.split('/').pop() + + const { + data: { + account, + category, + channel, + embedPath, + language, + name, + state, + previewPath, + views, + }, + } = await axios.get(`https://tv.undersco.re/api/v1/videos/${id}`) + + const item = { + name, + account, + category, + channel, + description: calItem.description, + embedPath, + language, + state, + previewPath, + views, + start: calItem.start, + end: calItem.end, + id, + } + setFeedData(arr => [...arr, item]) + } + }) + } + }, [data]) return (
- {/* {false ? ( + {false ? (
) } diff --git a/package.json b/package.json index cca5985..9dd4d37 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "dependencies": { "@peertube/embed-api": "^0.0.4", "axios": "^0.21.1", + "date-fns": "^2.19.0", + "ical": "^0.8.0", "preact": "^10.5.12", "prop-types": "^15.7.2", "styled-components": "^5.2.1" diff --git a/src/assets/fonts/Karla/Karla-Regular.ttf b/src/assets/fonts/Karla/Karla-Regular.ttf new file mode 100644 index 0000000..a08cc9b Binary files /dev/null and b/src/assets/fonts/Karla/Karla-Regular.ttf differ diff --git a/src/assets/hooks/calendar.js b/src/assets/hooks/calendar.js deleted file mode 100644 index b2c88b8..0000000 --- a/src/assets/hooks/calendar.js +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect, useState } from 'preact/hooks' -import axios from 'axios' - -export const useFetch = url => { - const [data, setData] = useState(null) - const [loading, setLoading] = useState(true) - - async function fetchData() { - setLoading(true) - - const { data: responseData } = await axios.get(url) - console.log('url', url) - console.log('responseData', responseData) - setData(responseData) - setLoading(false) - } - - useEffect(() => { - fetchData() - }, []) - - return { loading, data } -} diff --git a/src/assets/js/index.js b/src/assets/js/index.js deleted file mode 100644 index 7bc7711..0000000 --- a/src/assets/js/index.js +++ /dev/null @@ -1,68 +0,0 @@ -import { PeerTubePlayer } from '@peertube/embed-api'; -import 'regenerator-runtime/runtime'; -import { toggleVideoPlaying } from './video-controls'; -import { getVideoData } from './scheduling'; - -const streamData = getVideoData(); - -const videoWrapper = document.getElementById('video-wrapper'); -const overlay = document.getElementById('overlay'); -const videoiFrame = document.querySelector('iframe.video-player'); - -const video = { - isPlaying: false, - overlayVisible: false, -}; - -export const setUpPlayer = async () => { - const player = new PeerTubePlayer(videoiFrame); - - await player.ready; - - onPlayerReady(player); - app(); -}; - -const app = () => { - if (streamData && streamData) { - console.log({ streamData }); - } -}; - -const onPlayerReady = (player) => { - videoWrapper.addEventListener('mousemove', showOverlay); - - videoWrapper.addEventListener('click', () => { - toggleVideoPlaying({ - player, - video, - onPlay: () => { - hideOverlay(); - }, - onPause: () => { - showOverlay(); - }, - }); - video.isPlaying = !video.isPlaying; - }); -}; - -const showOverlay = () => { - if (!video.overlayVisible) { - videoWrapper.classList.add('active'); - overlay.classList.add('active'); - - setTimeout(hideOverlay, 2000); - } - video.overlayVisible = true; -}; - -const hideOverlay = () => { - if (video.isPlaying) { - video.overlayVisible = false; - videoWrapper.classList.remove('active'); - overlay.classList.remove('active'); - } -}; - -setUpPlayer(); diff --git a/src/assets/js/scheduling.js b/src/assets/js/scheduling.js deleted file mode 100644 index 44a8fa2..0000000 --- a/src/assets/js/scheduling.js +++ /dev/null @@ -1,12 +0,0 @@ -import axios from 'axios'; -import 'regenerator-runtime/runtime'; -import config from '../../data/conf.json'; - -export const getVideoData = async () => { - console.log({ config }); - const { data } = await axios.get(`https://tv.undersco.re/api/v1/videos/${config.next_stream.peertube_id}`); - - console.log(data); - - return data; -}; diff --git a/src/assets/js/video-controls.js b/src/assets/js/video-controls.js deleted file mode 100644 index a02933e..0000000 --- a/src/assets/js/video-controls.js +++ /dev/null @@ -1,15 +0,0 @@ -export const toggleVideoPlaying = ({ player, video, onPlay, onPause }) => { - console.log('video', video); - - if (video.isPlaying) { - player.pause(); - if (typeof onPause === 'function') { - onPause(); - } - } else { - player.play(); - if (typeof onPlay === 'function') { - onPlay(); - } - } -}; diff --git a/src/assets/styles/fontface.scss b/src/assets/styles/fontface.scss index 8f5fde1..836a2b0 100644 --- a/src/assets/styles/fontface.scss +++ b/src/assets/styles/fontface.scss @@ -1,3 +1,11 @@ +/* Karla Regular */ +@font-face { + font-family: 'Karla'; + src: url('../fonts/Karla/Karla-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; +} + /* Karla Medium */ @font-face { font-family: 'Karla'; diff --git a/src/components/Info/helpers.js b/src/components/Info/helpers.js new file mode 100644 index 0000000..4b9b9e6 --- /dev/null +++ b/src/components/Info/helpers.js @@ -0,0 +1,4 @@ +export const sortData = data => + Object.values(data) + .filter(feedItem => feedItem.type === 'VEVENT') + .sort((a, b) => new Date(a.start) - new Date(b.start)) diff --git a/src/components/Info/index.js b/src/components/Info/index.js index 46d1ad1..60a06f2 100644 --- a/src/components/Info/index.js +++ b/src/components/Info/index.js @@ -1,42 +1,51 @@ -import { h } from 'preact' +/* eslint-disable react/prop-types */ +import { h, Fragment } from 'preact' import { useEffect, useRef, useState } from 'preact/hooks' +import { isBefore } from 'date-fns' -import { H1, H2, P } from '../Text' -import { - Wrapper, - PositionedLogo as Logo, - TaglineContainer, - Top, -} from './styles' +import { P } from '../Text' import translations from '../../data/strings' import InfoLayout from '../InfoLayout' +import { VideoCard, Title, InfoContent } from './styles' -const allowedChannels = [7, 4] // ReclaimFutures, NDC +const Info = ({ data, loading }) => { + const now = new Date() + const pastStreams = + data && data.length + ? data.filter(feeditem => isBefore(new Date(feeditem.end), now)) + : [] -const Info = ({ data }) => { - // const [feed, setFeed] = useState([]) - - // useEffect(() => { - // setFeed(sortData(data)) - // }, [data]) - - useEffect(() => { - console.log({ data }) - }, [data]) + const futureStreams = + data && data.length + ? data.filter(feeditem => isBefore(now, new Date(feeditem.start))) + : [] return ( - -

ding dong

+ + {!loading && ( + + {futureStreams.map(feeditem => ( + + ))} + {pastStreams.length ? ( + + {translations.en.pastStream}: + {pastStreams.map(feeditem => ( + + ))} + + ) : null} + + )} ) } -const sortData = data => { - // if (!data || data?.length === 0) return - // console.log('data', data) - // return data.filter(feedItem => { - // return allowedChannels.includes(feedItem.channel.id) && feedItem.isLive - // }) -} - export default Info diff --git a/src/components/Info/styles.js b/src/components/Info/styles.js index 0b403ea..ebcef2a 100644 --- a/src/components/Info/styles.js +++ b/src/components/Info/styles.js @@ -1,6 +1,12 @@ +import { format } from 'date-fns' +import { h, Fragment } from 'preact' import styled from 'styled-components' import { colours } from '../../assets/theme' +import config from '../../data/config' import Logo from '../Logo' +import translations from '../../data/strings' + +import { P, H1, H2, Span, Label } from '../Text' export const Wrapper = styled.div` height: 100vh; @@ -27,6 +33,10 @@ export const Wrapper = styled.div` export const Top = styled.div`` +export const InfoContent = styled.div` + padding-bottom: 1em; +` + export const PositionedLogo = styled(Logo)` margin-bottom: 64px; ` @@ -36,3 +46,54 @@ export const TaglineContainer = styled.div` margin-top: 32px; } ` + +export const Title = styled(H1)` + margin: 0.3em 0; +` + +const VCWrapper = styled.div` + max-width: 600px; + margin-bottom: 3em; + border-left: 7px solid ${colours.midnightDarker}; + padding-left: 1em; +` + +const VCImg = styled.img` + width: 100%; +` + +const ItemTitle = styled(H2)` + margin-bottom: 0.3em; +` + +const DateLabel = styled(Label)` + margin: 1em 0; + display: block; +` + +export const VideoCard = ({ + name, + description, + start, + end, + previewPath, + hasPassed, +}) => { + return ( + + {name} + + + {`${ + hasPassed + ? translations.en.streamDatePast + : translations.en.streamDateFuture + }`} + + {format(new Date(start), 'hh:mm dd/MM/yy')} + + +

{description}

+
+ ) +} diff --git a/src/components/InfoLayout/index.js b/src/components/InfoLayout/index.js index ab3ec95..12ccc79 100644 --- a/src/components/InfoLayout/index.js +++ b/src/components/InfoLayout/index.js @@ -1,34 +1,53 @@ import { h } from 'preact' import { useEffect, useRef, useState } from 'preact/hooks' +import { bool, string } from 'prop-types' -import { H1, H2, P } from '../Text' +import { H1, H2 } from '../Text' import { Wrapper, PositionedLogo as Logo, TaglineContainer, - Top, + Title, Content, + Fade, } from './styles' import translations from '../../data/strings' import { colours } from '../../assets/theme' +import Loader from '../Loader' +import { useTimeout } from '../../hooks/timerHooks' -const InfoLayout = ({ title, children }) => { +const InfoLayout = ({ title, children, loading }) => { return ( - + + + -

{title}

+ {loading ? ( +
+ +
+ ) : ( + {title} + )} {children}
{translations && translations.en.underscoreTagline.map(line => ( -

{line}

+

+ {line} +

))}
) } +InfoLayout.propTypes = { + title: string, + loading: bool, +} + export default InfoLayout diff --git a/src/components/InfoLayout/styles.js b/src/components/InfoLayout/styles.js index e2a385a..01b9afa 100644 --- a/src/components/InfoLayout/styles.js +++ b/src/components/InfoLayout/styles.js @@ -1,5 +1,8 @@ import styled from 'styled-components' import { colours } from '../../assets/theme' + +import { H1 } from '../Text' + import Logo from '../Logo' export const Wrapper = styled.div` @@ -41,14 +44,52 @@ export const Wrapper = styled.div` export const Top = styled.div` width: 50%; ` -export const Content = styled.div`` -export const PositionedLogo = styled(Logo)` +const gradientColour = '#F8E5E2' +const getGradient = direction => + `linear-gradient(to ${direction}, ${gradientColour}ee 0%,${gradientColour}00 100%);` + +// prettier-ignore +export const Fade = styled.div` + width: 100%; + background-color: linear; position: fixed; - top: 2em; + padding: 2em 0 1em 2em; + top: 0; + left: 0; + background: ${getGradient('bottom')}; ` +export const Title = styled(H1)` + margin: 0.5em 0; +` + +export const Content = styled.div` + /* margin-bottom: 3em; */ +` + +export const PositionedLogo = styled(Logo)`` export const TaglineContainer = styled.div` + background: ${getGradient('top')}; position: fixed; - bottom: 2em; + bottom: 0em; + padding-bottom: 0.5em; + right: 1em; + pointer-events: none; + + @media screen and (max-width: 1200px) { + width: 100vw; + right: auto; + left: 1.5em; + h1 { + font-size: 32px; + text-align: left; + } + } + + @media screen and (max-width: 800px) { + h1 { + font-size: 24px; + } + } ` diff --git a/src/components/Loader/index.js b/src/components/Loader/index.js new file mode 100644 index 0000000..b8b7ed6 --- /dev/null +++ b/src/components/Loader/index.js @@ -0,0 +1,38 @@ +import { h } from 'preact' +import { useRef, useState } from 'preact/hooks' +import { useInterval, useTimeout } from '../../hooks/timerHooks' +import { colours } from '../../assets/theme' +import { H1 } from '../Text' + +// const symbols = ['⌏', '⌎', '⌌', '⌍'] + +const Loader = ({ + active = true, + offset = 0, + animation = [':..', '.:.', '..:', '...'], +}) => { + const [text, setText] = useState('.') + const arrayPosition = useRef(offset) + const rate = 350 + + useInterval( + () => { + setText(animation[arrayPosition.current]) + + if (arrayPosition.current === animation.length - 1) { + arrayPosition.current = 0 + } else { + arrayPosition.current += 1 + } + }, + active ? rate : null + ) + + return ( +

+ {text} +

+ ) +} + +export default Loader diff --git a/src/assets/js/chat.js b/src/components/Loader/styles.js similarity index 100% rename from src/assets/js/chat.js rename to src/components/Loader/styles.js diff --git a/src/components/Text/index.js b/src/components/Text/index.js index 1d97be1..5f89fb3 100644 --- a/src/components/Text/index.js +++ b/src/components/Text/index.js @@ -25,7 +25,7 @@ const Text = ({ weight={weight} lineHeight={lineHeight} $fontFamily={fontFamily} - size={size} + $size={size} selectable={selectable} {...rest} > @@ -66,7 +66,7 @@ export const H1 = ({ children, ...rest }) => { ( ( export const P = ({ children, ...rest }) => ( @@ -102,15 +102,15 @@ export const P = ({ children, ...rest }) => ( ) export const Span = ({ children, ...rest }) => ( - + {children} ) -export const Label = ({ children, ...rest }) => ( +export const Label = ({ children, size, ...rest }) => ( { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + + async function fetchData() { + setLoading(true) + + const { data: responseData } = await axios.get(`${config.calendar}`) + const streamsData = Object.values(ical.parseICS(responseData)) + .filter(feedItem => feedItem.type === 'VEVENT') + .sort((a, b) => new Date(a.start) - new Date(b.start)) + setData(streamsData) + setLoading(false) + } + + useEffect(() => { + fetchData() + }, []) + + return { loading, data } +} + +// export const useCalendar = () => { +// const [data, setData] = useState(null) +// const [loading, setLoading] = useState(true) + +// async function fetchData() { +// setLoading(true) + +// const { data: responseData } = await axios.get(`${config.calendar}`) +// const streamsData = Object.values(ical.parseICS(responseData)) +// .filter(feedItem => feedItem.type === 'VEVENT') +// .sort((a, b) => new Date(a.start) - new Date(b.start)) +// setData(streamsData) +// setLoading(false) +// } + +// useEffect(() => { +// fetchData() +// }, []) + +// return { loading, data } +// } +// useEffect(() => { +// const feedPromise = +// data && +// data.map(async feedItem => { +// if (feedItem.url) { +// const id = feedItem.url.val.split('/').pop() + +// const { +// data: { +// account, +// category, +// channel, +// description, +// embedPath, +// language, +// name, +// state, +// previewPath, +// views, +// }, +// } = await axios.get(`https://tv.undersco.re/api/v1/videos/${id}`) + +// const item = { +// name, +// account, +// category, +// channel, +// description, +// embedPath, +// language, +// state, +// previewPath, +// views, +// start: feedItem.start, +// end: feedItem.end, +// } + +// console.log(item) + +// return item +// } +// return null +// }) + +// feedPromise.then(result => { +// console.log(result) +// }) +// }, [data]) diff --git a/src/hooks/timerHooks.js b/src/hooks/timerHooks.js new file mode 100644 index 0000000..4383a31 --- /dev/null +++ b/src/hooks/timerHooks.js @@ -0,0 +1,38 @@ +/* eslint-disable consistent-return */ +import { useEffect, useRef } from 'preact/hooks' + +export const useInterval = (callback, interval) => { + const savedCallback = useRef() + + useEffect(() => { + savedCallback.current = callback + }, [callback]) + + useEffect(() => { + function tick() { + savedCallback.current() + } + if (interval !== null) { + const id = setInterval(tick, interval) + return () => clearInterval(id) + } + }, [interval]) +} + +export const useTimeout = (callback, timeout) => { + const savedCallback = useRef() + + useEffect(() => { + savedCallback.current = callback + }, [callback]) + + useEffect(() => { + function tick() { + savedCallback.current() + } + if (timeout !== null) { + const id = setTimeout(tick, timeout) + return () => clearTimeout(id) + } + }, [timeout]) +} diff --git a/yarn.lock b/yarn.lock index e7fc05b..358eca8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2483,6 +2483,11 @@ data-urls@^1.1.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" +date-fns@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1" + integrity sha512-X3bf2iTPgCAQp9wvjOQytnf5vO5rESYRXlPIVcgSbtT5OTScPcsf9eZU+B/YIkKAtYr5WeCii58BgATrNitlWg== + deasync@^0.1.14: version "0.1.21" resolved "https://registry.yarnpkg.com/deasync/-/deasync-0.1.21.tgz#bb11eabd4466c0d8776f0d82deb8a6126460d30f" @@ -3612,6 +3617,13 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= +ical@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/ical/-/ical-0.8.0.tgz#aa93f021dfead58e54aaa22076a11ca07d65886b" + integrity sha512-/viUSb/RGLLnlgm0lWRlPBtVeQguQRErSPYl3ugnUaKUnzQswKqOG3M8/P1v1AB5NJwlHTuvTq1cs4mpeG2rCg== + dependencies: + rrule "2.4.1" + iconv-lite@0.4.24, iconv-lite@^0.4.17: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -4263,6 +4275,11 @@ lru-cache@^4.0.1, lru-cache@^4.1.5: pseudomap "^1.0.2" yallist "^2.1.2" +luxon@^1.3.3: + version "1.26.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.26.0.tgz#d3692361fda51473948252061d0f8561df02b578" + integrity sha512-+V5QIQ5f6CDXQpWNICELwjwuHdqeJM1UenlZWx5ujcRMc9venvluCjFb4t5NYLhb6IhkbMVOxzVuOqkgMxee2A== + magic-string@^0.22.4: version "0.22.5" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e" @@ -5792,6 +5809,13 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +rrule@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/rrule/-/rrule-2.4.1.tgz#1d0db4e45f2b0e92e2cca62d2f7093729ac7ec94" + integrity sha512-+NcvhETefswZq13T8nkuEnnQ6YgUeZaqMqVbp+ZiFDPCbp3AVgQIwUvNVDdMNrP05bKZG9ddDULFp0qZZYDrxg== + optionalDependencies: + luxon "^1.3.3" + run-async@^2.2.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"