Compare commits

...

10 Commits

Author SHA1 Message Date
sunda
3ab937bdb2 added footer images on series type + other dertails 2022-09-27 14:29:09 +02:00
sunda
2651e1cb37 replacing chat with widgetbot for now 2022-09-07 16:40:27 +02:00
sunda
c24124c970 ch ch changes 2022-09-07 14:37:49 +02:00
sunda
fdc031c64b UNCHANGED (git weirdness) 2022-09-07 14:37:34 +02:00
sunda
6462f327c9 fix 'last stream' message on series card 2022-05-06 15:10:53 +02:00
sunda
e29bc39075 upgrade to parcel 2 2022-05-06 15:10:40 +02:00
sunda
16e45c3030 fixed timezones for current stream lol 2021-11-19 16:37:36 +01:00
sunda
a6a3e6c28e fixing timezone displays 2021-11-11 17:05:25 +01:00
sunda
15180937f9 added timezones to program items 2021-11-11 12:48:27 +01:00
sunda
1489a0380b style tweaks 2021-11-11 12:29:11 +01:00
99 changed files with 420 additions and 293 deletions

0
.eslintrc Normal file → Executable file
View File

1
.gitignore vendored Normal file → Executable file
View File

@ -10,3 +10,4 @@ public
.DS_Store .DS_Store
.env* .env*
yarn.lock yarn.lock
.parcel-cache

0
.prettierrc Normal file → Executable file
View File

0
.vscode/settings.json vendored Normal file → Executable file
View File

0
app.js Normal file → Executable file
View File

2
index.html Normal file → Executable file
View File

@ -53,7 +53,7 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script src="./index.js"></script> <script src="./index.js" type="module"></script>
<!-- Matomo --> <!-- Matomo -->
<script type="text/javascript"> <script type="text/javascript">
var _paq = (window._paq = window._paq || []) var _paq = (window._paq = window._paq || [])

39
index.js Normal file → Executable file
View File

@ -16,13 +16,16 @@ import Series from './src/pages/Series'
import Program from './src/pages/Program' import Program from './src/pages/Program'
import StreamPreview from './src/components/StreamPreview' import StreamPreview from './src/components/StreamPreview'
import Video from './src/components/Video' import Video from './src/components/Video'
import Chat from './src/components/Chat'
// import { useWindowSize } from './src/hooks/dom' // import { useWindowSize } from './src/hooks/dom'
const App = () => { const App = () => {
const { theme } = useTheme((store) => store) const { theme } = useTheme(store => store)
const { data, loading: eventsLoading, error } = useEventApi() const { data, loading: eventsLoading, error } = useEventApi()
const [minLoadTimePassed, setMinTimeUp] = useState(false) const [minLoadTimePassed, setMinTimeUp] = useState(false)
const { setCurrentStream, currentStream, streamIsLive } = useStreamStore(store => store) const { setCurrentStream, currentStream, streamIsLive } = useStreamStore(
store => store
)
const streamActive = useUiStore(store => store.streamActive) const streamActive = useUiStore(store => store.streamActive)
usePeertubeApi(data.episodes) usePeertubeApi(data.episodes)
@ -37,14 +40,17 @@ const App = () => {
new Date(stream.beginsOn), new Date(stream.beginsOn),
'Europe/Berlin' 'Europe/Berlin'
) )
const utcEndDate = zonedTimeToUtc(new Date(stream.endsOn), 'Europe/Berlin') const utcEndDate = zonedTimeToUtc(
new Date(stream.endsOn),
'Europe/Berlin'
)
const { timeZone } = Intl.DateTimeFormat().resolvedOptions() const { timeZone } = Intl.DateTimeFormat().resolvedOptions()
const zonedStartDate = utcToZonedTime(utcStartDate, timeZone) const zonedStartDate = utcToZonedTime(utcStartDate, 'Europe/Berlin')
const zonedEndDate = utcToZonedTime(utcEndDate, timeZone) const zonedEndDate = utcToZonedTime(utcEndDate, 'Europe/Berlin')
if ( if (
isWithinInterval(new Date(), { isWithinInterval(new Date(), {
start: subHours(zonedStartDate, 1), start: subHours(zonedStartDate, 2),
end: addHours(zonedEndDate, 1), end: addHours(zonedEndDate, 1),
}) })
) { ) {
@ -54,8 +60,6 @@ const App = () => {
} }
}, [eventsLoading]) }, [eventsLoading])
// console.log({ episodes: data.episodes, series: data.series })
const seriesData = data.series ? Object.values(data.series) : [] const seriesData = data.series ? Object.values(data.series) : []
return ( return (
@ -69,23 +73,30 @@ const App = () => {
<Route exact path="/" component={Program} /> <Route exact path="/" component={Program} />
<Route exact path="/series" component={Series} /> <Route exact path="/series" component={Series} />
<Route exact path="/program" component={Program} /> <Route exact path="/program" component={Program} />
{seriesData.length ? seriesData.map(series => ( {seriesData.length
? seriesData.map(series => (
<Route exact path={`/series/${series.slug}`}> <Route exact path={`/series/${series.slug}`}>
<SeriesPage data={series} /> <SeriesPage data={series} />
</Route>)) : null} </Route>
))
: null}
<Route path="*"> <Route path="*">
<FourOhFour /> <FourOhFour />
</Route> </Route>
</Switch> </Switch>
</BrowserRouter> </BrowserRouter>
{streamActive ? <Video stream={currentStream} /> :
<StreamPreview stream={currentStream} isLive={streamIsLive} />} {streamActive ? (
<Video stream={currentStream} />
) : (
<StreamPreview stream={currentStream} isLive={streamIsLive} />
)}
</Fragment> </Fragment>
)} )}
</ThemeProvider>) </ThemeProvider>
)
} }
const appEl = document.getElementById('app') const appEl = document.getElementById('app')
render(<App />, appEl) render(<App />, appEl)

0
netlify.toml Normal file → Executable file
View File

9
package.json Normal file → Executable file
View File

