lots of polish

This commit is contained in:
sunda 2021-11-01 23:34:29 +01:00
parent e6e05d90ef
commit c2c7e0f3a5
35 changed files with 889 additions and 230 deletions

View File

@ -8,19 +8,22 @@ import { zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz'
import Main from './app' import Main from './app'
import SeriesPage from './src/pages/SeriesPage' import SeriesPage from './src/pages/SeriesPage'
import { useEventApi, usePeertubeApi } from './src/hooks/data' import { useEventApi, usePeertubeApi } from './src/hooks/data'
import { useStreamStore, useTheme } from './src/store' import { useStreamStore, useTheme, useUiStore } from './src/store'
import { useTimeout } from './src/hooks/timerHooks' import { useTimeout } from './src/hooks/timerHooks'
import LoaderLayout from './src/pages/LoaderLayout' import LoaderLayout from './src/pages/LoaderLayout'
import FourOhFour from './src/pages/404' import FourOhFour from './src/pages/404'
import Series from './src/pages/Series' import Series from './src/pages/Series'
import Program from './src/pages/Program' import Program from './src/pages/Program'
import StreamPreview from './src/components/StreamPreview' import StreamPreview from './src/components/StreamPreview'
import Video from './src/components/Video'
// import { useWindowSize } from './src/hooks/dom'
const App = () => { const App = () => {
const { theme } = useTheme((store) => store) const { theme } = useTheme((store) => store)
const { data, loading: eventsLoading } = 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)
usePeertubeApi(data.episodes) usePeertubeApi(data.episodes)
useTimeout(() => { useTimeout(() => {
@ -55,16 +58,15 @@ const App = () => {
const seriesData = data.series ? Object.values(data.series) : [] const seriesData = data.series ? Object.values(data.series) : []
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
{!seriesData.length || eventsLoading || !minLoadTimePassed ? ( {!seriesData.length || eventsLoading || !minLoadTimePassed || error ? (
<LoaderLayout /> <LoaderLayout error={error} />
) : ( ) : (
<Fragment> <Fragment>
<BrowserRouter> <BrowserRouter>
<Switch> <Switch>
<Route exact path="/" component={Main} /> <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 ? seriesData.map(series => (
@ -76,7 +78,8 @@ const App = () => {
</Route> </Route>
</Switch> </Switch>
</BrowserRouter> </BrowserRouter>
<StreamPreview stream={currentStream} isLive={streamIsLive} /> {streamActive ? <Video stream={currentStream} /> :
<StreamPreview stream={currentStream} isLive={streamIsLive} />}
</Fragment> </Fragment>
)} )}
</ThemeProvider>) </ThemeProvider>)

View File

@ -15,7 +15,7 @@
"react-dom": "preact/compat" "react-dom": "preact/compat"
}, },
"dependencies": { "dependencies": {
"@peertube/embed-api": "^0.0.4", "@peertube/embed-api": "^0.0.5",
"axios": "^0.21.1", "axios": "^0.21.1",
"date-fns": "^2.19.0", "date-fns": "^2.19.0",
"date-fns-tz": "^1.1.4", "date-fns-tz": "^1.1.4",

View File

@ -3,11 +3,11 @@ import { colours } from '../../assets/theme'
const Button = styled.button` const Button = styled.button`
background-color: transparent; background-color: transparent;
border: 1px solid ${colours.rose}; border: 1px solid ${props => props.colour || colours.rose};
padding: 0.3em 1em; padding: 0.3em 1em;
font-family: Karla; font-family: Karla;
font-weight: inherit; font-weight: inherit;
color: ${colours.rose}; color: ${props => props.colour || colours.rose};
opacity: 1; opacity: 1;
text-decoration: none; text-decoration: none;
font-size: 24px; font-size: 24px;
@ -21,7 +21,7 @@ const Button = styled.button`
} }
:hover { :hover {
background-color: ${colours.rose}; background-color: ${props => props.hoverColour || colours.rose};
color: ${colours.midnightDarker}; color: ${colours.midnightDarker};
svg { svg {

View File

@ -37,8 +37,8 @@ const Chat = ({ overlayActive }) => {
</ChatFrame> </ChatFrame>
</ChatWrapper> </ChatWrapper>
) : ( ) : (
<ChatHeader chatIsOpen={false} onClick={toggleChatOpen}> <ChatHeader chatIsOpen={false} onClick={toggleChatOpen} $active={overlayActive}>
<Label weight="400" size={24}> <Label weight="400" size={16} colour={colours.midnightDarker}>
CHAT CHAT
</Label> </Label>
<OpenIcon colour={colours.white} size={16} /> <OpenIcon colour={colours.white} size={16} />

View File

@ -1,4 +1,4 @@
import styled from 'styled-components' import styled, { css } from 'styled-components'
import { colours, ui } from '../../assets/theme' import { colours, ui } from '../../assets/theme'
import CrossSvg from '../Svg/Cross' import CrossSvg from '../Svg/Cross'
import ChevronSvg from '../Svg/Chevron' import ChevronSvg from '../Svg/Chevron'
@ -27,17 +27,15 @@ export const ChatHeader = styled.div`
border-radius: ${props => border-radius: ${props =>
props.chatIsOpen ? `${ui.borderRadius}px 0 0 0` : '0'}; props.chatIsOpen ? `${ui.borderRadius}px 0 0 0` : '0'};
z-index: 2; z-index: 2;
background-color: ${props => (props.chatIsOpen ? '#00000036' : '#ffffffba')}; background-color: ${props => (props.chatIsOpen ? `${colours.midnightDarker}db` : '#ffffffba')};
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
height: 32px;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
align-items: center; align-items: center;
width: ${props => (props.chatIsOpen ? '100%' : 'fit-content')}; width: ${props => (props.chatIsOpen ? '100%' : 'fit-content')};
justify-content: space-between; justify-content: space-between;
padding: 0px 0px 3px 0px; padding: ${props => props.chatIsOpen ? '0px 0px 3px 0px' : '0.3em 0'};
right: ${props => (props.chatIsOpen ? '0' : '32px')}; right: ${props => (props.chatIsOpen ? '0' : '32px')};
box-sizing: content-box; box-sizing: content-box;
border: ${props => border: ${props =>
@ -50,12 +48,25 @@ export const ChatHeader = styled.div`
margin-left: 12px; margin-left: 12px;
margin-right: ${props => (props.chatIsOpen ? '0' : '12px')}; margin-right: ${props => (props.chatIsOpen ? '0' : '12px')};
color: ${props => color: ${props =>
props.chatIsOpen ? colours.white : colours.midnightDarker}; props.chatIsOpen ? colours.white : colours.midnightDarker};
} }
svg path { svg path {
fill: ${props => fill: ${props =>
props.chatIsOpen ? colours.white : colours.midnightDarker}; props.chatIsOpen ? colours.white : colours.midnightDarker};
} }
opacity: 0.001;
transform: translateY(20%);
transition: all 0.2s ease-in-out;
transition-delay: 0.2s;
cursor: pointer;
${props =>
(props.$active || props.chatIsOpen) &&
css`
transform: translateY(0%);
opacity: 1;
`};
` `
export const CloseBox = styled(CrossSvg)` export const CloseBox = styled(CrossSvg)`

View File

@ -4,6 +4,14 @@ import { Label, H2 } from '../Text'
import Flexbox, { Row as FlexRow, Column as FlexColumn } from '../Flex' import Flexbox, { Row as FlexRow, Column as FlexColumn } from '../Flex'
import Button from '../Button' import Button from '../Button'
const imageWidthStyles = `
width: 25vw;
@media screen and (max-width: ${screenSizes.md}px) {
width: 85vw;
}
`
export const ButtonRow = styled(Flexbox)` export const ButtonRow = styled(Flexbox)`
width: 100%; width: 100%;
align-items: stretch; align-items: stretch;
@ -29,11 +37,10 @@ export const ButtonRow = styled(Flexbox)`
` `
export const Left = styled(FlexColumn)` export const Left = styled(FlexColumn)`
margin-right: 1em; margin-right: 2em;
width: 20vw; ${imageWidthStyles};
@media screen and (max-width: ${screenSizes.md}px) { @media screen and (max-width: ${screenSizes.md}px) {
width: 80vw;
margin-right: 0em; margin-right: 0em;
} }
` `
@ -45,15 +52,7 @@ export const Center = styled(FlexColumn)`
` `
export const Title = styled(H2)` export const Title = styled(H2)`
max-width: 60%; max-width: 80%;
@media screen and (max-width: ${screenSizes.md}px) {
max-width: 80%;
}
@media screen and (max-width: ${screenSizes.sm}px) {
max-width: 70%;
}
margin-bottom: 1em; margin-bottom: 1em;
` `
@ -68,7 +67,6 @@ export const Right = styled.div`
} }
` `
export const StyledButton = styled(Button)` export const StyledButton = styled(Button)`
/* width: max-content; */
margin-top: 0.5em; margin-top: 0.5em;
padding: 0.3em 2em; padding: 0.3em 2em;
@ -79,14 +77,9 @@ export const StyledButton = styled(Button)`
export const Img = styled.div` export const Img = styled.div`
background: url(${({ src }) => src}); background: url(${({ src }) => src});
/* height: 215px; */
width: 20vw;
padding-bottom: calc((9 / 16) * 100%); padding-bottom: calc((9 / 16) * 100%);
background-size: cover; background-size: cover;
position: relative; position: relative;
background-position: center; background-position: center;
${imageWidthStyles};
@media screen and (max-width: ${screenSizes.md}px) {
width: 80vw;
}
` `

View File

@ -9,6 +9,7 @@ import { Span } from '../Text'
import navigation from '../../data/navigation' import navigation from '../../data/navigation'
import { colours, screenSizes, textSizes } from '../../assets/theme' import { colours, screenSizes, textSizes } from '../../assets/theme'
import { useTheme, useUiStore } from '../../store' import { useTheme, useUiStore } from '../../store'
import strings from '../../data/strings'
const Navigation = ({ theme = {}, lang = 'en', miniHeader, toggleMobileMenu }) => navigation[lang].map(navItem => ( const Navigation = ({ theme = {}, lang = 'en', miniHeader, toggleMobileMenu }) => navigation[lang].map(navItem => (
<Link <Link
@ -64,7 +65,9 @@ export const MobileMenuToggle = ({ miniHeader, ...props }) => {
colour={miniHeader ? theme.background : theme.foreground || colours.rose} colour={miniHeader ? theme.background : theme.foreground || colours.rose}
fontFamily="Lunchtype24" fontFamily="Lunchtype24"
{...props} {...props}
> Menu</Span>) >
{strings.en.menu}
</Span>)
} }

View File

@ -15,7 +15,7 @@ export const RRLink = styled(Link)`
color: ${colours.highlight}; color: ${colours.highlight};
font-size: 21px; font-size: 21px;
position: absolute; position: absolute;
top: 50%; top: 45%;
font-family: 'Karla'; font-family: 'Karla';
transform: translateY(-50%); transform: translateY(-50%);
} }

View File

@ -12,10 +12,10 @@ const Loader = ({
offset = 0, offset = 0,
animation = defaultLoader, animation = defaultLoader,
colour = colours.rose, colour = colours.rose,
rate = 300
}) => { }) => {
const [text, setText] = useState('.') const [text, setText] = useState('.')
const arrayPosition = useRef(offset) const arrayPosition = useRef(offset)
const rate = 300
useInterval( useInterval(
() => { () => {

View File

@ -0,0 +1,69 @@
import { Fragment, h } from 'preact'
import { arrayOf, number, shape, string } from 'prop-types'
import { useCallback, useRef, useState } from 'preact/hooks'
import { withTheme } from 'styled-components'
import { useOnClickOutside } from '../../hooks/dom'
import { Label } from '../Text'
import { Box, Item, Container, OptionsWrapper, ChevronIcon } from './styles'
import { colours } from '../../assets/theme'
const Select = withTheme(({ options, selectedIndex, onChange, theme, bottom, withIcon = true, ...rest }) => {
const [isOpen, setIsOpen] = useState(false)
const close = () => { setIsOpen(false) }
const open = () => { setIsOpen(true) }
const optionsRef = useRef([])
const toggle = () => { setIsOpen(itsopen => !itsopen) }
const ref = useRef()
useOnClickOutside(ref, useCallback(() => close()))
const handleItemClick = (index) => {
if (typeof onChange === 'function') {
close()
onChange(index)
}
}
const handleKeyPress = (e) => {
e.preventDefault()
if (e.keyCode === 32) {
open()
}
if (e.keyCode === 40) {
open()
console.log('optionsRef.current', optionsRef.current)
// optionsRef.current[1].focus();
}
}
return (
<Container ref={ref} {...rest}>
{options && options.length ? (
<Fragment>
<Box onClick={toggle} tabIndex={0} onKeyDown={handleKeyPress} data-hoverable>
<Label colour={colours.midnight}>{options[selectedIndex].label}</Label>
{withIcon && <ChevronIcon colour={colours.midnight} size={14} />}
</Box>
{isOpen && options && options.length ? (
<OptionsWrapper ref={optionsRef} bottom={bottom} >
{options.map((option, optionIndex) => option.value !== options[selectedIndex].value && (
<Item tabIndex={0} data-hoverable onClick={() => handleItemClick(optionIndex)} key={option.value} ref={optionsRef[optionIndex]}>
{option.label}
</Item>
))}
</OptionsWrapper>
) : null}
</Fragment>
) : <Box />}
</Container>
)
})
Select.propTypes = {
options: arrayOf(shape({
label: string,
})),
selectedIndex: number,
}
export default Select

View File

@ -0,0 +1,63 @@
import { h } from 'preact'
import styled, { css } from 'styled-components'
import { colours } from '../../assets/theme'
import ChevronSvg from '../Svg/Chevron'
import { Label } from '../Text'
export const Container = styled.div`
position: relative;
max-width: min-content;
margin-left: 8px;
background-color: red;
`
export const OptionsWrapper = styled.div`
position: absolute;
display: flex;
flex-direction: ${props => props.bottom ? 'column-reverse' : 'column'};
bottom: ${props => props.bottom ? '100%' : 'auto'};
transform: translateY(${props => props.bottom ? '1' : '-1'}px);
div {
margin-${props => props.bottom ? 'top' : 'bottom'}: -1px;
}
`
export const Box = styled.div`
padding: 4px 16px;
/* border: 1px solid ${({ theme }) => theme.foreground}; */
/* background: ${({ theme }) => theme.background}; */
position: relative;
display: flex;
flex-direction: row;
align-items: center;
border: 1px solid ${colours.midnight};
min-width: 45px;
text-align: center;
label {
position: relative;
top: 1px;
line-height: 1;
}
${({ selectable }) => selectable && css`
&:hover {
background: ${colours.white};
label {
color: ${colours.midnightDarker};
}
}
`}
`
export const Item = ({ label, children, ...rest }) => (
<Box selectable {...rest}>
<Label>{label || children}</Label>
</Box>
)
export const ChevronIcon = styled(ChevronSvg)`
margin-left: 8px
`

View File

@ -19,12 +19,12 @@ const SeriesCard = ({ series: { image, episodes: allEpisodes, title, subtitle, h
<LabelBlock <LabelBlock
$position="top" $position="top"
> >
{episodes.length} {strings.en.episodes} {episodes.length} {episodes.length === 1 ? strings.en.episode : strings.en.episodes}
</LabelBlock> </LabelBlock>
<LabelBlock <LabelBlock
$position="bottom" $position="bottom"
> >
{isPast ? strings.en.lastStream : strings.en.nextStream} {episodes && episodes.length && formatDistanceToNow(new Date(episodes[0].endsOn), { addSuffix: true })} {isPast ? strings.en.lastStream : strings.en.nextStream} {episodes && episodes.length && formatDistanceToNow(new Date(episodes[0][isPast ? 'endsOn' : 'beginsOn']), { addSuffix: true })}
</LabelBlock> </LabelBlock>
</Img> </Img>

View File

@ -5,20 +5,25 @@ import { string } from 'prop-types'
import Link from '../Link' import Link from '../Link'
import { Label } from '../Text' import { Label } from '../Text'
import strings from '../../data/strings' import strings from '../../data/strings'
import { Box, Img, Iframe } from './styles'
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'
import { useUiStore } from '../../store' import { useUiStore } from '../../store'
import { getLabel } from './helpers' import { getLabel } from './helpers'
import Chevron from '../Svg/Chevron' import Chevron from '../Svg/Chevron'
import { Frame, Img, Iframe, InnerWrapper } from './styles'
// import { useEventApi } from '../../hooks/data' // 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 } = useUiStore(store => ({ isMinimized: store.streamPreviewMinimized, toggleMinimized: store.toggleStreamPreviewMinimized })) const { isMinimized, toggleMinimized, setStreamActive } = useUiStore(store => ({
isMinimized: store.streamPreviewMinimized,
toggleMinimized: store.toggleStreamPreviewMinimized,
setStreamActive: store.setStreamActive
}))
useEffect(() => { useEffect(() => {
@ -35,14 +40,18 @@ const StreamPreview = ({ stream, isLive, ...rest }) => {
} }
}, [isLive, isMinimized]) }, [isLive, isMinimized])
const activateStream = () => {
setStreamActive(true)
}
return stream ? ( return stream ? (
<Box isMinimized={isMinimized}> <Frame isMinimized={isMinimized}>
<Row justify="space-between"> <Row justify="space-between">
<Label colour={colours.midnightDarker} size={textSizes.lg}>{getLabel(stream, isLive, isMinimized)}</Label> <Label 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} />} {isMinimized ? <Chevron colour={colours.midnightDarker} size={14} onClick={toggleMinimized} /> : <CrossSvg colour={colours.midnightDarker} size={16} onClick={toggleMinimized} />}
</Row> </Row>
{!isMinimized ? {!isMinimized ?
<Fragment> <InnerWrapper onClick={activateStream}>
{isLive ? {isLive ?
<Iframe <Iframe
width="560" width="560"
@ -55,9 +64,9 @@ const StreamPreview = ({ stream, isLive, ...rest }) => {
allowFullScreen allowFullScreen
ref={videoiFrame} ref={videoiFrame}
/> />
: <Img src={stream.image} />} : <Img src={stream.image} onClick={activateStream} />}
</Fragment> : null} </InnerWrapper> : null}
</Box> </Frame>
) : null ) : null
} }

View File

@ -1,7 +1,15 @@
import styled from 'styled-components' import styled, { css } from 'styled-components'
import { colours, screenSizes } from '../../assets/theme' import { colours, screenSizes } from '../../assets/theme'
export const Box = styled.div` const ratio169 = css`
&:before {
display: block;
content: "";
width: 100%;
padding-top: calc((9 / 16) * 100%);
};`
export const Frame = styled.div`
position: fixed; position: fixed;
bottom: ${props => props.isMinimized ? 0 : '2em'}; bottom: ${props => props.isMinimized ? 0 : '2em'};
right: ${props => props.isMinimized ? 0 : '2em'}; right: ${props => props.isMinimized ? 0 : '2em'};
@ -24,11 +32,40 @@ export const Box = styled.div`
` `
export const Img = styled.div` export const Img = styled.div`
background: url(${({ src }) => src}); background: url(${({ src }) => src});
width: 16vw;
padding-bottom: calc((9 / 16) * 100%);
background-size: cover; background-size: cover;
position: relative; position: relative;
background-position: center; background-position: center;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
`
export const Iframe = styled.iframe`
pointer-events: none;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
`
export const InnerWrapper = styled.div`
cursor: pointer;
position: relative;
width: 16vw;
&:before {
display: block;
content: "";
width: 100%;
padding-top: calc((9 / 16) * 100%);
};
@media screen and (max-width: ${screenSizes.xl}px) { @media screen and (max-width: ${screenSizes.xl}px) {
width: 20vw; width: 20vw;
@ -49,9 +86,19 @@ export const Img = styled.div`
@media screen and (max-width: ${screenSizes.xs}px) { @media screen and (max-width: ${screenSizes.xs}px) {
width: 60vw; width: 60vw;
} }
` &:hover {
opacity: 0.5;
export const Iframe = styled.iframe`
width: 20vw; &:after {
height: 11.2vw; display: block;
content: url("data:image/svg+xml,<svg width='32' viewbox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><path stroke-width='2' stroke='white' fill='transparent' d='M2.12436,1.73205 C2.12436,0.96225 2.95769,0.481125 3.62436,0.866025 L21.6244,11.2583 C22.291,11.6432 22.291,12.6055 21.6244,12.9904 L3.62436,23.3827 C2.95769,23.7676 2.12436,23.2865 2.12436,22.5167 L2.12436,1.73205 Z'/></svg>");
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
height: 24px;
width: 24px;
};
}
` `

View File

@ -0,0 +1,62 @@
import { Fragment, h } from 'preact'
import { bool, string } from 'prop-types'
import styled from 'styled-components'
import { colours } from '../../../assets/theme'
import { useUiStore } from '../../../store'
import { InfoButton, PositionedCross as CrossSvg, OverlayWrapper, TopLeft } from './styles'
const StyledP = styled(P)`
&:first-of-type {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
&:last-of-type {
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
`
const renderTitles = titles =>
titles.split('\\n').map(title => (
<StyledP key={title} size={18}>
{title}
</StyledP>
))
const VideoOverlay = ({
active,
title,
org,
onClick,
onClickFullscreen,
isFullscreen,
streamIsLive,
}) => {
const setStreamActive = useUiStore(store => store.setStreamActive)
const closeStream = () => setStreamActive(false)
return (
<Fragment>
<OverlayWrapper onClick={onClick}>
<TopLeft $active={active}>
{title ? renderTitles(title) : null}
</TopLeft>
</OverlayWrapper>
<CrossSvg colour={colours.white} size={32} $active={active} onClick={closeStream} />
<InfoButton $active={active} onClick={onClickFullscreen} postition="bl" colour={colours.midnight} hoverColour={colours.offwhite}>
{isFullscreen ? 'EXIT FULLSCREEN' : 'FULLSCREEN'}
</InfoButton>
</Fragment>
)
}
VideoOverlay.propTypes = {
active: bool,
title: string.isRequired,
org: string,
}
export default VideoOverlay

View File

@ -0,0 +1,84 @@
import styled, { css } from 'styled-components'
import { colours } from '../../assets/theme'
import Button from '../Button'
import Cross from '../Svg/Cross'
export const OverlayWrapper = styled.div`
z-index: 2;
position: fixed;
height: 100vh;
width: 100vw;
`
export const TopLeft = styled.div`
opacity: 0;
transform: translateY(-20%);
transition: all 0.2s ease-in-out;
padding: 2em;
${props =>
props.$active &&
css`
transform: translateY(0%);
opacity: 1;
`};
p,
svg {
backdrop-filter: blur(20px);
background-color: ${colours.midnight}40;
padding: 4px 8px;
display: inline-block;
margin-right: 45%;
}
`
export const PositionedCross = styled(Cross)`
position: fixed;
right: 32px;
top: 32px;
opacity: 0.001;
transform: translateY(-20%);
transition: all 0.2s ease-in-out;
transition-delay: 0.2s;
z-index: 100;
${props =>
props.$active &&
css`
transform: translateY(0%);
opacity: 1;
`};
&:hover {
opacity: 0.7;
}
`
export const InfoButton = styled(Button)`
opacity: 0.001;
transform: translateY(
${props => (props.postition === 'bl' ? '20%' : '-20%')}
);
transition: all 0.2s ease-in-out;
transition-delay: 0.2s;
position: fixed;
right: ${props => (props.postition === 'bl' ? 'initial' : '32px')};
top: ${props => (props.postition === 'bl' ? 'initial' : '32px')};
bottom: ${props => (props.postition === 'bl' ? '0' : 'initial')};
left: ${props => (props.postition === 'bl' ? '32px' : 'initial')};
z-index: 100;
width: auto;
background-color: #ffffffba;
padding: 0.1em 0.5em;
font-size: 21px;
${props =>
props.$active &&
css`
transform: translateY(0%);
opacity: 1;
`};
&:hover {
opacity: 0.7;
}
`

View File

@ -15,15 +15,22 @@ import Overlay from '../VideoOverlay'
import { VideoWrapper, Iframe, PlayButton } from './styles' import { VideoWrapper, Iframe, PlayButton } from './styles'
import config from '../../data/config' import config from '../../data/config'
import { useToggle } from '../../hooks/utility' import { useToggle } from '../../hooks/utility'
import { useInterval } from '../../hooks/timerHooks'
import { useStreamStore, useUiStore } from '../../store'
const Video = ({ video, org, setInfoActive }) => { const Video = () => {
const { currentStream: stream, streamIsLive } = useStreamStore(store => store)
const [isPlaying, setPlaying] = useState(false) const [isPlaying, setPlaying] = useState(false)
const [isFullscreen, toggleIsFullscreen] = useToggle(false) const [isFullscreen, toggleIsFullscreen] = useToggle(false)
// const [is1080p, toggle1080p] = useToggle(true)
const videoiFrame = useRef(null) const videoiFrame = useRef(null)
const overlayTimeout = useRef(null) const overlayTimeout = useRef(null)
const [videoReady, setVideoReady] = useState(false) const [videoReady, setVideoReady] = useState(false)
const [overlayActive, setOverlayActiveState] = useState(true) const [overlayActive, setOverlayActiveState] = useState(true)
const ptVideo = useRef(null) const ptVideo = useRef(null)
const resolutions = useRef(null)
const [volume, setVolume] = useState(1)
useEffect(() => { useEffect(() => {
const setVideo = async () => { const setVideo = async () => {
@ -31,12 +38,26 @@ const Video = ({ video, org, setInfoActive }) => {
await player.ready await player.ready
ptVideo.current = player ptVideo.current = player
player.setVolume(100) player.setVolume(1)
resolutions.current = await player.getResolutions()
if (streamIsLive) {
setPlaying(true)
try {
player.play()
} catch (error) {
console.log({ error })
setOverlayActiveState(true)
setPlaying(false)
}
}
setVideoReady(true) setVideoReady(true)
} }
setVideo() setVideo()
}, []) }, [])
const playVideo = () => { const playVideo = () => {
const { current: player } = ptVideo const { current: player } = ptVideo
if (!videoReady) return if (!videoReady) return
@ -63,6 +84,7 @@ const Video = ({ video, org, setInfoActive }) => {
} }
} }
const toggleVideo = () => { const toggleVideo = () => {
if (isPlaying) { if (isPlaying) {
pauseVideo() pauseVideo()
@ -71,6 +93,15 @@ const Video = ({ video, org, setInfoActive }) => {
} }
} }
useEffect(() => {
toggleVideo()
}, [streamIsLive])
useEffect(() => {
if (!videoReady) return
ptVideo.current.setVolume(volume)
}, [volume])
const toggleFullscreen = () => { const toggleFullscreen = () => {
toggleIsFullscreen() toggleIsFullscreen()
if (!document.fullscreenElement) { if (!document.fullscreenElement) {
@ -80,6 +111,21 @@ const Video = ({ video, org, setInfoActive }) => {
} }
} }
const volumeUp = () => {
if (volume.current === 1) return
setVolume(volume + 0.1)
}
const volumeDown = async () => {
if (volume.current === 0) return
console.log()
setVolume(volume - 0.1)
const vol = await ptVideo.current.getVolume()
console.log({ vol })
}
const handleKeyPress = keyCode => { const handleKeyPress = keyCode => {
if (keyCode === 32) { if (keyCode === 32) {
// key == 'space' // key == 'space'
@ -89,6 +135,16 @@ const Video = ({ video, org, setInfoActive }) => {
// key == 'f' // key == 'f'
toggleFullscreen() toggleFullscreen()
} }
if (keyCode === 38) {
// key == 'arrow UP'
console.log('volup')
volumeUp()
}
if (keyCode === 40) {
// key == 'arrow DOWN'
console.log('voldown')
volumeDown()
}
} }
useEffect(() => { useEffect(() => {
@ -114,49 +170,50 @@ const Video = ({ video, org, setInfoActive }) => {
<VideoWrapper <VideoWrapper
$active={overlayActive || !isPlaying} $active={overlayActive || !isPlaying}
onMouseMove={activateOverlay} onMouseMove={activateOverlay}
isLive={streamIsLive}
> >
<Overlay <Overlay
onClick={toggleVideo} onClick={streamIsLive ? toggleVideo : null}
active={overlayActive || !isPlaying} active={overlayActive || !isPlaying}
title={video.title} title={stream.title}
setInfoActive={setInfoActive}
onClickFullscreen={toggleFullscreen} onClickFullscreen={toggleFullscreen}
isFullscreen={isFullscreen} isFullscreen={isFullscreen}
resolutions={resolutions.current}
videoRef={ptVideo.current}
streamIsLive={streamIsLive}
/> />
{!isPlaying && <PlayButton />} {!isPlaying && <PlayButton streamIsLive={streamIsLive} />}
<Iframe <Iframe
sandbox="allow-same-origin allow-scripts allow-popups" sandbox="allow-same-origin allow-scripts allow-popups"
src={`${config.peertube_root}${video.embedPath}?api=1&controls=false&vq=hd1080`} src={`${config.peertube_root}${stream.embedPath}?api=1&controls=false&vq=hd1080`}
frameborder="0" frameborder="0"
allowfullscreen allowfullscreen
allow="autoplay" allow="autoplay"
ref={videoiFrame} ref={videoiFrame}
/> />
<Chat overlayActive={overlayActive} /> <Chat overlayActive={overlayActive || !isPlaying} />
</VideoWrapper> </VideoWrapper>
) )
} }
Video.propTypes = { Video.propTypes = {
video: shape({ // stream: shape({
account: object, // account: object,
category: object, // category: object,
channel: object, // channel: object,
description: string, // description: string,
duration: number, // duration: number,
embedPath: string, // embedPath: string,
end: instanceOf(Date), // end: instanceOf(Date),
id: string, // id: string,
language: object, // language: object,
previewPath: string, // previewPath: string,
start: instanceOf(Date), // start: instanceOf(Date),
state: object, // state: object,
title: 'Testing a livesteam :)', // videoUrl: string,
videoUrl: string, // views: number,
views: number, // }),
}), // title: string.isRequired,
title: string.isRequired,
org: string,
} }
export default Video export default Video

View File

@ -3,8 +3,14 @@ import styled from 'styled-components'
import { Label } from '../Text' import { Label } from '../Text'
import translations from '../../data/strings' import translations from '../../data/strings'
import Button from '../Button' import Button from '../Button'
import Loader from '../Loader'
import { colours } from '../../assets/theme' import { colours } from '../../assets/theme'
const getCursor = ({ isLive, $active }) => {
if (!isLive) return 'default'
return $active ? 'pointer' : 'none'
}
export const VideoWrapper = styled.div` export const VideoWrapper = styled.div`
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
@ -14,11 +20,16 @@ export const VideoWrapper = styled.div`
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
cursor: ${props => (props.$active ? 'pointer' : 'none')}; cursor: ${props => getCursor(props)};
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
` `
// const g = Label?.f?.e
export const Iframe = styled.iframe` export const Iframe = styled.iframe`
z-index: -1; z-index: -1;
width: 100vw; width: 100vw;
@ -38,15 +49,15 @@ const ButtonWrapper = styled.div`
padding: 1em 2em; padding: 1em 2em;
background-color: #ffffffba; background-color: #ffffffba;
display: flex; display: flex;
flex-direction: row; flex-direction: column;
align-items: center; align-items: center;
border: 1px solid ${colours.midnight}; border: 1px solid ${colours.midnight};
} }
label { label {
color: ${colours.midnightDarker}; color: ${colours.midnightDarker};
margin-left: 8px; margin: ${props => props.streamIsLive ? '0' : '8px 0 0 8px'};
font-size: 20px; font-size: 16px;
} }
:hover div { :hover div {
@ -57,7 +68,8 @@ const ButtonWrapper = styled.div`
export const PlayButton = props => ( export const PlayButton = props => (
<ButtonWrapper {...props}> <ButtonWrapper {...props}>
<div> <div>
<Label>{translations.en.joinStream}</Label> {!props.streamIsLive && <Loader colour={colours.midnight} rate={500} />}
<Label>{props.streamIsLive ? translations.en.joinStream : translations.en.streamStartingSoon}</Label>
</div> </div>
</ButtonWrapper> </ButtonWrapper>
) )

View File

@ -1,10 +1,12 @@
import { Fragment, h } from 'preact' import { Fragment, h } from 'preact'
import { useEffect, useState } from 'preact/hooks'
import { bool, string } from 'prop-types' import { bool, string } from 'prop-types'
import styled from 'styled-components' import styled from 'styled-components'
import { colours } from '../../assets/theme'
import { useUiStore } from '../../store'
import Logo from '../Logo'
import { H2, P } from '../Text' import { H2, P } from '../Text'
import { InfoButton, OverlayWrapper, TopLeft } from './styles' import { InfoButton, PositionedCross as CrossSvg, OverlayWrapper, TopLeft, ButtonRow, ResoltionSelect } from './styles'
const StyledP = styled(P)` const StyledP = styled(P)`
&:first-of-type { &:first-of-type {
@ -25,32 +27,72 @@ const renderTitles = titles =>
</StyledP> </StyledP>
)) ))
const resolutions = [
{
value: -1,
label: 'AUTO',
},
{
value: 3,
label: '240p',
},
{
value: 0,
label: '480p',
},
{
value: 1,
label: '360p',
},
{
value: 2,
label: '720p',
},
{
value: 4,
label: '1080p',
}
]
const VideoOverlay = ({ const VideoOverlay = ({
active, active,
title, title,
org, org,
setInfoActive,
onClick, onClick,
onClickFullscreen, onClickFullscreen,
isFullscreen, isFullscreen,
}) => ( streamIsLive,
// const displayTitle = `${title}${org ? ` — ${org}` : ''}` videoRef
}) => {
const setStreamActive = useUiStore(store => store.setStreamActive)
const closeStream = () => setStreamActive(false)
const [resoltionIndex, setResolutionIndex] = useState(0)
<Fragment>
<OverlayWrapper onClick={onClick}> useEffect(() => {
<TopLeft $active={active}> if (videoRef) {
{/* <Logo active={active} /> */} videoRef.setResolution(resolutions[resoltionIndex] ? resolutions[resoltionIndex].value : -1)
{title ? renderTitles(title) : null} }
</TopLeft> }, [resoltionIndex])
</OverlayWrapper>
<InfoButton $active={active} onClick={() => setInfoActive(true)}> return (
INFO <Fragment>
</InfoButton> <OverlayWrapper onClick={onClick}>
<InfoButton $active={active} onClick={onClickFullscreen} postition="bl"> <TopLeft $active={active}>
{isFullscreen ? 'EXIT FULLSCREEN' : 'FULLSCREEN'} {/* <Logo active={active} /> */}
</InfoButton> {title ? renderTitles(title) : null}
</Fragment> </TopLeft>
) </OverlayWrapper>
<CrossSvg colour={colours.white} size={32} $active={active} onClick={closeStream} />
<ButtonRow>
<InfoButton $active={active} onClick={onClickFullscreen} postition="bl" colour={colours.midnightDarker} hoverColour={colours.offwhite}>
{isFullscreen ? 'EXIT FULLSCREEN' : 'FULLSCREEN'}
</InfoButton>
{streamIsLive && resolutions ? <ResoltionSelect options={resolutions} onChange={setResolutionIndex} selectedIndex={resoltionIndex} $active={active} bottom withIcon={false} /> : null}
</ButtonRow>
</Fragment>
)
}
VideoOverlay.propTypes = { VideoOverlay.propTypes = {
active: bool, active: bool,

View File

@ -1,14 +1,14 @@
import styled, { css } from 'styled-components' import styled, { css } from 'styled-components'
import { colours } from '../../assets/theme' import { colours } from '../../assets/theme'
import burb from '../../assets/img/IconSM.png'
import Button from '../Button' import Button from '../Button'
import Select from '../Select'
import Cross from '../Svg/Cross'
export const OverlayWrapper = styled.div` export const OverlayWrapper = styled.div`
z-index: 2; z-index: 2;
position: fixed; position: fixed;
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
/* pointer-events: none; */
` `
export const TopLeft = styled.div` export const TopLeft = styled.div`
opacity: 0; opacity: 0;
@ -32,21 +32,15 @@ export const TopLeft = styled.div`
} }
` `
export const InfoButton = styled(Button)` export const PositionedCross = styled(Cross)`
position: fixed;
right: 32px;
top: 32px;
opacity: 0.001; opacity: 0.001;
transform: translateY( transform: translateY(-20%);
${props => (props.postition === 'bl' ? '20%' : '-20%')}
);
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
transition-delay: 0.2s; transition-delay: 0.2s;
position: fixed;
right: ${props => (props.postition === 'bl' ? 'initial' : '32px')};
top: ${props => (props.postition === 'bl' ? 'initial' : '32px')};
bottom: ${props => (props.postition === 'bl' ? '0' : 'initial')};
left: ${props => (props.postition === 'bl' ? '32px' : 'initial')};
z-index: 100; z-index: 100;
width: auto;
background-color: #ffffffba;
${props => ${props =>
props.$active && props.$active &&
@ -59,3 +53,74 @@ export const InfoButton = styled(Button)`
opacity: 0.7; opacity: 0.7;
} }
` `
export const InfoButton = styled(Button)`
opacity: 0.001;
transform: translateY(
${props => (props.postition === 'bl' ? '20%' : '-20%')}
);
transition: all 0.2s ease-in-out;
transition-delay: 0.2s;
z-index: 100;
width: auto;
background-color: #ffffffba;
padding: 0.1em 0.5em;
font-size: 21px;
${props =>
props.$active &&
css`
transform: translateY(0%);
opacity: 1;
`};
&:hover {
opacity: 0.7;
}
`
export const ButtonRow = styled.div`
position: fixed;
bottom: 0;
left: 32px;
display: flex;
flex-direction: row;
z-index: 100;
button:not(:first-of-type) {
margin-left: -1px;
}
label, button {
font-size: 16px;
}
@media screen and (max-width: 1000px) {
left: 0;
}
`
export const ResoltionSelect = styled(Select)`
z-index: 100;
background-color: #ffffffba;
font-size: 21px;
position: relative;
bottom: -1px;
opacity: 0.001;
transform: translateY(20%);
transition: all 0.2s ease-in-out;
transition-delay: 0.2s;
cursor: pointer;
label {
cursor: pointer;
}
${props =>
props.$active &&
css`
transform: translateY(0%);
opacity: 1;
`};
`

View File

@ -2,7 +2,7 @@ export default {
en: [ en: [
{ {
label: 'Program guide', label: 'Program guide',
to: '/program' to: '/'
}, },
{ {
label: 'Series', label: 'Series',

View File

@ -4,6 +4,7 @@ export default {
pastStream: 'Previous Episodes', pastStream: 'Previous Episodes',
nowPlaying: 'Currently streaming', nowPlaying: 'Currently streaming',
startingSoon: 'Starting soon', startingSoon: 'Starting soon',
streamStartingSoon: 'Stream starting soon',
noStreams: 'No upcoming streams, check back soon.', noStreams: 'No upcoming streams, check back soon.',
underscoreTagline: ['LEAVE THE', 'SURVEILLANCE ECONOMY', '— TOGETHER.'], underscoreTagline: ['LEAVE THE', 'SURVEILLANCE ECONOMY', '— TOGETHER.'],
streamDateFuture: 'Going live at: ', streamDateFuture: 'Going live at: ',
@ -20,9 +21,13 @@ export default {
pastSeries: 'Past Series', pastSeries: 'Past Series',
lastStream: 'Last stream', lastStream: 'Last stream',
nextStream: 'Next stream', nextStream: 'Next stream',
episode: 'episode',
episodes: 'episodes', episodes: 'episodes',
today: 'today', today: 'today',
tomorrow: 'tomorrow', tomorrow: 'tomorrow',
eventDetails: 'Event details', eventDetails: 'Event details',
errorTitle: 'Hmm...',
errorBody: 'Something went wrong, please try again later',
menu: 'Menu'
}, },
} }

View File

@ -103,20 +103,27 @@ export const useEventCalendar = () => {
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 [loading, setLoading] = useState(!!data.length) const [loading, setLoading] = useState(!!data.length)
async function fetchData() { async function fetchData() {
if (!data.length) { if (!data.length) {
setLoading(true) setLoading(true)
const { data: responseData } = await axios.get( try {
`${config.EVENTS_API_URL}/events`
)
const { data: responseData } = await axios.get(
setData(responseData) `${config.EVENTS_API_URL}/events`
console.log({ data: responseData }) )
setLoading(false) setData(responseData)
console.log({ data: responseData })
setLoading(false)
}
catch (err) {
console.log('ERROR')
setError(err)
setLoading(false)
}
} }
} }
@ -124,7 +131,7 @@ export const useEventApi = () => {
fetchData() fetchData()
}, []) }, [])
return { loading, data } return { loading, data, error }
} }
export const usePeertubeApi = async () => { export const usePeertubeApi = async () => {
@ -133,13 +140,13 @@ export const usePeertubeApi = async () => {
if (!currentStream) return if (!currentStream) return
const fetchData = async () => { const fetchData = async () => {
if (!currentStream.peertubeId) return const { peertubeLive } = currentStream
const { peertubeId } = currentStream if (!peertubeLive || !peertubeLive.id) return
const { const {
data: { state, embedPath } data: { state, embedPath }
} = await axios.get(`https://tv.undersco.re/api/v1/videos/${peertubeId}`) } = 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 })

View File

@ -1,27 +1,27 @@
import { useState, useEffect } from 'preact/hooks' import { useState, useEffect } from 'preact/hooks'
const getWidth = () => const getClientWidth = () =>
window.innerWidth || window.innerWidth ||
document.documentElement.clientWidth || document.documentElement.clientWidth ||
document.body.clientWidth document.body.clientWidth
const getHeight = () => const getClientHeight = () =>
window.innerHeight || window.innerHeight ||
document.documentElement.clientHeight || document.documentElement.clientHeight ||
document.body.clientHeight document.body.clientHeight
// save current window width in the state object // save current window width in the state object
export const useWindowDimensions = () => { export const useWindowDimensions = () => {
const [width, setWidth] = useState(getWidth()) const [width, setWidth] = useState(getClientWidth())
const [height, setHeight] = useState(getHeight()) const [height, setHeight] = useState(getClientHeight())
useEffect(() => { useEffect(() => {
let timeoutId = null let timeoutId = null
const resizeListener = () => { const resizeListener = () => {
clearTimeout(timeoutId) clearTimeout(timeoutId)
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
setWidth(getWidth()) setWidth(getClientWidth())
setHeight(getHeight()) setHeight(getClientHeight())
}, 50) }, 50)
} }
window.addEventListener('resize', resizeListener) window.addEventListener('resize', resizeListener)
@ -63,3 +63,29 @@ export const useWindowSize = () => {
}, []) // Empty array ensures that effect is only run on mount }, []) // Empty array ensures that effect is only run on mount
return windowSize return windowSize
} }
export const useOnClickOutside = (ref, handler) => {
useEffect(() => {
const listener = (event) => {
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target)) {
return
}
handler(event)
}
document.addEventListener('mousedown', listener)
document.addEventListener('touchstart', listener)
return () => {
document.removeEventListener('mousedown', listener)
document.removeEventListener('touchstart', listener)
}
},
// Add ref and handler to effect dependencies
// It's worth noting that because passed in handler is a new ...
// ... function on every render that will cause this effect ...
// ... callback/cleanup to run every render. It's not a big deal ...
// ... but to optimize you can wrap handler in useCallback before ...
// ... passing it into this hook.
[ref, handler],
)
}

View File

@ -13,28 +13,36 @@ import {
import Header from '../../components/Header' import Header from '../../components/Header'
import { ImageLogo } from '../../components/Logo' import { ImageLogo } from '../../components/Logo'
import { useWindowSize } from '../../hooks/dom'
import { screenSizes } from '../../assets/theme'
const InfoLayout = ({ title, subtitle, image, children, theme }) => ( const InfoLayout = ({ title, subtitle, image, children, theme }) => {
<Wrapper theme={theme}> const { width: screenWidth } = useWindowSize()
<PositionedLink to="/" theme={theme}> const isMobile = screenWidth < screenSizes.md
<ImageLogo />
</PositionedLink> return (
<Content> <Wrapper theme={theme}>
{children} <PositionedLink to="/" $theme={theme}>
</Content> <ImageLogo />
<Hero image={image}> </PositionedLink>
<Header theme={theme} miniHeader /> {isMobile ? <Header theme={theme} /> : null}
<H1>{title}</H1> <Content>
<H1 {children}
css={` </Content>
<Hero image={image}>
<Header theme={theme} miniHeader />
<H1 colour={theme.foreground}>{title}</H1>
<H1 colour={theme.foreground}
css={`
max-width: 50%; max-width: 50%;
`} `}
> >
{subtitle} {subtitle}
</H1> </H1>
<FadeTop colour={theme.foreground} /> <FadeTop colour={theme.foreground} />
</Hero> </Hero>
</Wrapper> </Wrapper>
) )
}
export default InfoLayout export default InfoLayout

View File

@ -18,7 +18,12 @@ export const Wrapper = styled.div`
@media screen and (max-width: ${screenSizes.lg}px) { @media screen and (max-width: ${screenSizes.lg}px) {
padding: 1.5em; padding: 1.5em;
} }
@media screen and (max-width: ${screenSizes.sm}px) { @media screen and (max-width: ${screenSizes.md}px) {
/* padding: 1.5em 1.5em 1.5em 10em; */
display: flex;
justify-content: center;
}
@media screen and (max-width: ${screenSizes.sm}px)
padding: 1em; padding: 1em;
} }
@ -78,9 +83,17 @@ export const Hero = styled.div`
h1{ h1{
margin-bottom: 0.2em; margin-bottom: 0.2em;
&:not(:last-of-type) { &:not(:last-of-type) {
font-size: 12vw; font-size: 12.5vw;
@media screen and (max-width: ${screenSizes.lg}px) {
font-size: 9vw;
}
@media screen and (min-width: ${screenSizes.lg}px) {
font-size: 14vw;
}
}} }}
@media screen and (max-width: ${screenSizes.md}px) { @media screen and (max-width: ${screenSizes.md}px) {
display: none; display: none;
} }
@ -115,7 +128,7 @@ export const PositionedLink = styled(Link)`
z-index: 2; z-index: 2;
top: 0; top: 0;
left: 0; left: 0;
background-color: ${({ theme }) => theme.background}; background-color: ${({ $theme }) => $theme.background};
&:hover img { &:hover img {
filter: invert(1); filter: invert(1);

View File

@ -1,27 +1,33 @@
import { h } from 'preact' import { h } from 'preact'
import translations from '../../data/strings' import translations from '../../data/strings'
import { H1 } from '../../components/Text' import { H1, H2 } from '../../components/Text'
import { import {
Wrapper, Wrapper,
LoaderWrapper, LoaderWrapper,
Hero, Hero,
PositionedLogo as Logo, PositionedLogo as Logo,
TaglineContainer, TaglineContainer,
ErrorBlock
} from './styles' } from './styles'
import Loader from '../../components/Loader' import Loader from '../../components/Loader'
import { colours } from '../../assets/theme'
const LoaderLayout = () => ( const LoaderLayout = ({ error }) => (
<Wrapper> <Wrapper>
<Logo active /> <Logo active />
<LoaderWrapper> <LoaderWrapper>
<Loader /> {error ? (
<ErrorBlock>
<H1 colour={colours.white}>{translations.en.errorTitle}</H1>
<H2 colour={colours.white}>{translations.en.errorBody}</H2>
</ErrorBlock>) : <Loader />}
</LoaderWrapper> </LoaderWrapper>
<Hero /> <Hero />
<TaglineContainer> <TaglineContainer>
{translations && {translations &&
translations.en.underscoreTagline.map(line => ( translations.en.underscoreTagline.map(line => (
<H1 key={line}>{line}</H1> <H1 key={line} colour={colours.midnightDarker}>{line}</H1>
))} ))}
</TaglineContainer> </TaglineContainer>
</Wrapper> </Wrapper>

View File

@ -17,11 +17,13 @@ export const Wrapper = styled.div`
box-sizing: border-box; box-sizing: border-box;
position: fixed; position: fixed;
overflow-y: scroll; overflow-y: scroll;
`
p, export const ErrorBlock = styled.div`
h1, padding: 1em;
h2 {
color: ${colours.midnightDarker}; h1 {
margin-bottom: 0.5em
} }
` `
@ -108,7 +110,7 @@ export const FadeBottom = styled.div`
` `
export const TaglineContainer = styled.div` export const TaglineContainer = styled.div`
width: 100%; width: ${heroWidth};
bottom: 0em; bottom: 0em;
padding-bottom: 0; padding-bottom: 0;
right: 1em; right: 1em;
@ -122,9 +124,11 @@ export const TaglineContainer = styled.div`
h1 { h1 {
margin-bottom: 0.2em; margin-bottom: 0.2em;
text-align: right;
} }
@media screen and (max-width: ${screenSizes.md}px) { @media screen and (max-width: ${screenSizes.md}px) {
width: 100%;
h1 { h1 {
color: ${colours.rose}; color: ${colours.rose};
font-size: 24px; font-size: 24px;

View File

@ -1,7 +1,7 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
import { isWithinInterval } from 'date-fns' import { isWithinInterval } from 'date-fns'
import { h } from 'preact' import { h } from 'preact'
import { H1, H2 } from '../../components/Text' import { H1, H2, Label } from '../../components/Text'
import strings from '../../data/strings' import strings from '../../data/strings'
import { useEventApi } from '../../hooks/data' import { useEventApi } from '../../hooks/data'
import { Content, ScheduleList, Day } from './styles' import { Content, ScheduleList, Day } from './styles'
@ -9,7 +9,7 @@ import { Content, ScheduleList, Day } from './styles'
import Page from '../../layouts/Page' import Page from '../../layouts/Page'
import { formatDay, getScheduleFromData } from './helpers' import { formatDay, getScheduleFromData } from './helpers'
import EpisodeCard from '../../components/EpisodeCard' import EpisodeCard from '../../components/EpisodeCard'
import { colours } from '../../assets/theme' import { colours, textSizes } from '../../assets/theme'
const Program = () => { const Program = () => {
const { data } = useEventApi() const { data } = useEventApi()
@ -19,18 +19,20 @@ const Program = () => {
return ( return (
<Page title={strings.en.program}> <Page title={strings.en.program}>
<Content> <Content>
<ScheduleList> {Object.entries(episodes).length === 0 ? (
<Label size={textSizes.lg}>There are currently no streams scheduled, check back soon!</Label>
) :
<ScheduleList>
{Object.keys(episodes || {}).sort((a, b) => new Date(a) - new Date(b)).map(day => (
<Day>
<H1 colour={colours.rose}>{formatDay(day)}</H1>
{episodes[day].map(episode => (
<EpisodeCard {...episode} />
))}
{Object.keys(episodes || {}).sort((a, b) => new Date(a) - new Date(b)).map(day => ( </Day>
<Day> ))}
<H1 colour={colours.rose}>{formatDay(day)}</H1> </ScheduleList>}
{episodes[day].map(episode => (
<EpisodeCard {...episode} />
))}
</Day>
))}
</ScheduleList>
{/* <H1>Program</H1> */} {/* <H1>Program</H1> */}
</Content> </Content>

View File

@ -19,13 +19,21 @@ export const Day = styled.div`
` `
export const Content = styled.div` export const Content = styled.div`
width: 80vw; width: 85vw;
max-width: ${screenSizes.lg}px; max-width: ${screenSizes.lg}px;
margin: 0 auto; margin: 0 auto;
padding: 64px 0; padding: 64px 0;
overflow-y: scroll; overflow-y: scroll;
min-height: 50vh;
display: flex;
justify-content: center;
align-items: center;
& > label {
line-height: 1;
max-width: 35%;
text-align: center;
}
::-webkit-scrollbar { ::-webkit-scrollbar {
display: none; display: none;

View File

@ -4,7 +4,7 @@ import { Fragment, h } from 'preact'
import { H1, H2 } from '../../components/Text' import { H1, H2 } from '../../components/Text'
import strings from '../../data/strings' import strings from '../../data/strings'
import { useEventApi } from '../../hooks/data' import { useEventApi } from '../../hooks/data'
import { Content, SeriesGrid, SeriesRow } from './styles' import { Content, SeriesGrid, SeriesRow, Title } from './styles'
import Page from '../../layouts/Page' import Page from '../../layouts/Page'
import SeriesCard from '../../components/SeriesCard' import SeriesCard from '../../components/SeriesCard'
@ -29,21 +29,19 @@ const Series = () => {
return ( return (
<Page title={strings.en.series}> <Page title={strings.en.series}>
<Content> <Content>
{currentSeries.map(series => (
<Fragment>
<Title colour={colours.rose}>{strings.en.currentSeries}</Title>
<SeriesGrid>
<SeriesCard series={series} />
</SeriesGrid>
</Fragment>
))}
<Title colour={colours.rose}>{strings.en.pastSeries}</Title>
<SeriesGrid> <SeriesGrid>
{currentSeries.map(series => ( {pastSeries.map(series => (
<Fragment> <SeriesCard series={series} isPast />
<H1 colour={colours.rose}>{strings.en.currentSeries}</H1>
<SeriesRow>
<SeriesCard series={series} />
</SeriesRow>
</Fragment>
))} ))}
<H1 colour={colours.rose}>{strings.en.pastSeries}</H1>
<SeriesRow>
{pastSeries.map(series => (
<SeriesCard series={series} isPast />
))}
</SeriesRow>
</SeriesGrid> </SeriesGrid>
</Content> </Content>
</Page> </Page>

View File

@ -1,14 +1,43 @@
import styled from 'styled-components' import styled from 'styled-components'
import { screenSizes } from '../../assets/theme'
import { Row } from '../../components/Flex' import { Row } from '../../components/Flex'
import { H1 } from '../../components/Text'
export const spacing = [2, 4, 8, 16, 24, 32, 48, 64, 128, 256, 512]
export const Content = styled.div` export const Content = styled.div`
padding-top: 64px; width: 85vw;
max-width: ${screenSizes.lg + 150}px;
margin: 0 auto;
padding: 64px 0;
overflow-y: scroll;
min-height: 50vh;
::-webkit-scrollbar {
display: none;
}
` `
export const SeriesGrid = styled.div` export const SeriesGrid = styled.div`
margin-left: 32px; display: grid;
grid-column-gap: ${spacing[4]}px;
grid-row-gap: ${spacing[6]}px;
margin-bottom: 5em;
padding: 0 2px;
@media screen and (min-width: ${screenSizes.sm}px) {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@media screen and (min-width: ${screenSizes.md}px) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media screen and (min-width: ${screenSizes.lg}px) {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media screen and (min-width: ${screenSizes.xl}px) {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
` `
export const SeriesRow = styled(Row)` export const Title = styled(H1)`
margin: 3em 0 6em 0; margin-bottom: 0.5em
` `

View File

@ -3,7 +3,7 @@ import { h, Fragment } from 'preact'
import { useEffect } from 'preact/hooks' import { useEffect } from 'preact/hooks'
import striptags from 'striptags' import striptags from 'striptags'
import { H1 } from '../../components/Text' import { H1, Label } from '../../components/Text'
import Markdown from '../../components/Markdown' import Markdown from '../../components/Markdown'
import translations from '../../data/strings' import translations from '../../data/strings'
import InfoLayout from '../../layouts/InfoLayout' import InfoLayout from '../../layouts/InfoLayout'
@ -13,6 +13,7 @@ import {
Title, Title,
InfoContent, InfoContent,
Row, Row,
LogosRow,
ActionButton as Button, ActionButton as Button,
TrailerContainer, TrailerContainer,
} from './styles' } from './styles'
@ -23,12 +24,15 @@ import { defaultTheme } from '../../assets/theme'
const SeriesPage = ({ data }) => { const SeriesPage = ({ data }) => {
const theme = data.theme || defaultTheme const theme = data.theme || defaultTheme
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 dateString = `${new Date()}` const dateString = `${new Date()}`
let tzShort = let tzShort =
// Works for the majority of modern browsers // Works for the majority of modern browsers
@ -106,7 +110,16 @@ const SeriesPage = ({ data }) => {
<Markdown theme={theme}>{credits}</Markdown> <Markdown theme={theme}>{credits}</Markdown>
</InfoContent> : null} </InfoContent> : null}
</Fragment> </Fragment>
{orgsList.length ? <LogosRow $wrap>
{orgsList.map((org, index) => (
<Fragment>
<a href={org.orgUrl}>
<img src={org.logoUrl} alt={`${org.orgName} logo`} />
</a>
{orgsList.length === 2 && index + 1 !== orgsList.length ? <Label colour={theme.foreground}>{'//'}</Label> : null}
</Fragment>
))}
</LogosRow> : null}
</InfoLayout> </InfoLayout>
</Page> </Page>
) )

View File

@ -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;
@ -59,6 +59,26 @@ export const Row = styled.div`
} }
` `
export const LogosRow = styled(Row)`
align-items: center;
max-width: 600px;
justify-content: space-between;
padding: 32px 0 ;
a {
width: auto;
margin-right: 0;
&[href]:hover {
opacity: 0.7
}
}
img {
height: 64px;
}
`
export const InfoContent = styled.div` export const InfoContent = styled.div`
max-width: 600px; max-width: 600px;
margin: 0 0 0em 2px; margin: 0 0 0em 2px;
@ -138,7 +158,6 @@ const LinkBlock = styled(Link)`
const renderTitles = titles => const renderTitles = titles =>
titles.split('\\n').map(title => <H2 key={title}>{title}</H2>) titles.split('\\n').map(title => <H2 key={title}>{title}</H2>)
export const EpisodeCard = ({ export const EpisodeCard = ({
title, title,
image, image,
@ -151,6 +170,7 @@ export const EpisodeCard = ({
onClickButton, onClickButton,
tzShort, tzShort,
theme, theme,
peertubeReplay,
id id
}) => { }) => {
const startDate = new Date(beginsOn) const startDate = new Date(beginsOn)
@ -181,7 +201,7 @@ export const EpisodeCard = ({
)} )}
<Markdown theme={theme}>{description}</Markdown> <Markdown theme={theme}>{description}</Markdown>
{hasPassed ? ( {hasPassed ? (
<Button onClick={onClickButton}>{translations.en.watchEpisode}</Button> <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} />
)} )}

View File

@ -16,7 +16,7 @@ export const useSeriesStore = create((set, get) => ({
} }
})) }))
export const [useTheme] = create(set => ({ export const useTheme = create(set => ({
theme: defaultTheme, theme: defaultTheme,
// Methods // Methods
@ -24,18 +24,18 @@ export const [useTheme] = create(set => ({
setDefaultTheme: () => set({ theme: defaultTheme }) setDefaultTheme: () => set({ theme: defaultTheme })
})) }))
export const [useUiStore] = create((set, get) => ({ export const useUiStore = create((set, get) => ({
mobileMenuOpen: false, mobileMenuOpen: false,
streamPreviewMinimized: false, streamPreviewMinimized: true,
streamActive: false, streamActive: false,
// Methods // Methods
toggleMobileMenu: () => set({ mobileMenuOpen: !get().mobileMenuOpen }), toggleMobileMenu: () => set({ mobileMenuOpen: !get().mobileMenuOpen }),
toggleStreamPreviewMinimized: () => set({ streamPreviewMinimized: !get().streamPreviewMinimized }), toggleStreamPreviewMinimized: () => set({ streamPreviewMinimized: !get().streamPreviewMinimized }),
toggleStreamActive: () => set({ streamActive: !get().streamActive }), setStreamActive: (streamActive) => set({ streamActive }),
})) }))
export const [useStreamStore] = create((set) => ({ export const useStreamStore = create((set) => ({
currentStream: null, currentStream: null,
streamIsLive: false, streamIsLive: false,