More work on logic flow + info page

This commit is contained in:
Benjamin Jones 2021-03-11 21:24:44 +01:00
parent 4973f351aa
commit 0ba8deacd0
21 changed files with 449 additions and 179 deletions

65
app.js
View File

@ -1,14 +1,13 @@
import { h } from 'preact' import { h } from 'preact'
import axios from 'axios' import { useState, useEffect } from 'preact/hooks'
// eslint-disable-next-line import/no-extraneous-dependencies
import 'regenerator-runtime/runtime' import 'regenerator-runtime/runtime'
import { useEffect, useState } from 'preact/hooks' import axios from 'axios'
import Video from './src/components/Video' import Video from './src/components/Video'
import config from './src/data/config' import config from './src/data/config'
import Info from './src/components/Info' import Info from './src/components/Info'
import { useFetch } from './src/assets/hooks/calendar' import { useCalendar } from './src/hooks/data'
import { P } from './src/components/Text' import { useTimeout } from './src/hooks/timerHooks'
// const appStates = [ // const appStates = [
// 'noStream', // 'noStream',
@ -19,13 +18,58 @@ import { P } from './src/components/Text'
export default () => { export default () => {
const [isPlaying, setIsPlaying] = useState(false) const [isPlaying, setIsPlaying] = useState(false)
const [videoUrl, setVideoUrl] = useState(null) const [videoUrl, setVideoUrl] = useState(null)
// const [feedData, setFeedData] = useState([]) const [feedData, setFeedData] = useState([])
const [minLoadTimePassed, setMinTimeUp] = useState(false)
const { data, loading } = useCalendar()
const { data: feedData, loading } = useFetch(`${config.calendar}`) useTimeout(() => {
setMinTimeUp(true)
}, 1500)
useEffect(() => {
if (data && data.length) {
data.forEach(async (calItem, index) => {
if (calItem.url) {
const id = calItem.url.val.split('/').pop()
const {
data: {
account,
category,
channel,
embedPath,
language,
name,
state,
previewPath,
views,
},
} = await axios.get(`https://tv.undersco.re/api/v1/videos/${id}`)
const item = {
name,
account,
category,
channel,
description: calItem.description,
embedPath,
language,
state,
previewPath,
views,
start: calItem.start,
end: calItem.end,
id,
}
setFeedData(arr => [...arr, item])
}
})
}
}, [data])
return ( return (
<div> <div>
{/* {false ? ( {false ? (
<Video <Video
playing={isPlaying} playing={isPlaying}
setPlaying={setIsPlaying} setPlaying={setIsPlaying}
@ -33,8 +77,9 @@ export default () => {
title={config.next_stream.title} title={config.next_stream.title}
org={config.next_stream.org} org={config.next_stream.org}
/> />
) : ( */} ) : (
{loading ? <P>LOADING :)</P> : <Info data={feedData} />} <Info data={feedData} loading={loading || !minLoadTimePassed} />
)}
</div> </div>
) )
} }

View File