@ -1,13 +1,11 @@
{ {
"name": "underscore_stream", "name": "underscore_stream",
"version": "0.0.0", "version": "0.0.0",
"main": "index.js",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"start": "yarn parcel index.html", "start": "yarn parcel index.html",
"dev": "yarn parcel watch index.html", "dev": "yarn parcel watch index.html",
"build": "parcel build index.html && cp -r static dist/static", "build": "parcel build index.html && cp -r static dist/static"
"deploy": "yarn parcel build; rsync rsync -aP ./dist/ inu@95.216.203.71:/media/www/stream.undersco.re/"
}, },
"alias": { "alias": {
"react": "preact/compat", "react": "preact/compat",
@ -26,7 +24,7 @@
"ical.js": "^1.4.0", "ical.js": "^1.4.0",
"markdown-to-jsx": "^7.1.2", "markdown-to-jsx": "^7.1.2",
"preact": "^10.5.12", "preact": "^10.5.12",
"prop-types": "^15.7.2", "prop-types": "^15.8.1",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-router-dom": "^5.3.0", "react-router-dom": "^5.3.0",
"striptags": "^3.2.0", "striptags": "^3.2.0",
@ -35,6 +33,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.13.10", "@babel/eslint-parser": "^7.13.10",
"@parcel/transformer-sass": "2.5.0",
"babel": "^6.23.0", "babel": "^6.23.0",
"babel-plugin-transform-react-jsx": "^6.24.1", "babel-plugin-transform-react-jsx": "^6.24.1",
"babel-preset-env": "^1.6.1", "babel-preset-env": "^1.6.1",
@ -42,7 +41,7 @@
"eslint-config-flying-rocket": "^1.1.1", "eslint-config-flying-rocket": "^1.1.1",
"marked": "^2.0.3", "marked": "^2.0.3",
"module-alias": "^2.0.3", "module-alias": "^2.0.3",
"parcel-bundler": "1.12.3", "parcel": "^2.0.0",
"parcel-plugin-ogimage": "^1.2.0", "parcel-plugin-ogimage": "^1.2.0",
"prettier": "^1.9.1", "prettier": "^1.9.1",
"sass": "^1.32.8", "sass": "^1.32.8",

0
src/assets/fonts/Karla/Karla-Bold.ttf Normal file → Executable file
View File

0
src/assets/fonts/Karla/Karla-Medium.ttf Normal file → Executable file
View File

0
src/assets/fonts/Karla/Karla-Regular.ttf Normal file → Executable file
View File

0
src/assets/fonts/Karla/Karla-VariableFont_wght.ttf Normal file → Executable file
View File

0
src/assets/fonts/Lunchtype/lunchtype24-medium.ttf Normal file → Executable file
View File

0
src/assets/img/IconSM.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

0
src/assets/img/hero/1lg.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 906 KiB

After

Width:  |  Height:  |  Size: 906 KiB

0
src/assets/img/hero/1sm.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

0
src/assets/img/hero/2lg.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 603 KiB

After

Width:  |  Height:  |  Size: 603 KiB

0
src/assets/img/hero/2md.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 178 KiB

0
src/assets/img/hero/2sm.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

0
src/assets/styles/fontface.scss Normal file → Executable file
View File

0
src/assets/styles/index.scss Normal file → Executable file
View File

0
src/assets/styles/reset.scss Normal file → Executable file
View File

0
src/assets/theme/index.js Normal file → Executable file
View File

0
src/components/Button/index.js Normal file → Executable file
View File

10
src/components/Chat/index.js Normal file → Executable file
View File

@ -27,17 +27,21 @@ const Chat = ({ overlayActive }) => {
<CloseBox colour={colours.white} size={18} onClick={toggleChatOpen} /> <CloseBox colour={colours.white} size={18} onClick={toggleChatOpen} />
</ChatHeader> </ChatHeader>
<iframe <iframe
src={`https://titanembeds.com/embed/${config.chat.guildId}?css=${config.chat.css}&defaultchannel=${config.chat.channelId}&lang=en_EN`} src={`https://e.widgetbot.io/channels/${config.chat.guildId}/${config.chat.channelId}`}
height={(height / 4) * 3} height={(height / 4) * 3}
width="350" width="350"
frameBorder="0" frameBorder="0"
title="discord-chat"
className="titanembed" className="titanembed"
title="discord-chat"
/> />
</ChatFrame> </ChatFrame>
</ChatWrapper> </ChatWrapper>
) : ( ) : (
<ChatHeader chatIsOpen={false} onClick={toggleChatOpen} $active={overlayActive}> <ChatHeader
chatIsOpen={false}
onClick={toggleChatOpen}
$active={overlayActive}
>
<Label weight="400" size={16} colour={colours.midnightDarker}> <Label weight="400" size={16} colour={colours.midnightDarker}>
CHAT CHAT
</Label> </Label>

0
src/components/Chat/styles.js Normal file → Executable file
View File

42
src/components/EpisodeCard/index.js Normal file → Executable file
View File

@ -1,25 +1,44 @@
import { h } from 'preact' import { h } from 'preact'
import { ICalendar } from 'datebook' import { ICalendar } from 'datebook'
import { format } from 'date-fns' // import { format } from 'date-fns'
import striptags from 'striptags' import striptags from 'striptags'
import { utcToZonedTime, zonedTimeToUtc, format } from 'date-fns-tz'
import enGB from 'date-fns/locale/en-GB'
import Link from '../Link' import Link from '../Link'
import { H2, H3, Label } from '../Text' import { H2, H3, Label } from '../Text'
import strings from '../../data/strings'
import { andList } from '../../helpers/string'
import { colours, screenSizes } from '../../assets/theme'
import { Img, Left, Right, Center, Title, ButtonRow, StyledButton as Button } from './styles'
import { useEventApi } from '../../hooks/data'
import { useWindowSize } from '../../hooks/dom'
import Flex from '../Flex' import Flex from '../Flex'
import { Img, Left, Right, Center, Title, ButtonRow, StyledButton as Button } from './styles'
import strings from '../../data/strings'
import { colours, screenSizes } from '../../assets/theme'
import { andList } from '../../helpers/string'
import { useEventApi } from '../../hooks/data'
import { useWindowSize } from '../../hooks/dom'
const EpisodeCard = ({ image, title, seriesId, beginsOn, endsOn, id, url, description, ...rest }) => {
const EpisodeCard = ({ image, title, seriesId, beginsOn, endsOn, id: episodeId, url, description, ...rest }) => {
const { data: { series: allSeries } } = useEventApi() const { data: { series: allSeries } } = useEventApi()
const series = seriesId ? allSeries.filter(({ id }) => id === seriesId)[0] : {} const series = seriesId ? allSeries.filter(({ id }) => id === seriesId)[0] : {}
const hosts = series.hosts ? series.hosts.map(host => host.actor.name) : null const hosts = series.hosts ? series.hosts.map(host => host.actor.name) : null
const startTime = format(new Date(beginsOn), 'h:mma') const startDate = new Date(beginsOn)
const { timeZone } = Intl.DateTimeFormat().resolvedOptions()
const utcDate = zonedTimeToUtc(startDate, timeZone)
const zonedDate = utcToZonedTime(utcDate, timeZone)
const startTime = format(zonedDate, 'h:mma', {
timeZone,
locale: enGB,
})
const tzShort = format(zonedDate, 'zzz', {
timeZone,
locale: enGB,
})
const { width: screenWidth } = useWindowSize() const { width: screenWidth } = useWindowSize()
const isMobile = screenWidth < screenSizes.md const isMobile = screenWidth < screenSizes.md
@ -28,7 +47,7 @@ const EpisodeCard = ({ image, title, seriesId, beginsOn, endsOn, id, url, descri
return ( return (
<Flex align="stretch" direction={isMobile ? 'column' : 'row'} {...rest}> <Flex align="stretch" direction={isMobile ? 'column' : 'row'} {...rest}>
<Left> <Left>
<Link to={`/series/${series.slug}#${id}`}> <Link to={`/series/${series.slug}#${episodeId}`}>
<Img src={image} /> <Img src={image} />
</Link> </Link>
<ButtonsRows title={title} description={description} beginsOn={beginsOn} endsOn={endsOn} url={url} /> <ButtonsRows title={title} description={description} beginsOn={beginsOn} endsOn={endsOn} url={url} />
@ -43,7 +62,8 @@ const EpisodeCard = ({ image, title, seriesId, beginsOn, endsOn, id, url, descri
</div> </div>
</Center> </Center>
<Right> <Right>
<Label size={24} colour={colours.rose} weight="600">{startTime}</Label> <Label size={24} colour={colours.rose} weight="600" align="right" block>{startTime}</Label>
<Label size={18} colour={colours.rose} weight="600" align="right" block>{tzShort}</Label>
</Right> </Right>
</Flex> </Flex>
) )

17
src/components/EpisodeCard/styles.js Normal file → Executable file
View File

@ -16,7 +16,8 @@ export const ButtonRow = styled(Flexbox)`
width: 100%; width: 100%;
align-items: stretch; align-items: stretch;
button, a{ button,
a {
font-size: 16px; font-size: 16px;
width: 49%; width: 49%;
height: 100%; height: 100%;
@ -29,11 +30,12 @@ export const ButtonRow = styled(Flexbox)`
@media screen and (min-width: ${screenSizes.md}px) and (max-width: ${screenSizes.lg}px) { @media screen and (min-width: ${screenSizes.md}px) and (max-width: ${screenSizes.lg}px) {
flex-direction: column; flex-direction: column;
button, a { button,
a {
width: 100%; width: 100%;
height: auto;
} }
} }
` `
export const Left = styled(FlexColumn)` export const Left = styled(FlexColumn)`
@ -48,6 +50,8 @@ export const Left = styled(FlexColumn)`
export const Center = styled(FlexColumn)` export const Center = styled(FlexColumn)`
@media screen and (max-width: ${screenSizes.md}px) { @media screen and (max-width: ${screenSizes.md}px) {
order: 2; order: 2;
position: relative;
top: -1em;
} }
` `
@ -60,9 +64,14 @@ export const Right = styled.div`
flex: 1; flex: 1;
text-align: right; text-align: right;
label {
display: block;
margin-bottom: 0.2em;
}
@media screen and (max-width: ${screenSizes.md}px) { @media screen and (max-width: ${screenSizes.md}px) {
position: relative; position: relative;
top: 1em; top: 1.2em;
order: 1; order: 1;
} }
` `

0
src/components/Flex/index.js Normal file → Executable file
View File

0
src/components/Header/index.js Normal file → Executable file
View File

0
src/components/Header/styles.js Normal file → Executable file
View File

0
src/components/Link/index.js Normal file → Executable file
View File

0
src/components/Link/styles.js Normal file → Executable file
View File

0
src/components/Loader/index.js Normal file → Executable file
View File

0
src/components/Logo/index.js Normal file → Executable file
View File

0
src/components/Markdown/index.js Normal file → Executable file
View File

4
src/components/Markdown/styles.js Normal file → Executable file
View File

@ -28,6 +28,10 @@ export const MarkdownWrapper = styled.span`
p { p {
margin-bottom: ${props => (props.$withLinebreaks ? '32px' : '0')}; margin-bottom: ${props => (props.$withLinebreaks ? '32px' : '0')};
&:last-of-type {
margin-bottom: ${props => (props.$withLinebreaks ? '8px' : '0')};
}
} }
p > p { p > p {

0
src/components/Select/index.js Normal file → Executable file
View File

0
src/components/Select/styles.js Normal file → Executable file
View File

0
src/components/Seo/index.js Normal file → Executable file
View File

49
src/components/SeriesCard/index.js Normal file → Executable file
View File

@ -7,15 +7,31 @@ import { Img, LabelBlock, Wrapper } from './styles'
import { andList } from '../../helpers/string' import { andList } from '../../helpers/string'
import { colours } from '../../assets/theme' import { colours } from '../../assets/theme'
const SeriesCard = ({ series: { image, episodes, title, subtitle, hosts: hostsArray, slug, }, isPast, ...rest }) => { const SeriesCard = ({
series: { image, episodes, title, subtitle, hosts: hostsArray, slug },
isPast,
...rest
}) => {
const hosts = hostsArray.map(({ actor }) => actor.name) const hosts = hostsArray.map(({ actor }) => actor.name)
const episodesLength = episodes.past.length + episodes.future.length const episodesLength = episodes.past.length + episodes.future.length
const getNextLastStreamText = () => { const getNextLastStreamText = () => {
const hasFutureEpisodes = episodes.future.length const hasFutureEpisodes = episodes.future.length
const prefix = hasFutureEpisodes ? strings.en.nextStream : strings.en.lastStream const prefix = hasFutureEpisodes
const mainText = formatDistanceToNow(new Date(episodes[hasFutureEpisodes ? 'future' : 'past'][0][hasFutureEpisodes ? 'endsOn' : 'beginsOn']), { addSuffix: true }) ? strings.en.nextStream
: strings.en.lastStream
const episodesList = episodes[hasFutureEpisodes ? 'future' : 'past']
const mainText = formatDistanceToNow(
new Date(
episodesList[hasFutureEpisodes ? 0 : episodesList.length - 1][
hasFutureEpisodes ? 'endsOn' : 'beginsOn'
]
),
{ addSuffix: true }
)
return `${prefix} ${mainText}` return `${prefix} ${mainText}`
} }
@ -24,21 +40,24 @@ const SeriesCard = ({ series: { image, episodes, title, subtitle, hosts: hostsAr
<Link to={`series/${slug}`}> <Link to={`series/${slug}`}>
<Wrapper {...rest}> <Wrapper {...rest}>
<Img src={image}> <Img src={image}>
<LabelBlock <LabelBlock $position="top">
$position="top" {episodesLength}{' '}
> {episodesLength === 1 ? strings.en.episode : strings.en.episodes}
{episodesLength} {episodesLength === 1 ? strings.en.episode : strings.en.episodes}
</LabelBlock>
<LabelBlock
$position="bottom"
>
{getNextLastStreamText()}
</LabelBlock> </LabelBlock>
<LabelBlock $position="bottom">{getNextLastStreamText()}</LabelBlock>
</Img> </Img>
<H2 size={32} colour={colours.rose}>{title}</H2> <H2 size={32} colour={colours.rose}>
<H3 size={21} colour={colours.rose}>{subtitle}</H3> {title}
{hosts.length ? <Label size={16} colour={colours.rose}> {andList(hosts)}</Label> : null} </H2>
<H3 size={21} colour={colours.rose}>
{subtitle}
</H3>
{hosts.length ? (
<Label size={16} colour={colours.rose}>
{andList(hosts)}
</Label>
) : null}
</Wrapper> </Wrapper>
</Link> </Link>
) )

0
src/components/SeriesCard/styles.js Normal file → Executable file
View File

0
src/components/StreamPreview/helpers.js Normal file → Executable file
View File

49
src/components/StreamPreview/index.js Normal file → Executable file
View File

@ -1,10 +1,7 @@
import { Fragment, h } from 'preact' import { h } from 'preact'
import { useEffect, useRef } from 'preact/hooks' import { useEffect, useRef } from 'preact/hooks'
import { PeerTubePlayer } from '@peertube/embed-api' import { PeerTubePlayer } from '@peertube/embed-api'
import { string } from 'prop-types'
import Link from '../Link'
import { Label } from '../Text' import { Label } from '../Text'
import strings from '../../data/strings'
import { colours, textSizes } from '../../assets/theme' import { colours, textSizes } from '../../assets/theme'
import { Row } from '../Flex' import { Row } from '../Flex'
import CrossSvg from '../Svg/Cross' import CrossSvg from '../Svg/Cross'
@ -13,18 +10,18 @@ import { getLabel } from './helpers'
import Chevron from '../Svg/Chevron' import Chevron from '../Svg/Chevron'
import { Frame, Img, Iframe, InnerWrapper } from './styles' import { Frame, Img, Iframe, InnerWrapper } from './styles'
// import { useEventApi } from '../../hooks/data'
const StreamPreview = ({ stream, isLive, ...rest }) => { const StreamPreview = ({ stream, isLive, ...rest }) => {
const currentLanguage = 'en' const currentLanguage = 'en'
const videoiFrame = useRef(null) const videoiFrame = useRef(null)
const ptVideo = useRef(null) const ptVideo = useRef(null)
const { isMinimized, toggleMinimized, setStreamActive } = useUiStore(store => ({ const { isMinimized, toggleMinimized, setStreamActive } = useUiStore(
store => ({
isMinimized: store.streamPreviewMinimized, isMinimized: store.streamPreviewMinimized,
toggleMinimized: store.toggleStreamPreviewMinimized, toggleMinimized: store.toggleStreamPreviewMinimized,
setStreamActive: store.setStreamActive setStreamActive: store.setStreamActive,
})) })
)
useEffect(() => { useEffect(() => {
const setupAndPlayVideo = async () => { const setupAndPlayVideo = async () => {
@ -47,12 +44,30 @@ const StreamPreview = ({ stream, isLive, ...rest }) => {
return stream ? ( return stream ? (
<Frame isMinimized={isMinimized}> <Frame isMinimized={isMinimized}>
<Row justify="space-between"> <Row justify="space-between">
<Label colour={colours.midnightDarker} size={textSizes.lg} onClick={activateStream}>{getLabel(stream, isLive, isMinimized)}</Label> <Label
{isMinimized ? <Chevron colour={colours.midnightDarker} size={14} onClick={toggleMinimized} /> : <CrossSvg colour={colours.midnightDarker} size={16} onClick={toggleMinimized} />} colour={colours.midnightDarker}
size={textSizes.lg}
onClick={activateStream}
>
{getLabel(stream, isLive, isMinimized)}
</Label>
{isMinimized ? (
<Chevron
colour={colours.midnightDarker}
size={14}
onClick={toggleMinimized}
/>
) : (
<CrossSvg
colour={colours.midnightDarker}
size={16}
onClick={toggleMinimized}
/>
)}
</Row> </Row>
{!isMinimized ? {!isMinimized ? (
<InnerWrapper onClick={activateStream}> <InnerWrapper onClick={activateStream}>
{isLive ? {isLive ? (
<Iframe <Iframe
width="560" width="560"
height="315" height="315"
@ -64,11 +79,13 @@ const StreamPreview = ({ stream, isLive, ...rest }) => {
allowFullScreen allowFullScreen
ref={videoiFrame} ref={videoiFrame}
/> />
: <Img src={stream.image} onClick={activateStream} />} ) : (
</InnerWrapper> : null} <Img src={stream.image} onClick={activateStream} />
)}
</InnerWrapper>
) : null}
</Frame> </Frame>
) : null ) : null
} }
export default StreamPreview export default StreamPreview

0
src/components/StreamPreview/styles.js Normal file → Executable file
View File

0
src/components/Svg/Chevron.js Normal file → Executable file
View File

0
src/components/Svg/Cross.js Normal file → Executable file
View File

0
src/components/Svg/Play.js Normal file → Executable file
View File

0
src/components/Svg/VideoOverlay/index.js Normal file → Executable file
View File

0
src/components/Svg/VideoOverlay/styles.js Normal file → Executable file
View File

0
src/components/Svg/base.js Normal file → Executable file
View File

0
src/components/Svg/proptypes.js Normal file → Executable file
View File

0
src/components/Text/index.js Normal file → Executable file
View File

0
src/components/Text/styles.js Normal file → Executable file
View File

13
src/components/Video/index.js Normal file → Executable file
View File

@ -1,13 +1,7 @@
import { h } from 'preact' import { h } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks' import { useEffect, useRef, useState } from 'preact/hooks'
import { import { instanceOf, number, object, shape, string } from 'prop-types'
instanceOf,
number,
object,
shape,
string,
} from 'prop-types'
import 'regenerator-runtime/runtime'
import { PeerTubePlayer } from '@peertube/embed-api' import { PeerTubePlayer } from '@peertube/embed-api'
import Chat from '../Chat' import Chat from '../Chat'
@ -57,7 +51,6 @@ const Video = () => {
setVideo() setVideo()
}, []) }, [])
const playVideo = () => { const playVideo = () => {
const { current: player } = ptVideo const { current: player } = ptVideo
if (!videoReady) return if (!videoReady) return
@ -84,7 +77,6 @@ const Video = () => {
} }
} }
const toggleVideo = () => { const toggleVideo = () => {
if (isPlaying) { if (isPlaying) {
pauseVideo() pauseVideo()
@ -125,7 +117,6 @@ const Video = () => {
console.log({ vol }) console.log({ vol })
} }
const handleKeyPress = keyCode => { const handleKeyPress = keyCode => {
if (keyCode === 32) { if (keyCode === 32) {
// key == 'space' // key == 'space'

0
src/components/Video/styles.js Normal file → Executable file
View File

0
src/components/VideoEmbed/index.js Normal file → Executable file
View File

0
src/components/VideoEmbed/styles.js Normal file → Executable file
View File

0
src/components/VideoOverlay/index.js Normal file → Executable file
View File

0
src/components/VideoOverlay/styles.js Normal file → Executable file
View File

6
src/data/config.js Normal file → Executable file
View File

@ -1,8 +1,9 @@
export default { export default {
meta: { meta: {
title: 'Underscore streams', title: 'Underscore streams',
description: 'Quality live programming from beyond the surveillance economy.', description:
img: 'https://stream.undersco.re/static/meta.png' 'Quality live programming from beyond the surveillance economy.',
img: 'https://stream.undersco.re/static/meta.png',
}, },
peertube_root: 'https://tv.undersco.re', peertube_root: 'https://tv.undersco.re',
EVENTS_API_URL: 'https://api.undersco.re', EVENTS_API_URL: 'https://api.undersco.re',
@ -11,6 +12,5 @@ export default {
chat: { chat: {
guildId: '709318870909059082', guildId: '709318870909059082',
channelId: '826751398757793842', channelId: '826751398757793842',
css: '215',
}, },
} }

0
src/data/navigation.js Normal file → Executable file
View File

0
src/data/strings.js Normal file → Executable file
View File

0
src/helpers/environment.js Normal file → Executable file
View File

0
src/helpers/string.js Normal file → Executable file
View File

0
src/helpers/utils.js Normal file → Executable file
View File

204
src/hooks/data.js Normal file → Executable file
View File

@ -1,108 +1,110 @@
import { h, render } from 'preact' import { h } from 'preact'
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from 'preact/hooks'
// eslint-disable-next-line import/no-extraneous-dependencies
import { secondsToMilliseconds } from 'date-fns'
import axios from 'axios' import axios from 'axios'
import ICAL from 'ical.js' // import ICAL from 'ical.js'
import config from '../data/config' import config from '../data/config'
import { useSeriesStore, useStreamStore } from '../store/index' import { useSeriesStore, useStreamStore } from '../store/index'
import 'regenerator-runtime/runtime'
import { useInterval } from './timerHooks' import { useInterval } from './timerHooks'
import { secondsToMilliseconds } from 'date-fns'
export const useEventCalendar = () => { // export const useEventCalendar = () => {
const [data, setData] = useState([]) // const [data, setData] = useState([])
const [loading, setLoading] = useState(true) // const [loading, setLoading] = useState(true)
async function fetchData() { // async function fetchData() {
setLoading(true) // setLoading(true)
const { data: responseData } = await axios.get( // const { data: responseData } = await axios.get(
`https://cloud.undersco.re/remote.php/dav/public-calendars/${config.calendarId}/?export` // `https://cloud.undersco.re/remote.php/dav/public-calendars/${config.calendarId}/?export`
) // )
const jCalData = ICAL.parse(responseData) // const jCalData = ICAL.parse(responseData)
const comp = new ICAL.Component(jCalData) // const comp = new ICAL.Component(jCalData)
const vevents = comp.getAllSubcomponents('vevent') // const vevents = comp.getAllSubcomponents('vevent')
const calEvents = vevents // const calEvents = vevents
.filter( // .filter(
vevent => // vevent =>
vevent.getFirstPropertyValue('status') === null || // vevent.getFirstPropertyValue('status') === null ||
(vevent.getFirstPropertyValue('status') && // (vevent.getFirstPropertyValue('status') &&
vevent.getFirstPropertyValue('status').toUpperCase() === // vevent.getFirstPropertyValue('status').toUpperCase() ===
'CONFIRMED') // 'CONFIRMED')
) // )
.map(vevent => { // .map(vevent => {
const event = new ICAL.Event(vevent) // const event = new ICAL.Event(vevent)
return event // return event
}) // })
.sort((a, b) => a.startDate.toJSDate() - b.startDate.toJSDate()) // .sort((a, b) => a.startDate.toJSDate() - b.startDate.toJSDate())
await Promise.all( // await Promise.all(
calEvents.map(async calItem => { // calEvents.map(async calItem => {
const url = calItem.component.getAllProperties('url')[0] // const url = calItem.component.getAllProperties('url')[0]
if (url) { // if (url) {
const id = url // const id = url
.getFirstValue() // .getFirstValue()
.split('/') // .split('/')
.pop() // .pop()
const { // const {
data: { // data: {
account, // account,
category, // category,
channel, // channel,
embedPath, // embedPath,
language, // language,
state, // state,
previewPath, // previewPath,
views, // views,
duration, // duration,
}, // },
} = await axios.get(`https://tv.undersco.re/api/v1/videos/${id}`) // } = await axios.get(`https://tv.undersco.re/api/v1/videos/${id}`)
const item = { // const item = {
title: calItem.summary, // title: calItem.summary,
account, // account,
category, // category,
channel, // channel,
description: calItem.description, // description: calItem.description,
embedPath, // embedPath,
language, // language,
state, // state,
previewPath, // previewPath,
views, // views,
start: calItem.startDate.toJSDate(), // start: calItem.startDate.toJSDate(),
end: calItem.endDate.toJSDate(), // end: calItem.endDate.toJSDate(),
id, // id,
duration, // duration,
videoUrl: url.getFirstValue(), // videoUrl: url.getFirstValue(),
} // }
setData(arr => [...arr, item]) // setData(arr => [...arr, item])
} else { // } else {
const item = { // const item = {
title: calItem.summary, // title: calItem.summary,
description: calItem.description, // description: calItem.description,
start: calItem.startDate.toJSDate(), // start: calItem.startDate.toJSDate(),
end: calItem.endDate.toJSDate(), // end: calItem.endDate.toJSDate(),
} // }
setData(arr => [...arr, item]) // setData(arr => [...arr, item])
} // }
}) // })
) // )
setLoading(false) // setLoading(false)
} // }
useEffect(() => {
fetchData()
}, [])
return { loading, data }
}
// useEffect(() => {
// fetchData()
// }, [])
// return { loading, data }
// }
export const useEventApi = () => { export const useEventApi = () => {
const [data, setData] = useSeriesStore(store => [store.series, store.setSeries]) const [data, setData] = useSeriesStore(store => [
store.series,
store.setSeries,
])
const [error, setError] = useState(null) const [error, setError] = useState(null)
const [loading, setLoading] = useState(!!data.length) const [loading, setLoading] = useState(!!data.length)
@ -111,15 +113,12 @@ export const useEventApi = () => {
setLoading(true) setLoading(true)
try { try {
const { data: responseData } = await axios.get( const { data: responseData } = await axios.get(
`${config.EVENTS_API_URL}/events` `${config.EVENTS_API_URL}/events`
) )
setData(responseData) setData(responseData)
console.log({ data: responseData })
setLoading(false) setLoading(false)
} } catch (err) {
catch (err) {
console.log('ERROR') console.log('ERROR')
setError(err) setError(err)
setLoading(false) setLoading(false)
@ -135,7 +134,12 @@ export const useEventApi = () => {
} }
export const usePeertubeApi = async () => { export const usePeertubeApi = async () => {
const { currentStream, setCurrentStream, setStreamIsLive, streamIsLive } = useStreamStore(store => store) const {
currentStream,
setCurrentStream,
setStreamIsLive,
streamIsLive,
} = useStreamStore(store => store)
if (!currentStream) return if (!currentStream) return
@ -143,10 +147,11 @@ export const usePeertubeApi = async () => {
const { peertubeLive } = currentStream const { peertubeLive } = currentStream
if (!peertubeLive || !peertubeLive.id) return if (!peertubeLive || !peertubeLive.id) return
const { const {
data: { state, embedPath } data: { state, embedPath },
} = await axios.get(`https://tv.undersco.re/api/v1/videos/${peertubeLive.id}`) } = await axios.get(
`https://tv.undersco.re/api/v1/videos/${peertubeLive.id}`
)
setStreamIsLive(state.id === 1) setStreamIsLive(state.id === 1)
setCurrentStream({ ...currentStream, embedPath }) setCurrentStream({ ...currentStream, embedPath })
@ -156,7 +161,10 @@ export const usePeertubeApi = async () => {
fetchData() fetchData()
}, []) }, [])
useInterval(() => { useInterval(
() => {
fetchData() fetchData()
}, streamIsLive ? secondsToMilliseconds(15) : secondsToMilliseconds(1)) },
streamIsLive ? secondsToMilliseconds(15) : secondsToMilliseconds(1)
)
} }

0
src/hooks/dom.js Normal file → Executable file
View File

0
src/hooks/timerHooks.js Normal file → Executable file
View File

0
src/hooks/utility.js Normal file → Executable file
View File

0
src/layouts/InfoLayout/index.js Normal file → Executable file
View File

0
src/layouts/InfoLayout/styles.js Normal file → Executable file
View File

0
src/layouts/Page/index.js Normal file → Executable file
View File

0
src/layouts/Page/styles.js Normal file → Executable file
View File

0
src/pages/404/index.js Normal file → Executable file
View File

0
src/pages/404/styles.js Normal file → Executable file
View File

0
src/pages/LoaderLayout/index.js Normal file → Executable file
View File

0
src/pages/LoaderLayout/styles.js Normal file → Executable file
View File

0
src/pages/Program/helpers.js Normal file → Executable file
View File

0
src/pages/Program/index.js Normal file → Executable file
View File

0
src/pages/Program/styles.js Normal file → Executable file
View File

18
src/pages/Series/index.js Normal file → Executable file
View File

@ -14,20 +14,23 @@ const Series = () => {
const { data } = useEventApi() const { data } = useEventApi()
const pastSeries = [] const pastSeries = []
const currentSeries = data.series ? data.series.filter(series => { const currentSeries = data.series
? data.series.filter(series => {
if (series.episodes.future.length) { if (series.episodes.future.length) {
return true return true
} }
if (series.episodes.past.every(({ beginsOn }) => isFuture(addYears(new Date(beginsOn), 1)))) { if (
series.episodes.past.every(({ beginsOn }) =>
isFuture(addYears(new Date(beginsOn), 1))
)
) {
return true return true
} }
pastSeries.push(series) pastSeries.push(series)
return false return false
}) : [] })
: []
console.log({ currentSeries })
return ( return (
<Page title={strings.en.series}> <Page title={strings.en.series}>
@ -48,7 +51,8 @@ const Series = () => {
<SeriesCard series={series} isPast /> <SeriesCard series={series} isPast />
))} ))}
</SeriesGrid> </SeriesGrid>
</Fragment>) : null} </Fragment>
) : null}
</Content> </Content>
</Page> </Page>
) )

