Wrap event page parts in suspense
This commit is contained in:
parent
1855188ea1
commit
8d9a8cf868
|
|
@ -1,6 +1,6 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Trans } from 'react-i18next/TransWithoutContext'
|
import { Trans } from 'react-i18next/TransWithoutContext'
|
||||||
|
|
||||||
import AvailabilityEditor from '/src/components/AvailabilityEditor/AvailabilityEditor'
|
import AvailabilityEditor from '/src/components/AvailabilityEditor/AvailabilityEditor'
|
||||||
|
|
@ -20,17 +20,16 @@ import { calculateTable, expandTimes, makeClass } from '/src/utils'
|
||||||
import styles from './page.module.scss'
|
import styles from './page.module.scss'
|
||||||
|
|
||||||
interface EventAvailabilitiesProps {
|
interface EventAvailabilitiesProps {
|
||||||
event: EventResponse
|
event?: EventResponse
|
||||||
people: PersonResponse[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EventAvailabilities = ({ event, ...data }: EventAvailabilitiesProps) => {
|
const EventAvailabilities = ({ event }: EventAvailabilitiesProps) => {
|
||||||
const { t, i18n } = useTranslation('event')
|
const { t, i18n } = useTranslation('event')
|
||||||
|
|
||||||
const timeFormat = useStore(useSettingsStore, state => state.timeFormat) ?? '12h'
|
const timeFormat = useStore(useSettingsStore, state => state.timeFormat) ?? '12h'
|
||||||
|
|
||||||
const [people, setPeople] = useState(data.people)
|
const [people, setPeople] = useState<PersonResponse[]>([])
|
||||||
const expandedTimes = useMemo(() => expandTimes(event.times), [event.times])
|
const expandedTimes = useMemo(() => expandTimes(event?.times ?? []), [event?.times])
|
||||||
|
|
||||||
const [user, setUser] = useState<PersonResponse>()
|
const [user, setUser] = useState<PersonResponse>()
|
||||||
const [password, setPassword] = useState<string>()
|
const [password, setPassword] = useState<string>()
|
||||||
|
|
@ -39,40 +38,40 @@ const EventAvailabilities = ({ event, ...data }: EventAvailabilitiesProps) => {
|
||||||
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone)
|
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone)
|
||||||
|
|
||||||
// Web worker for calculating the heatmap table
|
// 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, [])
|
const tableWorker = useRef<Worker>()
|
||||||
|
|
||||||
// Calculate table (using a web worker if available)
|
// Calculate table (using a web worker if available)
|
||||||
const [table, setTable] = useState<ReturnType<typeof calculateTable>>()
|
const [table, setTable] = useState<ReturnType<typeof calculateTable>>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!tableWorker.current) {
|
||||||
|
tableWorker.current = window.Worker ? new Worker(new URL('/src/workers/calculateTable', import.meta.url)) : undefined
|
||||||
|
}
|
||||||
const args = { times: expandedTimes, locale: i18n.language, timeFormat, timezone }
|
const args = { times: expandedTimes, locale: i18n.language, timeFormat, timezone }
|
||||||
if (tableWorker) {
|
if (tableWorker.current) {
|
||||||
tableWorker.postMessage(args)
|
tableWorker.current.onmessage = (e: MessageEvent<ReturnType<typeof calculateTable>>) => setTable(e.data)
|
||||||
|
tableWorker.current.postMessage(args)
|
||||||
setTable(undefined)
|
setTable(undefined)
|
||||||
} else {
|
} else {
|
||||||
setTable(calculateTable(args))
|
setTable(calculateTable(args))
|
||||||
}
|
}
|
||||||
}, [tableWorker, expandedTimes, i18n.language, timeFormat, timezone])
|
}, [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(() => {
|
||||||
|
if (event) {
|
||||||
addRecent({
|
addRecent({
|
||||||
id: event.id,
|
id: event.id,
|
||||||
name: event.name,
|
name: event.name,
|
||||||
created_at: event.created_at,
|
created_at: event.created_at,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}, [addRecent])
|
}, [addRecent])
|
||||||
|
|
||||||
// Refetch availabilities
|
// Refetch availabilities
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tab === 'group') {
|
if (tab === 'group' && event) {
|
||||||
getPeople(event.id)
|
getPeople(event.id)
|
||||||
.then(setPeople)
|
.then(setPeople)
|
||||||
.catch(console.warn)
|
.catch(console.warn)
|
||||||
|
|
@ -82,7 +81,7 @@ const EventAvailabilities = ({ event, ...data }: EventAvailabilitiesProps) => {
|
||||||
return <>
|
return <>
|
||||||
<Section id="login">
|
<Section id="login">
|
||||||
<Content>
|
<Content>
|
||||||
<Login eventId={event.id} user={user} onChange={(u, p) => {
|
<Login eventId={event?.id} user={user} onChange={(u, p) => {
|
||||||
setUser(u)
|
setUser(u)
|
||||||
setPassword(p)
|
setPassword(p)
|
||||||
setTab(u ? 'you' : 'group')
|
setTab(u ? 'you' : 'group')
|
||||||
|
|
@ -148,7 +147,7 @@ const EventAvailabilities = ({ event, ...data }: EventAvailabilitiesProps) => {
|
||||||
document.dispatchEvent(new CustomEvent('focusName'))
|
document.dispatchEvent(new CustomEvent('focusName'))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title={user ? '' : t<string>('tabs.you_tooltip')}
|
title={user ? '' : t('tabs.you_tooltip')}
|
||||||
>{t('tabs.you')}</button>
|
>{t('tabs.you')}</button>
|
||||||
<button
|
<button
|
||||||
className={makeClass(
|
className={makeClass(
|
||||||
|
|
@ -170,6 +169,7 @@ const EventAvailabilities = ({ event, ...data }: EventAvailabilitiesProps) => {
|
||||||
timezone={timezone}
|
timezone={timezone}
|
||||||
value={user.availability}
|
value={user.availability}
|
||||||
onChange={availability => {
|
onChange={availability => {
|
||||||
|
if (!event) return
|
||||||
const oldAvailability = [...user.availability]
|
const oldAvailability = [...user.availability]
|
||||||
setUser({ ...user, availability })
|
setUser({ ...user, availability })
|
||||||
updatePerson(event.id, user.name, { availability }, password)
|
updatePerson(event.id, user.name, { availability }, password)
|
||||||
|
|
|
||||||
|
|
@ -69,3 +69,12 @@
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bone {
|
||||||
|
width: 12em;
|
||||||
|
max-width: 100%;
|
||||||
|
background-color: var(--loading);
|
||||||
|
border-radius: 3px;
|
||||||
|
height: 1em;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Suspense } from 'react'
|
||||||
import { Trans } from 'react-i18next/TransWithoutContext'
|
import { Trans } from 'react-i18next/TransWithoutContext'
|
||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
import { notFound } from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
|
|
@ -5,7 +6,7 @@ import { Temporal } from '@js-temporal/polyfill'
|
||||||
|
|
||||||
import Content from '/src/components/Content/Content'
|
import Content from '/src/components/Content/Content'
|
||||||
import Copyable from '/src/components/Copyable/Copyable'
|
import Copyable from '/src/components/Copyable/Copyable'
|
||||||
import { getEvent, getPeople } from '/src/config/api'
|
import { EventResponse, getEvent } from '/src/config/api'
|
||||||
import { useTranslation } from '/src/i18n/server'
|
import { useTranslation } from '/src/i18n/server'
|
||||||
import { makeClass, relativeTimeFormat } from '/src/utils'
|
import { makeClass, relativeTimeFormat } from '/src/utils'
|
||||||
|
|
||||||
|
|
@ -26,14 +27,31 @@ export const generateMetadata = async ({ params }: PageProps): Promise<Metadata>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Page = async ({ params }: PageProps) => {
|
const Page = async ({ params }: PageProps) => {
|
||||||
const event = await getEvent(params.id).catch(() => undefined)
|
const event = getEvent(params.id)
|
||||||
const people = await getPeople(params.id).catch(() => undefined)
|
|
||||||
if (!event || !people) notFound()
|
return <>
|
||||||
|
<Suspense
|
||||||
|
fallback={<Content>
|
||||||
|
<h1 className={styles.name}><span className={styles.bone} /></h1>
|
||||||
|
<div className={styles.date}><span className={styles.bone} /></div>
|
||||||
|
<div className={styles.info}><span className={styles.bone} style={{ width: '20em' }} /></div>
|
||||||
|
<div className={styles.info}><span className={styles.bone} style={{ width: '20em' }} /></div>
|
||||||
|
</Content>}
|
||||||
|
>
|
||||||
|
<EventMeta event={event} />
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
|
<EventAvailabilities event={await event.catch(() => undefined)} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventMeta = async (props: { event: Promise<EventResponse> }) => {
|
||||||
|
const event = await props.event.catch(() => undefined)
|
||||||
|
if (!event) notFound()
|
||||||
|
|
||||||
const { t, i18n } = await useTranslation(['common', 'event'])
|
const { t, i18n } = await useTranslation(['common', 'event'])
|
||||||
|
|
||||||
return <>
|
return <Content>
|
||||||
<Content>
|
|
||||||
<h1 className={styles.name}>{event.name}</h1>
|
<h1 className={styles.name}>{event.name}</h1>
|
||||||
<span
|
<span
|
||||||
className={styles.date}
|
className={styles.date}
|
||||||
|
|
@ -44,12 +62,9 @@ const Page = async ({ params }: PageProps) => {
|
||||||
{`https://crab.fit/${event.id}`}
|
{`https://crab.fit/${event.id}`}
|
||||||
</Copyable>
|
</Copyable>
|
||||||
<p className={makeClass(styles.info, styles.noPrint)}>
|
<p className={makeClass(styles.info, styles.noPrint)}>
|
||||||
<Trans i18nKey="event:nav.shareinfo" t={t} i18n={i18n}>_<a href={`mailto:?subject=${encodeURIComponent(t<string>('event:nav.email_subject', { event_name: event.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${event.id}`)}`}>_</a>_</Trans>
|
<Trans i18nKey="event:nav.shareinfo" t={t} i18n={i18n}>_<a href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: event.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${event.id}`)}`}>_</a>_</Trans>
|
||||||
</p>
|
</p>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
<EventAvailabilities event={event} people={people} />
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page
|
export default Page
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ const AvailabilityEditor = ({ times, timezone, value = [], onChange, table }: Av
|
||||||
<Button isSmall onClick={selectInvert} title="Ctrl + I (⌘ I)">{t('you.select_invert')}</Button>
|
<Button isSmall onClick={selectInvert} title="Ctrl + I (⌘ I)">{t('you.select_invert')}</Button>
|
||||||
</div>
|
</div>
|
||||||
</Content>
|
</Content>
|
||||||
{times[0].length === 13 && <Content>
|
{times[0]?.length === 13 && <Content>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'center', gap: 12 }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'center', gap: 12 }}>
|
||||||
<GoogleCalendar
|
<GoogleCalendar
|
||||||
timezone={timezone}
|
timezone={timezone}
|
||||||
|
|
@ -106,7 +106,7 @@ const AvailabilityEditor = ({ times, timezone, value = [], onChange, table }: Av
|
||||||
if (!cell) return <div
|
if (!cell) return <div
|
||||||
className={makeClass(viewerStyles.timeSpace, viewerStyles.grey)}
|
className={makeClass(viewerStyles.timeSpace, viewerStyles.grey)}
|
||||||
key={y}
|
key={y}
|
||||||
title={t<string>('greyed_times')}
|
title={t('greyed_times')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
const isSelected = (
|
const isSelected = (
|
||||||
|
|
@ -162,7 +162,7 @@ const AvailabilityEditor = ({ times, timezone, value = [], onChange, table }: Av
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div> : <div className={viewerStyles.columnSpacer} />}
|
</div> : <div className={viewerStyles.columnSpacer} />}
|
||||||
</Fragment>) ?? <Skeleton isSpecificDates={times[0].length === 13} />}
|
</Fragment>) ?? <Skeleton isSpecificDates={times[0]?.length === 13} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ const AvailabilityViewer = ({ times, people, table }: AvailabilityViewerProps) =
|
||||||
if (!cell) return <div
|
if (!cell) return <div
|
||||||
className={makeClass(styles.timeSpace, styles.grey)}
|
className={makeClass(styles.timeSpace, styles.grey)}
|
||||||
key={y}
|
key={y}
|
||||||
title={t<string>('greyed_times')}
|
title={t('greyed_times')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
let peopleHere = availabilities.find(a => a.date === cell.serialized)?.people ?? []
|
let peopleHere = availabilities.find(a => a.date === cell.serialized)?.people ?? []
|
||||||
|
|
@ -106,7 +106,7 @@ const AvailabilityViewer = ({ times, people, table }: AvailabilityViewerProps) =
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div> : <div className={styles.columnSpacer} />}
|
</div> : <div className={styles.columnSpacer} />}
|
||||||
</Fragment>) ?? <Skeleton isSpecificDates={times[0].length === 13} />, [
|
</Fragment>) ?? <Skeleton isSpecificDates={times[0]?.length === 13} />, [
|
||||||
availabilities,
|
availabilities,
|
||||||
table?.columns,
|
table?.columns,
|
||||||
highlight,
|
highlight,
|
||||||
|
|
|
||||||
|
|
@ -55,13 +55,13 @@ const Month = ({ value, onChange }: MonthProps) => {
|
||||||
return <>
|
return <>
|
||||||
{useMemo(() => <div className={styles.header}>
|
{useMemo(() => <div className={styles.header}>
|
||||||
<Button
|
<Button
|
||||||
title={t<string>('form.dates.tooltips.previous')}
|
title={t('form.dates.tooltips.previous')}
|
||||||
onClick={() => setPage(page.subtract({ months: 1 }))}
|
onClick={() => setPage(page.subtract({ months: 1 }))}
|
||||||
icon={<ChevronLeft />}
|
icon={<ChevronLeft />}
|
||||||
/>
|
/>
|
||||||
<span>{page.toPlainDate({ day: 1 }).toLocaleString(i18n.language, { month: 'long', year: 'numeric' })}</span>
|
<span>{page.toPlainDate({ day: 1 }).toLocaleString(i18n.language, { month: 'long', year: 'numeric' })}</span>
|
||||||
<Button
|
<Button
|
||||||
title={t<string>('form.dates.tooltips.next')}
|
title={t('form.dates.tooltips.next')}
|
||||||
onClick={() => setPage(page.add({ months: 1 }))}
|
onClick={() => setPage(page.add({ months: 1 }))}
|
||||||
icon={<ChevronRight />}
|
icon={<ChevronRight />}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ const Weekdays = ({ value, onChange }: WeekdaysProps) => {
|
||||||
) && styles.selected,
|
) && styles.selected,
|
||||||
)}
|
)}
|
||||||
key={day.toString()}
|
key={day.toString()}
|
||||||
title={day.equals(Temporal.Now.plainDateISO()) ? t<string>('form.dates.tooltips.today') : undefined}
|
title={day.equals(Temporal.Now.plainDateISO()) ? t('form.dates.tooltips.today') : undefined}
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
if (e.key === ' ' || e.key === 'Enter') {
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
if (value.includes(day.dayOfWeek.toString())) {
|
if (value.includes(day.dayOfWeek.toString())) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { useTranslation } from '/src/i18n/client'
|
import { useTranslation } from '/src/i18n/client'
|
||||||
import { makeClass } from '/src/utils'
|
import { makeClass } from '/src/utils'
|
||||||
|
|
@ -16,6 +16,9 @@ const Copyable = ({ children, className, ...props }: CopyableProps) => {
|
||||||
|
|
||||||
const [copied, setCopied] = useState<React.ReactNode>()
|
const [copied, setCopied] = useState<React.ReactNode>()
|
||||||
|
|
||||||
|
const [canCopy, setCanCopy] = useState(false)
|
||||||
|
useEffect(() => { setCanCopy('clipboard' in navigator) }, [])
|
||||||
|
|
||||||
return <p
|
return <p
|
||||||
onClick={() => navigator.clipboard?.writeText(children)
|
onClick={() => navigator.clipboard?.writeText(children)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
@ -24,8 +27,8 @@ const Copyable = ({ children, className, ...props }: CopyableProps) => {
|
||||||
})
|
})
|
||||||
.catch(e => console.error('Failed to copy', e))
|
.catch(e => console.error('Failed to copy', e))
|
||||||
}
|
}
|
||||||
title={'clipboard' in navigator ? t<string>('nav.title') : undefined}
|
title={canCopy ? t('nav.title') : undefined}
|
||||||
className={makeClass(className, 'clipboard' in navigator && styles.copyable)}
|
className={makeClass(className, canCopy && styles.copyable)}
|
||||||
{...props}
|
{...props}
|
||||||
>{copied ?? children}</p>
|
>{copied ?? children}</p>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const EventInfo = ({ event }: EventInfoProps) => {
|
||||||
{`https://crab.fit/${event.id}`}
|
{`https://crab.fit/${event.id}`}
|
||||||
</Copyable>
|
</Copyable>
|
||||||
<p className={styles.info}>
|
<p className={styles.info}>
|
||||||
<Trans i18nKey="event:nav.shareinfo_alt" t={t} i18n={i18n}>_<a href={`mailto:?subject=${encodeURIComponent(t<string>('nav.email_subject', { event_name: event.name }))}&body=${encodeURIComponent(`${t('nav.email_body')} https://crab.fit/${event.id}`)}`} target="_blank">_</a>_</Trans>
|
<Trans i18nKey="event:nav.shareinfo_alt" t={t} i18n={i18n}>_<a href={`mailto:?subject=${encodeURIComponent(t('nav.email_subject', { event_name: event.name }))}&body=${encodeURIComponent(`${t('nav.email_body')} https://crab.fit/${event.id}`)}`} target="_blank">_</a>_</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,4 +39,10 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
border-radius: .2em;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid currentColor;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ const Footer = async ({ isSmall }: FooterProps) => {
|
||||||
<span>{t('donate.info')}</span>
|
<span>{t('donate.info')}</span>
|
||||||
<Button
|
<Button
|
||||||
isSmall
|
isSmall
|
||||||
title={t<string>('donate.title')}
|
title={t('donate.title')}
|
||||||
href="https://ko-fi.com/A06841WZ"
|
href="https://ko-fi.com/A06841WZ"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener payment"
|
rel="noreferrer noopener payment"
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ const Legend = ({ min, max, total, palette, onSegmentFocus }: LegendProps) => {
|
||||||
className={styles.bar}
|
className={styles.bar}
|
||||||
onMouseOut={() => onSegmentFocus(undefined)}
|
onMouseOut={() => onSegmentFocus(undefined)}
|
||||||
onClick={() => setHighlight?.(!highlight)}
|
onClick={() => setHighlight?.(!highlight)}
|
||||||
title={t<string>('group.legend_tooltip')}
|
title={t('group.legend_tooltip')}
|
||||||
>
|
>
|
||||||
{[...Array(max + 1 - min).keys()].map(i => i + min).map((i, j) =>
|
{[...Array(max + 1 - min).keys()].map(i => i + min).map((i, j) =>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,10 @@
|
||||||
grid-gap: 18px;
|
grid-gap: 18px;
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
@media (max-width: 400px) {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
||||||
& div:last-child {
|
& div:last-child {
|
||||||
--btn-width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const defaultValues = {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
eventId: string
|
eventId?: string
|
||||||
user: PersonResponse | undefined
|
user: PersonResponse | undefined
|
||||||
onChange: (user: PersonResponse | undefined, password?: string) => void
|
onChange: (user: PersonResponse | undefined, password?: string) => void
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +50,8 @@ const Login = ({ eventId, user, onChange }: LoginProps) => {
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!eventId) throw 'Event ID not set'
|
||||||
|
|
||||||
const resUser = await getPerson(eventId, username, password || undefined)
|
const resUser = await getPerson(eventId, username, password || undefined)
|
||||||
onChange(resUser, password || undefined)
|
onChange(resUser, password || undefined)
|
||||||
reset()
|
reset()
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ const Settings = () => {
|
||||||
type="button"
|
type="button"
|
||||||
className={makeClass(styles.openButton, isOpen && styles.open)}
|
className={makeClass(styles.openButton, isOpen && styles.open)}
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
title={t<string>('options.name')}
|
title={t('options.name')}
|
||||||
><SettingsIcon /></button>
|
><SettingsIcon /></button>
|
||||||
|
|
||||||
<dialog
|
<dialog
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const Video = () => {
|
||||||
width="560"
|
width="560"
|
||||||
height="315"
|
height="315"
|
||||||
src="https://www.youtube.com/embed/yXGd4VXZzcY?modestbranding=1&rel=0&autoplay=1"
|
src="https://www.youtube.com/embed/yXGd4VXZzcY?modestbranding=1&rel=0&autoplay=1"
|
||||||
title={t<string>('video.title')}
|
title={t('video.title')}
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
|
|
@ -34,7 +34,7 @@ const Video = () => {
|
||||||
setIsPlaying(true)
|
setIsPlaying(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img src={video_thumb.src} alt={t<string>('video.button')} />
|
<img src={video_thumb.src} alt={t('video.button')} />
|
||||||
<span>{t('video.button')}</span>
|
<span>{t('video.button')}</span>
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ interface AvailabilityInfo {
|
||||||
* group availability for each date passed in.
|
* group availability for each date passed in.
|
||||||
*/
|
*/
|
||||||
export const calculateAvailability = (dates: string[], people: Person[]): AvailabilityInfo => {
|
export const calculateAvailability = (dates: string[], people: Person[]): AvailabilityInfo => {
|
||||||
let min = Infinity
|
let min = 0
|
||||||
let max = -Infinity
|
let max = people.length
|
||||||
|
|
||||||
const availabilities: Availability[] = dates.map(date => {
|
const availabilities: Availability[] = dates.map(date => {
|
||||||
const names = people.flatMap(p => p.availability.some(d => d === date) ? [p.name] : [])
|
const names = people.flatMap(p => p.availability.some(d => d === date) ? [p.name] : [])
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export const calculateTable = ({
|
||||||
const columns = calculateColumns(dates)
|
const columns = calculateColumns(dates)
|
||||||
|
|
||||||
// Is specific dates or just days of the week
|
// Is specific dates or just days of the week
|
||||||
const isSpecificDates = times[0].length === 13
|
const isSpecificDates = times[0]?.length === 13
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows: rows.map(row => row && row.minute === 0 ? {
|
rows: rows.map(row => row && row.minute === 0 ? {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Temporal } from '@js-temporal/polyfill'
|
||||||
* @param timezone The target timezone
|
* @param timezone The target timezone
|
||||||
*/
|
*/
|
||||||
export const convertTimesToDates = (times: string[], timezone: string): Temporal.ZonedDateTime[] => {
|
export const convertTimesToDates = (times: string[], timezone: string): Temporal.ZonedDateTime[] => {
|
||||||
const isSpecificDates = times[0].length === 13
|
const isSpecificDates = times[0]?.length === 13
|
||||||
|
|
||||||
return times.map(time => isSpecificDates ?
|
return times.map(time => isSpecificDates ?
|
||||||
parseSpecificDate(time).withTimeZone(timezone)
|
parseSpecificDate(time).withTimeZone(timezone)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue