import { Link } from 'react-router-dom'; import { useForm } from 'react-hook-form'; import { useState, useEffect } from 'react'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; import customParseFormat from 'dayjs/plugin/customParseFormat'; import { Center, Donate, TextField, SelectField, Button, Legend, AvailabilityViewer, AvailabilityEditor, Error, } from 'components'; import { StyledMain, Footer, Logo, Title, EventName, LoginForm, LoginSection, Info, ShareInfo, Tabs, Tab, } from './eventStyle'; import api from 'services'; import { useSettingsStore } from 'stores'; import logo from 'res/logo.svg'; import timezones from 'res/timezones.json'; dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(customParseFormat); const Event = (props) => { const timeFormat = useSettingsStore(state => state.timeFormat); const weekStart = useSettingsStore(state => state.weekStart); 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); document.title = `${response.data.name} | Crab Fit`; } catch (e) { console.error(e); } finally { setIsLoading(false); } }; fetchEvent(); }, [id]); 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]); 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('Password is incorrect. Check your name is spelled right.'); } 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('Failed to create user. Please try again.'); } } } finally { setIsLoginLoading(false); gtag('event', 'login', { 'event_category': 'event', }); } }; return ( <>
CRAB FIT
Create your own
{(!!event || isLoading) ? ( <> {event?.name} navigator.clipboard?.writeText(`https://crab.fit/${id}`) .then(() => { setCopied('Copied!'); setTimeout(() => setCopied(null), 1000); gtag('event', 'copy_link', { 'event_category': 'event', }); }) .catch((e) => console.error('Failed to copy', e)) } title={!!navigator.clipboard ? 'Click to copy' : ''} >{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(`Scheduling ${event?.name}`)}&body=${encodeURIComponent(`Visit this link to enter your availabilities: https://crab.fit/${id}`)}`}>email. } ) : ( offline ? (
You are offline A Crab Fit doesn't work offline.
Make sure you're connected to the internet and try again.
) : (
Event not found Check that the url you entered is correct.
) )}
{(!!event || isLoading) && ( <> {user ? (

Signed in as {user.name}

) : ( <>

Sign in to add your availability

{error && setError(null)}>{error}} These details are only for this event. Use a password to prevent others from changing your availability. )} setTimezone(event.currentTarget.value)} options={timezones} />
{ e.preventDefault(); if (user) { setTab('you'); } }} selected={tab === 'you'} disabled={!user} title={user ? '' : 'Login to set your availability'} >Your availability { e.preventDefault(); setTab('group'); }} selected={tab === 'group'} >Group availability {tab === 'group' ? (
p.availability.length > 0).length} />
Hover or tap the calendar below to see who is available
p.availability.length > 0)} min={min} max={max} />
) : (
Click and drag the calendar below to set your availabilities
{ 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 }); } }} />
)} )} ); }; export default Event;