Update event page

This commit is contained in:
Ben Grant 2022-08-16 15:23:05 +10:00
parent 2d32a1b036
commit 9ac969ec78
7 changed files with 311 additions and 322 deletions

View file

@ -66,8 +66,8 @@ const App = () => {
<Route path="/" element={<Pages.Home />} /> <Route path="/" element={<Pages.Home />} />
<Route path="/how-to" element={<Pages.Help />} /> <Route path="/how-to" element={<Pages.Help />} />
<Route path="/privacy" element={<Pages.Privacy />} /> <Route path="/privacy" element={<Pages.Privacy />} />
{/* <Route path="/create" element={<Pages.Create />} /> <Route path="/create" element={<Pages.Create />} />
<Route path="/:id" element={<Pages.Event />} /> */} <Route path="/:id" element={<Pages.Event />} />
</Routes> </Routes>
</Suspense> </Suspense>

View file

@ -1,12 +1,12 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react'
import { useHistory } from 'react-router-dom'; import { useNavigate } from 'react-router-dom'
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form'
import { useTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next'
import dayjs from 'dayjs'; import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'; import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'; import timezone from 'dayjs/plugin/timezone'
import customParseFormat from 'dayjs/plugin/customParseFormat'; import customParseFormat from 'dayjs/plugin/customParseFormat'
import { import {
TextField, TextField,
@ -17,7 +17,7 @@ import {
Error, Error,
Recents, Recents,
Footer, Footer,
} from 'components'; } from '/src/components'
import { import {
StyledMain, StyledMain,
@ -27,80 +27,80 @@ import {
P, P,
OfflineMessage, OfflineMessage,
ShareInfo, ShareInfo,
} from './createStyle'; } from './Create.styles'
import api from 'services'; import api from '/src/services'
import { useRecentsStore } from 'stores'; import { useRecentsStore } from '/src/stores'
import timezones from 'res/timezones.json'; import timezones from '/src/res/timezones.json'
dayjs.extend(utc); dayjs.extend(utc)
dayjs.extend(timezone); dayjs.extend(timezone)
dayjs.extend(customParseFormat); dayjs.extend(customParseFormat)
const Create = ({ offline }) => { const Create = ({ offline }) => {
const { register, handleSubmit, setValue } = useForm({ const { register, handleSubmit, setValue } = useForm({
defaultValues: { defaultValues: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
}, },
}); })
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null); const [error, setError] = useState(null)
const [createdEvent, setCreatedEvent] = useState(null); const [createdEvent, setCreatedEvent] = useState(null)
const [copied, setCopied] = useState(null); const [copied, setCopied] = useState(null)
const [showFooter, setShowFooter] = useState(true); const [showFooter, setShowFooter] = useState(true)
const { push } = useHistory(); const navigate = useNavigate()
const { t } = useTranslation(['common', 'home', 'event']); const { t } = useTranslation(['common', 'home', 'event'])
const addRecent = useRecentsStore(state => state.addRecent); const addRecent = useRecentsStore(state => state.addRecent)
useEffect(() => { useEffect(() => {
if (window.self === window.top) { if (window.self === window.top) {
push('/'); navigate('/')
} }
document.title = 'Create a Crab Fit'; document.title = 'Create a Crab Fit'
if (window.parent) { if (window.parent) {
window.parent.postMessage('crabfit-create', '*'); window.parent.postMessage('crabfit-create', '*')
window.addEventListener('message', e => { window.addEventListener('message', e => {
if (e.data === 'safari-extension') { if (e.data === 'safari-extension') {
setShowFooter(false); setShowFooter(false)
} }
}, { }, {
once: true once: true
}); })
} }
}, [push]); }, [navigate])
const onSubmit = async data => { const onSubmit = async data => {
setIsLoading(true); setIsLoading(true)
setError(null); setError(null)
try { try {
const { start, end } = JSON.parse(data.times); const { start, end } = JSON.parse(data.times)
const dates = JSON.parse(data.dates); const dates = JSON.parse(data.dates)
if (dates.length === 0) { if (dates.length === 0) {
return setError(t('home:form.errors.no_dates')); return setError(t('home:form.errors.no_dates'))
} }
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8; const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8
if (start === end) { if (start === end) {
return setError(t('home:form.errors.same_times')); return setError(t('home:form.errors.same_times'))
} }
let times = dates.reduce((times, date) => { const times = dates.reduce((times, date) => {
let day = []; const day = []
for (let i = start; i < (start > end ? 24 : end); i++) { for (let i = start; i < (start > end ? 24 : end); i++) {
if (isSpecificDates) { if (isSpecificDates) {
day.push( day.push(
dayjs.tz(date, 'DDMMYYYY', data.timezone) dayjs.tz(date, 'DDMMYYYY', data.timezone)
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY') .hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
); )
} else { } else {
day.push( day.push(
dayjs().tz(data.timezone) dayjs().tz(data.timezone)
.day(date).hour(i).minute(0).utc().format('HHmm-d') .day(date).hour(i).minute(0).utc().format('HHmm-d')
); )
} }
} }
if (start > end) { if (start > end) {
@ -109,20 +109,20 @@ const Create = ({ offline }) => {
day.push( day.push(
dayjs.tz(date, 'DDMMYYYY', data.timezone) dayjs.tz(date, 'DDMMYYYY', data.timezone)
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY') .hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
); )
} else { } else {
day.push( day.push(
dayjs().tz(data.timezone) dayjs().tz(data.timezone)
.day(date).hour(i).minute(0).utc().format('HHmm-d') .day(date).hour(i).minute(0).utc().format('HHmm-d')
); )
} }
} }
} }
return [...times, ...day]; return [...times, ...day]
}, []); }, [])
if (times.length === 0) { if (times.length === 0) {
return setError(t('home:form.errors.no_time')); return setError(t('home:form.errors.no_time'))
} }
const response = await api.post('/event', { const response = await api.post('/event', {
@ -131,23 +131,23 @@ const Create = ({ offline }) => {
times: times, times: times,
timezone: data.timezone, timezone: data.timezone,
}, },
}); })
setCreatedEvent(response.data); setCreatedEvent(response.data)
addRecent({ addRecent({
id: response.data.id, id: response.data.id,
created: response.data.created, created: response.data.created,
name: response.data.name, name: response.data.name,
}); })
gtag('event', 'create_event', { gtag('event', 'create_event', {
'event_category': 'create', 'event_category': 'create',
}); })
} catch (e) { } catch (e) {
setError(t('home:form.errors.unknown')); setError(t('home:form.errors.unknown'))
console.error(e); console.error(e)
} finally { } finally {
setIsLoading(false); setIsLoading(false)
}
} }
};
return ( return (
<> <>
@ -163,15 +163,15 @@ const Create = ({ offline }) => {
<ShareInfo <ShareInfo
onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${createdEvent.id}`) onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${createdEvent.id}`)
.then(() => { .then(() => {
setCopied(t('event:nav.copied')); setCopied(t('event:nav.copied'))
setTimeout(() => setCopied(null), 1000); setTimeout(() => setCopied(null), 1000)
gtag('event', 'copy_link', { gtag('event', 'copy_link', {
'event_category': 'event', 'event_category': 'event',
});
}) })
.catch((e) => console.error('Failed to copy', e)) })
.catch(e => console.error('Failed to copy', e))
} }
title={!!navigator.clipboard ? t('event:nav.title') : ''} title={navigator.clipboard ? t('event:nav.title') : ''}
>{copied ?? `https://crab.fit/${createdEvent?.id}`}</ShareInfo> >{copied ?? `https://crab.fit/${createdEvent?.id}`}</ShareInfo>
<ShareInfo> <ShareInfo>
{/* eslint-disable-next-line */} {/* eslint-disable-next-line */}
@ -236,7 +236,7 @@ const Create = ({ offline }) => {
</> </>
)} )}
</> </>
); )
}; }
export default Create; export default Create

