Merge pull request #272 from GRA0007/feat/web-worker

Web worker for heatmap table calculation
This commit is contained in:
Benji Grant 2023-06-13 12:45:19 +10:00 committed by GitHub
commit 08f6646339
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 134 additions and 61 deletions

View file

@ -12,8 +12,10 @@ import SelectField from '/src/components/SelectField/SelectField'
import { EventResponse, getPeople, PersonResponse, updatePerson } from '/src/config/api' import { EventResponse, getPeople, PersonResponse, updatePerson } from '/src/config/api'
import { useTranslation } from '/src/i18n/client' import { useTranslation } from '/src/i18n/client'
import timezones from '/src/res/timezones.json' import timezones from '/src/res/timezones.json'
import { useStore } from '/src/stores'
import useRecentsStore from '/src/stores/recentsStore' import useRecentsStore from '/src/stores/recentsStore'
import { expandTimes, makeClass } from '/src/utils' import useSettingsStore from '/src/stores/settingsStore'
import { calculateTable, expandTimes, makeClass } from '/src/utils'
import styles from './page.module.scss' import styles from './page.module.scss'
@ -25,6 +27,8 @@ interface EventAvailabilitiesProps {
const EventAvailabilities = ({ event, ...data }: EventAvailabilitiesProps) => { const EventAvailabilities = ({ event, ...data }: EventAvailabilitiesProps) => {
const { t, i18n } = useTranslation('event') const { t, i18n } = useTranslation('event')
const timeFormat = useStore(useSettingsStore, state => state.timeFormat) ?? '12h'
const [people, setPeople] = useState(data.people) const [people, setPeople] = useState(data.people)
const expandedTimes = useMemo(() => expandTimes(event.times), [event.times]) const expandedTimes = useMemo(() => expandTimes(event.times), [event.times])
@ -34,6 +38,28 @@ const EventAvailabilities = ({ event, ...data }: EventAvailabilitiesProps) => {
const [tab, setTab] = useState<'group' | 'you'>('group') const [tab, setTab] = useState<'group' | 'you'>('group')
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone) const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone)
// Web worker for calculating the heatmap table
const tableWorker = useMemo(() => (typeof window !== undefined && window.Worker) ? new Worker(new URL('/src/workers/calculateTable', import.meta.url)) : undefined, [])
// Calculate table (using a web worker if available)
const [table, setTable] = useState<ReturnType<typeof calculateTable>>()
useEffect(() => {
const args = { times: expandedTimes, locale: i18n.language, timeFormat, timezone }
if (tableWorker) {
tableWorker.postMessage(args)
setTable(undefined)
} else {
setTable(calculateTable(args))
}
}, [tableWorker, expandedTimes, i18n.language, timeFormat, timezone])
useEffect(() => {
if (tableWorker) {
tableWorker.onmessage = (e: MessageEvent<ReturnType<typeof calculateTable>>) => setTable(e.data)
}
}, [tableWorker])
// Add this event to recents // Add this event to recents
const addRecent = useRecentsStore(state => state.addRecent) const addRecent = useRecentsStore(state => state.addRecent)
useEffect(() => { useEffect(() => {
@ -138,7 +164,7 @@ const EventAvailabilities = ({ event, ...data }: EventAvailabilitiesProps) => {
{tab === 'group' ? <AvailabilityViewer {tab === 'group' ? <AvailabilityViewer
times={expandedTimes} times={expandedTimes}
people={people} people={people}
timezone={timezone} table={table}
/> : user && <AvailabilityEditor /> : user && <AvailabilityEditor
times={expandedTimes} times={expandedTimes}
timezone={timezone} timezone={timezone}
@ -152,6 +178,7 @@ const EventAvailabilities = ({ event, ...data }: EventAvailabilitiesProps) => {
setUser({ ...user, availability: oldAvailability }) setUser({ ...user, availability: oldAvailability })
}) })
}} }}
table={table}
/>} />}
</> </>
} }

View file