0
src/pages/Series/styles.js Normal file → Executable file
View File

83
src/pages/SeriesPage/index.js Normal file → Executable file
View File

@ -1,7 +1,7 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
import { h, Fragment } from 'preact' import { h, Fragment } from 'preact'
import { useEffect } from 'preact/hooks' import { useEffect } from 'preact/hooks'
import striptags from 'striptags' import enGB from 'date-fns/locale/en-GB'
import { H1, Label } from '../../components/Text' import { H1, Label } from '../../components/Text'
import Markdown from '../../components/Markdown' import Markdown from '../../components/Markdown'
@ -14,6 +14,7 @@ import {
InfoContent, InfoContent,
Row, Row,
LogosRow, LogosRow,
FooterImage,
ActionButton as Button, ActionButton as Button,
TrailerContainer, TrailerContainer,
} from './styles' } from './styles'
@ -26,37 +27,38 @@ const SeriesPage = ({ data }) => {
const theme = data.theme || defaultTheme const theme = data.theme || defaultTheme
const { orgs } = data const { orgs } = data
const credits = data.credits ? ` const credits = data.credits
? `
## Credits ## Credits
${data.credits} ${data.credits}
` : null `
: null
const orgsList = Object.values(orgs || {}) const orgsList = Object.values(orgs || {})
const dateString = `${new Date()}` const footerImages = data?.resources?.filter(({ title }) =>
let tzShort = title.includes('FOOTER_IMG')
// Works for the majority of modern browsers )
dateString.match(/\(([^\)]+)\)$/) ||
// IE outputs date strings in a different format:
dateString.match(/([A-Z]+) [\d]{4}$/)
if (tzShort) {
// Old Firefox uses the long timezone name (e.g., "Central
// Daylight Time" instead of "CDT")
tzShort = tzShort[1].match(/[A-Z]/g).join('')
}
const links = data.links.length ? splitArray(data.links, 2) : null const links = data.links.length ? splitArray(data.links, 2) : null
return ( return (
<Page title={data.title} theme={data.theme} withHeader={false}> <Page title={data.title} theme={data.theme} withHeader={false}>
<InfoLayout title={data.title} subtitle={data.subtitle} image={data.image} theme={theme}> <InfoLayout
title={data.title}
subtitle={data.subtitle}
image={data.image}
theme={theme}
>
<Fragment> <Fragment>
<InfoContent> <InfoContent>
<H1>{data.title}:</H1> <H1>{data.title}:</H1>
<H1>{data.subtitle}</H1> <H1>{data.subtitle}</H1>
{data.description ? <Markdown withLinebreaks theme={theme}>{data.description}</Markdown> : null} {data.description ? (
<Markdown withLinebreaks theme={theme}>
{data.description}
</Markdown>
) : null}
{data.trailer ? ( {data.trailer ? (
<TrailerContainer> <TrailerContainer>
@ -64,8 +66,9 @@ const SeriesPage = ({ data }) => {
</TrailerContainer> </TrailerContainer>
) : null} ) : null}
{links ? {links
links.map(linkRow => <Row> ? links.map(linkRow => (
<Row>
{linkRow.map(link => ( {linkRow.map(link => (
<a <a
href={link.resourceUrl} href={link.resourceUrl}
@ -75,19 +78,15 @@ const SeriesPage = ({ data }) => {
<Button>{link.summary}</Button> <Button>{link.summary}</Button>
</a> </a>
))} ))}
</Row>) : null </Row>
} ))
: null}
</InfoContent> </InfoContent>
{data.episodes.future.length ? ( {data.episodes.future.length ? (
<Fragment> <Fragment>
<Title>{translations.en.program}:</Title> <Title>{translations.en.program}:</Title>
{data.episodes.future.map(feeditem => ( {data.episodes.future.map(feeditem => (
<EpisodeCard <EpisodeCard theme={theme} key={feeditem.start} {...feeditem} />
theme={theme}
key={feeditem.start}
tzShort={tzShort}
{...feeditem}
/>
))} ))}
</Fragment> </Fragment>
) : null} ) : null}
@ -105,20 +104,34 @@ const SeriesPage = ({ data }) => {
))} ))}
</Fragment> </Fragment>
) : null} ) : null}
{credits ? <InfoContent> {credits ? (
<InfoContent>
<Markdown theme={theme}>{credits}</Markdown> <Markdown theme={theme}>{credits}</Markdown>
</InfoContent> : null} </InfoContent>
) : null}
</Fragment> </Fragment>
{orgsList.length ? <LogosRow $wrap> {orgsList.length ? (
<LogosRow $wrap>
{orgsList.map((org, index) => ( {orgsList.map((org, index) => (
<Fragment> <Fragment>
<a href={org.orgUrl}>
<img src={org.logoUrl} alt={`${org.orgName} logo`} /> <img src={org.logoUrl} alt={`${org.orgName} logo`} />
</a> {orgsList.length < 4 !== 1 &&
{orgsList.length === 2 && index + 1 !== orgsList.length ? <Label colour={theme.foreground}>{'//'}</Label> : null} orgsList.length < 4 &&
index + 1 !== orgsList.length ? (
<Label colour={theme.foreground}>{'//'}</Label>
) : null}
</Fragment> </Fragment>
))} ))}
</LogosRow> : null} </LogosRow>
) : null}
{footerImages.length
? footerImages.map(image => (
<FooterImage
src={image.resourceUrl}
alt={`${image.orgName} logo`}
/>
))
: null}
</InfoLayout> </InfoLayout>
</Page> </Page>
) )