@ -16,6 +16,8 @@
"dependencies": { "dependencies": {
"@peertube/embed-api": "^0.0.4", "@peertube/embed-api": "^0.0.4",
"axios": "^0.21.1", "axios": "^0.21.1",
"date-fns": "^2.19.0",
"ical": "^0.8.0",
"preact": "^10.5.12", "preact": "^10.5.12",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"styled-components": "^5.2.1" "styled-components": "^5.2.1"

Binary file not shown.

View File

@ -1,23 +0,0 @@
import { useEffect, useState } from 'preact/hooks'
import axios from 'axios'
export const useFetch = url => {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
async function fetchData() {
setLoading(true)
const { data: responseData } = await axios.get(url)
console.log('url', url)
console.log('responseData', responseData)
setData(responseData)
setLoading(false)
}
useEffect(() => {
fetchData()
}, [])
return { loading, data }
}

View File

@ -1,68 +0,0 @@
import { PeerTubePlayer } from '@peertube/embed-api';
import 'regenerator-runtime/runtime';
import { toggleVideoPlaying } from './video-controls';
import { getVideoData } from './scheduling';
const streamData = getVideoData();
const videoWrapper = document.getElementById('video-wrapper');
const overlay = document.getElementById('overlay');
const videoiFrame = document.querySelector('iframe.video-player');
const video = {
isPlaying: false,
overlayVisible: false,
};
export const setUpPlayer = async () => {
const player = new PeerTubePlayer(videoiFrame);
await player.ready;
onPlayerReady(player);
app();
};
const app = () => {
if (streamData && streamData) {
console.log({ streamData });
}
};
const onPlayerReady = (player) => {
videoWrapper.addEventListener('mousemove', showOverlay);
videoWrapper.addEventListener('click', () => {
toggleVideoPlaying({
player,
video,
onPlay: () => {
hideOverlay();
},
onPause: () => {
showOverlay();
},
});
video.isPlaying = !video.isPlaying;
});
};
const showOverlay = () => {
if (!video.overlayVisible) {
videoWrapper.classList.add('active');
overlay.classList.add('active');
setTimeout(hideOverlay, 2000);
}
video.overlayVisible = true;
};
const hideOverlay = () => {
if (video.isPlaying) {
video.overlayVisible = false;
videoWrapper.classList.remove('active');
overlay.classList.remove('active');
}
};
setUpPlayer();

View File

@ -1,12 +0,0 @@
import axios from 'axios';
import 'regenerator-runtime/runtime';
import config from '../../data/conf.json';
export const getVideoData = async () => {
console.log({ config });
const { data } = await axios.get(`https://tv.undersco.re/api/v1/videos/${config.next_stream.peertube_id}`);
console.log(data);
return data;
};

View File

@ -1,15 +0,0 @@
export const toggleVideoPlaying = ({ player, video, onPlay, onPause }) => {
console.log('video', video);
if (video.isPlaying) {
player.pause();
if (typeof onPause === 'function') {
onPause();
}
} else {
player.play();
if (typeof onPlay === 'function') {
onPlay();
}
}
};

View File

@ -1,3 +1,11 @@
/* Karla Regular */
@font-face {
font-family: 'Karla';
src: url('../fonts/Karla/Karla-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
}
/* Karla Medium */ /* Karla Medium */
@font-face { @font-face {
font-family: 'Karla'; font-family: 'Karla';

View File

@ -0,0 +1,4 @@
export const sortData = data =>
Object.values(data)
.filter(feedItem => feedItem.type === 'VEVENT')
.sort((a, b) => new Date(a.start) - new Date(b.start))

View File

@ -1,42 +1,51 @@
import { h } from 'preact' /* eslint-disable react/prop-types */
import { h, Fragment } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks' import { useEffect, useRef, useState } from 'preact/hooks'
import { isBefore } from 'date-fns'
import { H1, H2, P } from '../Text' import { P } from '../Text'
import {
Wrapper,
PositionedLogo as Logo,
TaglineContainer,
Top,
} from './styles'
import translations from '../../data/strings' import translations from '../../data/strings'
import InfoLayout from '../InfoLayout' import InfoLayout from '../InfoLayout'
import { VideoCard, Title, InfoContent } from './styles'
const allowedChannels = [7, 4] // ReclaimFutures, NDC const Info = ({ data, loading }) => {
const now = new Date()
const pastStreams =
data && data.length
? data.filter(feeditem => isBefore(new Date(feeditem.end), now))
: []
const Info = ({ data }) => { const futureStreams =
// const [feed, setFeed] = useState([]) data && data.length
? data.filter(feeditem => isBefore(now, new Date(feeditem.start)))
// useEffect(() => { : []
// setFeed(sortData(data))
// }, [data])
useEffect(() => {
console.log({ data })
}, [data])
return ( return (
<InfoLayout title={translations.en.noStreams}> <InfoLayout
<P>ding dong</P> title={
data && data.length
? `${translations.en.nextStream}:`
: translations.en.noStreams
}
loading={loading}
>
{!loading && (
<InfoContent>
{futureStreams.map(feeditem => (
<VideoCard key={feeditem.start} {...feeditem} />
))}
{pastStreams.length ? (
<Fragment>
<Title>{translations.en.pastStream}:</Title>
{pastStreams.map(feeditem => (
<VideoCard key={feeditem.start} hasPassed {...feeditem} />
))}
</Fragment>
) : null}
</InfoContent>
)}
</InfoLayout> </InfoLayout>
) )
} }
const sortData = data => {
// if (!data || data?.length === 0) return
// console.log('data', data)
// return data.filter(feedItem => {
// return allowedChannels.includes(feedItem.channel.id) && feedItem.isLive
// })
}
export default Info export default Info

View File

@ -1,6 +1,12 @@
import { format } from 'date-fns'
import { h, Fragment } from 'preact'
import styled from 'styled-components' import styled from 'styled-components'
import { colours } from '../../assets/theme' import { colours } from '../../assets/theme'
import config from '../../data/config'
import Logo from '../Logo' import Logo from '../Logo'
import translations from '../../data/strings'
import { P, H1, H2, Span, Label } from '../Text'
export const Wrapper = styled.div` export const Wrapper = styled.div`
height: 100vh; height: 100vh;
@ -27,6 +33,10 @@ export const Wrapper = styled.div`
export const Top = styled.div`` export const Top = styled.div``
export const InfoContent = styled.div`
padding-bottom: 1em;
`
export const PositionedLogo = styled(Logo)` export const PositionedLogo = styled(Logo)`
margin-bottom: 64px; margin-bottom: 64px;
` `
@ -36,3 +46,54 @@ export const TaglineContainer = styled.div`
margin-top: 32px; margin-top: 32px;
} }
` `
export const Title = styled(H1)`
margin: 0.3em 0;
`
const VCWrapper = styled.div`
max-width: 600px;
margin-bottom: 3em;
border-left: 7px solid ${colours.midnightDarker};
padding-left: 1em;
`
const VCImg = styled.img`
width: 100%;
`
const ItemTitle = styled(H2)`
margin-bottom: 0.3em;
`
const DateLabel = styled(Label)`
margin: 1em 0;
display: block;
`
export const VideoCard = ({
name,
description,
start,
end,
previewPath,
hasPassed,
}) => {
return (
<VCWrapper>
<ItemTitle>{name}</ItemTitle>
<VCImg src={`${config.peertube_root}${previewPath}`} alt="" />
<DateLabel colour={colours.midnight} size="18">
{`${
hasPassed
? translations.en.streamDatePast
: translations.en.streamDateFuture
}`}
<Span bold colour={colours.midnight}>
{format(new Date(start), 'hh:mm dd/MM/yy')}
</Span>
</DateLabel>
<P>{description}</P>
</VCWrapper>
)
}

View File

@ -1,34 +1,53 @@
import { h } from 'preact' import { h } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks' import { useEffect, useRef, useState } from 'preact/hooks'
import { bool, string } from 'prop-types'
import { H1, H2, P } from '../Text' import { H1, H2 } from '../Text'
import { import {
Wrapper, Wrapper,
PositionedLogo as Logo, PositionedLogo as Logo,
TaglineContainer, TaglineContainer,
Top, Title,
Content, Content,
Fade,
} from './styles' } from './styles'
import translations from '../../data/strings' import translations from '../../data/strings'
import { colours } from '../../assets/theme' import { colours } from '../../assets/theme'
import Loader from '../Loader'
import { useTimeout } from '../../hooks/timerHooks'
const InfoLayout = ({ title, children }) => { const InfoLayout = ({ title, children, loading }) => {
return ( return (
<Wrapper> <Wrapper>
<Logo active colour={colours.midnightDarker} /> <Fade>
<Logo active colour={colours.midnightDarker} />
</Fade>
<Content> <Content>
<H2>{title}</H2> {loading ? (
<div>
<Loader />
</div>
) : (
<Title>{title}</Title>
)}
{children} {children}
</Content> </Content>
<TaglineContainer> <TaglineContainer>
{translations && {translations &&
translations.en.underscoreTagline.map(line => ( translations.en.underscoreTagline.map(line => (
<H1 key={line}>{line}</H1> <H1 align="right" key={line}>
{line}
</H1>
))} ))}
</TaglineContainer> </TaglineContainer>
</Wrapper> </Wrapper>
) )
} }
InfoLayout.propTypes = {
title: string,
loading: bool,
}
export default InfoLayout export default InfoLayout

View File

@ -1,5 +1,8 @@
import styled from 'styled-components' import styled from 'styled-components'
import { colours } from '../../assets/theme' import { colours } from '../../assets/theme'
import { H1 } from '../Text'
import Logo from '../Logo' import Logo from '../Logo'
export const Wrapper = styled.div` export const Wrapper = styled.div`
@ -41,14 +44,52 @@ export const Wrapper = styled.div`
export const Top = styled.div` export const Top = styled.div`
width: 50%; width: 50%;
` `
export const Content = styled.div``
export const PositionedLogo = styled(Logo)` const gradientColour = '#F8E5E2'
const getGradient = direction =>
`linear-gradient(to ${direction}, ${gradientColour}ee 0%,${gradientColour}00 100%);`
// prettier-ignore
export const Fade = styled.div`
width: 100%;
background-color: linear;
position: fixed; position: fixed;
top: 2em; padding: 2em 0 1em 2em;
top: 0;
left: 0;
background: ${getGradient('bottom')};
` `
export const Title = styled(H1)`
margin: 0.5em 0;
`
export const Content = styled.div`
/* margin-bottom: 3em; */
`
export const PositionedLogo = styled(Logo)``
export const TaglineContainer = styled.div` export const TaglineContainer = styled.div`
background: ${getGradient('top')};
position: fixed; position: fixed;
bottom: 2em; bottom: 0em;
padding-bottom: 0.5em;
right: 1em;
pointer-events: none;
@media screen and (max-width: 1200px) {
width: 100vw;
right: auto;
left: 1.5em;
h1 {
font-size: 32px;
text-align: left;
}
}
@media screen and (max-width: 800px) {
h1 {
font-size: 24px;
}
}
` `

View File

@ -0,0 +1,38 @@
import { h } from 'preact'
import { useRef, useState } from 'preact/hooks'
import { useInterval, useTimeout } from '../../hooks/timerHooks'
import { colours } from '../../assets/theme'
import { H1 } from '../Text'
// const symbols = ['⌏', '⌎', '⌌', '⌍']
const Loader = ({
active = true,
offset = 0,
animation = [':..', '.:.', '..:', '...'],
}) => {
const [text, setText] = useState('.')
const arrayPosition = useRef(offset)
const rate = 350
useInterval(
() => {
setText(animation[arrayPosition.current])
if (arrayPosition.current === animation.length - 1) {
arrayPosition.current = 0
} else {
arrayPosition.current += 1
}
},
active ? rate : null
)
return (
<H1 as="span" colour={colours.midnightDarker}>
{text}
</H1>
)
}
export default Loader

View File

@ -25,7 +25,7 @@ const Text = ({
weight={weight} weight={weight}
lineHeight={lineHeight} lineHeight={lineHeight}
$fontFamily={fontFamily} $fontFamily={fontFamily}
size={size} $size={size}
selectable={selectable} selectable={selectable}
{...rest} {...rest}
> >
@ -66,7 +66,7 @@ export const H1 = ({ children, ...rest }) => {
<Text <Text
tag="h1" tag="h1"
weight="700" weight="700"
size="48" $size="48"
lineHeight="0.8" lineHeight="0.8"
fontFamily="Lunchtype24" fontFamily="Lunchtype24"
{...rest} {...rest}
@ -80,7 +80,7 @@ export const H2 = ({ children, ...rest }) => (
<Text <Text
tag="h2" tag="h2"
weight="700" weight="700"
size="25" $size="25"
lineHeight="1" lineHeight="1"
fontFamily="Lunchtype24" fontFamily="Lunchtype24"
{...rest} {...rest}
@ -92,9 +92,9 @@ export const H2 = ({ children, ...rest }) => (
export const P = ({ children, ...rest }) => ( export const P = ({ children, ...rest }) => (
<Text <Text
tag="p" tag="p"
weight="500" weight="400"
size="13" $size="15"
lineHeight="16px" lineHeight="21px"
fontFamily="Karla" fontFamily="Karla"
{...rest} {...rest}
> >
@ -102,15 +102,15 @@ export const P = ({ children, ...rest }) => (
</Text> </Text>
) )
export const Span = ({ children, ...rest }) => ( export const Span = ({ children, ...rest }) => (
<Text tag="span" size="inherit" weight="inherit" {...rest}> <Text tag="span" $size="inherit" weight="inherit" {...rest}>
{children} {children}
</Text> </Text>
) )
export const Label = ({ children, ...rest }) => ( export const Label = ({ children, size, ...rest }) => (
<Text <Text
tag="label" tag="label"
weight="500" weight="500"
size="13" $size={size || '15'}
lineHeight="13px" lineHeight="13px"
fontFamily="Karla" fontFamily="Karla"
{...rest} {...rest}

View File

@ -3,7 +3,7 @@ import { colours } from '../../assets/theme'
export const TextBase = styled.span` export const TextBase = styled.span`
${({ ${({
size, $size,
weight, weight,
colour, colour,
align, align,
@ -21,7 +21,7 @@ export const TextBase = styled.span`
opacity: ${opacity}; opacity: ${opacity};
user-select: ${selectable ? 'inherit' : 'none'}; user-select: ${selectable ? 'inherit' : 'none'};
text-decoration: ${underline ? 'underline' : 'none'}; text-decoration: ${underline ? 'underline' : 'none'};
font-size: ${size}px; font-size: ${$size}px;
::selection { ::selection {
background-color: ${colours.midnightDarker}; background-color: ${colours.midnightDarker};

View File

@ -1,7 +1,10 @@
export default { export default {
en: { en: {
nextStream: 'Next stream', nextStream: 'Next streams',
pastStream: 'Latest streams',
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: ',
streamDatePast: 'First broadcast: ',
}, },
} }

96
src/hooks/data.js Normal file
View File

@ -0,0 +1,96 @@
import { useEffect, useState } from 'preact/hooks'
import axios from 'axios'
import ical from 'ical'
import config from '../data/config'
export const useCalendar = () => {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
async function fetchData() {
setLoading(true)
const { data: responseData } = await axios.get(`${config.calendar}`)
const streamsData = Object.values(ical.parseICS(responseData))
.filter(feedItem => feedItem.type === 'VEVENT')
.sort((a, b) => new Date(a.start) - new Date(b.start))
setData(streamsData)
setLoading(false)
}
useEffect(() => {
fetchData()
}, [])
return { loading, data }
}
// export const useCalendar = () => {
// const [data, setData] = useState(null)
// const [loading, setLoading] = useState(true)
// async function fetchData() {
// setLoading(true)
// const { data: responseData } = await axios.get(`${config.calendar}`)
// const streamsData = Object.values(ical.parseICS(responseData))
// .filter(feedItem => feedItem.type === 'VEVENT')
// .sort((a, b) => new Date(a.start) - new Date(b.start))
// setData(streamsData)
// setLoading(false)
// }
// useEffect(() => {
// fetchData()
// }, [])
// return { loading, data }
// }
// useEffect(() => {
// const feedPromise =
// data &&
// data.map(async feedItem => {
// if (feedItem.url) {
// const id = feedItem.url.val.split('/').pop()
// const {
// data: {
// account,
// category,
// channel,
// description,
// embedPath,
// language,
// name,
// state,
// previewPath,
// views,
// },
// } = await axios.get(`https://tv.undersco.re/api/v1/videos/${id}`)
// const item = {
// name,
// account,
// category,
// channel,
// description,
// embedPath,
// language,
// state,
// previewPath,
// views,
// start: feedItem.start,
// end: feedItem.end,
// }
// console.log(item)
// return item
// }
// return null
// })
// feedPromise.then(result => {
// console.log(result)
// })
// }, [data])

38
src/hooks/timerHooks.js Normal file
View File

@ -0,0 +1,38 @@
/* eslint-disable consistent-return */
import { useEffect, useRef } from 'preact/hooks'
export const useInterval = (callback, interval) => {
const savedCallback = useRef()
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
function tick() {
savedCallback.current()
}
if (interval !== null) {
const id = setInterval(tick, interval)
return () => clearInterval(id)
}
}, [interval])
}
export const useTimeout = (callback, timeout) => {
const savedCallback = useRef()
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
function tick() {
savedCallback.current()
}
if (timeout !== null) {
const id = setTimeout(tick, timeout)
return () => clearTimeout(id)
}
}, [timeout])
}