View file

@ -1,50 +1,50 @@
import styled from '@emotion/styled'; import { styled } from 'goober'
export const StyledMain = styled.div` export const StyledMain = styled('div')`
width: 600px; width: 600px;
margin: 10px auto; margin: 10px auto;
max-width: calc(100% - 30px); max-width: calc(100% - 30px);
`; `
export const CreateForm = styled.form` export const CreateForm = styled('form')`
margin: 0 0 30px; margin: 0 0 30px;
`; `
export const TitleSmall = styled.span` export const TitleSmall = styled('span')`
display: block; display: block;
margin: 0; margin: 0;
font-size: 2rem; font-size: 2rem;
text-align: center; text-align: center;
font-family: 'Samurai Bob', sans-serif; font-family: 'Samurai Bob', sans-serif;
font-weight: 400; font-weight: 400;
color: ${props => props.theme.primaryDark}; color: var(--secondary);
line-height: 1em; line-height: 1em;
text-transform: uppercase; text-transform: uppercase;
`; `
export const TitleLarge = styled.h1` export const TitleLarge = styled('h1')`
margin: 0; margin: 0;
font-size: 2rem; font-size: 2rem;
text-align: center; text-align: center;
color: ${props => props.theme.primary}; color: var(--primary);
font-family: 'Molot', sans-serif; font-family: 'Molot', sans-serif;
font-weight: 400; font-weight: 400;
text-shadow: 0 3px 0 ${props => props.theme.primaryDark}; text-shadow: 0 3px 0 var(--secondary);
line-height: 1em; line-height: 1em;
text-transform: uppercase; text-transform: uppercase;
`; `
export const P = styled.p` export const P = styled('p')`
font-weight: 500; font-weight: 500;
line-height: 1.6em; line-height: 1.6em;
`; `
export const OfflineMessage = styled.div` export const OfflineMessage = styled('div')`
text-align: center; text-align: center;
margin: 50px 0 20px; margin: 50px 0 20px;
`; `
export const ShareInfo = styled.p` export const ShareInfo = styled('p')`
margin: 6px 0; margin: 6px 0;
text-align: center; text-align: center;
font-size: 15px; font-size: 15px;
@ -54,7 +54,7 @@ export const ShareInfo = styled.p`
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: ${props.theme.primaryDark}; color: var(--secondary);
} }
`} `}
`; `

View file

@ -1,12 +1,13 @@
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form'
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react'
import { useTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next'
import { useParams } from 'react-router-dom'
import dayjs from 'dayjs'; import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'; import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'; import timezone from 'dayjs/plugin/timezone'
import customParseFormat from 'dayjs/plugin/customParseFormat'; import customParseFormat from 'dayjs/plugin/customParseFormat'
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime'
import { import {
Footer, Footer,
@ -17,9 +18,9 @@ import {
AvailabilityEditor, AvailabilityEditor,
Error, Error,
Logo, Logo,
} from 'components'; } from '/src/components'
import { StyledMain } from '../Home/homeStyle'; import { StyledMain } from '../Home/Home.styles'
import { import {
EventName, EventName,
@ -30,232 +31,227 @@ import {
ShareInfo, ShareInfo,
Tabs, Tabs,
Tab, Tab,
} from './eventStyle'; } from './Event.styles'
import api from 'services'; import api from '/src/services'
import { useSettingsStore, useRecentsStore, useLocaleUpdateStore } from 'stores'; import { useSettingsStore, useRecentsStore, useLocaleUpdateStore } from '/src/stores'
import timezones from 'res/timezones.json'; import timezones from '/src/res/timezones.json'
dayjs.extend(utc); dayjs.extend(utc)
dayjs.extend(timezone); dayjs.extend(timezone)
dayjs.extend(customParseFormat); dayjs.extend(customParseFormat)
dayjs.extend(relativeTime); dayjs.extend(relativeTime)
const Event = (props) => { const Event = () => {
const timeFormat = useSettingsStore(state => state.timeFormat); const timeFormat = useSettingsStore(state => state.timeFormat)
const weekStart = useSettingsStore(state => state.weekStart); const weekStart = useSettingsStore(state => state.weekStart)
const addRecent = useRecentsStore(state => state.addRecent); const addRecent = useRecentsStore(state => state.addRecent)
const removeRecent = useRecentsStore(state => state.removeRecent); const removeRecent = useRecentsStore(state => state.removeRecent)
const locale = useLocaleUpdateStore(state => state.locale); const locale = useLocaleUpdateStore(state => state.locale)
const { t } = useTranslation(['common', 'event']); const { t } = useTranslation(['common', 'event'])
const { register, handleSubmit, setFocus, reset } = useForm(); const { register, handleSubmit, setFocus, reset } = useForm()
const { id } = props.match.params; const { id } = useParams()
const { offline } = props; const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone)
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone); const [user, setUser] = useState(null)
const [user, setUser] = useState(null); const [password, setPassword] = useState(null)
const [password, setPassword] = useState(null); const [tab, setTab] = useState(user ? 'you' : 'group')
const [tab, setTab] = useState(user ? 'you' : 'group'); const [isLoading, setIsLoading] = useState(true)
const [isLoading, setIsLoading] = useState(true); const [isLoginLoading, setIsLoginLoading] = useState(false)
const [isLoginLoading, setIsLoginLoading] = useState(false); const [error, setError] = useState(null)
const [error, setError] = useState(null); const [event, setEvent] = useState(null)
const [event, setEvent] = useState(null); const [people, setPeople] = useState([])
const [people, setPeople] = useState([]);
const [times, setTimes] = useState([]); const [times, setTimes] = useState([])
const [timeLabels, setTimeLabels] = useState([]); const [timeLabels, setTimeLabels] = useState([])
const [dates, setDates] = useState([]); const [dates, setDates] = useState([])
const [min, setMin] = useState(0); const [min, setMin] = useState(0)
const [max, setMax] = useState(0); const [max, setMax] = useState(0)
const [copied, setCopied] = useState(null); const [copied, setCopied] = useState(null)
useEffect(() => { useEffect(() => {
const fetchEvent = async () => { const fetchEvent = async () => {
try { try {
const response = await api.get(`/event/${id}`); const response = await api.get(`/event/${id}`)
setEvent(response.data); setEvent(response.data)
addRecent({ addRecent({
id: response.data.id, id: response.data.id,
created: response.data.created, created: response.data.created,
name: response.data.name, name: response.data.name,
}); })
document.title = `${response.data.name} | Crab Fit`; document.title = `${response.data.name} | Crab Fit`
} catch (e) { } catch (e) {
console.error(e); console.error(e)
if (e.status === 404) { if (e.status === 404) {
removeRecent(id); removeRecent(id)
} }
} finally { } finally {
setIsLoading(false); setIsLoading(false)
}
} }
};
fetchEvent(); fetchEvent()
}, [id, addRecent, removeRecent]); }, [id, addRecent, removeRecent])
useEffect(() => { useEffect(() => {
const fetchPeople = async () => { const fetchPeople = async () => {
try { try {
const response = await api.get(`/event/${id}/people`); const response = await api.get(`/event/${id}/people`)
const adjustedPeople = response.data.people.map(person => ({ const adjustedPeople = response.data.people.map(person => ({
...person, ...person,
availability: (!!person.availability.length && person.availability[0].length === 13) 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-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')), : person.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
})); }))
setPeople(adjustedPeople); setPeople(adjustedPeople)
} catch (e) { } catch (e) {
console.error(e); console.error(e)
} }
} }
if (tab === 'group') { if (tab === 'group') {
fetchPeople(); fetchPeople()
} }
}, [tab, id, timezone]); }, [tab, id, timezone])
// Convert to timezone and expand minute segments // Convert to timezone and expand minute segments
useEffect(() => { useEffect(() => {
if (event) { if (event) {
const isSpecificDates = event.times[0].length === 13; const isSpecificDates = event.times[0].length === 13
setTimes(event.times.reduce( setTimes(event.times.reduce(
(allTimes, time) => { (allTimes, time) => {
const date = isSpecificDates ? const date = isSpecificDates ?
dayjs(time, 'HHmm-DDMMYYYY').utc(true).tz(timezone) dayjs(time, 'HHmm-DDMMYYYY').utc(true).tz(timezone)
: dayjs(time, 'HHmm').day(time.substring(5)).utc(true).tz(timezone); : dayjs(time, 'HHmm').day(time.substring(5)).utc(true).tz(timezone)
const format = isSpecificDates ? 'HHmm-DDMMYYYY' : 'HHmm-d'; const format = isSpecificDates ? 'HHmm-DDMMYYYY' : 'HHmm-d'
return [ return [
...allTimes, ...allTimes,
date.minute(0).format(format), date.minute(0).format(format),
date.minute(15).format(format), date.minute(15).format(format),
date.minute(30).format(format), date.minute(30).format(format),
date.minute(45).format(format), date.minute(45).format(format),
]; ]
}, },
[] []
).sort((a, b) => { ).sort((a, b) => {
if (isSpecificDates) { if (isSpecificDates) {
return dayjs(a, 'HHmm-DDMMYYYY').diff(dayjs(b, 'HHmm-DDMMYYYY')); return dayjs(a, 'HHmm-DDMMYYYY').diff(dayjs(b, 'HHmm-DDMMYYYY'))
} else { } else {
return dayjs(a, 'HHmm').day((parseInt(a.substring(5))-weekStart % 7 + 7) % 7) 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)); .diff(dayjs(b, 'HHmm').day((parseInt(b.substring(5))-weekStart % 7 + 7) % 7))
} }
})); }))
} }
}, [event, timezone, weekStart]); }, [event, timezone, weekStart])
useEffect(() => { useEffect(() => {
if (!!times.length && !!people.length) { if (!!times.length && !!people.length) {
setMin(times.reduce((min, time) => { setMin(times.reduce((min, time) => {
let total = people.reduce( const total = people.reduce(
(total, person) => person.availability.includes(time) ? total+1 : total, (total, person) => person.availability.includes(time) ? total+1 : total,
0 0
); )
return total < min ? total : min; return total < min ? total : min
}, }, Infinity))
Infinity
));
setMax(times.reduce((max, time) => { setMax(times.reduce((max, time) => {
let total = people.reduce( const total = people.reduce(
(total, person) => person.availability.includes(time) ? total+1 : total, (total, person) => person.availability.includes(time) ? total+1 : total,
0 0
); )
return total > max ? total : max; return total > max ? total : max
}, }, -Infinity))
-Infinity
));
} }
}, [times, people]); }, [times, people])
useEffect(() => { useEffect(() => {
if (!!times.length) { if (times.length) {
setTimeLabels(times.reduce((labels, datetime) => { setTimeLabels(times.reduce((labels, datetime) => {
const time = datetime.substring(0, 4); const time = datetime.substring(0, 4)
if (labels.includes(time)) return labels; if (labels.includes(time)) return labels
return [...labels, time]; return [...labels, time]
}, []) }, [])
.sort((a, b) => parseInt(a) - parseInt(b)) .sort((a, b) => parseInt(a) - parseInt(b))
.reduce((labels, time, i, allTimes) => { .reduce((labels, time, i, allTimes) => {
if (time.substring(2) === '30') return [...labels, { label: '', time }]; if (time.substring(2) === '30') return [...labels, { label: '', time }]
if (allTimes.length - 1 === i) return [ if (allTimes.length - 1 === i) return [
...labels, ...labels,
{ label: '', time }, { label: '', time },
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: null } { 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 [ if (allTimes.length - 1 > i && parseInt(allTimes[i+1].substring(0, 2))-1 > parseInt(time.substring(0, 2))) return [
...labels, ...labels,
{ label: '', time }, { label: '', time },
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: 'space' }, { label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: 'space' },
{ label: '', time: 'space' }, { label: '', time: 'space' },
{ label: '', time: 'space' }, { label: '', time: 'space' },
]; ]
if (time.substring(2) !== '00') return [...labels, { label: '', time }]; if (time.substring(2) !== '00') return [...labels, { label: '', time }]
return [...labels, { label: dayjs(time, 'HHmm').format(timeFormat === '12h' ? 'h A' : 'HH'), time }]; return [...labels, { label: dayjs(time, 'HHmm').format(timeFormat === '12h' ? 'h A' : 'HH'), time }]
}, [])); }, []))
setDates(times.reduce((allDates, time) => { setDates(times.reduce((allDates, time) => {
if (time.substring(2, 4) !== '00') return allDates; if (time.substring(2, 4) !== '00') return allDates
const date = time.substring(5); const date = time.substring(5)
if (allDates.includes(date)) return allDates; if (allDates.includes(date)) return allDates
return [...allDates, date]; return [...allDates, date]
}, [])); }, []))
} }
}, [times, timeFormat, locale]); }, [times, timeFormat, locale])
useEffect(() => { useEffect(() => {
const fetchUser = async () => { const fetchUser = async () => {
try { try {
const response = await api.post(`/event/${id}/people/${user.name}`, { person: { password } }); const response = await api.post(`/event/${id}/people/${user.name}`, { person: { password } })
const adjustedUser = { const adjustedUser = {
...response.data, ...response.data,
availability: (!!response.data.availability.length && response.data.availability[0].length === 13) 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-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')), : 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);
} }
}; setUser(adjustedUser)
} catch (e) {
console.log(e)
}
}
if (user) { if (user) {
fetchUser(); fetchUser()
} }
// eslint-disable-next-line // eslint-disable-next-line
}, [timezone]); }, [timezone]);
const onSubmit = async data => { const onSubmit = async data => {
if (!data.name || data.name.length === 0) { if (!data.name || data.name.length === 0) {
setFocus('name'); setFocus('name')
return setError(t('event:form.errors.name_required')); return setError(t('event:form.errors.name_required'))
} }
setIsLoginLoading(true); setIsLoginLoading(true)
setError(null); setError(null)
try { try {
const response = await api.post(`/event/${id}/people/${data.name}`, { const response = await api.post(`/event/${id}/people/${data.name}`, {
person: { person: {
password: data.password, password: data.password,
}, },
}); })
setPassword(data.password); setPassword(data.password)
const adjustedUser = { const adjustedUser = {
...response.data, ...response.data,
availability: (!!response.data.availability.length && response.data.availability[0].length === 13) 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-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')), : response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
}; }
setUser(adjustedUser); setUser(adjustedUser)
setTab('you'); setTab('you')
} catch (e) { } catch (e) {
if (e.status === 401) { if (e.status === 401) {
setError(t('event:form.errors.password_incorrect')); setError(t('event:form.errors.password_incorrect'))
} else if (e.status === 404) { } else if (e.status === 404) {
// Create user // Create user
try { try {
@ -264,25 +260,25 @@ const Event = (props) => {
name: data.name, name: data.name,
password: data.password, password: data.password,
}, },
}); })
setPassword(data.password); setPassword(data.password)
setUser({ setUser({
name: data.name, name: data.name,
availability: [], availability: [],
}); })
setTab('you'); setTab('you')
} catch (e) { } catch (e) {
setError(t('event:form.errors.unknown')); setError(t('event:form.errors.unknown'))
} }
} }
} finally { } finally {
setIsLoginLoading(false); setIsLoginLoading(false)
gtag('event', 'login', { gtag('event', 'login', {
'event_category': 'event', 'event_category': 'event',
}); })
reset(); reset()
}
} }
};
return ( return (
<> <>
@ -291,39 +287,32 @@ const Event = (props) => {
{(!!event || isLoading) ? ( {(!!event || isLoading) ? (
<> <>
<EventName isLoading={isLoading}>{event?.name}</EventName> <EventName $isLoading={isLoading}>{event?.name}</EventName>
<EventDate isLoading={isLoading} locale={locale} title={event?.created && dayjs.unix(event?.created).format('D MMMM, YYYY')}>{event?.created && t('common:created', { date: dayjs.unix(event?.created).fromNow() })}</EventDate> <EventDate $isLoading={isLoading} locale={locale} title={event?.created && dayjs.unix(event?.created).format('D MMMM, YYYY')}>{event?.created && t('common:created', { date: dayjs.unix(event?.created).fromNow() })}</EventDate>
<ShareInfo <ShareInfo
onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${id}`) onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${id}`)
.then(() => { .then(() => {
setCopied(t('event:nav.copied')); setCopied(t('event:nav.copied'))
setTimeout(() => setCopied(null), 1000); setTimeout(() => setCopied(null), 1000)
gtag('event', 'copy_link', { gtag('event', 'copy_link', {
'event_category': 'event', 'event_category': 'event',
});
}) })
.catch((e) => console.error('Failed to copy', e)) })
.catch(e => console.error('Failed to copy', e))
} }
title={!!navigator.clipboard ? t('event:nav.title') : ''} title={navigator.clipboard ? t('event:nav.title') : ''}
>{copied ?? `https://crab.fit/${id}`}</ShareInfo> >{copied ?? `https://crab.fit/${id}`}</ShareInfo>
<ShareInfo isLoading={isLoading} className="instructions"> <ShareInfo $isLoading={isLoading} className="instructions">
{!!event?.name && {!!event?.name &&
<Trans i18nKey="event:nav.shareinfo">Copy the link to this page, or share via <a onClick={() => 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</a>.</Trans> <Trans i18nKey="event:nav.shareinfo">Copy the link to this page, or share via <a onClick={() => 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</a>.</Trans>
} }
</ShareInfo> </ShareInfo>
</> </>
) : (
offline ? (
<div style={{ margin: '100px 0' }}>
<EventName>{t('event:offline.title')}</EventName>
<ShareInfo><Trans i18nKey="event:offline.body" /></ShareInfo>
</div>
) : ( ) : (
<div style={{ margin: '100px 0' }}> <div style={{ margin: '100px 0' }}>
<EventName>{t('event:error.title')}</EventName> <EventName>{t('event:error.title')}</EventName>
<ShareInfo>{t('event:error.body')}</ShareInfo> <ShareInfo>{t('event:error.body')}</ShareInfo>
</div> </div>
)
)} )}
</StyledMain> </StyledMain>
@ -335,9 +324,9 @@ const Event = (props) => {
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '20px 0', flexWrap: 'wrap', gap: '10px' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '20px 0', flexWrap: 'wrap', gap: '10px' }}>
<h2 style={{ margin: 0 }}>{t('event:form.signed_in', { name: user.name })}</h2> <h2 style={{ margin: 0 }}>{t('event:form.signed_in', { name: user.name })}</h2>
<Button small onClick={() => { <Button small onClick={() => {
setTab('group'); setTab('group')
setUser(null); setUser(null)
setPassword(null); setPassword(null)
}}>{t('event:form.logout_button')}</Button> }}>{t('event:form.logout_button')}</Button>
</div> </div>
) : ( ) : (
@ -363,7 +352,7 @@ const Event = (props) => {
<Button <Button
type="submit" type="submit"
isLoading={isLoginLoading} $isLoading={isLoginLoading}
disabled={isLoginLoading || isLoading} disabled={isLoginLoading || isLoading}
>{t('event:form.button')}</Button> >{t('event:form.button')}</Button>
</LoginForm> </LoginForm>
@ -383,8 +372,8 @@ const Event = (props) => {
/> />
{/* eslint-disable-next-line */} {/* eslint-disable-next-line */}
{event?.timezone && event.timezone !== timezone && <p><Trans i18nKey="event:form.created_in_timezone">This event was created in the timezone <strong>{{timezone: event.timezone}}</strong>. <a href="#" onClick={e => { {event?.timezone && event.timezone !== timezone && <p><Trans i18nKey="event:form.created_in_timezone">This event was created in the timezone <strong>{{timezone: event.timezone}}</strong>. <a href="#" onClick={e => {
e.preventDefault(); e.preventDefault()
setTimezone(event.timezone); setTimezone(event.timezone)
}}>Click here</a> to use it.</Trans></p>} }}>Click here</a> to use it.</Trans></p>}
{(( {((
Intl.DateTimeFormat().resolvedOptions().timeZone !== timezone Intl.DateTimeFormat().resolvedOptions().timeZone !== timezone
@ -395,8 +384,8 @@ const Event = (props) => {
)) && ( )) && (
/* eslint-disable-next-line */ /* eslint-disable-next-line */
<p><Trans i18nKey="event:form.local_timezone">Your local timezone is detected to be <strong>{{timezone: Intl.DateTimeFormat().resolvedOptions().timeZone}}</strong>. <a href="#" onClick={e => { <p><Trans i18nKey="event:form.local_timezone">Your local timezone is detected to be <strong>{{timezone: Intl.DateTimeFormat().resolvedOptions().timeZone}}</strong>. <a href="#" onClick={e => {
e.preventDefault(); e.preventDefault()
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone); setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone)
}}>Click here</a> to use it.</Trans></p> }}>Click here</a> to use it.</Trans></p>
)} )}
</StyledMain> </StyledMain>
@ -407,11 +396,11 @@ const Event = (props) => {
<Tab <Tab
href="#you" href="#you"
onClick={e => { onClick={e => {
e.preventDefault(); e.preventDefault()
if (user) { if (user) {
setTab('you'); setTab('you')
} else { } else {
setFocus('name'); setFocus('name')
} }
}} }}
selected={tab === 'you'} selected={tab === 'you'}
@ -421,8 +410,8 @@ const Event = (props) => {
<Tab <Tab
href="#group" href="#group"
onClick={e => { onClick={e => {
e.preventDefault(); e.preventDefault()
setTab('group'); setTab('group')
}} }}
selected={tab === 'group'} selected={tab === 'group'}
>{t('event:tabs.group')}</Tab> >{t('event:tabs.group')}</Tab>
@ -451,21 +440,21 @@ const Event = (props) => {
isSpecificDates={!!dates.length && dates[0].length === 8} isSpecificDates={!!dates.length && dates[0].length === 8}
value={user.availability} value={user.availability}
onChange={async availability => { onChange={async availability => {
const oldAvailability = [...user.availability]; const oldAvailability = [...user.availability]
const utcAvailability = (!!availability.length && availability[0].length === 13) 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-DDMMYYYY', timezone).utc().format('HHmm-DDMMYYYY'))
: availability.map(date => dayjs.tz(date, 'HHmm', timezone).day(date.substring(5)).utc().format('HHmm-d')); : availability.map(date => dayjs.tz(date, 'HHmm', timezone).day(date.substring(5)).utc().format('HHmm-d'))
setUser({ ...user, availability }); setUser({ ...user, availability })
try { try {
await api.patch(`/event/${id}/people/${user.name}`, { await api.patch(`/event/${id}/people/${user.name}`, {
person: { person: {
password, password,
availability: utcAvailability, availability: utcAvailability,
}, },
}); })
} catch (e) { } catch (e) {
console.log(e); console.log(e)
setUser({ ...user, oldAvailability }); setUser({ ...user, oldAvailability })
} }
}} }}
/> />
@ -476,7 +465,7 @@ const Event = (props) => {
<Footer /> <Footer />
</> </>
); )
}; }
export default Event; export default Event

View file

@ -1,24 +1,24 @@
import styled from '@emotion/styled'; import { styled } from 'goober'
export const EventName = styled.h1` export const EventName = styled('h1')`
text-align: center; text-align: center;
font-weight: 800; font-weight: 800;
margin: 20px 0 5px; margin: 20px 0 5px;
${props => props.isLoading && ` ${props => props.$isLoading && `
&:after { &:after {
content: ''; content: '';
display: inline-block; display: inline-block;
height: 1em; height: 1em;
width: 400px; width: 400px;
max-width: 100%; max-width: 100%;
background-color: ${props.theme.loading}; background-color: var(--loading);
border-radius: 3px; border-radius: 3px;
} }
`} `}
`; `
export const EventDate = styled.span` export const EventDate = styled('span')`
display: block; display: block;
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
@ -27,14 +27,14 @@ export const EventDate = styled.span`
font-weight: 500; font-weight: 500;
letter-spacing: .01em; letter-spacing: .01em;
${props => props.isLoading && ` ${props => props.$isLoading && `
&:after { &:after {
content: ''; content: '';
display: inline-block; display: inline-block;
height: 1em; height: 1em;
width: 200px; width: 200px;
max-width: 100%; max-width: 100%;
background-color: ${props.theme.loading}; background-color: var(--loading);
border-radius: 3px; border-radius: 3px;
} }
`} `}
@ -44,9 +44,9 @@ export const EventDate = styled.span`
content: ' - ' attr(title); content: ' - ' attr(title);
} }
} }
`; `
export const LoginForm = styled.form` export const LoginForm = styled('form')`
display: grid; display: grid;
grid-template-columns: 1fr 1fr auto; grid-template-columns: 1fr 1fr auto;
align-items: flex-end; align-items: flex-end;
@ -62,36 +62,36 @@ export const LoginForm = styled.form`
--btn-width: 100%; --btn-width: 100%;
} }
} }
`; `
export const LoginSection = styled.section` export const LoginSection = styled('section')`
background-color: ${props => props.theme.primaryBackground}; background-color: var(--surface);
padding: 10px 0; padding: 10px 0;
@media print { @media print {
display: none; display: none;
} }
`; `
export const Info = styled.p` export const Info = styled('p')`
margin: 18px 0; margin: 18px 0;
opacity: .6; opacity: .6;
font-size: 12px; font-size: 12px;
`; `
export const ShareInfo = styled.p` export const ShareInfo = styled('p')`
margin: 6px 0; margin: 6px 0;
text-align: center; text-align: center;
font-size: 15px; font-size: 15px;
${props => props.isLoading && ` ${props => props.$isLoading && `
&:after { &:after {
content: ''; content: '';
display: inline-block; display: inline-block;
height: 1em; height: 1em;
width: 300px; width: 300px;
max-width: 100%; max-width: 100%;
background-color: ${props.theme.loading}; background-color: var(--loading);
border-radius: 3px; border-radius: 3px;
} }
`} `}
@ -100,7 +100,7 @@ export const ShareInfo = styled.p`
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight}; color: var(--secondary);
} }
`} `}
@ -109,9 +109,9 @@ export const ShareInfo = styled.p`
display: none; display: none;
} }
} }
`; `
export const Tabs = styled.div` export const Tabs = styled('div')`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -120,29 +120,29 @@ export const Tabs = styled.div`
@media print { @media print {
display: none; display: none;
} }
`; `
export const Tab = styled.a` export const Tab = styled('a')`
user-select: none; user-select: none;
text-decoration: none; text-decoration: none;
display: block; display: block;
color: ${props => props.theme.text}; color: var(--text);
padding: 8px 18px; padding: 8px 18px;
background-color: ${props => props.theme.primaryBackground}; background-color: var(--surface);
border: 1px solid ${props => props.theme.primary}; border: 1px solid var(--primary);
border-bottom: 0; border-bottom: 0;
margin: 0 4px; margin: 0 4px;
border-top-left-radius: 5px; border-top-left-radius: 5px;
border-top-right-radius: 5px; border-top-right-radius: 5px;
${props => props.selected && ` ${props => props.$selected && `
color: #FFF; color: #FFF;
background-color: ${props.theme.primary}; background-color: var(--primary);
border-color: ${props.theme.primary}; border-color: var(--primary);
`} `}
${props => props.disabled && ` ${props => props.disabled && `
opacity: .5; opacity: .5;
cursor: not-allowed; cursor: not-allowed;
`} `}
`; `

