Compare commits
10 Commits
f10932b821
...
3ab937bdb2
Author | SHA1 | Date | |
---|---|---|---|
|
3ab937bdb2 | ||
|
2651e1cb37 | ||
|
c24124c970 | ||
|
fdc031c64b | ||
|
6462f327c9 | ||
|
e29bc39075 | ||
|
16e45c3030 | ||
|
a6a3e6c28e | ||
|
15180937f9 | ||
|
1489a0380b |
1
.gitignore
vendored
Normal file → Executable file
@ -10,3 +10,4 @@ public
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.env*
|
.env*
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
.parcel-cache
|
||||||
|
0
.prettierrc
Normal file → Executable file
0
.vscode/settings.json
vendored
Normal file → Executable file
2
index.html
Normal file → Executable file
@ -53,7 +53,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js" type="module"></script>
|
||||||
<!-- Matomo -->
|
<!-- Matomo -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var _paq = (window._paq = window._paq || [])
|
var _paq = (window._paq = window._paq || [])
|
||||||
|
43
index.js
Normal file → Executable file
@ -16,13 +16,16 @@ import Series from './src/pages/Series'
|
|||||||
import Program from './src/pages/Program'
|
import Program from './src/pages/Program'
|
||||||
import StreamPreview from './src/components/StreamPreview'
|
import StreamPreview from './src/components/StreamPreview'
|
||||||
import Video from './src/components/Video'
|
import Video from './src/components/Video'
|
||||||
|
import Chat from './src/components/Chat'
|
||||||
// import { useWindowSize } from './src/hooks/dom'
|
// import { useWindowSize } from './src/hooks/dom'
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const { theme } = useTheme((store) => store)
|
const { theme } = useTheme(store => store)
|
||||||
const { data, loading: eventsLoading, error } = useEventApi()
|
const { data, loading: eventsLoading, error } = useEventApi()
|
||||||
const [minLoadTimePassed, setMinTimeUp] = useState(false)
|
const [minLoadTimePassed, setMinTimeUp] = useState(false)
|
||||||
const { setCurrentStream, currentStream, streamIsLive } = useStreamStore(store => store)
|
const { setCurrentStream, currentStream, streamIsLive } = useStreamStore(
|
||||||
|
store => store
|
||||||
|
)
|
||||||
const streamActive = useUiStore(store => store.streamActive)
|
const streamActive = useUiStore(store => store.streamActive)
|
||||||
usePeertubeApi(data.episodes)
|
usePeertubeApi(data.episodes)
|
||||||
|
|
||||||
@ -37,14 +40,17 @@ const App = () => {
|
|||||||
new Date(stream.beginsOn),
|
new Date(stream.beginsOn),
|
||||||
'Europe/Berlin'
|
'Europe/Berlin'
|
||||||
)
|
)
|
||||||
const utcEndDate = zonedTimeToUtc(new Date(stream.endsOn), 'Europe/Berlin')
|
const utcEndDate = zonedTimeToUtc(
|
||||||
|
new Date(stream.endsOn),
|
||||||
|
'Europe/Berlin'
|
||||||
|
)
|
||||||
const { timeZone } = Intl.DateTimeFormat().resolvedOptions()
|
const { timeZone } = Intl.DateTimeFormat().resolvedOptions()
|
||||||
|
|
||||||
const zonedStartDate = utcToZonedTime(utcStartDate, timeZone)
|
const zonedStartDate = utcToZonedTime(utcStartDate, 'Europe/Berlin')
|
||||||
const zonedEndDate = utcToZonedTime(utcEndDate, timeZone)
|
const zonedEndDate = utcToZonedTime(utcEndDate, 'Europe/Berlin')
|
||||||
if (
|
if (
|
||||||
isWithinInterval(new Date(), {
|
isWithinInterval(new Date(), {
|
||||||
start: subHours(zonedStartDate, 1),
|
start: subHours(zonedStartDate, 2),
|
||||||
end: addHours(zonedEndDate, 1),
|
end: addHours(zonedEndDate, 1),
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
@ -54,8 +60,6 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
}, [eventsLoading])
|
}, [eventsLoading])
|
||||||
|
|
||||||
// console.log({ episodes: data.episodes, series: data.series })
|
|
||||||
|
|
||||||
const seriesData = data.series ? Object.values(data.series) : []
|
const seriesData = data.series ? Object.values(data.series) : []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -69,23 +73,30 @@ const App = () => {
|
|||||||
<Route exact path="/" component={Program} />
|
<Route exact path="/" component={Program} />
|
||||||
<Route exact path="/series" component={Series} />
|
<Route exact path="/series" component={Series} />
|
||||||
<Route exact path="/program" component={Program} />
|
<Route exact path="/program" component={Program} />
|
||||||
{seriesData.length ? seriesData.map(series => (
|
{seriesData.length
|
||||||
<Route exact path={`/series/${series.slug}`}>
|
? seriesData.map(series => (
|
||||||
<SeriesPage data={series} />
|
<Route exact path={`/series/${series.slug}`}>
|
||||||
</Route>)) : null}
|
<SeriesPage data={series} />
|
||||||
|
</Route>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
<Route path="*">
|
<Route path="*">
|
||||||
<FourOhFour />
|
<FourOhFour />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
{streamActive ? <Video stream={currentStream} /> :
|
|
||||||
<StreamPreview stream={currentStream} isLive={streamIsLive} />}
|
{streamActive ? (
|
||||||
|
<Video stream={currentStream} />
|
||||||
|
) : (
|
||||||
|
<StreamPreview stream={currentStream} isLive={streamIsLive} />
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
</ThemeProvider>)
|
</ThemeProvider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const appEl = document.getElementById('app')
|
const appEl = document.getElementById('app')
|
||||||
|
|
||||||
render(<App />, appEl)
|
render(<App />, appEl)
|
||||||
|
0
netlify.toml
Normal file → Executable file
11
package.json
Normal file → Executable file
@ -1,13 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "underscore_stream",
|
"name": "underscore_stream",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "index.js",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "yarn parcel index.html",
|
"start": "yarn parcel index.html",
|
||||||
"dev": "yarn parcel watch index.html",
|
"dev": "yarn parcel watch index.html",
|
||||||
"build": "parcel build index.html && cp -r static dist/static",
|
"build": "parcel build index.html && cp -r static dist/static"
|
||||||
"deploy": "yarn parcel build; rsync rsync -aP ./dist/ inu@95.216.203.71:/media/www/stream.undersco.re/"
|
|
||||||
},
|
},
|
||||||
"alias": {
|
"alias": {
|
||||||
"react": "preact/compat",
|
"react": "preact/compat",
|
||||||
@ -26,7 +24,7 @@
|
|||||||
"ical.js": "^1.4.0",
|
"ical.js": "^1.4.0",
|
||||||
"markdown-to-jsx": "^7.1.2",
|
"markdown-to-jsx": "^7.1.2",
|
||||||
"preact": "^10.5.12",
|
"preact": "^10.5.12",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.8.1",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-router-dom": "^5.3.0",
|
"react-router-dom": "^5.3.0",
|
||||||
"striptags": "^3.2.0",
|
"striptags": "^3.2.0",
|
||||||
@ -35,6 +33,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "^7.13.10",
|
"@babel/eslint-parser": "^7.13.10",
|
||||||
|
"@parcel/transformer-sass": "2.5.0",
|
||||||
"babel": "^6.23.0",
|
"babel": "^6.23.0",
|
||||||
"babel-plugin-transform-react-jsx": "^6.24.1",
|
"babel-plugin-transform-react-jsx": "^6.24.1",
|
||||||
"babel-preset-env": "^1.6.1",
|
"babel-preset-env": "^1.6.1",
|
||||||
@ -42,10 +41,10 @@
|
|||||||
"eslint-config-flying-rocket": "^1.1.1",
|
"eslint-config-flying-rocket": "^1.1.1",
|
||||||
"marked": "^2.0.3",
|
"marked": "^2.0.3",
|
||||||
"module-alias": "^2.0.3",
|
"module-alias": "^2.0.3",
|
||||||
"parcel-bundler": "1.12.3",
|
"parcel": "^2.0.0",
|
||||||
"parcel-plugin-ogimage": "^1.2.0",
|
"parcel-plugin-ogimage": "^1.2.0",
|
||||||
"prettier": "^1.9.1",
|
"prettier": "^1.9.1",
|
||||||
"sass": "^1.32.8",
|
"sass": "^1.32.8",
|
||||||
"scss": "^0.2.4"
|
"scss": "^0.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
0
src/assets/fonts/Karla/Karla-Bold.ttf
Normal file → Executable file
0
src/assets/fonts/Karla/Karla-Medium.ttf
Normal file → Executable file
0
src/assets/fonts/Karla/Karla-Regular.ttf
Normal file → Executable file
0
src/assets/fonts/Karla/Karla-VariableFont_wght.ttf
Normal file → Executable file
0
src/assets/fonts/Lunchtype/lunchtype24-medium.ttf
Normal file → Executable file
0
src/assets/img/IconSM.png
Normal file → Executable file
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
0
src/assets/img/hero/1lg.png
Normal file → Executable file
Before Width: | Height: | Size: 906 KiB After Width: | Height: | Size: 906 KiB |
0
src/assets/img/hero/1sm.png
Normal file → Executable file
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
0
src/assets/img/hero/2lg.png
Normal file → Executable file
Before Width: | Height: | Size: 603 KiB After Width: | Height: | Size: 603 KiB |
0
src/assets/img/hero/2md.png
Normal file → Executable file
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 178 KiB |
0
src/assets/img/hero/2sm.png
Normal file → Executable file
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
0
src/assets/styles/fontface.scss
Normal file → Executable file
0
src/assets/styles/index.scss
Normal file → Executable file
0
src/assets/styles/reset.scss
Normal file → Executable file
0
src/assets/theme/index.js
Normal file → Executable file
0
src/components/Button/index.js
Normal file → Executable file
10
src/components/Chat/index.js
Normal file → Executable file
@ -27,17 +27,21 @@ const Chat = ({ overlayActive }) => {
|
|||||||
<CloseBox colour={colours.white} size={18} onClick={toggleChatOpen} />
|
<CloseBox colour={colours.white} size={18} onClick={toggleChatOpen} />
|
||||||
</ChatHeader>
|
</ChatHeader>
|
||||||
<iframe
|
<iframe
|
||||||
src={`https://titanembeds.com/embed/${config.chat.guildId}?css=${config.chat.css}&defaultchannel=${config.chat.channelId}&lang=en_EN`}
|
src={`https://e.widgetbot.io/channels/${config.chat.guildId}/${config.chat.channelId}`}
|
||||||
height={(height / 4) * 3}
|
height={(height / 4) * 3}
|
||||||
width="350"
|
width="350"
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
title="discord-chat"
|
|
||||||
className="titanembed"
|
className="titanembed"
|
||||||
|
title="discord-chat"
|
||||||
/>
|
/>
|
||||||
</ChatFrame>
|
</ChatFrame>
|
||||||
</ChatWrapper>
|
</ChatWrapper>
|
||||||
) : (
|
) : (
|
||||||
<ChatHeader chatIsOpen={false} onClick={toggleChatOpen} $active={overlayActive}>
|
<ChatHeader
|
||||||
|
chatIsOpen={false}
|
||||||
|
onClick={toggleChatOpen}
|
||||||
|
$active={overlayActive}
|
||||||
|
>
|
||||||
<Label weight="400" size={16} colour={colours.midnightDarker}>
|
<Label weight="400" size={16} colour={colours.midnightDarker}>
|
||||||
CHAT
|
CHAT
|
||||||
</Label>
|
</Label>
|
||||||
|
0
src/components/Chat/styles.js
Normal file → Executable file
42
src/components/EpisodeCard/index.js
Normal file → Executable file
@ -1,25 +1,44 @@
|
|||||||
import { h } from 'preact'
|
import { h } from 'preact'
|
||||||
import { ICalendar } from 'datebook'
|
import { ICalendar } from 'datebook'
|
||||||
import { format } from 'date-fns'
|
// import { format } from 'date-fns'
|
||||||
import striptags from 'striptags'
|
import striptags from 'striptags'
|
||||||
|
import { utcToZonedTime, zonedTimeToUtc, format } from 'date-fns-tz'
|
||||||
|
import enGB from 'date-fns/locale/en-GB'
|
||||||
|
|
||||||
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 { andList } from '../../helpers/string'
|
|
||||||
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'
|
import Flex from '../Flex'
|
||||||
|
|
||||||
|
import { Img, Left, Right, Center, Title, ButtonRow, StyledButton as Button } from './styles'
|
||||||
|
|
||||||
|
import strings from '../../data/strings'
|
||||||
|
import { colours, screenSizes } from '../../assets/theme'
|
||||||
|
import { andList } from '../../helpers/string'
|
||||||
|
import { useEventApi } from '../../hooks/data'
|
||||||
|
import { useWindowSize } from '../../hooks/dom'
|
||||||
|
|
||||||
|
|
||||||
const EpisodeCard = ({ image, title, seriesId, beginsOn, endsOn, id, url, description, ...rest }) => {
|
|
||||||
|
const EpisodeCard = ({ image, title, seriesId, beginsOn, endsOn, id: episodeId, 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), 'h:mma')
|
const startDate = new Date(beginsOn)
|
||||||
|
|
||||||
|
const { timeZone } = Intl.DateTimeFormat().resolvedOptions()
|
||||||
|
const utcDate = zonedTimeToUtc(startDate, timeZone)
|
||||||
|
|
||||||
|
const zonedDate = utcToZonedTime(utcDate, timeZone)
|
||||||
|
|
||||||
|
const startTime = format(zonedDate, 'h:mma', {
|
||||||
|
timeZone,
|
||||||
|
locale: enGB,
|
||||||
|
})
|
||||||
|
const tzShort = format(zonedDate, 'zzz', {
|
||||||
|
timeZone,
|
||||||
|
locale: enGB,
|
||||||
|
})
|
||||||
|
|
||||||
const { width: screenWidth } = useWindowSize()
|
const { width: screenWidth } = useWindowSize()
|
||||||
const isMobile = screenWidth < screenSizes.md
|
const isMobile = screenWidth < screenSizes.md
|
||||||
@ -28,7 +47,7 @@ const EpisodeCard = ({ image, title, seriesId, beginsOn, endsOn, id, url, descri
|
|||||||
return (
|
return (
|
||||||
<Flex align="stretch" direction={isMobile ? 'column' : 'row'} {...rest}>
|
<Flex align="stretch" direction={isMobile ? 'column' : 'row'} {...rest}>
|
||||||
<Left>
|
<Left>
|
||||||
<Link to={`/series/${series.slug}#${id}`}>
|
<Link to={`/series/${series.slug}#${episodeId}`}>
|
||||||
<Img src={image} />
|
<Img src={image} />
|
||||||
</Link>
|
</Link>
|
||||||
<ButtonsRows title={title} description={description} beginsOn={beginsOn} endsOn={endsOn} url={url} />
|
<ButtonsRows title={title} description={description} beginsOn={beginsOn} endsOn={endsOn} url={url} />
|
||||||
@ -43,7 +62,8 @@ const EpisodeCard = ({ image, title, seriesId, beginsOn, endsOn, id, url, descri
|
|||||||
</div>
|
</div>
|
||||||
</Center>
|
</Center>
|
||||||
<Right>
|
<Right>
|
||||||
<Label size={24} colour={colours.rose} weight="600">{startTime}</Label>
|
<Label size={24} colour={colours.rose} weight="600" align="right" block>{startTime}</Label>
|
||||||
|
<Label size={18} colour={colours.rose} weight="600" align="right" block>{tzShort}</Label>
|
||||||
</Right>
|
</Right>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
|
35
src/components/EpisodeCard/styles.js
Normal file → Executable file
@ -16,7 +16,8 @@ export const ButtonRow = styled(Flexbox)`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
||||||
button, a{
|
button,
|
||||||
|
a {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
width: 49%;
|
width: 49%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -29,40 +30,48 @@ export const ButtonRow = styled(Flexbox)`
|
|||||||
@media screen and (min-width: ${screenSizes.md}px) and (max-width: ${screenSizes.lg}px) {
|
@media screen and (min-width: ${screenSizes.md}px) and (max-width: ${screenSizes.lg}px) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
button, a {
|
button,
|
||||||
|
a {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
export const Left = styled(FlexColumn)`
|
export const Left = styled(FlexColumn)`
|
||||||
margin-right: 2em;
|
margin-right: 2em;
|
||||||
${imageWidthStyles};
|
${imageWidthStyles};
|
||||||
|
|
||||||
@media screen and (max-width: ${screenSizes.md}px) {
|
@media screen and (max-width: ${screenSizes.md}px) {
|
||||||
margin-right: 0em;
|
margin-right: 0em;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const Center = styled(FlexColumn)`
|
export const Center = styled(FlexColumn)`
|
||||||
@media screen and (max-width: ${screenSizes.md}px) {
|
@media screen and (max-width: ${screenSizes.md}px) {
|
||||||
order: 2;
|
order: 2;
|
||||||
}
|
position: relative;
|
||||||
`
|
top: -1em;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export const Title = styled(H2)`
|
export const Title = styled(H2)`
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
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;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: ${screenSizes.md}px) {
|
@media screen and (max-width: ${screenSizes.md}px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 1em;
|
top: 1.2em;
|
||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@ -82,4 +91,4 @@ export const Img = styled.div`
|
|||||||
position: relative;
|
position: relative;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
${imageWidthStyles};
|
${imageWidthStyles};
|
||||||
`
|
`
|
||||||
|
0
src/components/Flex/index.js
Normal file → Executable file
0
src/components/Header/index.js
Normal file → Executable file
0
src/components/Header/styles.js
Normal file → Executable file
0
src/components/Link/index.js
Normal file → Executable file
0
src/components/Link/styles.js
Normal file → Executable file
0
src/components/Loader/index.js
Normal file → Executable file
0
src/components/Logo/index.js
Normal file → Executable file
0
src/components/Markdown/index.js
Normal file → Executable file
4
src/components/Markdown/styles.js
Normal file → Executable file
@ -28,6 +28,10 @@ export const MarkdownWrapper = styled.span`
|
|||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: ${props => (props.$withLinebreaks ? '32px' : '0')};
|
margin-bottom: ${props => (props.$withLinebreaks ? '32px' : '0')};
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: ${props => (props.$withLinebreaks ? '8px' : '0')};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p > p {
|
p > p {
|
||||||
|
0
src/components/Select/index.js
Normal file → Executable file
0
src/components/Select/styles.js
Normal file → Executable file
0
src/components/Seo/index.js
Normal file → Executable file
49
src/components/SeriesCard/index.js
Normal file → Executable file
@ -7,15 +7,31 @@ import { Img, LabelBlock, Wrapper } from './styles'
|
|||||||
import { andList } from '../../helpers/string'
|
import { andList } from '../../helpers/string'
|
||||||
import { colours } from '../../assets/theme'
|
import { colours } from '../../assets/theme'
|
||||||
|
|
||||||
const SeriesCard = ({ series: { image, episodes, title, subtitle, hosts: hostsArray, slug, }, isPast, ...rest }) => {
|
const SeriesCard = ({
|
||||||
|
series: { image, episodes, title, subtitle, hosts: hostsArray, slug },
|
||||||
|
isPast,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
const hosts = hostsArray.map(({ actor }) => actor.name)
|
const hosts = hostsArray.map(({ actor }) => actor.name)
|
||||||
|
|
||||||
const episodesLength = episodes.past.length + episodes.future.length
|
const episodesLength = episodes.past.length + episodes.future.length
|
||||||
|
|
||||||
const getNextLastStreamText = () => {
|
const getNextLastStreamText = () => {
|
||||||
const hasFutureEpisodes = episodes.future.length
|
const hasFutureEpisodes = episodes.future.length
|
||||||
const prefix = hasFutureEpisodes ? strings.en.nextStream : strings.en.lastStream
|
const prefix = hasFutureEpisodes
|
||||||
const mainText = formatDistanceToNow(new Date(episodes[hasFutureEpisodes ? 'future' : 'past'][0][hasFutureEpisodes ? 'endsOn' : 'beginsOn']), { addSuffix: true })
|
? strings.en.nextStream
|
||||||
|
: strings.en.lastStream
|
||||||
|
|
||||||
|
const episodesList = episodes[hasFutureEpisodes ? 'future' : 'past']
|
||||||
|
|
||||||
|
const mainText = formatDistanceToNow(
|
||||||
|
new Date(
|
||||||
|
episodesList[hasFutureEpisodes ? 0 : episodesList.length - 1][
|
||||||
|
hasFutureEpisodes ? 'endsOn' : 'beginsOn'
|
||||||
|
]
|
||||||
|
),
|
||||||
|
{ addSuffix: true }
|
||||||
|
)
|
||||||
|
|
||||||
return `${prefix} ${mainText}`
|
return `${prefix} ${mainText}`
|
||||||
}
|
}
|
||||||
@ -24,21 +40,24 @@ const SeriesCard = ({ series: { image, episodes, title, subtitle, hosts: hostsAr
|
|||||||
<Link to={`series/${slug}`}>
|
<Link to={`series/${slug}`}>
|
||||||
<Wrapper {...rest}>
|
<Wrapper {...rest}>
|
||||||
<Img src={image}>
|
<Img src={image}>
|
||||||
<LabelBlock
|
<LabelBlock $position="top">
|
||||||
$position="top"
|
{episodesLength}{' '}
|
||||||
>
|
{episodesLength === 1 ? strings.en.episode : strings.en.episodes}
|
||||||
{episodesLength} {episodesLength === 1 ? strings.en.episode : strings.en.episodes}
|
|
||||||
</LabelBlock>
|
|
||||||
<LabelBlock
|
|
||||||
$position="bottom"
|
|
||||||
>
|
|
||||||
{getNextLastStreamText()}
|
|
||||||
</LabelBlock>
|
</LabelBlock>
|
||||||
|
<LabelBlock $position="bottom">{getNextLastStreamText()}</LabelBlock>
|
||||||
</Img>
|
</Img>
|
||||||
|
|
||||||
<H2 size={32} colour={colours.rose}>{title}</H2>
|
<H2 size={32} colour={colours.rose}>
|
||||||
<H3 size={21} colour={colours.rose}>{subtitle}</H3>
|
{title}
|
||||||
{hosts.length ? <Label size={16} colour={colours.rose}>— {andList(hosts)}</Label> : null}
|
</H2>
|
||||||
|
<H3 size={21} colour={colours.rose}>
|
||||||
|
{subtitle}
|
||||||
|
</H3>
|
||||||
|
{hosts.length ? (
|
||||||
|
<Label size={16} colour={colours.rose}>
|
||||||
|
— {andList(hosts)}
|
||||||
|
</Label>
|
||||||
|
) : null}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
0
src/components/SeriesCard/styles.js
Normal file → Executable file
0
src/components/StreamPreview/helpers.js
Normal file → Executable file
53
src/components/StreamPreview/index.js
Normal file → Executable file
@ -1,10 +1,7 @@
|
|||||||
import { Fragment, h } from 'preact'
|
import { h } from 'preact'
|
||||||
import { useEffect, useRef } from 'preact/hooks'
|
import { useEffect, useRef } from 'preact/hooks'
|
||||||
import { PeerTubePlayer } from '@peertube/embed-api'
|
import { PeerTubePlayer } from '@peertube/embed-api'
|
||||||
import { string } from 'prop-types'
|
|
||||||
import Link from '../Link'
|
|
||||||
import { Label } from '../Text'
|
import { Label } from '../Text'
|
||||||
import strings from '../../data/strings'
|
|
||||||
import { colours, textSizes } from '../../assets/theme'
|
import { colours, textSizes } from '../../assets/theme'
|
||||||
import { Row } from '../Flex'
|
import { Row } from '../Flex'
|
||||||
import CrossSvg from '../Svg/Cross'
|
import CrossSvg from '../Svg/Cross'
|
||||||
@ -13,18 +10,18 @@ import { getLabel } from './helpers'
|
|||||||
import Chevron from '../Svg/Chevron'
|
import Chevron from '../Svg/Chevron'
|
||||||
|
|
||||||
import { Frame, Img, Iframe, InnerWrapper } from './styles'
|
import { Frame, Img, Iframe, InnerWrapper } from './styles'
|
||||||
// import { useEventApi } from '../../hooks/data'
|
|
||||||
|
|
||||||
const StreamPreview = ({ stream, isLive, ...rest }) => {
|
const StreamPreview = ({ stream, isLive, ...rest }) => {
|
||||||
const currentLanguage = 'en'
|
const currentLanguage = 'en'
|
||||||
const videoiFrame = useRef(null)
|
const videoiFrame = useRef(null)
|
||||||
const ptVideo = useRef(null)
|
const ptVideo = useRef(null)
|
||||||
const { isMinimized, toggleMinimized, setStreamActive } = useUiStore(store => ({
|
const { isMinimized, toggleMinimized, setStreamActive } = useUiStore(
|
||||||
isMinimized: store.streamPreviewMinimized,
|
store => ({
|
||||||
toggleMinimized: store.toggleStreamPreviewMinimized,
|
isMinimized: store.streamPreviewMinimized,
|
||||||
setStreamActive: store.setStreamActive
|
toggleMinimized: store.toggleStreamPreviewMinimized,
|
||||||
}))
|
setStreamActive: store.setStreamActive,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setupAndPlayVideo = async () => {
|
const setupAndPlayVideo = async () => {
|
||||||
@ -47,12 +44,30 @@ const StreamPreview = ({ stream, isLive, ...rest }) => {
|
|||||||
return stream ? (
|
return stream ? (
|
||||||
<Frame isMinimized={isMinimized}>
|
<Frame isMinimized={isMinimized}>
|
||||||
<Row justify="space-between">
|
<Row justify="space-between">
|
||||||
<Label colour={colours.midnightDarker} size={textSizes.lg} onClick={activateStream}>{getLabel(stream, isLive, isMinimized)}</Label>
|
<Label
|
||||||
{isMinimized ? <Chevron colour={colours.midnightDarker} size={14} onClick={toggleMinimized} /> : <CrossSvg colour={colours.midnightDarker} size={16} onClick={toggleMinimized} />}
|
colour={colours.midnightDarker}
|
||||||
|
size={textSizes.lg}
|
||||||
|
onClick={activateStream}
|
||||||
|
>
|
||||||
|
{getLabel(stream, isLive, isMinimized)}
|
||||||
|
</Label>
|
||||||
|
{isMinimized ? (
|
||||||
|
<Chevron
|
||||||
|
colour={colours.midnightDarker}
|
||||||
|
size={14}
|
||||||
|
onClick={toggleMinimized}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CrossSvg
|
||||||
|
colour={colours.midnightDarker}
|
||||||
|
size={16}
|
||||||
|
onClick={toggleMinimized}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
{!isMinimized ?
|
{!isMinimized ? (
|
||||||
<InnerWrapper onClick={activateStream}>
|
<InnerWrapper onClick={activateStream}>
|
||||||
{isLive ?
|
{isLive ? (
|
||||||
<Iframe
|
<Iframe
|
||||||
width="560"
|
width="560"
|
||||||
height="315"
|
height="315"
|
||||||
@ -64,11 +79,13 @@ const StreamPreview = ({ stream, isLive, ...rest }) => {
|
|||||||
allowFullScreen
|
allowFullScreen
|
||||||
ref={videoiFrame}
|
ref={videoiFrame}
|
||||||
/>
|
/>
|
||||||
: <Img src={stream.image} onClick={activateStream} />}
|
) : (
|
||||||
</InnerWrapper> : null}
|
<Img src={stream.image} onClick={activateStream} />
|
||||||
|
)}
|
||||||
|
</InnerWrapper>
|
||||||
|
) : null}
|
||||||
</Frame>
|
</Frame>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default StreamPreview
|
export default StreamPreview
|
||||||
|
0
src/components/StreamPreview/styles.js
Normal file → Executable file
0
src/components/Svg/Chevron.js
Normal file → Executable file
0
src/components/Svg/Cross.js
Normal file → Executable file
0
src/components/Svg/Play.js
Normal file → Executable file
0
src/components/Svg/VideoOverlay/index.js
Normal file → Executable file
0
src/components/Svg/VideoOverlay/styles.js
Normal file → Executable file
0
src/components/Svg/base.js
Normal file → Executable file
0
src/components/Svg/proptypes.js
Normal file → Executable file
0
src/components/Text/index.js
Normal file → Executable file
0
src/components/Text/styles.js
Normal file → Executable file
13
src/components/Video/index.js
Normal file → Executable file
@ -1,13 +1,7 @@
|
|||||||
import { h } from 'preact'
|
import { h } from 'preact'
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||||
import {
|
import { instanceOf, number, object, shape, string } from 'prop-types'
|
||||||
instanceOf,
|
|
||||||
number,
|
|
||||||
object,
|
|
||||||
shape,
|
|
||||||
string,
|
|
||||||
} from 'prop-types'
|
|
||||||
import 'regenerator-runtime/runtime'
|
|
||||||
import { PeerTubePlayer } from '@peertube/embed-api'
|
import { PeerTubePlayer } from '@peertube/embed-api'
|
||||||
|
|
||||||
import Chat from '../Chat'
|
import Chat from '../Chat'
|
||||||
@ -57,7 +51,6 @@ const Video = () => {
|
|||||||
setVideo()
|
setVideo()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
const playVideo = () => {
|
const playVideo = () => {
|
||||||
const { current: player } = ptVideo
|
const { current: player } = ptVideo
|
||||||
if (!videoReady) return
|
if (!videoReady) return
|
||||||
@ -84,7 +77,6 @@ const Video = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const toggleVideo = () => {
|
const toggleVideo = () => {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
pauseVideo()
|
pauseVideo()
|
||||||
@ -125,7 +117,6 @@ const Video = () => {
|
|||||||
console.log({ vol })
|
console.log({ vol })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const handleKeyPress = keyCode => {
|
const handleKeyPress = keyCode => {
|
||||||
if (keyCode === 32) {
|
if (keyCode === 32) {
|
||||||
// key == 'space'
|
// key == 'space'
|
||||||
|
0
src/components/Video/styles.js
Normal file → Executable file
0
src/components/VideoEmbed/index.js
Normal file → Executable file
0
src/components/VideoEmbed/styles.js
Normal file → Executable file
0
src/components/VideoOverlay/index.js
Normal file → Executable file
0
src/components/VideoOverlay/styles.js
Normal file → Executable file
6
src/data/config.js
Normal file → Executable file
@ -1,8 +1,9 @@
|
|||||||
export default {
|
export default {
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Underscore streams',
|
title: 'Underscore streams',
|
||||||
description: 'Quality live programming from beyond the surveillance economy.',
|
description:
|
||||||
img: 'https://stream.undersco.re/static/meta.png'
|
'Quality live programming from beyond the surveillance economy.',
|
||||||
|
img: 'https://stream.undersco.re/static/meta.png',
|
||||||
},
|
},
|
||||||
peertube_root: 'https://tv.undersco.re',
|
peertube_root: 'https://tv.undersco.re',
|
||||||
EVENTS_API_URL: 'https://api.undersco.re',
|
EVENTS_API_URL: 'https://api.undersco.re',
|
||||||
@ -11,6 +12,5 @@ export default {
|
|||||||
chat: {
|
chat: {
|
||||||
guildId: '709318870909059082',
|
guildId: '709318870909059082',
|
||||||
channelId: '826751398757793842',
|
channelId: '826751398757793842',
|
||||||
css: '215',
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
0
src/data/navigation.js
Normal file → Executable file
0
src/data/strings.js
Normal file → Executable file
0
src/helpers/environment.js
Normal file → Executable file
0
src/helpers/string.js
Normal file → Executable file
0
src/helpers/utils.js
Normal file → Executable file
208
src/hooks/data.js
Normal file → Executable file
@ -1,108 +1,110 @@
|
|||||||
import { h, render } from 'preact'
|
import { h } from 'preact'
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
|
||||||
|
import { secondsToMilliseconds } from 'date-fns'
|
||||||
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, useStreamStore } from '../store/index'
|
import { useSeriesStore, useStreamStore } from '../store/index'
|
||||||
import 'regenerator-runtime/runtime'
|
|
||||||
import { useInterval } from './timerHooks'
|
import { useInterval } from './timerHooks'
|
||||||
import { secondsToMilliseconds } from 'date-fns'
|
|
||||||
|
|
||||||
export const useEventCalendar = () => {
|
// export const useEventCalendar = () => {
|
||||||
const [data, setData] = useState([])
|
// const [data, setData] = useState([])
|
||||||
const [loading, setLoading] = useState(true)
|
// const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
async function fetchData() {
|
// async function fetchData() {
|
||||||
setLoading(true)
|
// setLoading(true)
|
||||||
|
|
||||||
const { data: responseData } = await axios.get(
|
// const { data: responseData } = await axios.get(
|
||||||
`https://cloud.undersco.re/remote.php/dav/public-calendars/${config.calendarId}/?export`
|
// `https://cloud.undersco.re/remote.php/dav/public-calendars/${config.calendarId}/?export`
|
||||||
)
|
// )
|
||||||
const jCalData = ICAL.parse(responseData)
|
// const jCalData = ICAL.parse(responseData)
|
||||||
const comp = new ICAL.Component(jCalData)
|
// const comp = new ICAL.Component(jCalData)
|
||||||
|
|
||||||
const vevents = comp.getAllSubcomponents('vevent')
|
// const vevents = comp.getAllSubcomponents('vevent')
|
||||||
|
|
||||||
const calEvents = vevents
|
// const calEvents = vevents
|
||||||
.filter(
|
// .filter(
|
||||||
vevent =>
|
// vevent =>
|
||||||
vevent.getFirstPropertyValue('status') === null ||
|
// vevent.getFirstPropertyValue('status') === null ||
|
||||||
(vevent.getFirstPropertyValue('status') &&
|
// (vevent.getFirstPropertyValue('status') &&
|
||||||
vevent.getFirstPropertyValue('status').toUpperCase() ===
|
// vevent.getFirstPropertyValue('status').toUpperCase() ===
|
||||||
'CONFIRMED')
|
// 'CONFIRMED')
|
||||||
)
|
// )
|
||||||
.map(vevent => {
|
// .map(vevent => {
|
||||||
const event = new ICAL.Event(vevent)
|
// const event = new ICAL.Event(vevent)
|
||||||
return event
|
// return event
|
||||||
})
|
// })
|
||||||
.sort((a, b) => a.startDate.toJSDate() - b.startDate.toJSDate())
|
// .sort((a, b) => a.startDate.toJSDate() - b.startDate.toJSDate())
|
||||||
|
|
||||||
await Promise.all(
|
// await Promise.all(
|
||||||
calEvents.map(async calItem => {
|
// calEvents.map(async calItem => {
|
||||||
const url = calItem.component.getAllProperties('url')[0]
|
// const url = calItem.component.getAllProperties('url')[0]
|
||||||
if (url) {
|
// if (url) {
|
||||||
const id = url
|
// const id = url
|
||||||
.getFirstValue()
|
// .getFirstValue()
|
||||||
.split('/')
|
// .split('/')
|
||||||
.pop()
|
// .pop()
|
||||||
const {
|
// const {
|
||||||
data: {
|
// data: {
|
||||||
account,
|
// account,
|
||||||
category,
|
// category,
|
||||||
channel,
|
// channel,
|
||||||
embedPath,
|
// embedPath,
|
||||||
language,
|
// language,
|
||||||
state,
|
// state,
|
||||||
previewPath,
|
// previewPath,
|
||||||
views,
|
// views,
|
||||||
duration,
|
// duration,
|
||||||
},
|
// },
|
||||||
} = await axios.get(`https://tv.undersco.re/api/v1/videos/${id}`)
|
// } = await axios.get(`https://tv.undersco.re/api/v1/videos/${id}`)
|
||||||
|
|
||||||
const item = {
|
// const item = {
|
||||||
title: calItem.summary,
|
// title: calItem.summary,
|
||||||
account,
|
// account,
|
||||||
category,
|
// category,
|
||||||
channel,
|
// channel,
|
||||||
description: calItem.description,
|
// description: calItem.description,
|
||||||
embedPath,
|
// embedPath,
|
||||||
language,
|
// language,
|
||||||
state,
|
// state,
|
||||||
previewPath,
|
// previewPath,
|
||||||
views,
|
// views,
|
||||||
start: calItem.startDate.toJSDate(),
|
// start: calItem.startDate.toJSDate(),
|
||||||
end: calItem.endDate.toJSDate(),
|
// end: calItem.endDate.toJSDate(),
|
||||||
id,
|
// id,
|
||||||
duration,
|
// duration,
|
||||||
videoUrl: url.getFirstValue(),
|
// videoUrl: url.getFirstValue(),
|
||||||
}
|
// }
|
||||||
setData(arr => [...arr, item])
|
// setData(arr => [...arr, item])
|
||||||
} else {
|
// } else {
|
||||||
const item = {
|
// const item = {
|
||||||
title: calItem.summary,
|
// title: calItem.summary,
|
||||||
description: calItem.description,
|
// description: calItem.description,
|
||||||
start: calItem.startDate.toJSDate(),
|
// start: calItem.startDate.toJSDate(),
|
||||||
end: calItem.endDate.toJSDate(),
|
// end: calItem.endDate.toJSDate(),
|
||||||
}
|
// }
|
||||||
setData(arr => [...arr, item])
|
// setData(arr => [...arr, item])
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
)
|
// )
|
||||||
|
|
||||||
setLoading(false)
|
// setLoading(false)
|
||||||
}
|
// }
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return { loading, data }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// fetchData()
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
// return { loading, data }
|
||||||
|
// }
|
||||||
|
|
||||||
export const useEventApi = () => {
|
export const useEventApi = () => {
|
||||||
const [data, setData] = useSeriesStore(store => [store.series, store.setSeries])
|
const [data, setData] = useSeriesStore(store => [
|
||||||
|
store.series,
|
||||||
|
store.setSeries,
|
||||||
|
])
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [loading, setLoading] = useState(!!data.length)
|
const [loading, setLoading] = useState(!!data.length)
|
||||||
|
|
||||||
@ -111,15 +113,12 @@ export const useEventApi = () => {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const { data: responseData } = await axios.get(
|
const { data: responseData } = await axios.get(
|
||||||
`${config.EVENTS_API_URL}/events`
|
`${config.EVENTS_API_URL}/events`
|
||||||
)
|
)
|
||||||
setData(responseData)
|
setData(responseData)
|
||||||
console.log({ data: responseData })
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
} catch (err) {
|
||||||
catch (err) {
|
|
||||||
console.log('ERROR')
|
console.log('ERROR')
|
||||||
setError(err)
|
setError(err)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@ -135,7 +134,12 @@ export const useEventApi = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const usePeertubeApi = async () => {
|
export const usePeertubeApi = async () => {
|
||||||
const { currentStream, setCurrentStream, setStreamIsLive, streamIsLive } = useStreamStore(store => store)
|
const {
|
||||||
|
currentStream,
|
||||||
|
setCurrentStream,
|
||||||
|
setStreamIsLive,
|
||||||
|
streamIsLive,
|
||||||
|
} = useStreamStore(store => store)
|
||||||
|
|
||||||
if (!currentStream) return
|
if (!currentStream) return
|
||||||
|
|
||||||
@ -143,10 +147,11 @@ export const usePeertubeApi = async () => {
|
|||||||
const { peertubeLive } = currentStream
|
const { peertubeLive } = currentStream
|
||||||
if (!peertubeLive || !peertubeLive.id) return
|
if (!peertubeLive || !peertubeLive.id) return
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: { state, embedPath }
|
data: { state, embedPath },
|
||||||
} = await axios.get(`https://tv.undersco.re/api/v1/videos/${peertubeLive.id}`)
|
} = await axios.get(
|
||||||
|
`https://tv.undersco.re/api/v1/videos/${peertubeLive.id}`
|
||||||
|
)
|
||||||
|
|
||||||
setStreamIsLive(state.id === 1)
|
setStreamIsLive(state.id === 1)
|
||||||
setCurrentStream({ ...currentStream, embedPath })
|
setCurrentStream({ ...currentStream, embedPath })
|
||||||
@ -156,7 +161,10 @@ export const usePeertubeApi = async () => {
|
|||||||
fetchData()
|
fetchData()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useInterval(() => {
|
useInterval(
|
||||||
fetchData()
|
() => {
|
||||||
}, streamIsLive ? secondsToMilliseconds(15) : secondsToMilliseconds(1))
|
fetchData()
|
||||||
}
|
},
|
||||||
|
streamIsLive ? secondsToMilliseconds(15) : secondsToMilliseconds(1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
0
src/hooks/dom.js
Normal file → Executable file
0
src/hooks/timerHooks.js
Normal file → Executable file
0
src/hooks/utility.js
Normal file → Executable file
0
src/layouts/InfoLayout/index.js
Normal file → Executable file
0
src/layouts/InfoLayout/styles.js
Normal file → Executable file
0
src/layouts/Page/index.js
Normal file → Executable file
0
src/layouts/Page/styles.js
Normal file → Executable file
0
src/pages/404/index.js
Normal file → Executable file
0
src/pages/404/styles.js
Normal file → Executable file
0
src/pages/LoaderLayout/index.js
Normal file → Executable file
0
src/pages/LoaderLayout/styles.js
Normal file → Executable file
0
src/pages/Program/helpers.js
Normal file → Executable file
0
src/pages/Program/index.js
Normal file → Executable file
0
src/pages/Program/styles.js
Normal file → Executable file
32
src/pages/Series/index.js
Normal file → Executable file
@ -14,20 +14,23 @@ const Series = () => {
|
|||||||
const { data } = useEventApi()
|
const { data } = useEventApi()
|
||||||
const pastSeries = []
|
const pastSeries = []
|
||||||
|
|
||||||
const currentSeries = data.series ? data.series.filter(series => {
|
const currentSeries = data.series
|
||||||
if (series.episodes.future.length) {
|
? data.series.filter(series => {
|
||||||
return true
|
if (series.episodes.future.length) {
|
||||||
}
|
return true
|
||||||
if (series.episodes.past.every(({ beginsOn }) => isFuture(addYears(new Date(beginsOn), 1)))) {
|
}
|
||||||
return true
|
if (
|
||||||
}
|
series.episodes.past.every(({ beginsOn }) =>
|
||||||
|
isFuture(addYears(new Date(beginsOn), 1))
|
||||||
pastSeries.push(series)
|
)
|
||||||
return false
|
) {
|
||||||
}) : []
|
return true
|
||||||
|
}
|
||||||
console.log({ currentSeries })
|
|
||||||
|
|
||||||
|
pastSeries.push(series)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page title={strings.en.series}>
|
<Page title={strings.en.series}>
|
||||||
@ -48,7 +51,8 @@ const Series = () => {
|
|||||||
<SeriesCard series={series} isPast />
|
<SeriesCard series={series} isPast />
|
||||||
))}
|
))}
|
||||||
</SeriesGrid>
|
</SeriesGrid>
|
||||||
</Fragment>) : null}
|
</Fragment>
|
||||||
|
) : null}
|
||||||
</Content>
|
</Content>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
0
src/pages/Series/styles.js
Normal file → Executable file
111
src/pages/SeriesPage/index.js
Normal file → Executable file
@ -1,7 +1,7 @@
|
|||||||
/* 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 { useEffect } from 'preact/hooks'
|
||||||
import striptags from 'striptags'
|
import enGB from 'date-fns/locale/en-GB'
|
||||||
|
|
||||||
import { H1, Label } from '../../components/Text'
|
import { H1, Label } from '../../components/Text'
|
||||||
import Markdown from '../../components/Markdown'
|
import Markdown from '../../components/Markdown'
|
||||||
@ -14,6 +14,7 @@ import {
|
|||||||
InfoContent,
|
InfoContent,
|
||||||
Row,
|
Row,
|
||||||
LogosRow,
|
LogosRow,
|
||||||
|
FooterImage,
|
||||||
ActionButton as Button,
|
ActionButton as Button,
|
||||||
TrailerContainer,
|
TrailerContainer,
|
||||||
} from './styles'
|
} from './styles'
|
||||||
@ -26,37 +27,38 @@ const SeriesPage = ({ data }) => {
|
|||||||
const theme = data.theme || defaultTheme
|
const theme = data.theme || defaultTheme
|
||||||
const { orgs } = data
|
const { orgs } = data
|
||||||
|
|
||||||
const credits = data.credits ? `
|
const credits = data.credits
|
||||||
|
? `
|
||||||
## Credits
|
## Credits
|
||||||
${data.credits}
|
${data.credits}
|
||||||
` : null
|
`
|
||||||
|
: null
|
||||||
|
|
||||||
const orgsList = Object.values(orgs || {})
|
const orgsList = Object.values(orgs || {})
|
||||||
|
|
||||||
const dateString = `${new Date()}`
|
const footerImages = data?.resources?.filter(({ title }) =>
|
||||||
let tzShort =
|
title.includes('FOOTER_IMG')
|
||||||
// Works for the majority of modern browsers
|
)
|
||||||
dateString.match(/\(([^\)]+)\)$/) ||
|
|
||||||
// IE outputs date strings in a different format:
|
|
||||||
dateString.match(/([A-Z]+) [\d]{4}$/)
|
|
||||||
|
|
||||||
if (tzShort) {
|
|
||||||
// Old Firefox uses the long timezone name (e.g., "Central
|
|
||||||
// Daylight Time" instead of "CDT")
|
|
||||||
tzShort = tzShort[1].match(/[A-Z]/g).join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = data.links.length ? splitArray(data.links, 2) : null
|
const links = data.links.length ? splitArray(data.links, 2) : null
|
||||||
|
|
||||||
|
|
||||||
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={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={theme}>{data.description}</Markdown> : null}
|
{data.description ? (
|
||||||
|
<Markdown withLinebreaks theme={theme}>
|
||||||
|
{data.description}
|
||||||
|
</Markdown>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{data.trailer ? (
|
{data.trailer ? (
|
||||||
<TrailerContainer>
|
<TrailerContainer>
|
||||||
@ -64,30 +66,27 @@ const SeriesPage = ({ data }) => {
|
|||||||
</TrailerContainer>
|
</TrailerContainer>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{links ?
|
{links
|
||||||
links.map(linkRow => <Row>
|
? links.map(linkRow => (
|
||||||
{linkRow.map(link => (
|
<Row>
|
||||||
<a
|
{linkRow.map(link => (
|
||||||
href={link.resourceUrl}
|
<a
|
||||||
target="_blank"
|
href={link.resourceUrl}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
>
|
rel="noopener noreferrer"
|
||||||
<Button>{link.summary}</Button>
|
>
|
||||||
</a>
|
<Button>{link.summary}</Button>
|
||||||
))}
|
</a>
|
||||||
</Row>) : null
|
))}
|
||||||
}
|
</Row>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
</InfoContent>
|
</InfoContent>
|
||||||
{data.episodes.future.length ? (
|
{data.episodes.future.length ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Title>{translations.en.program}:</Title>
|
<Title>{translations.en.program}:</Title>
|
||||||
{data.episodes.future.map(feeditem => (
|
{data.episodes.future.map(feeditem => (
|
||||||
<EpisodeCard
|
<EpisodeCard theme={theme} key={feeditem.start} {...feeditem} />
|
||||||
theme={theme}
|
|
||||||
key={feeditem.start}
|
|
||||||
tzShort={tzShort}
|
|
||||||
{...feeditem}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
) : null}
|
) : null}
|
||||||
@ -105,20 +104,34 @@ const SeriesPage = ({ data }) => {
|
|||||||
))}
|
))}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
) : null}
|
) : null}
|
||||||
{credits ? <InfoContent>
|
{credits ? (
|
||||||
<Markdown theme={theme}>{credits}</Markdown>
|
<InfoContent>
|
||||||
</InfoContent> : null}
|
<Markdown theme={theme}>{credits}</Markdown>
|
||||||
|
</InfoContent>
|
||||||
|
) : null}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
{orgsList.length ? <LogosRow $wrap>
|
{orgsList.length ? (
|
||||||
{orgsList.map((org, index) => (
|
<LogosRow $wrap>
|
||||||
<Fragment>
|
{orgsList.map((org, index) => (
|
||||||
<a href={org.orgUrl}>
|
<Fragment>
|
||||||
<img src={org.logoUrl} alt={`${org.orgName} logo`} />
|
<img src={org.logoUrl} alt={`${org.orgName} logo`} />
|
||||||
</a>
|
{orgsList.length < 4 !== 1 &&
|
||||||
{orgsList.length === 2 && index + 1 !== orgsList.length ? <Label colour={theme.foreground}>{'//'}</Label> : null}
|
orgsList.length < 4 &&
|
||||||
</Fragment>
|
index + 1 !== orgsList.length ? (
|
||||||
))}
|
<Label colour={theme.foreground}>{'//'}</Label>
|
||||||
</LogosRow> : null}
|
) : null}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</LogosRow>
|
||||||
|
) : null}
|
||||||
|
{footerImages.length
|
||||||
|
? footerImages.map(image => (
|
||||||
|
<FooterImage
|
||||||
|
src={image.resourceUrl}
|
||||||
|
alt={`${image.orgName} logo`}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
</InfoLayout>
|
</InfoLayout>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
93
src/pages/SeriesPage/styles.js
Normal file → Executable file
@ -1,23 +1,24 @@
|
|||||||
import { h, Fragment } from 'preact'
|
import { h, Fragment } from 'preact'
|
||||||
import { zonedTimeToUtc, utcToZonedTime, format } from 'date-fns-tz'
|
import { zonedTimeToUtc, utcToZonedTime, format } from 'date-fns-tz'
|
||||||
|
import enGB from 'date-fns/locale/en-GB'
|
||||||
import { bool, func, instanceOf, string } from 'prop-types'
|
import { bool, func, instanceOf, string } from 'prop-types'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { colours, textSizes } from '../../assets/theme'
|
|
||||||
import config from '../../data/config'
|
|
||||||
|
|
||||||
import Markdown from '../../components/Markdown'
|
import Markdown from '../../components/Markdown'
|
||||||
import Logo from '../../components/Logo'
|
import Logo from '../../components/Logo'
|
||||||
import translations from '../../data/strings'
|
|
||||||
import CrossSvg from '../../components/Svg/Cross'
|
|
||||||
|
|
||||||
import { H1, H2, Span, Label } from '../../components/Text'
|
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 CrossSvg from '../../components/Svg/Cross'
|
||||||
import { ButtonsRows } from '../../components/EpisodeCard'
|
import { ButtonsRows } from '../../components/EpisodeCard'
|
||||||
|
|
||||||
|
import translations from '../../data/strings'
|
||||||
|
import { colours, textSizes } from '../../assets/theme'
|
||||||
|
import config from '../../data/config'
|
||||||
|
import { slugify } from '../../helpers/string'
|
||||||
|
|
||||||
export const TrailerContainer = styled.div`
|
export const TrailerContainer = styled.div`
|
||||||
height: 22em;
|
height: 21em;
|
||||||
border: 1px solid ${colours.midnightDarker};
|
border: 1px solid ${colours.midnightDarker};
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
|
||||||
@ -39,7 +40,6 @@ export const TrailerContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
||||||
export const ActionButton = styled(Button)`
|
export const ActionButton = styled(Button)`
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
`
|
`
|
||||||
@ -48,7 +48,7 @@ export const Row = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
flex-wrap: ${props => props.$wrap ? 'wrap' : 'nowrap'};
|
flex-wrap: ${props => (props.$wrap ? 'wrap' : 'nowrap')};
|
||||||
|
|
||||||
a {
|
a {
|
||||||
display: block;
|
display: block;
|
||||||
@ -60,23 +60,29 @@ export const Row = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
export const LogosRow = styled(Row)`
|
export const LogosRow = styled(Row)`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 32px 0 ;
|
padding: 32px 0;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
width: auto;
|
width: auto;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
|
||||||
&[href]:hover {
|
&[href]:hover {
|
||||||
opacity: 0.7
|
opacity: 0.7;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 64px;
|
max-height: 42px;
|
||||||
}
|
width: 25%;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const FooterImage = styled.img`
|
||||||
|
max-width: 600px;
|
||||||
|
padding: 32px 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const InfoContent = styled.div`
|
export const InfoContent = styled.div`
|
||||||
@ -167,25 +173,32 @@ export const EpisodeCard = ({
|
|||||||
url,
|
url,
|
||||||
hasPassed,
|
hasPassed,
|
||||||
videoUrl,
|
videoUrl,
|
||||||
onClickButton,
|
|
||||||
tzShort,
|
|
||||||
theme,
|
theme,
|
||||||
peertubeReplay,
|
peertubeReplay,
|
||||||
id
|
id,
|
||||||
}) => {
|
}) => {
|
||||||
const startDate = new Date(beginsOn)
|
const startDate = new Date(beginsOn)
|
||||||
const utcDate = zonedTimeToUtc(startDate, 'Europe/Berlin')
|
|
||||||
|
|
||||||
const { timeZone } = Intl.DateTimeFormat().resolvedOptions()
|
const { timeZone } = Intl.DateTimeFormat().resolvedOptions() // client timezone eg. 'Europe/Berlin'
|
||||||
|
const utcDate = zonedTimeToUtc(startDate, timeZone) // convert the start date to UTC
|
||||||
|
|
||||||
const zonedDate = utcToZonedTime(utcDate, timeZone)
|
const zonedDate = utcToZonedTime(utcDate, timeZone)
|
||||||
|
|
||||||
|
const timezoneLabel = format(
|
||||||
|
zonedDate,
|
||||||
|
hasPassed ? 'dd/MM/yy' : 'do LLLL y // HH:mm zzz',
|
||||||
|
{
|
||||||
|
timeZone,
|
||||||
|
locale: enGB,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VCWrapper id={id}>
|
<VCWrapper id={id}>
|
||||||
<DateLabel size={textSizes.lg} colour={theme.foreground}>
|
<DateLabel size={textSizes.lg} colour={theme.foreground}>
|
||||||
{`${hasPassed ? translations.en.streamDatePast : ''}`}
|
{`${hasPassed ? translations.en.streamDatePast : ''}`}
|
||||||
<Span bold colour={theme.foreground}>
|
<Span bold colour={theme.foreground}>
|
||||||
{hasPassed
|
{timezoneLabel}
|
||||||
? format(zonedDate, 'dd/MM/yy')
|
|
||||||
: `${format(zonedDate, 'do LLLL y // HH:mm')} ${tzShort}`}
|
|
||||||
</Span>
|
</Span>
|
||||||
</DateLabel>
|
</DateLabel>
|
||||||
{videoUrl && hasPassed ? (
|
{videoUrl && hasPassed ? (
|
||||||
@ -199,11 +212,25 @@ export const EpisodeCard = ({
|
|||||||
<VCImg src={image} alt="" />
|
<VCImg src={image} alt="" />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
<Markdown theme={theme}>{description}</Markdown>
|
<Markdown withLinebreaks theme={theme}>
|
||||||
|
{description}
|
||||||
|
</Markdown>
|
||||||
{hasPassed ? (
|
{hasPassed ? (
|
||||||
<a href={peertubeReplay.url || url}><Button>{peertubeReplay.url ? translations.en.watchEpisode : translations.en.eventDetails}</Button></a>
|
<a href={peertubeReplay.url || url}>
|
||||||
|
<Button>
|
||||||
|
{peertubeReplay.url
|
||||||
|
? translations.en.watchEpisode
|
||||||
|
: translations.en.eventDetails}
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<ButtonsRows title={title} description={description} beginsOn={beginsOn} endsOn={endsOn} url={url} />
|
<ButtonsRows
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
beginsOn={beginsOn}
|
||||||
|
endsOn={endsOn}
|
||||||
|
url={url}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</VCWrapper>
|
</VCWrapper>
|
||||||
)
|
)
|
||||||
|
0
src/store/index.js
Normal file → Executable file
0
static/android-chrome-192x192.png
Normal file → Executable file
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
0
static/apple-touch-icon.png
Normal file → Executable file
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
0
static/browserconfig.xml
Normal file → Executable file
0
static/favicon-16x16.png
Normal file → Executable file
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
0
static/favicon-32x32.png
Normal file → Executable file
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
0
static/favicon.ico
Normal file → Executable file
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
0
static/logo.png
Normal file → Executable file
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
0
static/meta.png
Normal file → Executable file
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 232 KiB |
0
static/mstile-150x150.png
Normal file → Executable file
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
0
static/safari-pinned-tab.svg
Normal file → Executable file
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |