Merge pull request #272 from GRA0007/feat/web-worker
Web worker for heatmap table calculation
This commit is contained in:
commit
08f6646339
|
|
@ -12,8 +12,10 @@ import SelectField from '/src/components/SelectField/SelectField'
|
|||
import { EventResponse, getPeople, PersonResponse, updatePerson } from '/src/config/api'
|
||||
import { useTranslation } from '/src/i18n/client'
|
||||
import timezones from '/src/res/timezones.json'
|
||||
import { useStore } from '/src/stores'
|
||||
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'
|
||||
|
||||
|
|
@ -25,6 +27,8 @@ interface EventAvailabilitiesProps {
|
|||
const EventAvailabilities = ({ event, ...data }: EventAvailabilitiesProps) => {
|
||||
const { t, i18n } = useTranslation('event')
|
||||
|
||||
const timeFormat = useStore(useSettingsStore, state => state.timeFormat) ?? '12h'
|
||||
|
||||
const [people, setPeople] = useState(data.people)
|
||||
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 [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
|
||||
const addRecent = useRecentsStore(state => state.addRecent)
|
||||
useEffect(() => {
|
||||
|
|
@ -138,7 +164,7 @@ const EventAvailabilities = ({ event, ...data }: EventAvailabilitiesProps) => {
|
|||
{tab === 'group' ? <AvailabilityViewer
|
||||
times={expandedTimes}
|
||||
people={people}
|
||||
timezone={timezone}
|
||||
table={table}
|
||||
/> : user && <AvailabilityEditor
|
||||
times={expandedTimes}
|
||||
timezone={timezone}
|
||||
|
|
@ -152,6 +178,7 @@ const EventAvailabilities = ({ event, ...data }: EventAvailabilitiesProps) => {
|
|||
setUser({ ...user, availability: oldAvailability })
|
||||
})
|
||||
}}
|
||||
table={table}
|
||||
/>}
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import Section from '/src/components/Section/Section'
|
|||
import TimeRangeField from '/src/components/TimeRangeField/TimeRangeField'
|
||||
import Video from '/src/components/Video/Video'
|
||||
import { useTranslation } from '/src/i18n/server'
|
||||
import { getWeekdayNames } from '/src/utils'
|
||||
import { calculateTable, getWeekdayNames } from '/src/utils'
|
||||
|
||||
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 { t, i18n } = await useTranslation(['common', 'help'])
|
||||
|
||||
const table = calculateTable({ times, locale: i18n.language, timezone: 'UTC', timeFormat: '12h' })
|
||||
|
||||
return <>
|
||||
<Content>
|
||||
<Header />
|
||||
|
|
@ -53,9 +57,9 @@ const Page = async () => {
|
|||
<P>{t('help:p6')}</P>
|
||||
<P>{t('help:p7')}</P>
|
||||
<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'] }]}
|
||||
timezone="UTC"
|
||||
table={table}
|
||||
/>
|
||||
|
||||
<h2 className={styles.step}>{t('help:s3')}</h2>
|
||||
|
|
@ -63,7 +67,7 @@ const Page = async () => {
|
|||
<P>{t('help:p9')}</P>
|
||||
<P>{t('help:p10')}</P>
|
||||
<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'] },
|
||||
{ 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: '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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 GoogleCalendar from '/src/components/GoogleCalendar/GoogleCalendar'
|
||||
import { usePalette } from '/src/hooks/usePalette'
|
||||
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 styles from '../AvailabilityViewer/AvailabilityViewer.module.scss'
|
||||
import Skeleton from '../AvailabilityViewer/components/Skeleton/Skeleton'
|
||||
|
||||
interface AvailabilityEditorProps {
|
||||
times: string[]
|
||||
timezone: string
|
||||
value: string[]
|
||||
onChange: (value: string[]) => void
|
||||
table?: ReturnType<typeof calculateTable>
|
||||
}
|
||||
|
||||
const AvailabilityEditor = ({
|
||||
times,
|
||||
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])
|
||||
const AvailabilityEditor = ({ times, timezone, value = [], onChange, table }: AvailabilityEditorProps) => {
|
||||
const { t } = useTranslation('event')
|
||||
|
||||
// Ref and state required to rerender but also access static version in callbacks
|
||||
const selectingRef = useRef<string[]>([])
|
||||
|
|
@ -64,24 +52,24 @@ const AvailabilityEditor = ({
|
|||
<div>
|
||||
<div className={styles.heatmap}>
|
||||
<div className={styles.timeLabels}>
|
||||
{rows.map((row, i) =>
|
||||
{table?.rows.map((row, i) =>
|
||||
<div className={styles.timeSpace} key={i}>
|
||||
{row && <label className={styles.timeLabel}>
|
||||
{row.label}
|
||||
</label>}
|
||||
</div>
|
||||
)}
|
||||
) ?? null}
|
||||
</div>
|
||||
|
||||
{columns.map((column, x) => <Fragment key={x}>
|
||||
{table?.columns.map((column, x) => <Fragment key={x}>
|
||||
{column ? <div className={styles.dateColumn}>
|
||||
{column.header.dateLabel && <label className={styles.dateLabel}>{column.header.dateLabel}</label>}
|
||||
<label className={styles.dayLabel}>{column.header.weekdayLabel}</label>
|
||||
|
||||
<div
|
||||
className={styles.times}
|
||||
data-border-left={x === 0 || columns.at(x - 1) === null}
|
||||
data-border-right={x === columns.length - 1 || columns.at(x + 1) === null}
|
||||
data-border-left={x === 0 || table.columns.at(x - 1) === null}
|
||||
data-border-right={x === table.columns.length - 1 || table.columns.at(x + 1) === null}
|
||||
>
|
||||
{column.cells.map((cell, y) => {
|
||||
if (y === column.cells.length - 1) return null
|
||||
|
|
@ -132,7 +120,7 @@ const AvailabilityEditor = ({
|
|||
}
|
||||
}
|
||||
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)) {
|
||||
return [serialized]
|
||||
}
|
||||
|
|
@ -144,7 +132,7 @@ const AvailabilityEditor = ({
|
|||
})}
|
||||
</div>
|
||||
</div> : <div className={styles.columnSpacer} />}
|
||||
</Fragment>)}
|
||||
</Fragment>) ?? <Skeleton isSpecificDates={times[0].length === 13} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,17 +13,17 @@ import useSettingsStore from '/src/stores/settingsStore'
|
|||
import { calculateAvailability, calculateTable, makeClass, relativeTimeFormat } from '/src/utils'
|
||||
|
||||
import styles from './AvailabilityViewer.module.scss'
|
||||
import Skeleton from './components/Skeleton/Skeleton'
|
||||
|
||||
interface AvailabilityViewerProps {
|
||||
times: string[]
|
||||
timezone: string
|
||||
people: PersonResponse[]
|
||||
table?: ReturnType<typeof calculateTable>
|
||||
}
|
||||
|
||||
const AvailabilityViewer = ({ times, timezone, people }: AvailabilityViewerProps) => {
|
||||
const AvailabilityViewer = ({ times, people, table }: AvailabilityViewerProps) => {
|
||||
const { t, i18n } = useTranslation('event')
|
||||
|
||||
const timeFormat = useStore(useSettingsStore, state => state.timeFormat) ?? '12h'
|
||||
const highlight = useStore(useSettingsStore, state => state.highlight)
|
||||
const [filteredPeople, setFilteredPeople] = useState(people.map(p => p.name))
|
||||
const [tempFocus, setTempFocus] = useState<string>()
|
||||
|
|
@ -38,11 +38,6 @@ const AvailabilityViewer = ({ times, timezone, people }: AvailabilityViewerProps
|
|||
people: string[]
|
||||
}>()
|
||||
|
||||
// Calculate table
|
||||
const { rows, columns } = useMemo(() =>
|
||||
calculateTable(times, i18n.language, timeFormat, timezone),
|
||||
[times, i18n.language, timeFormat, timezone])
|
||||
|
||||
// Calculate availabilities
|
||||
const { availabilities, min, max } = useMemo(() =>
|
||||
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))
|
||||
}, [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.header.dateLabel && <label className={styles.dateLabel}>{column.header.dateLabel}</label>}
|
||||
<label className={styles.dayLabel}>{column.header.weekdayLabel}</label>
|
||||
|
||||
<div
|
||||
className={styles.times}
|
||||
data-border-left={x === 0 || columns.at(x - 1) === null}
|
||||
data-border-right={x === columns.length - 1 || columns.at(x + 1) === null}
|
||||
data-border-left={x === 0 || table.columns.at(x - 1) === null}
|
||||
data-border-right={x === table.columns.length - 1 || table.columns.at(x + 1) === null}
|
||||
>
|
||||
{column.cells.map((cell, y) => {
|
||||
if (y === column.cells.length - 1) return null
|
||||
|
|
@ -110,9 +105,9 @@ const AvailabilityViewer = ({ times, timezone, people }: AvailabilityViewerProps
|
|||
})}
|
||||
</div>
|
||||
</div> : <div className={styles.columnSpacer} />}
|
||||
</Fragment>), [
|
||||
</Fragment>) ?? <Skeleton isSpecificDates={times[0].length === 13} />, [
|
||||
availabilities,
|
||||
columns,
|
||||
table?.columns,
|
||||
highlight,
|
||||
max,
|
||||
min,
|
||||
|
|
@ -167,14 +162,14 @@ const AvailabilityViewer = ({ times, timezone, people }: AvailabilityViewerProps
|
|||
<div>
|
||||
<div className={styles.heatmap}>
|
||||
{useMemo(() => <div className={styles.timeLabels}>
|
||||
{rows.map((row, i) =>
|
||||
{table?.rows.map((row, i) =>
|
||||
<div className={styles.timeSpace} key={i}>
|
||||
{row && <label className={styles.timeLabel}>
|
||||
{row.label}
|
||||
</label>}
|
||||
</div>
|
||||
)}
|
||||
</div>, [rows])}
|
||||
) ?? null}
|
||||
</div>, [table?.rows])}
|
||||
|
||||
{heatmap}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -108,7 +108,7 @@ const Handle = ({ value, onChange, labelPadding }: HandleProps) => {
|
|||
left: `calc(${value * 4.166}% - 11px)`,
|
||||
'--extra-padding': labelPadding,
|
||||
} 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={() => {
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
isMoving.current = true
|
||||
|
|
|
|||
|
|
@ -3,16 +3,24 @@ import { calculateRows } from '/src/utils/calculateRows'
|
|||
import { convertTimesToDates } from '/src/utils/convertTimesToDates'
|
||||
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
|
||||
*/
|
||||
export const calculateTable = (
|
||||
export const calculateTable = ({
|
||||
/** As `HHmm-DDMMYYYY` or `HHmm-d` strings */
|
||||
times: string[],
|
||||
locale: string,
|
||||
timeFormat: '12h' | '24h',
|
||||
timezone: string,
|
||||
) => {
|
||||
times,
|
||||
locale,
|
||||
timeFormat,
|
||||
timezone,
|
||||
}: CalculateTableArgs) => {
|
||||
const dates = convertTimesToDates(times, timezone)
|
||||
const rows = calculateRows(dates)
|
||||
const columns = calculateColumns(dates)
|
||||
|
|
@ -22,7 +30,7 @@ export const calculateTable = (
|
|||
|
||||
return {
|
||||
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(),
|
||||
} : null),
|
||||
|
||||
|
|
@ -44,8 +52,8 @@ export const calculateTable = (
|
|||
serialized,
|
||||
minute: date.minute,
|
||||
label: isSpecificDates
|
||||
? date.toLocaleString(locale, { dateStyle: 'long', timeStyle: 'short', hour12: timeFormat === '12h' })
|
||||
: `${date.toLocaleString(locale, { timeStyle: 'short', hour12: timeFormat === '12h' })}, ${date.toLocaleString(locale, { weekday: 'long' })}`,
|
||||
? date.toLocaleString(locale, { dateStyle: 'long', timeStyle: 'short', hourCycle: timeFormat === '12h' ? 'h12' : 'h24' })
|
||||
: `${date.toLocaleString(locale, { timeStyle: 'short', hourCycle: timeFormat === '12h' ? 'h12' : 'h24' })}, ${date.toLocaleString(locale, { weekday: 'long' })}`,
|
||||
}
|
||||
})
|
||||
} : null)
|
||||
|
|
|
|||
|
|
@ -39,10 +39,7 @@ const parseWeekdayDate = (str: string): Temporal.ZonedDateTime => {
|
|||
|
||||
// Extract values
|
||||
const [hour, minute] = [Number(str.substring(0, 2)), Number(str.substring(2, 4))]
|
||||
let dayOfWeek = Number(str.substring(5))
|
||||
if (dayOfWeek === 0) {
|
||||
dayOfWeek = 7 // Sunday is 7 in ISO8601
|
||||
}
|
||||
const dayOfWeek = Number(str.substring(5))
|
||||
|
||||
// Construct PlainDateTime from today
|
||||
const today = Temporal.Now.zonedDateTimeISO('UTC').round('day')
|
||||
|
|
|
|||
5
frontend/src/workers/calculateTable.ts
Normal file
5
frontend/src/workers/calculateTable.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { calculateTable, CalculateTableArgs } from '/src/utils'
|
||||
|
||||
self.onmessage = (e: MessageEvent<CalculateTableArgs>) => {
|
||||
self.postMessage(calculateTable(e.data))
|
||||
}
|
||||
Loading…
Reference in a new issue