View file

@ -1,7 +1,7 @@
import { lazy } from 'react' import { lazy } from 'react'
export const Home = lazy(() => import('./Home/Home')) export const Home = lazy(() => import('./Home/Home'))
//export const Event = lazy(() => import('./Event/Event')) export const Event = lazy(() => import('./Event/Event'))
//export const Create = lazy(() => import('./Create/Create')) export const Create = lazy(() => import('./Create/Create'))
export const Help = lazy(() => import('./Help/Help')) export const Help = lazy(() => import('./Help/Help'))
export const Privacy = lazy(() => import('./Privacy/Privacy')) export const Privacy = lazy(() => import('./Privacy/Privacy'))

View file

@ -1,38 +1,38 @@
/* eslint-disable no-restricted-globals */ /* eslint-disable no-restricted-globals */
import { clientsClaim, skipWaiting } from 'workbox-core'; import { clientsClaim, skipWaiting } from 'workbox-core'
import { ExpirationPlugin } from 'workbox-expiration'; import { ExpirationPlugin } from 'workbox-expiration'
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'; import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'
import { registerRoute } from 'workbox-routing'; import { registerRoute } from 'workbox-routing'
import { StaleWhileRevalidate, NetworkFirst } from 'workbox-strategies'; import { StaleWhileRevalidate, NetworkFirst } from 'workbox-strategies'
skipWaiting(); skipWaiting()
clientsClaim(); clientsClaim()
// Injection point // Injection point
precacheAndRoute(self.__WB_MANIFEST); precacheAndRoute(self.__WB_MANIFEST)
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$'); const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$')
registerRoute( registerRoute(
// Return false to exempt requests from being fulfilled by index.html. // Return false to exempt requests from being fulfilled by index.html.
({ request, url }) => { ({ request, url }) => {
// If this isn't a navigation, skip. // If this isn't a navigation, skip.
if (request.mode !== 'navigate') { if (request.mode !== 'navigate') {
return false; return false
} // If this is a URL that starts with /_, skip. } // If this is a URL that starts with /_, skip.
if (url.pathname.startsWith('/_')) { if (url.pathname.startsWith('/_')) {
return false; return false
} // If this looks like a URL for a resource, because it contains // a file extension, skip. } // If this looks like a URL for a resource, because it contains // a file extension, skip.
if (url.pathname.match(fileExtensionRegexp)) { if (url.pathname.match(fileExtensionRegexp)) {
return false; return false
} // Return true to signal that we want to use the handler. } // Return true to signal that we want to use the handler.
return true; return true
}, },
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
); )
registerRoute( registerRoute(
// Add in any other file extensions or routing criteria as needed. // Add in any other file extensions or routing criteria as needed.
@ -54,7 +54,7 @@ registerRoute(
new ExpirationPlugin({ maxEntries: 50 }), new ExpirationPlugin({ maxEntries: 50 }),
], ],
}) })
); )
registerRoute( registerRoute(
// Add in any other file extensions or routing criteria as needed. // Add in any other file extensions or routing criteria as needed.
@ -62,4 +62,4 @@ registerRoute(
new NetworkFirst({ new NetworkFirst({
cacheName: 'i18n', cacheName: 'i18n',
}) })
); )