lots of polish
This commit is contained in:
parent
e6e05d90ef
commit
c2c7e0f3a5
17
index.js
17
index.js
@ -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>)
|
||||||
|
@ -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",
|
||||||
|
@ -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 {
|
||||||
|
@ -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} />
|
||||||
|
@ -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)`
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
`
|
`
|
@ -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>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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%);
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
() => {
|
() => {
|
||||||
|
69
src/components/Select/index.js
Normal file
69
src/components/Select/index.js
Normal 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
|
63
src/components/Select/styles.js
Normal file
63
src/components/Select/styles.js
Normal 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
|
||||||
|
`
|
@ -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>
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
`
|
`
|
62
src/components/Svg/VideoOverlay/index.js
Normal file
62
src/components/Svg/VideoOverlay/index.js
Normal 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
|
84
src/components/Svg/VideoOverlay/styles.js
Normal file
84
src/components/Svg/VideoOverlay/styles.js
Normal 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;
|
||||||
|
}
|
||||||
|
`
|
@ -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
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
`};
|
||||||
|
`
|
@ -2,7 +2,7 @@ export default {
|
|||||||
en: [
|
en: [
|
||||||
{
|
{
|
||||||
label: 'Program guide',
|
label: 'Program guide',
|
||||||
to: '/program'
|
to: '/'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Series',
|
label: 'Series',
|
||||||
|
@ -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'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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 })
|
||||||
|
@ -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],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
`
|
`
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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} />
|
||||||
)}
|
)}
|
||||||
|
@ -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,
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user