View event availabilities

This commit is contained in:
Ben Grant 2023-05-28 23:15:58 +10:00
parent cdea567bf3
commit 1a6d34ac59
17 changed files with 483 additions and 68 deletions

View file

@ -0,0 +1,118 @@
'use client'
import { useMemo, useState } from 'react'
import { Trans } from 'react-i18next/TransWithoutContext'
import AvailabilityViewer from '/src/components/AvailabilityViewer/AvailabilityViewer'
import Content from '/src/components/Content/Content'
import Login from '/src/components/Login/Login'
import Section from '/src/components/Section/Section'
import SelectField from '/src/components/SelectField/SelectField'
import { EventResponse, PersonResponse } from '/src/config/api'
import { useTranslation } from '/src/i18n/client'
import timezones from '/src/res/timezones.json'
import { expandTimes, makeClass } from '/src/utils'
import styles from './page.module.scss'
const EventAvailabilities = ({ event, people }: { event: EventResponse, people: PersonResponse[] }) => {
const { t, i18n } = useTranslation('event')
const expandedTimes = useMemo(() => expandTimes(event.times), [event.times])
const [user, setUser] = useState<PersonResponse>()
const [password, setPassword] = useState<string>()
const [tab, setTab] = useState<'group' | 'you'>('group')
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone)
return <>
<Section id="login">
<Content>
<Login eventId={event.id} user={user} onChange={(u, p) => {
setUser(u)
setPassword(p)
}} />
<SelectField
label={t('form.timezone')}
name="timezone"
id="timezone"
isInline
value={timezone}
onChange={event => setTimezone(event.currentTarget.value)}
options={timezones}
/>
{event?.timezone && event.timezone !== timezone && <p>
<Trans i18nKey="form.created_in_timezone" t={t} i18n={i18n}>
{/* eslint-disable-next-line */}
{/* @ts-ignore */}
_<strong>{{timezone: event.timezone}}</strong>
_<a href="#" onClick={e => {
e.preventDefault()
setTimezone(event.timezone)
}}>_</a>_
</Trans>
</p>}
{((
Intl.DateTimeFormat().resolvedOptions().timeZone !== timezone
&& (event?.timezone && event.timezone !== Intl.DateTimeFormat().resolvedOptions().timeZone)
) || (
event?.timezone === undefined
&& Intl.DateTimeFormat().resolvedOptions().timeZone !== timezone
)) && (
<p>
<Trans i18nKey="form.local_timezone" t={t} i18n={i18n}>
{/* eslint-disable-next-line */}
{/* @ts-ignore */}
_<strong>{{timezone: Intl.DateTimeFormat().resolvedOptions().timeZone}}</strong>
_<a href="#" onClick={e => {
e.preventDefault()
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone)
}}>_</a>_
</Trans>
</p>
)}
</Content>
</Section>
<Content>
<div className={styles.tabs}>
<button
className={makeClass(
styles.tab,
tab === 'you' && styles.tabSelected,
!user && styles.tabDisabled,
)}
type="button"
onClick={() => {
if (user) {
setTab('you')
} else {
document.dispatchEvent(new CustomEvent('focusName'))
}
}}
title={user ? '' : t<string>('tabs.you_tooltip')}
>{t('tabs.you')}</button>
<button
className={makeClass(
styles.tab,
tab === 'group' && styles.tabSelected,
)}
type="button"
onClick={() => setTab('group')}
>{t('tabs.group')}</button>
</div>
</Content>
{tab === 'group' && <AvailabilityViewer
times={expandedTimes}
people={people}
timezone={timezone}
/>}
</>
}
export default EventAvailabilities

View file

@ -0,0 +1,17 @@
import Content from '/src/components/Content/Content'
import Footer from '/src/components/Footer/Footer'
import Header from '/src/components/Header/Header'
const Layout = async ({ children }: { children: React.ReactNode }) => <>
<Content>
{/* @ts-expect-error Async Server Component */}
<Header />
</Content>
{children}
{/* @ts-expect-error Async Server Component */}
<Footer />
</>
export default Layout

View file

@ -0,0 +1,17 @@
import Content from '/src/components/Content/Content'
import { useTranslation } from '/src/i18n/server'
import styles from './page.module.scss'
const NotFound = async () => {
const { t } = await useTranslation('event')
return <Content>
<div style={{ marginBlock: 100 }}>
<h1 className={styles.name}>{t('error.title')}</h1>
<p className={styles.info}>{t('error.body')}</p>
</div>
</Content>
}
export default NotFound

View file

@ -0,0 +1,66 @@
.name {
text-align: center;
font-weight: 800;
margin: 20px 0 5px;
}
.info {
margin: 6px 0;
text-align: center;
font-size: 15px;
}
.noPrint {
@media print {
display: none;
}
}
.date {
display: block;
text-align: center;
font-size: 14px;
opacity: .8;
margin: 0 0 10px;
font-weight: 500;
letter-spacing: .01em;
@media print {
&::after {
content: ' - ' attr(title);
}
}
}
.tabs {
display: flex;
align-items: center;
justify-content: center;
margin: 30px 0 20px;
}
.tab {
user-select: none;
display: block;
color: var(--text);
padding: 8px 18px;
background-color: var(--surface);
border: 1px solid var(--primary);
border-bottom: 0;
margin: 0 4px;
font: inherit;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
cursor: pointer;
}
.tabSelected {
color: #FFF;
background-color: var(--primary);
border-color: var(--primary);
}
.tabDisabled {
opacity: .5;
cursor: not-allowed;
}

View file

@ -0,0 +1,41 @@
import { Trans } from 'react-i18next/TransWithoutContext'
import { notFound } from 'next/navigation'
import { Temporal } from '@js-temporal/polyfill'
import Content from '/src/components/Content/Content'
import Copyable from '/src/components/Copyable/Copyable'
import { getEvent, getPeople } from '/src/config/api'
import { useTranslation } from '/src/i18n/server'
import { makeClass, relativeTimeFormat } from '/src/utils'
import EventAvailabilities from './EventAvailabilities'
import styles from './page.module.scss'
const Page = async ({ params }: { params: { id: string } }) => {
const event = await getEvent(params.id).catch(() => undefined)
const people = await getPeople(params.id).catch(() => undefined)
if (!event || !people) notFound()
const { t, i18n } = await useTranslation(['common', 'event'])
return <>
<Content>
<h1 className={styles.name}>{event.name}</h1>
<span
className={styles.date}
title={Temporal.Instant.fromEpochSeconds(event.created_at).toLocaleString(i18n.language, { dateStyle: 'long' })}
>{t('common:created', { date: relativeTimeFormat(Temporal.Instant.fromEpochSeconds(event.created_at), i18n.language) })}</span>
<Copyable className={styles.info}>
{`https://crab.fit/${event.id}`}
</Copyable>
<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>
</p>
</Content>
<EventAvailabilities event={event} people={people} />
</>
}
export default Page