@ -13,7 +13,7 @@ import Section from '/src/components/Section/Section'
import TimeRangeField from '/src/components/TimeRangeField/TimeRangeField' import TimeRangeField from '/src/components/TimeRangeField/TimeRangeField'
import Video from '/src/components/Video/Video' import Video from '/src/components/Video/Video'
import { useTranslation } from '/src/i18n/server' import { useTranslation } from '/src/i18n/server'
import { getWeekdayNames } from '/src/utils' import { calculateTable, getWeekdayNames } from '/src/utils'
import styles from './page.module.scss' import styles from './page.module.scss'
@ -25,9 +25,13 @@ export const generateMetadata = async (): Promise<Metadata> => {
} }
} }
const times = ['1100-12042021', '1115-12042021', '1130-12042021', '1145-12042021', '1200-12042021', '1215-12042021', '1230-12042021', '1245-12042021', '1300-12042021', '1315-12042021', '1330-12042021', '1345-12042021', '1400-12042021', '1415-12042021', '1430-12042021', '1445-12042021', '1500-12042021', '1515-12042021', '1530-12042021', '1545-12042021', '1600-12042021', '1615-12042021', '1630-12042021', '1645-12042021', '1100-13042021', '1115-13042021', '1130-13042021', '1145-13042021', '1200-13042021', '1215-13042021', '1230-13042021', '1245-13042021', '1300-13042021', '1315-13042021', '1330-13042021', '1345-13042021', '1400-13042021', '1415-13042021', '1430-13042021', '1445-13042021', '1500-13042021', '1515-13042021', '1530-13042021', '1545-13042021', '1600-13042021', '1615-13042021', '1630-13042021', '1645-13042021', '1100-14042021', '1115-14042021', '1130-14042021', '1145-14042021', '1200-14042021', '1215-14042021', '1230-14042021', '1245-14042021', '1300-14042021', '1315-14042021', '1330-14042021', '1345-14042021', '1400-14042021', '1415-14042021', '1430-14042021', '1445-14042021', '1500-14042021', '1515-14042021', '1530-14042021', '1545-14042021', '1600-14042021', '1615-14042021', '1630-14042021', '1645-14042021', '1100-15042021', '1115-15042021', '1130-15042021', '1145-15042021', '1200-15042021', '1215-15042021', '1230-15042021', '1245-15042021', '1300-15042021', '1315-15042021', '1330-15042021', '1345-15042021', '1400-15042021', '1415-15042021', '1430-15042021', '1445-15042021', '1500-15042021', '1515-15042021', '1530-15042021', '1545-15042021', '1600-15042021', '1615-15042021', '1630-15042021', '1645-15042021', '1100-16042021', '1115-16042021', '1130-16042021', '1145-16042021', '1200-16042021', '1215-16042021', '1230-16042021', '1245-16042021', '1300-16042021', '1315-16042021', '1330-16042021', '1345-16042021', '1400-16042021', '1415-16042021', '1430-16042021', '1445-16042021', '1500-16042021', '1515-16042021', '1530-16042021', '1545-16042021', '1600-16042021', '1615-16042021', '1630-16042021', '1645-16042021']
const Page = async () => { const Page = async () => {
const { t, i18n } = await useTranslation(['common', 'help']) const { t, i18n } = await useTranslation(['common', 'help'])
const table = calculateTable({ times, locale: i18n.language, timezone: 'UTC', timeFormat: '12h' })
return <> return <>
<Content> <Content>
<Header /> <Header />
@ -53,9 +57,9 @@ const Page = async () => {
<P>{t('help:p6')}</P> <P>{t('help:p6')}</P>
<P>{t('help:p7')}</P> <P>{t('help:p7')}</P>
<AvailabilityViewer <AvailabilityViewer
times={['1100-12042021', '1115-12042021', '1130-12042021', '1145-12042021', '1200-12042021', '1215-12042021', '1230-12042021', '1245-12042021', '1300-12042021', '1315-12042021', '1330-12042021', '1345-12042021', '1400-12042021', '1415-12042021', '1430-12042021', '1445-12042021', '1500-12042021', '1515-12042021', '1530-12042021', '1545-12042021', '1600-12042021', '1615-12042021', '1630-12042021', '1645-12042021', '1100-13042021', '1115-13042021', '1130-13042021', '1145-13042021', '1200-13042021', '1215-13042021', '1230-13042021', '1245-13042021', '1300-13042021', '1315-13042021', '1330-13042021', '1345-13042021', '1400-13042021', '1415-13042021', '1430-13042021', '1445-13042021', '1500-13042021', '1515-13042021', '1530-13042021', '1545-13042021', '1600-13042021', '1615-13042021', '1630-13042021', '1645-13042021', '1100-14042021', '1115-14042021', '1130-14042021', '1145-14042021', '1200-14042021', '1215-14042021', '1230-14042021', '1245-14042021', '1300-14042021', '1315-14042021', '1330-14042021', '1345-14042021', '1400-14042021', '1415-14042021', '1430-14042021', '1445-14042021', '1500-14042021', '1515-14042021', '1530-14042021', '1545-14042021', '1600-14042021', '1615-14042021', '1630-14042021', '1645-14042021', '1100-15042021', '1115-15042021', '1130-15042021', '1145-15042021', '1200-15042021', '1215-15042021', '1230-15042021', '1245-15042021', '1300-15042021', '1315-15042021', '1330-15042021', '1345-15042021', '1400-15042021', '1415-15042021', '1430-15042021', '1445-15042021', '1500-15042021', '1515-15042021', '1530-15042021', '1545-15042021', '1600-15042021', '1615-15042021', '1630-15042021', '1645-15042021', '1100-16042021', '1115-16042021', '1130-16042021', '1145-16042021', '1200-16042021', '1215-16042021', '1230-16042021', '1245-16042021', '1300-16042021', '1315-16042021', '1330-16042021', '1345-16042021', '1400-16042021', '1415-16042021', '1430-16042021', '1445-16042021', '1500-16042021', '1515-16042021', '1530-16042021', '1545-16042021', '1600-16042021', '1615-16042021', '1630-16042021', '1645-16042021']} times={times}
people={[{ name: 'Jenny', created_at: 1618232400, availability: ['1100-12042021', '1100-13042021', '1100-14042021', '1100-15042021', '1115-12042021', '1115-13042021', '1115-14042021', '1115-15042021', '1130-12042021', '1130-13042021', '1130-14042021', '1130-15042021', '1145-12042021', '1145-13042021', '1145-14042021', '1145-15042021', '1200-12042021', '1200-13042021', '1200-14042021', '1200-15042021', '1215-12042021', '1215-13042021', '1215-14042021', '1215-15042021', '1230-12042021', '1230-13042021', '1230-14042021', '1230-15042021', '1245-12042021', '1245-13042021', '1245-14042021', '1245-15042021', '1300-12042021', '1300-13042021', '1300-14042021', '1300-15042021', '1300-16042021', '1315-12042021', '1315-13042021', '1315-14042021', '1315-15042021', '1315-16042021', '1330-12042021', '1330-13042021', '1330-14042021', '1330-15042021', '1330-16042021', '1345-12042021', '1345-13042021', '1345-14042021', '1345-15042021', '1345-16042021', '1400-12042021', '1400-13042021', '1400-14042021', '1400-15042021', '1400-16042021', '1415-12042021', '1415-13042021', '1415-14042021', '1415-15042021', '1415-16042021', '1430-12042021', '1430-13042021', '1430-14042021', '1430-15042021', '1430-16042021', '1445-12042021', '1445-13042021', '1445-14042021', '1445-15042021', '1445-16042021', '1500-12042021', '1500-15042021', '1500-16042021', '1515-12042021', '1515-15042021', '1515-16042021', '1530-12042021', '1530-15042021', '1530-16042021', '1545-12042021', '1545-15042021', '1545-16042021', '1600-12042021', '1600-15042021', '1600-16042021', '1615-12042021', '1615-15042021', '1615-16042021', '1630-12042021', '1630-15042021', '1630-16042021', '1645-12042021', '1645-15042021', '1645-16042021'] }]} people={[{ name: 'Jenny', created_at: 1618232400, availability: ['1100-12042021', '1100-13042021', '1100-14042021', '1100-15042021', '1115-12042021', '1115-13042021', '1115-14042021', '1115-15042021', '1130-12042021', '1130-13042021', '1130-14042021', '1130-15042021', '1145-12042021', '1145-13042021', '1145-14042021', '1145-15042021', '1200-12042021', '1200-13042021', '1200-14042021', '1200-15042021', '1215-12042021', '1215-13042021', '1215-14042021', '1215-15042021', '1230-12042021', '1230-13042021', '1230-14042021', '1230-15042021', '1245-12042021', '1245-13042021', '1245-14042021', '1245-15042021', '1300-12042021', '1300-13042021', '1300-14042021', '1300-15042021', '1300-16042021', '1315-12042021', '1315-13042021', '1315-14042021', '1315-15042021', '1315-16042021', '1330-12042021', '1330-13042021', '1330-14042021', '1330-15042021', '1330-16042021', '1345-12042021', '1345-13042021', '1345-14042021', '1345-15042021', '1345-16042021', '1400-12042021', '1400-13042021', '1400-14042021', '1400-15042021', '1400-16042021', '1415-12042021', '1415-13042021', '1415-14042021', '1415-15042021', '1415-16042021', '1430-12042021', '1430-13042021', '1430-14042021', '1430-15042021', '1430-16042021', '1445-12042021', '1445-13042021', '1445-14042021', '1445-15042021', '1445-16042021', '1500-12042021', '1500-15042021', '1500-16042021', '1515-12042021', '1515-15042021', '1515-16042021', '1530-12042021', '1530-15042021', '1530-16042021', '1545-12042021', '1545-15042021', '1545-16042021', '1600-12042021', '1600-15042021', '1600-16042021', '1615-12042021', '1615-15042021', '1615-16042021', '1630-12042021', '1630-15042021', '1630-16042021', '1645-12042021', '1645-15042021', '1645-16042021'] }]}
timezone="UTC" table={table}
/> />
<h2 className={styles.step}>{t('help:s3')}</h2> <h2 className={styles.step}>{t('help:s3')}</h2>
@ -63,7 +67,7 @@ const Page = async () => {
<P>{t('help:p9')}</P> <P>{t('help:p9')}</P>
<P>{t('help:p10')}</P> <P>{t('help:p10')}</P>
<AvailabilityViewer <AvailabilityViewer
times={['1100-12042021', '1115-12042021', '1130-12042021', '1145-12042021', '1200-12042021', '1215-12042021', '1230-12042021', '1245-12042021', '1300-12042021', '1315-12042021', '1330-12042021', '1345-12042021', '1400-12042021', '1415-12042021', '1430-12042021', '1445-12042021', '1500-12042021', '1515-12042021', '1530-12042021', '1545-12042021', '1600-12042021', '1615-12042021', '1630-12042021', '1645-12042021', '1100-13042021', '1115-13042021', '1130-13042021', '1145-13042021', '1200-13042021', '1215-13042021', '1230-13042021', '1245-13042021', '1300-13042021', '1315-13042021', '1330-13042021', '1345-13042021', '1400-13042021', '1415-13042021', '1430-13042021', '1445-13042021', '1500-13042021', '1515-13042021', '1530-13042021', '1545-13042021', '1600-13042021', '1615-13042021', '1630-13042021', '1645-13042021', '1100-14042021', '1115-14042021', '1130-14042021', '1145-14042021', '1200-14042021', '1215-14042021', '1230-14042021', '1245-14042021', '1300-14042021', '1315-14042021', '1330-14042021', '1345-14042021', '1400-14042021', '1415-14042021', '1430-14042021', '1445-14042021', '1500-14042021', '1515-14042021', '1530-14042021', '1545-14042021', '1600-14042021', '1615-14042021', '1630-14042021', '1645-14042021', '1100-15042021', '1115-15042021', '1130-15042021', '1145-15042021', '1200-15042021', '1215-15042021', '1230-15042021', '1245-15042021', '1300-15042021', '1315-15042021', '1330-15042021', '1345-15042021', '1400-15042021', '1415-15042021', '1430-15042021', '1445-15042021', '1500-15042021', '1515-15042021', '1530-15042021', '1545-15042021', '1600-15042021', '1615-15042021', '1630-15042021', '1645-15042021', '1100-16042021', '1115-16042021', '1130-16042021', '1145-16042021', '1200-16042021', '1215-16042021', '1230-16042021', '1245-16042021', '1300-16042021', '1315-16042021', '1330-16042021', '1345-16042021', '1400-16042021', '1415-16042021', '1430-16042021', '1445-16042021', '1500-16042021', '1515-16042021', '1530-16042021', '1545-16042021', '1600-16042021', '1615-16042021', '1630-16042021', '1645-16042021']} times={times}
people={[ people={[
{ name: 'Jenny', created_at: 1618232400, availability: ['1100-12042021', '1100-13042021', '1100-14042021', '1100-15042021', '1115-12042021', '1115-13042021', '1115-14042021', '1115-15042021', '1130-12042021', '1130-13042021', '1130-14042021', '1130-15042021', '1145-12042021', '1145-13042021', '1145-14042021', '1145-15042021', '1200-12042021', '1200-13042021', '1200-14042021', '1200-15042021', '1215-12042021', '1215-13042021', '1215-14042021', '1215-15042021', '1230-12042021', '1230-13042021', '1230-14042021', '1230-15042021', '1245-12042021', '1245-13042021', '1245-14042021', '1245-15042021', '1300-12042021', '1300-13042021', '1300-14042021', '1300-15042021', '1300-16042021', '1315-12042021', '1315-13042021', '1315-14042021', '1315-15042021', '1315-16042021', '1330-12042021', '1330-13042021', '1330-14042021', '1330-15042021', '1330-16042021', '1345-12042021', '1345-13042021', '1345-14042021', '1345-15042021', '1345-16042021', '1400-12042021', '1400-13042021', '1400-14042021', '1400-15042021', '1400-16042021', '1415-12042021', '1415-13042021', '1415-14042021', '1415-15042021', '1415-16042021', '1430-12042021', '1430-13042021', '1430-14042021', '1430-15042021', '1430-16042021', '1445-12042021', '1445-13042021', '1445-14042021', '1445-15042021', '1445-16042021', '1500-12042021', '1500-15042021', '1500-16042021', '1515-12042021', '1515-15042021', '1515-16042021', '1530-12042021', '1530-15042021', '1530-16042021', '1545-12042021', '1545-15042021', '1545-16042021', '1600-12042021', '1600-15042021', '1600-16042021', '1615-12042021', '1615-15042021', '1615-16042021', '1630-12042021', '1630-15042021', '1630-16042021', '1645-12042021', '1645-15042021', '1645-16042021'] }, { name: 'Jenny', created_at: 1618232400, availability: ['1100-12042021', '1100-13042021', '1100-14042021', '1100-15042021', '1115-12042021', '1115-13042021', '1115-14042021', '1115-15042021', '1130-12042021', '1130-13042021', '1130-14042021', '1130-15042021', '1145-12042021', '1145-13042021', '1145-14042021', '1145-15042021', '1200-12042021', '1200-13042021', '1200-14042021', '1200-15042021', '1215-12042021', '1215-13042021', '1215-14042021', '1215-15042021', '1230-12042021', '1230-13042021', '1230-14042021', '1230-15042021', '1245-12042021', '1245-13042021', '1245-14042021', '1245-15042021', '1300-12042021', '1300-13042021', '1300-14042021', '1300-15042021', '1300-16042021', '1315-12042021', '1315-13042021', '1315-14042021', '1315-15042021', '1315-16042021', '1330-12042021', '1330-13042021', '1330-14042021', '1330-15042021', '1330-16042021', '1345-12042021', '1345-13042021', '1345-14042021', '1345-15042021', '1345-16042021', '1400-12042021', '1400-13042021', '1400-14042021', '1400-15042021', '1400-16042021', '1415-12042021', '1415-13042021', '1415-14042021', '1415-15042021', '1415-16042021', '1430-12042021', '1430-13042021', '1430-14042021', '1430-15042021', '1430-16042021', '1445-12042021', '1445-13042021', '1445-14042021', '1445-15042021', '1445-16042021', '1500-12042021', '1500-15042021', '1500-16042021', '1515-12042021', '1515-15042021', '1515-16042021', '1530-12042021', '1530-15042021', '1530-16042021', '1545-12042021', '1545-15042021', '1545-16042021', '1600-12042021', '1600-15042021', '1600-16042021', '1615-12042021', '1615-15042021', '1615-16042021', '1630-12042021', '1630-15042021', '1630-16042021', '1645-12042021', '1645-15042021', '1645-16042021'] },
{ name: 'Dakota', created_at: 1618232400, availability: ['1300-14042021', '1300-15042021', '1300-16042021', '1315-13042021', '1315-14042021', '1315-15042021', '1315-16042021', '1330-13042021', '1330-14042021', '1330-15042021', '1330-16042021', '1345-13042021', '1345-14042021', '1345-15042021', '1345-16042021', '1400-13042021', '1400-14042021', '1400-15042021', '1400-16042021', '1415-13042021', '1415-14042021', '1415-15042021', '1415-16042021', '1430-13042021', '1430-14042021', '1430-15042021', '1430-16042021', '1445-13042021', '1445-14042021', '1445-15042021', '1445-16042021', '1300-13042021', '1100-12042021', '1100-13042021', '1115-12042021', '1115-13042021', '1130-12042021', '1130-13042021', '1145-12042021', '1145-13042021'] }, { name: 'Dakota', created_at: 1618232400, availability: ['1300-14042021', '1300-15042021', '1300-16042021', '1315-13042021', '1315-14042021', '1315-15042021', '1315-16042021', '1330-13042021', '1330-14042021', '1330-15042021', '1330-16042021', '1345-13042021', '1345-14042021', '1345-15042021', '1345-16042021', '1400-13042021', '1400-14042021', '1400-15042021', '1400-16042021', '1415-13042021', '1415-14042021', '1415-15042021', '1415-16042021', '1430-13042021', '1430-14042021', '1430-15042021', '1430-16042021', '1445-13042021', '1445-14042021', '1445-15042021', '1445-16042021', '1300-13042021', '1100-12042021', '1100-13042021', '1115-12042021', '1115-13042021', '1130-12042021', '1130-13042021', '1145-12042021', '1145-13042021'] },
@ -71,7 +75,7 @@ const Page = async () => {
{ name: 'Mark', created_at: 1618232400, availability: ['1200-12042021', '1200-13042021', '1200-14042021', '1200-16042021', '1215-12042021', '1215-13042021', '1215-14042021', '1215-16042021', '1230-12042021', '1230-13042021', '1230-14042021', '1230-16042021', '1245-12042021', '1245-13042021', '1245-14042021', '1245-16042021', '1300-12042021', '1300-13042021', '1300-14042021', '1300-16042021', '1315-12042021', '1315-13042021', '1315-14042021', '1315-16042021', '1330-12042021', '1330-13042021', '1330-14042021', '1330-16042021', '1345-12042021', '1345-13042021', '1345-14042021', '1345-16042021', '1400-12042021', '1400-13042021', '1400-14042021', '1400-16042021', '1415-12042021', '1415-13042021', '1415-14042021', '1415-16042021', '1430-12042021', '1430-13042021', '1430-14042021', '1430-16042021', '1445-12042021', '1445-13042021', '1445-14042021', '1445-16042021', '1500-12042021', '1500-13042021', '1500-14042021', '1500-16042021', '1515-12042021', '1515-13042021', '1515-14042021', '1515-16042021', '1530-12042021', '1530-13042021', '1530-14042021', '1530-16042021', '1545-12042021', '1545-13042021', '1545-14042021', '1545-16042021'] }, { name: 'Mark', created_at: 1618232400, availability: ['1200-12042021', '1200-13042021', '1200-14042021', '1200-16042021', '1215-12042021', '1215-13042021', '1215-14042021', '1215-16042021', '1230-12042021', '1230-13042021', '1230-14042021', '1230-16042021', '1245-12042021', '1245-13042021', '1245-14042021', '1245-16042021', '1300-12042021', '1300-13042021', '1300-14042021', '1300-16042021', '1315-12042021', '1315-13042021', '1315-14042021', '1315-16042021', '1330-12042021', '1330-13042021', '1330-14042021', '1330-16042021', '1345-12042021', '1345-13042021', '1345-14042021', '1345-16042021', '1400-12042021', '1400-13042021', '1400-14042021', '1400-16042021', '1415-12042021', '1415-13042021', '1415-14042021', '1415-16042021', '1430-12042021', '1430-13042021', '1430-14042021', '1430-16042021', '1445-12042021', '1445-13042021', '1445-14042021', '1445-16042021', '1500-12042021', '1500-13042021', '1500-14042021', '1500-16042021', '1515-12042021', '1515-13042021', '1515-14042021', '1515-16042021', '1530-12042021', '1530-13042021', '1530-14042021', '1530-16042021', '1545-12042021', '1545-13042021', '1545-14042021', '1545-16042021'] },
{ name: 'Alex', created_at: 1618232400, availability: ['1200-13042021', '1200-14042021', '1215-13042021', '1215-14042021', '1230-13042021', '1230-14042021', '1245-13042021', '1245-14042021', '1300-13042021', '1300-14042021', '1315-13042021', '1315-14042021', '1330-13042021', '1330-14042021', '1345-13042021', '1345-14042021', '1400-13042021', '1400-14042021', '1415-13042021', '1415-14042021', '1430-13042021', '1430-14042021', '1445-13042021', '1445-14042021', '1500-13042021', '1500-14042021', '1515-13042021', '1515-14042021', '1530-13042021', '1530-14042021', '1545-13042021', '1545-14042021', '1200-12042021', '1215-12042021', '1545-12042021', '1230-12042021', '1245-12042021', '1300-12042021', '1315-12042021', '1330-12042021', '1345-12042021', '1400-12042021', '1415-12042021', '1430-12042021', '1445-12042021', '1500-12042021', '1515-12042021', '1530-12042021', '1100-15042021', '1100-16042021', '1115-15042021', '1115-16042021', '1130-15042021', '1130-16042021', '1145-15042021', '1145-16042021', '1200-15042021', '1200-16042021', '1215-15042021', '1215-16042021', '1230-15042021', '1230-16042021', '1245-15042021', '1245-16042021', '1300-15042021', '1300-16042021', '1315-15042021', '1315-16042021', '1330-15042021', '1330-16042021', '1345-15042021', '1345-16042021', '1400-15042021', '1400-16042021', '1415-15042021', '1415-16042021', '1430-15042021', '1430-16042021', '1445-15042021', '1445-16042021', '1500-15042021', '1500-16042021', '1515-15042021', '1515-16042021', '1530-15042021', '1530-16042021', '1545-15042021', '1545-16042021', '1600-15042021', '1600-16042021', '1615-15042021', '1615-16042021', '1630-15042021', '1630-16042021', '1645-15042021', '1645-16042021'] }, { name: 'Alex', created_at: 1618232400, availability: ['1200-13042021', '1200-14042021', '1215-13042021', '1215-14042021', '1230-13042021', '1230-14042021', '1245-13042021', '1245-14042021', '1300-13042021', '1300-14042021', '1315-13042021', '1315-14042021', '1330-13042021', '1330-14042021', '1345-13042021', '1345-14042021', '1400-13042021', '1400-14042021', '1415-13042021', '1415-14042021', '1430-13042021', '1430-14042021', '1445-13042021', '1445-14042021', '1500-13042021', '1500-14042021', '1515-13042021', '1515-14042021', '1530-13042021', '1530-14042021', '1545-13042021', '1545-14042021', '1200-12042021', '1215-12042021', '1545-12042021', '1230-12042021', '1245-12042021', '1300-12042021', '1315-12042021', '1330-12042021', '1345-12042021', '1400-12042021', '1415-12042021', '1430-12042021', '1445-12042021', '1500-12042021', '1515-12042021', '1530-12042021', '1100-15042021', '1100-16042021', '1115-15042021', '1115-16042021', '1130-15042021', '1130-16042021', '1145-15042021', '1145-16042021', '1200-15042021', '1200-16042021', '1215-15042021', '1215-16042021', '1230-15042021', '1230-16042021', '1245-15042021', '1245-16042021', '1300-15042021', '1300-16042021', '1315-15042021', '1315-16042021', '1330-15042021', '1330-16042021', '1345-15042021', '1345-16042021', '1400-15042021', '1400-16042021', '1415-15042021', '1415-16042021', '1430-15042021', '1430-16042021', '1445-15042021', '1445-16042021', '1500-15042021', '1500-16042021', '1515-15042021', '1515-16042021', '1530-15042021', '1530-16042021', '1545-15042021', '1545-16042021', '1600-15042021', '1600-16042021', '1615-15042021', '1615-16042021', '1630-15042021', '1630-16042021', '1645-15042021', '1645-16042021'] },
]} ]}
timezone="UTC" table={table}
/> />
</Content> </Content>

View file

@ -1,36 +1,24 @@
import { Fragment, useCallback, useMemo, useRef, useState } from 'react' import { Fragment, useCallback, useRef, useState } from 'react'
import Content from '/src/components/Content/Content' import Content from '/src/components/Content/Content'
import GoogleCalendar from '/src/components/GoogleCalendar/GoogleCalendar' import GoogleCalendar from '/src/components/GoogleCalendar/GoogleCalendar'
import { usePalette } from '/src/hooks/usePalette' import { usePalette } from '/src/hooks/usePalette'
import { useTranslation } from '/src/i18n/client' import { useTranslation } from '/src/i18n/client'
import { useStore } from '/src/stores'
import useSettingsStore from '/src/stores/settingsStore'
import { calculateTable, makeClass, parseSpecificDate } from '/src/utils' import { calculateTable, makeClass, parseSpecificDate } from '/src/utils'
import styles from '../AvailabilityViewer/AvailabilityViewer.module.scss' import styles from '../AvailabilityViewer/AvailabilityViewer.module.scss'
import Skeleton from '../AvailabilityViewer/components/Skeleton/Skeleton'
interface AvailabilityEditorProps { interface AvailabilityEditorProps {
times: string[] times: string[]
timezone: string timezone: string
value: string[] value: string[]
onChange: (value: string[]) => void onChange: (value: string[]) => void
table?: ReturnType<typeof calculateTable>
} }
const AvailabilityEditor = ({ const AvailabilityEditor = ({ times, timezone, value = [], onChange, table }: AvailabilityEditorProps) => {
times, const { t } = useTranslation('event')
timezone,
value = [],
onChange,
}: AvailabilityEditorProps) => {
const { t, i18n } = useTranslation('event')
const timeFormat = useStore(useSettingsStore, state => state.timeFormat) ?? '12h'
// Calculate table
const { rows, columns } = useMemo(() =>
calculateTable(times, i18n.language, timeFormat, timezone),
[times, i18n.language, timeFormat, timezone])
// Ref and state required to rerender but also access static version in callbacks // Ref and state required to rerender but also access static version in callbacks
const selectingRef = useRef<string[]>([]) const selectingRef = useRef<string[]>([])
@ -64,24 +52,24 @@ const AvailabilityEditor = ({
<div> <div>
<div className={styles.heatmap}> <div className={styles.heatmap}>
<div className={styles.timeLabels}> <div className={styles.timeLabels}>
{rows.map((row, i) => {table?.rows.map((row, i) =>
<div className={styles.timeSpace} key={i}> <div className={styles.timeSpace} key={i}>
{row && <label className={styles.timeLabel}> {row && <label className={styles.timeLabel}>
{row.label} {row.label}
</label>} </label>}
</div> </div>
)} ) ?? null}
</div> </div>
{columns.map((column, x) => <Fragment key={x}> {table?.columns.map((column, x) => <Fragment key={x}>
{column ? <div className={styles.dateColumn}> {column ? <div className={styles.dateColumn}>
{column.header.dateLabel && <label className={styles.dateLabel}>{column.header.dateLabel}</label>} {column.header.dateLabel && <label className={styles.dateLabel}>{column.header.dateLabel}</label>}
<label className={styles.dayLabel}>{column.header.weekdayLabel}</label> <label className={styles.dayLabel}>{column.header.weekdayLabel}</label>
<div <div
className={styles.times} className={styles.times}
data-border-left={x === 0 || columns.at(x - 1) === null} data-border-left={x === 0 || table.columns.at(x - 1) === null}
data-border-right={x === columns.length - 1 || columns.at(x + 1) === null} data-border-right={x === table.columns.length - 1 || table.columns.at(x + 1) === null}
> >
{column.cells.map((cell, y) => { {column.cells.map((cell, y) => {
if (y === column.cells.length - 1) return null if (y === column.cells.length - 1) return null
@ -132,7 +120,7 @@ const AvailabilityEditor = ({
} }
} }
setSelecting(found.flatMap(d => { setSelecting(found.flatMap(d => {
const serialized = columns[d.x]?.cells[d.y]?.serialized const serialized = table.columns[d.x]?.cells[d.y]?.serialized
if (serialized && times.includes(serialized)) { if (serialized && times.includes(serialized)) {
return [serialized] return [serialized]
} }
@ -144,7 +132,7 @@ const AvailabilityEditor = ({
})} })}
</div> </div>
</div> : <div className={styles.columnSpacer} />} </div> : <div className={styles.columnSpacer} />}
</Fragment>)} </Fragment>) ?? <Skeleton isSpecificDates={times[0].length === 13} />}
</div> </div>
</div> </div>
</div> </div>

View file

@ -13,17 +13,17 @@ import useSettingsStore from '/src/stores/settingsStore'
import { calculateAvailability, calculateTable, makeClass, relativeTimeFormat } from '/src/utils' import { calculateAvailability, calculateTable, makeClass, relativeTimeFormat } from '/src/utils'
import styles from './AvailabilityViewer.module.scss' import styles from './AvailabilityViewer.module.scss'
import Skeleton from './components/Skeleton/Skeleton'
interface AvailabilityViewerProps { interface AvailabilityViewerProps {
times: string[] times: string[]
timezone: string
people: PersonResponse[] people: PersonResponse[]
table?: ReturnType<typeof calculateTable>
} }
const AvailabilityViewer = ({ times, timezone, people }: AvailabilityViewerProps) => { const AvailabilityViewer = ({ times, people, table }: AvailabilityViewerProps) => {
const { t, i18n } = useTranslation('event') const { t, i18n } = useTranslation('event')
const timeFormat = useStore(useSettingsStore, state => state.timeFormat) ?? '12h'
const highlight = useStore(useSettingsStore, state => state.highlight) const highlight = useStore(useSettingsStore, state => state.highlight)
const [filteredPeople, setFilteredPeople] = useState(people.map(p => p.name)) const [filteredPeople, setFilteredPeople] = useState(people.map(p => p.name))
const [tempFocus, setTempFocus] = useState<string>() const [tempFocus, setTempFocus] = useState<string>()
@ -38,11 +38,6 @@ const AvailabilityViewer = ({ times, timezone, people }: AvailabilityViewerProps
people: string[] people: string[]
}>() }>()
// Calculate table
const { rows, columns } = useMemo(() =>
calculateTable(times, i18n.language, timeFormat, timezone),
[times, i18n.language, timeFormat, timezone])
// Calculate availabilities // Calculate availabilities
const { availabilities, min, max } = useMemo(() => const { availabilities, min, max } = useMemo(() =>
calculateAvailability(times, people.filter(p => filteredPeople.includes(p.name))), calculateAvailability(times, people.filter(p => filteredPeople.includes(p.name))),
@ -56,15 +51,15 @@ const AvailabilityViewer = ({ times, timezone, people }: AvailabilityViewerProps
setFilteredPeople(people.map(p => p.name)) setFilteredPeople(people.map(p => p.name))
}, [people.length]) }, [people.length])
const heatmap = useMemo(() => columns.map((column, x) => <Fragment key={x}> const heatmap = useMemo(() => table?.columns.map((column, x) => <Fragment key={x}>
{column ? <div className={styles.dateColumn}> {column ? <div className={styles.dateColumn}>
{column.header.dateLabel && <label className={styles.dateLabel}>{column.header.dateLabel}</label>} {column.header.dateLabel && <label className={styles.dateLabel}>{column.header.dateLabel}</label>}
<label className={styles.dayLabel}>{column.header.weekdayLabel}</label> <label className={styles.dayLabel}>{column.header.weekdayLabel}</label>
<div <div
className={styles.times} className={styles.times}
data-border-left={x === 0 || columns.at(x - 1) === null} data-border-left={x === 0 || table.columns.at(x - 1) === null}
data-border-right={x === columns.length - 1 || columns.at(x + 1) === null} data-border-right={x === table.columns.length - 1 || table.columns.at(x + 1) === null}
> >
{column.cells.map((cell, y) => { {column.cells.map((cell, y) => {
if (y === column.cells.length - 1) return null if (y === column.cells.length - 1) return null
@ -110,9 +105,9 @@ const AvailabilityViewer = ({ times, timezone, people }: AvailabilityViewerProps
})} })}
</div> </div>
</div> : <div className={styles.columnSpacer} />} </div> : <div className={styles.columnSpacer} />}
</Fragment>), [ </Fragment>) ?? <Skeleton isSpecificDates={times[0].length === 13} />, [
availabilities, availabilities,
columns, table?.columns,
highlight, highlight,
max, max,
min, min,
@ -167,14 +162,14 @@ const AvailabilityViewer = ({ times, timezone, people }: AvailabilityViewerProps
<div> <div>
<div className={styles.heatmap}> <div className={styles.heatmap}>
{useMemo(() => <div className={styles.timeLabels}> {useMemo(() => <div className={styles.timeLabels}>
{rows.map((row, i) => {table?.rows.map((row, i) =>
<div className={styles.timeSpace} key={i}> <div className={styles.timeSpace} key={i}>
{row && <label className={styles.timeLabel}> {row && <label className={styles.timeLabel}>
{row.label} {row.label}
</label>} </label>}
</div> </div>
)} ) ?? null}
</div>, [rows])} </div>, [table?.rows])}
{heatmap} {heatmap}
</div> </div>

View file

@ -0,0 +1,34 @@
.skeleton {
opacity: .5;
& > div:last-of-type {
height: 382px;
width: 300px;
border: 2px solid currentColor;
border-radius: 3px;
margin-block: 2px 10px;
position: relative;
}
}
.dayLabels {
display: flex;
justify-content: space-around;
span {
height: .9em;
display: block;
width: 3ch;
background: currentColor;
border-radius: .2em;
}
}
.dateLabels {
font-size: 12px;
margin-block-end: 3px;
span {
width: 5ch;
}
}

View file

@ -0,0 +1,15 @@
import { makeClass } from '/src/utils'
import styles from './Skeleton.module.scss'
interface SkeletonProps {
isSpecificDates?: boolean
}
const Skeleton = ({ isSpecificDates }: SkeletonProps) => <div className={styles.skeleton}>
{isSpecificDates ? <div className={makeClass(styles.dayLabels, styles.dateLabels)}>{Array.from({ length: 5 }).map((_, i) => <span key={i} />)}</div> : null}
<div className={styles.dayLabels}>{Array.from({ length: 5 }).map((_, i) => <span key={i} />)}</div>
<div />
</div>
export default Skeleton

View file

@ -108,7 +108,7 @@ const Handle = ({ value, onChange, labelPadding }: HandleProps) => {
left: `calc(${value * 4.166}% - 11px)`, left: `calc(${value * 4.166}% - 11px)`,
'--extra-padding': labelPadding, '--extra-padding': labelPadding,
} as React.CSSProperties} } as React.CSSProperties}
data-label={Temporal.PlainTime.from({ hour: Number(times[value] === '24' ? '00' : times[value]) }).toLocaleString(i18n.language, { hour: 'numeric', hour12: timeFormat === '12h' })} data-label={Temporal.PlainTime.from({ hour: Number(times[value] === '24' ? '00' : times[value]) }).toLocaleString(i18n.language, { hour: 'numeric', hourCycle: timeFormat === '12h' ? 'h12' : 'h24' })}
onMouseDown={() => { onMouseDown={() => {
document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mousemove', handleMouseMove)
isMoving.current = true isMoving.current = true

View file

@ -3,16 +3,24 @@ import { calculateRows } from '/src/utils/calculateRows'
import { convertTimesToDates } from '/src/utils/convertTimesToDates' import { convertTimesToDates } from '/src/utils/convertTimesToDates'
import { serializeTime } from '/src/utils/serializeTime' import { serializeTime } from '/src/utils/serializeTime'
export interface CalculateTableArgs {
/** As `HHmm-DDMMYYYY` or `HHmm-d` strings */
times: string[]
locale: string
timeFormat: '12h' | '24h'
timezone: string
}
/** /**
* Take rows and columns and turn them into a data structure representing an availability table * Take rows and columns and turn them into a data structure representing an availability table
*/ */
export const calculateTable = ( export const calculateTable = ({
/** As `HHmm-DDMMYYYY` or `HHmm-d` strings */ /** As `HHmm-DDMMYYYY` or `HHmm-d` strings */
times: string[], times,
locale: string, locale,
timeFormat: '12h' | '24h', timeFormat,
timezone: string, timezone,
) => { }: CalculateTableArgs) => {
const dates = convertTimesToDates(times, timezone) const dates = convertTimesToDates(times, timezone)
const rows = calculateRows(dates) const rows = calculateRows(dates)
const columns = calculateColumns(dates) const columns = calculateColumns(dates)
@ -22,7 +30,7 @@ export const calculateTable = (
return { return {
rows: rows.map(row => row && row.minute === 0 ? { rows: rows.map(row => row && row.minute === 0 ? {
label: row.toLocaleString(locale, { hour: 'numeric', hour12: timeFormat === '12h' }), label: row.toLocaleString(locale, { hour: 'numeric', hourCycle: timeFormat === '12h' ? 'h12' : 'h24' }),
string: row.toString(), string: row.toString(),
} : null), } : null),
@ -44,8 +52,8 @@ export const calculateTable = (
serialized, serialized,
minute: date.minute, minute: date.minute,
label: isSpecificDates label: isSpecificDates
? date.toLocaleString(locale, { dateStyle: 'long', timeStyle: 'short', hour12: timeFormat === '12h' }) ? date.toLocaleString(locale, { dateStyle: 'long', timeStyle: 'short', hourCycle: timeFormat === '12h' ? 'h12' : 'h24' })
: `${date.toLocaleString(locale, { timeStyle: 'short', hour12: timeFormat === '12h' })}, ${date.toLocaleString(locale, { weekday: 'long' })}`, : `${date.toLocaleString(locale, { timeStyle: 'short', hourCycle: timeFormat === '12h' ? 'h12' : 'h24' })}, ${date.toLocaleString(locale, { weekday: 'long' })}`,
} }
}) })
} : null) } : null)

View file

@ -39,10 +39,7 @@ const parseWeekdayDate = (str: string): Temporal.ZonedDateTime => {
// Extract values // Extract values
const [hour, minute] = [Number(str.substring(0, 2)), Number(str.substring(2, 4))] const [hour, minute] = [Number(str.substring(0, 2)), Number(str.substring(2, 4))]
let dayOfWeek = Number(str.substring(5)) const dayOfWeek = Number(str.substring(5))
if (dayOfWeek === 0) {
dayOfWeek = 7 // Sunday is 7 in ISO8601
}
// Construct PlainDateTime from today // Construct PlainDateTime from today
const today = Temporal.Now.zonedDateTimeISO('UTC').round('day') const today = Temporal.Now.zonedDateTimeISO('UTC').round('day')

View file

@ -0,0 +1,5 @@
import { calculateTable, CalculateTableArgs } from '/src/utils'
self.onmessage = (e: MessageEvent<CalculateTableArgs>) => {
self.postMessage(calculateTable(e.data))
}