import { useForm } from 'react-hook-form'; import { useState, useEffect } from 'react'; import { useTranslation, Trans } from 'react-i18next'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; import customParseFormat from 'dayjs/plugin/customParseFormat'; import relativeTime from 'dayjs/plugin/relativeTime'; import { Footer, TextField, SelectField, Button, AvailabilityViewer, AvailabilityEditor, Error, Logo, } from 'components'; import { StyledMain } from '../Home/homeStyle'; import { EventName, EventDate, LoginForm, LoginSection, Info, ShareInfo, Tabs, Tab, } from './eventStyle'; import api from 'services'; import { useSettingsStore, useRecentsStore, useLocaleUpdateStore } from 'stores'; import timezones from 'res/timezones.json'; dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(customParseFormat); dayjs.extend(relativeTime); const Event = (props) => { const timeFormat = useSettingsStore(state => state.timeFormat); const weekStart = useSettingsStore(state => state.weekStart); const addRecent = useRecentsStore(state => state.addRecent); const locale = useLocaleUpdateStore(state => state.locale); const { t } = useTranslation(['common', 'event']); const { register, handleSubmit } = useForm(); const { id } = props.match.params; const { offline } = props; const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone); const [user, setUser] = useState(null); const [password, setPassword] = useState(null); const [tab, setTab] = useState(user ? 'you' : 'group'); const [isLoading, setIsLoading] = useState(true); const [isLoginLoading, setIsLoginLoading] = useState(false); const [error, setError] = useState(null); const [event, setEvent] = useState(null); const [people, setPeople] = useState([]); const [times, setTimes] = useState([]); const [timeLabels, setTimeLabels] = useState([]); const [dates, setDates] = useState([]); const [min, setMin] = useState(0); const [max, setMax] = useState(0); const [copied, setCopied] = useState(null); useEffect(() => { const fetchEvent = async () => { try { const response = await api.get(`/event/${id}`); setEvent(response.data); addRecent({ id: response.data.id, created: response.data.created, name: response.data.name, }); document.title = `${response.data.name} | Crab Fit`; } catch (e) { console.error(e); } finally { setIsLoading(false); } }; fetchEvent(); }, [id, addRecent]); useEffect(() => { const fetchPeople = async () => { try { const response = await api.get(`/event/${id}/people`); const adjustedPeople = response.data.people.map(person => ({ ...person, availability: (!!person.availability.length && person.availability[0].length === 13) ? person.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY')) : person.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')), })); setPeople(adjustedPeople); } catch (e) { console.error(e); } } if (tab === 'group') { fetchPeople(); } }, [tab, id, timezone]); // Convert to timezone and expand minute segments useEffect(() => { if (event) { const isSpecificDates = event.times[0].length === 13; setTimes(event.times.reduce( (allTimes, time) => { const date = isSpecificDates ? dayjs(time, 'HHmm-DDMMYYYY').utc(true).tz(timezone) : dayjs(time, 'HHmm').day(time.substring(5)).utc(true).tz(timezone); const format = isSpecificDates ? 'HHmm-DDMMYYYY' : 'HHmm-d'; return [ ...allTimes, date.minute(0).format(format), date.minute(15).format(format), date.minute(30).format(format), date.minute(45).format(format), ]; }, [] ).sort((a, b) => { if (isSpecificDates) { return dayjs(a, 'HHmm-DDMMYYYY').diff(dayjs(b, 'HHmm-DDMMYYYY')); } else { return dayjs(a, 'HHmm').day((parseInt(a.substring(5))-weekStart % 7 + 7) % 7) .diff(dayjs(b, 'HHmm').day((parseInt(b.substring(5))-weekStart % 7 + 7) % 7)); } })); } }, [event, timezone, weekStart]); useEffect(() => { if (!!times.length && !!people.length) { setMin(times.reduce((min, time) => { let total = people.reduce( (total, person) => person.availability.includes(time) ? total+1 : total, 0 ); return total < min ? total : min; }, Infinity )); setMax(times.reduce((max, time) => { let total = people.reduce( (total, person) => person.availability.includes(time) ? total+1 : total, 0 ); return total > max ? total : max; }, -Infinity )); } }, [times, people]); useEffect(() => { if (!!times.length) { setTimeLabels(times.reduce((labels, datetime) => { const time = datetime.substring(0, 4); if (labels.includes(time)) return labels; return [...labels, time]; }, []) .sort((a, b) => parseInt(a) - parseInt(b)) .reduce((labels, time, i, allTimes) => { if (time.substring(2) === '30') return [...labels, { label: '', time }]; if (allTimes.length - 1 === i) return [ ...labels, { label: '', time }, { label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: null } ]; if (allTimes.length - 1 > i && parseInt(allTimes[i+1].substring(0, 2))-1 > parseInt(time.substring(0, 2))) return [ ...labels, { label: '', time }, { label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: 'space' }, { label: '', time: 'space' }, { label: '', time: 'space' }, ]; if (time.substring(2) !== '00') return [...labels, { label: '', time }]; return [...labels, { label: dayjs(time, 'HHmm').format(timeFormat === '12h' ? 'h A' : 'HH'), time }]; }, [])); setDates(times.reduce((allDates, time) => { if (time.substring(2, 4) !== '00') return allDates; const date = time.substring(5); if (allDates.includes(date)) return allDates; return [...allDates, date]; }, [])); } }, [times, timeFormat, locale]); useEffect(() => { const fetchUser = async () => { try { const response = await api.post(`/event/${id}/people/${user.name}`, { person: { password } }); const adjustedUser = { ...response.data, availability: (!!response.data.availability.length && response.data.availability[0].length === 13) ? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY')) : response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')), }; setUser(adjustedUser); } catch (e) { console.log(e); } }; if (user) { fetchUser(); } // eslint-disable-next-line }, [timezone]); const onSubmit = async data => { setIsLoginLoading(true); setError(null); try { const response = await api.post(`/event/${id}/people/${data.name}`, { person: { password: data.password, }, }); setPassword(data.password); const adjustedUser = { ...response.data, availability: (!!response.data.availability.length && response.data.availability[0].length === 13) ? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY')) : response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')), }; setUser(adjustedUser); setTab('you'); } catch (e) { if (e.status === 401) { setError(t('event:form.errors.password_incorrect')); } else if (e.status === 404) { // Create user try { await api.post(`/event/${id}/people`, { person: { name: data.name, password: data.password, }, }); setPassword(data.password); setUser({ name: data.name, availability: [], }); setTab('you'); } catch (e) { setError(t('event:form.errors.unknown')); } } } finally { setIsLoginLoading(false); gtag('event', 'login', { 'event_category': 'event', }); } }; return ( <> {(!!event || isLoading) ? ( <> {event?.name} {event?.created && t('common:created', { date: dayjs.unix(event?.created).fromNow() })} navigator.clipboard?.writeText(`https://crab.fit/${id}`) .then(() => { setCopied(t('event:nav.copied')); setTimeout(() => setCopied(null), 1000); gtag('event', 'copy_link', { 'event_category': 'event', }); }) .catch((e) => console.error('Failed to copy', e)) } title={!!navigator.clipboard ? t('event:nav.title') : ''} >{copied ?? `https://crab.fit/${id}`} {!!event?.name && Copy the link to this page, or share via gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: event?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${id}`)}`}>email. } ) : ( offline ? (
{t('event:offline.title')}
) : (
{t('event:error.title')} {t('event:error.body')}
) )}
{(!!event || isLoading) && ( <> {user ? (

{t('event:form.signed_in', { name: user.name })}

) : ( <>

{t('event:form.signed_out')}

setError(null)}>{error} {t('event:form.info')} )} setTimezone(event.currentTarget.value)} options={timezones} /> {/* eslint-disable-next-line */} {event?.timezone && event.timezone !== timezone &&

This event was created in the timezone {{timezone: event.timezone}}. { e.preventDefault(); setTimezone(event.timezone); }}>Click here to use it.

} {(( Intl.DateTimeFormat().resolvedOptions().timeZone !== timezone && (event?.timezone && event.timezone !== Intl.DateTimeFormat().resolvedOptions().timeZone) ) || ( event?.timezone === undefined && Intl.DateTimeFormat().resolvedOptions().timeZone !== timezone )) && ( /* eslint-disable-next-line */

Your local timezone is detected to be {{timezone: Intl.DateTimeFormat().resolvedOptions().timeZone}}. { e.preventDefault(); setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone); }}>Click here to use it.

)}
{ e.preventDefault(); if (user) { setTab('you'); } }} selected={tab === 'you'} disabled={!user} title={user ? '' : t('event:tabs.you_tooltip')} >{t('event:tabs.you')} { e.preventDefault(); setTab('group'); }} selected={tab === 'group'} >{t('event:tabs.group')} {tab === 'group' ? (
p.availability.length > 0)} min={min} max={max} />
) : (
{ const oldAvailability = [...user.availability]; const utcAvailability = (!!availability.length && availability[0].length === 13) ? availability.map(date => dayjs.tz(date, 'HHmm-DDMMYYYY', timezone).utc().format('HHmm-DDMMYYYY')) : availability.map(date => dayjs.tz(date, 'HHmm', timezone).day(date.substring(5)).utc().format('HHmm-d')); setUser({ ...user, availability }); try { await api.patch(`/event/${id}/people/${user.name}`, { person: { password, availability: utcAvailability, }, }); } catch (e) { console.log(e); setUser({ ...user, oldAvailability }); } }} />
)} )}