View File

@ -2483,6 +2483,11 @@ data-urls@^1.1.0:
whatwg-mimetype "^2.2.0" whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0" whatwg-url "^7.0.0"
date-fns@^2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1"
integrity sha512-X3bf2iTPgCAQp9wvjOQytnf5vO5rESYRXlPIVcgSbtT5OTScPcsf9eZU+B/YIkKAtYr5WeCii58BgATrNitlWg==
deasync@^0.1.14: deasync@^0.1.14:
version "0.1.21" version "0.1.21"
resolved "https://registry.yarnpkg.com/deasync/-/deasync-0.1.21.tgz#bb11eabd4466c0d8776f0d82deb8a6126460d30f" resolved "https://registry.yarnpkg.com/deasync/-/deasync-0.1.21.tgz#bb11eabd4466c0d8776f0d82deb8a6126460d30f"
@ -3612,6 +3617,13 @@ https-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
ical@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/ical/-/ical-0.8.0.tgz#aa93f021dfead58e54aaa22076a11ca07d65886b"
integrity sha512-/viUSb/RGLLnlgm0lWRlPBtVeQguQRErSPYl3ugnUaKUnzQswKqOG3M8/P1v1AB5NJwlHTuvTq1cs4mpeG2rCg==
dependencies:
rrule "2.4.1"
iconv-lite@0.4.24, iconv-lite@^0.4.17: iconv-lite@0.4.24, iconv-lite@^0.4.17:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -4263,6 +4275,11 @@ lru-cache@^4.0.1, lru-cache@^4.1.5:
pseudomap "^1.0.2" pseudomap "^1.0.2"
yallist "^2.1.2" yallist "^2.1.2"
luxon@^1.3.3:
version "1.26.0"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.26.0.tgz#d3692361fda51473948252061d0f8561df02b578"
integrity sha512-+V5QIQ5f6CDXQpWNICELwjwuHdqeJM1UenlZWx5ujcRMc9venvluCjFb4t5NYLhb6IhkbMVOxzVuOqkgMxee2A==
magic-string@^0.22.4: magic-string@^0.22.4:
version "0.22.5" version "0.22.5"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e"
@ -5792,6 +5809,13 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0" hash-base "^3.0.0"
inherits "^2.0.1" inherits "^2.0.1"
rrule@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/rrule/-/rrule-2.4.1.tgz#1d0db4e45f2b0e92e2cca62d2f7093729ac7ec94"
integrity sha512-+NcvhETefswZq13T8nkuEnnQ6YgUeZaqMqVbp+ZiFDPCbp3AVgQIwUvNVDdMNrP05bKZG9ddDULFp0qZZYDrxg==
optionalDependencies:
luxon "^1.3.3"
run-async@^2.2.0: run-async@^2.2.0:
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"