71
src/pages/SeriesPage/styles.js Normal file → Executable file
View File

@ -1,23 +1,24 @@
import { h, Fragment } from 'preact' import { h, Fragment } from 'preact'
import { zonedTimeToUtc, utcToZonedTime, format } from 'date-fns-tz' import { zonedTimeToUtc, utcToZonedTime, format } from 'date-fns-tz'
import enGB from 'date-fns/locale/en-GB'
import { bool, func, instanceOf, string } from 'prop-types' import { bool, func, instanceOf, string } from 'prop-types'
import styled from 'styled-components' import styled from 'styled-components'
import { colours, textSizes } from '../../assets/theme'
import config from '../../data/config'
import Markdown from '../../components/Markdown' import Markdown from '../../components/Markdown'
import Logo from '../../components/Logo' import Logo from '../../components/Logo'
import translations from '../../data/strings'
import CrossSvg from '../../components/Svg/Cross'
import { H1, H2, Span, Label } from '../../components/Text' import { H1, H2, Span, Label } from '../../components/Text'
import Link from '../../components/Link' import Link from '../../components/Link'
import Button from '../../components/Button' import Button from '../../components/Button'
import { slugify } from '../../helpers/string' import CrossSvg from '../../components/Svg/Cross'
import { ButtonsRows } from '../../components/EpisodeCard' import { ButtonsRows } from '../../components/EpisodeCard'
import translations from '../../data/strings'
import { colours, textSizes } from '../../assets/theme'
import config from '../../data/config'
import { slugify } from '../../helpers/string'
export const TrailerContainer = styled.div` export const TrailerContainer = styled.div`
height: 22em; height: 21em;
border: 1px solid ${colours.midnightDarker}; border: 1px solid ${colours.midnightDarker};
margin-bottom: 16px; margin-bottom: 16px;
@ -39,7 +40,6 @@ export const TrailerContainer = styled.div`
} }
` `
export const ActionButton = styled(Button)` export const ActionButton = styled(Button)`
font-size: 18px; font-size: 18px;
` `
@ -48,7 +48,7 @@ export const Row = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-bottom: 32px; margin-bottom: 32px;
flex-wrap: ${props => props.$wrap ? 'wrap' : 'nowrap'}; flex-wrap: ${props => (props.$wrap ? 'wrap' : 'nowrap')};
a { a {
display: block; display: block;
@ -70,15 +70,21 @@ export const LogosRow = styled(Row)`
margin-right: 0; margin-right: 0;
&[href]:hover { &[href]:hover {
opacity: 0.7 opacity: 0.7;
} }
} }
img { img {
height: 64px; max-height: 42px;
width: 25%;
} }
` `
export const FooterImage = styled.img`
max-width: 600px;
padding: 32px 0;
`
export const InfoContent = styled.div` export const InfoContent = styled.div`
max-width: 600px; max-width: 600px;
margin: 0 0 0em 2px; margin: 0 0 0em 2px;
@ -167,25 +173,32 @@ export const EpisodeCard = ({
url, url,
hasPassed, hasPassed,
videoUrl, videoUrl,
onClickButton,
tzShort,
theme, theme,
peertubeReplay, peertubeReplay,
id id,
}) => { }) => {
const startDate = new Date(beginsOn) const startDate = new Date(beginsOn)
const utcDate = zonedTimeToUtc(startDate, 'Europe/Berlin')
const { timeZone } = Intl.DateTimeFormat().resolvedOptions() const { timeZone } = Intl.DateTimeFormat().resolvedOptions() // client timezone eg. 'Europe/Berlin'
const utcDate = zonedTimeToUtc(startDate, timeZone) // convert the start date to UTC
const zonedDate = utcToZonedTime(utcDate, timeZone) const zonedDate = utcToZonedTime(utcDate, timeZone)
const timezoneLabel = format(
zonedDate,
hasPassed ? 'dd/MM/yy' : 'do LLLL y // HH:mm zzz',
{
timeZone,
locale: enGB,
}
)
return ( return (
<VCWrapper id={id}> <VCWrapper id={id}>
<DateLabel size={textSizes.lg} colour={theme.foreground}> <DateLabel size={textSizes.lg} colour={theme.foreground}>
{`${hasPassed ? translations.en.streamDatePast : ''}`} {`${hasPassed ? translations.en.streamDatePast : ''}`}
<Span bold colour={theme.foreground}> <Span bold colour={theme.foreground}>
{hasPassed {timezoneLabel}
? format(zonedDate, 'dd/MM/yy')
: `${format(zonedDate, 'do LLLL y // HH:mm')} ${tzShort}`}
</Span> </Span>
</DateLabel> </DateLabel>
{videoUrl && hasPassed ? ( {videoUrl && hasPassed ? (
@ -199,11 +212,25 @@ export const EpisodeCard = ({
<VCImg src={image} alt="" /> <VCImg src={image} alt="" />
</Fragment> </Fragment>
)} )}
<Markdown theme={theme}>{description}</Markdown> <Markdown withLinebreaks theme={theme}>
{description}
</Markdown>
{hasPassed ? ( {hasPassed ? (
<a href={peertubeReplay.url || url}><Button>{peertubeReplay.url ? translations.en.watchEpisode : translations.en.eventDetails}</Button></a> <a href={peertubeReplay.url || url}>
<Button>
{peertubeReplay.url
? translations.en.watchEpisode
: translations.en.eventDetails}
</Button>
</a>
) : ( ) : (
<ButtonsRows title={title} description={description} beginsOn={beginsOn} endsOn={endsOn} url={url} /> <ButtonsRows
title={title}
description={description}
beginsOn={beginsOn}
endsOn={endsOn}
url={url}
/>
)} )}
</VCWrapper> </VCWrapper>
) )

0
src/store/index.js Normal file → Executable file
View File

0
static/android-chrome-192x192.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

0
static/apple-touch-icon.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

0
static/browserconfig.xml Normal file → Executable file
View File

0
static/favicon-16x16.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

0
static/favicon-32x32.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

0
static/favicon.ico Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

0
static/logo.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

0
static/meta.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 232 KiB

0
static/mstile-150x150.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

0
static/safari-pinned-tab.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

0
static/site.webmanifest Normal file → Executable file
View File