View event availabilities
This commit is contained in:
parent
cdea567bf3
commit
1a6d34ac59
17 changed files with 483 additions and 68 deletions
118
frontend/src/app/[id]/EventAvailabilities.tsx
Normal file
118
frontend/src/app/[id]/EventAvailabilities.tsx
Normal 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
|
||||
17
frontend/src/app/[id]/layout.tsx
Normal file
17
frontend/src/app/[id]/layout.tsx
Normal 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
|
||||
17
frontend/src/app/[id]/not-found.tsx
Normal file
17
frontend/src/app/[id]/not-found.tsx
Normal 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
|
||||
66
frontend/src/app/[id]/page.module.scss
Normal file
66
frontend/src/app/[id]/page.module.scss
Normal 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;
|
||||
}
|
||||
41
frontend/src/app/[id]/page.tsx
Normal file
41
frontend/src/app/[id]/page.tsx
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue