Merge branch 'dev' into main

This commit is contained in:
Ben Grant 2021-03-11 14:09:59 +11:00
commit 920f29e465
12 changed files with 269 additions and 125 deletions

View file

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, Suspense, lazy } from 'react';
import { import {
BrowserRouter, BrowserRouter,
Switch, Switch,
@ -6,14 +6,13 @@ import {
} from 'react-router-dom'; } from 'react-router-dom';
import { ThemeProvider, Global } from '@emotion/react'; import { ThemeProvider, Global } from '@emotion/react';
import { Settings } from 'components'; import { Settings, Loading } from 'components';
import {
Home,
Event,
} from 'pages';
import theme from 'theme'; import theme from 'theme';
const Home = lazy(() => import('pages/Home/Home'));
const Event = lazy(() => import('pages/Event/Event'));
const App = () => { const App = () => {
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)'); const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
const [isDark, setIsDark] = useState(darkQuery.matches); const [isDark, setIsDark] = useState(darkQuery.matches);
@ -61,8 +60,16 @@ const App = () => {
})} })}
/> />
<Switch> <Switch>
<Route path="/" component={Home} exact /> <Route path="/" exact render={props => (
<Route path="/:id" component={Event} exact /> <Suspense fallback={<Loading />}>
<Home {...props} />
</Suspense>
)} />
<Route path="/:id" exact render={props => (
<Suspense fallback={<Loading />}>
<Event {...props} />
</Suspense>
)} />
</Switch> </Switch>
<Settings /> <Settings />

View file

@ -24,6 +24,7 @@ const AvailabilityEditor = ({
times, times,
timeLabels, timeLabels,
dates, dates,
isSpecificDates,
value = [], value = [],
onChange, onChange,
...props ...props
@ -54,12 +55,12 @@ const AvailabilityEditor = ({
)} )}
</TimeLabels> </TimeLabels>
{dates.map((date, x) => { {dates.map((date, x) => {
const parsedDate = dayjs(date, 'DDMMYYYY'); const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
const last = dates.length === x+1 || dayjs(dates[x+1], 'DDMMYYYY').diff(parsedDate, 'day') > 1; const last = dates.length === x+1 || (isSpecificDates ? dayjs(dates[x+1], 'DDMMYYYY') : dayjs().day(dates[x+1])).diff(parsedDate, 'day') > 1;
return ( return (
<Fragment key={x}> <Fragment key={x}>
<Date> <Date>
<DateLabel>{parsedDate.format('MMM D')}</DateLabel> {isSpecificDates && <DateLabel>{parsedDate.format('MMM D')}</DateLabel>}
<DayLabel>{parsedDate.format('ddd')}</DayLabel> <DayLabel>{parsedDate.format('ddd')}</DayLabel>
<Times> <Times>

View file

@ -3,6 +3,8 @@ import dayjs from 'dayjs';
import localeData from 'dayjs/plugin/localeData'; import localeData from 'dayjs/plugin/localeData';
import customParseFormat from 'dayjs/plugin/customParseFormat'; import customParseFormat from 'dayjs/plugin/customParseFormat';
import { useSettingsStore } from 'stores';
import { import {
Wrapper, Wrapper,
Container, Container,
@ -28,12 +30,14 @@ const AvailabilityViewer = ({
times, times,
timeLabels, timeLabels,
dates, dates,
isSpecificDates,
people = [], people = [],
min = 0, min = 0,
max = 0, max = 0,
...props ...props
}) => { }) => {
const [tooltip, setTooltip] = useState(null); const [tooltip, setTooltip] = useState(null);
const timeFormat = useSettingsStore(state => state.timeFormat);
return ( return (
<Wrapper> <Wrapper>
@ -46,12 +50,12 @@ const AvailabilityViewer = ({
)} )}
</TimeLabels> </TimeLabels>
{dates.map((date, i) => { {dates.map((date, i) => {
const parsedDate = dayjs(date, 'DDMMYYYY'); const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
const last = dates.length === i+1 || dayjs(dates[i+1], 'DDMMYYYY').diff(parsedDate, 'day') > 1; const last = dates.length === i+1 || (isSpecificDates ? dayjs(dates[i+1], 'DDMMYYYY') : dayjs().day(dates[i+1])).diff(parsedDate, 'day') > 1;
return ( return (
<Fragment key={i}> <Fragment key={i}>
<Date> <Date>
<DateLabel>{parsedDate.format('MMM D')}</DateLabel> {isSpecificDates && <DateLabel>{parsedDate.format('MMM D')}</DateLabel>}
<DayLabel>{parsedDate.format('ddd')}</DayLabel> <DayLabel>{parsedDate.format('ddd')}</DayLabel>
<Times> <Times>
@ -76,11 +80,12 @@ const AvailabilityViewer = ({
minPeople={min} minPeople={min}
onMouseEnter={(e) => { onMouseEnter={(e) => {
const cellBox = e.currentTarget.getBoundingClientRect(); const cellBox = e.currentTarget.getBoundingClientRect();
const timeText = timeFormat === '12h' ? 'h:mma' : 'HH:mm';
setTooltip({ setTooltip({
x: Math.round(cellBox.x + cellBox.width/2), x: Math.round(cellBox.x + cellBox.width/2),
y: Math.round(cellBox.y + cellBox.height)+6, y: Math.round(cellBox.y + cellBox.height)+6,
available: `${peopleHere.length} / ${people.length} available`, available: `${peopleHere.length} / ${people.length} available`,
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(2, 4)).format('h:mma ddd, D MMM YYYY'), date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(2, 4)).format(isSpecificDates ? `${timeText} ddd, D MMM YYYY` : `${timeText} ddd`),
people: peopleHere.join(', '), people: peopleHere.join(', '),
}); });
}} }}

View file

@ -80,7 +80,7 @@ export const Tooltip = styled.div`
border: 1px solid ${props => props.theme.text}; border: 1px solid ${props => props.theme.text};
border-radius: 3px; border-radius: 3px;
padding: 4px 8px; padding: 4px 8px;
background-color: ${props => props.theme.background}99; background-color: ${props => props.theme.background}DD;
max-width: 200px; max-width: 200px;
pointer-events: none; pointer-events: none;
`; `;

View file

@ -4,7 +4,7 @@ import isToday from 'dayjs/plugin/isToday';
import localeData from 'dayjs/plugin/localeData'; import localeData from 'dayjs/plugin/localeData';
import updateLocale from 'dayjs/plugin/updateLocale'; import updateLocale from 'dayjs/plugin/updateLocale';
import { Button } from 'components'; import { Button, ToggleField } from 'components';
import { useSettingsStore } from 'stores'; import { useSettingsStore } from 'stores';
import { import {
@ -55,6 +55,8 @@ const CalendarField = ({
}) => { }) => {
const weekStart = useSettingsStore(state => state.weekStart); const weekStart = useSettingsStore(state => state.weekStart);
const [type, setType] = useState(0);
const [dates, setDates] = useState(calculateMonth(dayjs().month(), dayjs().year(), weekStart)); const [dates, setDates] = useState(calculateMonth(dayjs().month(), dayjs().year(), weekStart));
const [month, setMonth] = useState(dayjs().month()); const [month, setMonth] = useState(dayjs().month());
const [year, setYear] = useState(dayjs().year()); const [year, setYear] = useState(dayjs().year());
@ -67,6 +69,14 @@ const CalendarField = ({
_setSelectingDates(newDates); _setSelectingDates(newDates);
}; };
const [selectedDays, setSelectedDays] = useState([]);
const [selectingDays, _setSelectingDays] = useState([]);
const staticSelectingDays = useRef([]);
const setSelectingDays = newDays => {
staticSelectingDays.current = newDays;
_setSelectingDays(newDays);
};
const startPos = useRef({}); const startPos = useRef({});
const staticMode = useRef(null); const staticMode = useRef(null);
const [mode, _setMode] = useState(staticMode.current); const [mode, _setMode] = useState(staticMode.current);
@ -93,10 +103,20 @@ const CalendarField = ({
id={id} id={id}
type="hidden" type="hidden"
ref={register} ref={register}
value={JSON.stringify(selectedDates)} value={type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)}
{...props} {...props}
/> />
<ToggleField
id="calendarMode"
name="calendarMode"
options={['Specific dates', 'Days of the week']}
value={type ? 'Days of the week' : 'Specific dates'}
onChange={value => setType(value === 'Specific dates' ? 0 : 1)}
/>
{type === 0 ? (
<>
<CalendarHeader> <CalendarHeader>
<Button <Button
buttonHeight="30px" buttonHeight="30px"
@ -130,8 +150,8 @@ const CalendarField = ({
</CalendarHeader> </CalendarHeader>
<CalendarDays> <CalendarDays>
{dayjs.weekdaysShort().map((name, i) => {dayjs.weekdaysShort().map(name =>
<Day key={i}>{name}</Day> <Day key={name}>{name}</Day>
)} )}
</CalendarDays> </CalendarDays>
<CalendarBody> <CalendarBody>
@ -176,6 +196,46 @@ const CalendarField = ({
) )
)} )}
</CalendarBody> </CalendarBody>
</>
) : (
<CalendarBody>
{dayjs.weekdaysShort().map((name, i) =>
<Date
key={name}
isToday={dayjs.weekdaysShort()[dayjs().day()-weekStart] === name}
title={dayjs.weekdaysShort()[dayjs().day()-weekStart] === name ? 'Today' : ''}
selected={selectedDays.includes(((i + weekStart) % 7 + 7) % 7)}
selecting={selectingDays.includes(((i + weekStart) % 7 + 7) % 7)}
mode={mode}
onPointerDown={(e) => {
startPos.current = i;
setMode(selectedDays.includes(((i + weekStart) % 7 + 7) % 7) ? 'remove' : 'add');
setSelectingDays([((i + weekStart) % 7 + 7) % 7]);
e.currentTarget.releasePointerCapture(e.pointerId);
document.addEventListener('pointerup', () => {
if (staticMode.current === 'add') {
setSelectedDays([...selectedDays, ...staticSelectingDays.current]);
} else if (staticMode.current === 'remove') {
const toRemove = staticSelectingDays.current;
setSelectedDays(selectedDays.filter(d => !toRemove.includes(d)));
}
setMode(null);
}, { once: true });
}}
onPointerEnter={() => {
if (staticMode.current) {
let found = [];
for (let ci = Math.min(startPos.current, i); ci < Math.max(startPos.current, i)+1; ci++) {
found.push(((ci + weekStart) % 7 + 7) % 7);
}
setSelectingDays(found);
}
}}
>{name}</Date>
)}
</CalendarBody>
)}
</Wrapper> </Wrapper>
); );
}; };

View file

@ -12,7 +12,6 @@ export const StyledLabel = styled.label`
export const StyledSubLabel = styled.label` export const StyledSubLabel = styled.label`
display: block; display: block;
padding-bottom: 6px;
font-size: 13px; font-size: 13px;
opacity: .6; opacity: .6;
`; `;

View file

@ -0,0 +1,12 @@
import {
Wrapper,
Loader,
} from './loadingStyle';
const Loading = () => (
<Wrapper>
<Loader />
</Wrapper>
);
export default Loading;

View file

@ -0,0 +1,26 @@
import styled from '@emotion/styled';
export const Wrapper = styled.main`
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
`;
export const Loader = styled.div`
@keyframes load {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
height: 24px;
width: 24px;
border: 3px solid ${props => props.theme.primary};
border-left-color: transparent;
border-radius: 100px;
animation: load .5s linear infinite;
`;

View file

@ -26,7 +26,6 @@ export const HiddenInput = styled.input`
width: 0; width: 0;
position: absolute; position: absolute;
right: -1000px; right: -1000px;
top: 0;
&:checked + label { &:checked + label {
color: ${props => props.theme.background}; color: ${props => props.theme.background};
@ -36,8 +35,12 @@ export const HiddenInput = styled.input`
export const LabelButton = styled.label` export const LabelButton = styled.label`
padding: 6px; padding: 6px;
display: block; display: flex;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
height: 100%;
box-sizing: border-box;
align-items: center;
justify-content: center;
`; `;

View file

@ -9,6 +9,7 @@ export { default as Legend } from './Legend/Legend';
export { default as AvailabilityViewer } from './AvailabilityViewer/AvailabilityViewer'; export { default as AvailabilityViewer } from './AvailabilityViewer/AvailabilityViewer';
export { default as AvailabilityEditor } from './AvailabilityEditor/AvailabilityEditor'; export { default as AvailabilityEditor } from './AvailabilityEditor/AvailabilityEditor';
export { default as Error } from './Error/Error'; export { default as Error } from './Error/Error';
export { default as Loading } from './Loading/Loading';
export { default as Center } from './Center/Center'; export { default as Center } from './Center/Center';
export { default as Donate } from './Donate/Donate'; export { default as Donate } from './Donate/Donate';

View file

@ -45,6 +45,7 @@ dayjs.extend(customParseFormat);
const Event = (props) => { const Event = (props) => {
const timeFormat = useSettingsStore(state => state.timeFormat); const timeFormat = useSettingsStore(state => state.timeFormat);
const weekStart = useSettingsStore(state => state.weekStart);
const { register, handleSubmit } = useForm(); const { register, handleSubmit } = useForm();
const { id } = props.match.params; const { id } = props.match.params;
@ -87,7 +88,9 @@ const Event = (props) => {
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.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY')), 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); setPeople(adjustedPeople);
} catch (e) { } catch (e) {
@ -103,21 +106,32 @@ const Event = (props) => {
// 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;
setTimes(event.times.reduce( setTimes(event.times.reduce(
(allTimes, time) => { (allTimes, time) => {
const date = dayjs(time, 'HHmm-DDMMYYYY').utc(true).tz(timezone); 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 [ return [
...allTimes, ...allTimes,
date.minute(0).format('HHmm-DDMMYYYY'), date.minute(0).format(format),
date.minute(15).format('HHmm-DDMMYYYY'), date.minute(15).format(format),
date.minute(30).format('HHmm-DDMMYYYY'), date.minute(30).format(format),
date.minute(45).format('HHmm-DDMMYYYY'), date.minute(45).format(format),
]; ];
}, },
[] []
).sort((a, b) => dayjs(a, 'HHmm-DDMMYYYY').diff(dayjs(b, 'HHmm-DDMMYYYY')))); ).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]); }));
}
}, [event, timezone, weekStart]);
useEffect(() => { useEffect(() => {
if (!!times.length && !!people.length) { if (!!times.length && !!people.length) {
@ -183,7 +197,9 @@ const Event = (props) => {
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.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY')), 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); setUser(adjustedUser);
} catch (e) { } catch (e) {
@ -192,7 +208,6 @@ const Event = (props) => {
}; };
if (user) { if (user) {
console.log('FETCHING', timezone);
fetchUser(); fetchUser();
} }
// eslint-disable-next-line // eslint-disable-next-line
@ -210,7 +225,9 @@ const Event = (props) => {
setPassword(data.password); setPassword(data.password);
const adjustedUser = { const adjustedUser = {
...response.data, ...response.data,
availability: response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY')), 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); setUser(adjustedUser);
setTab('you'); setTab('you');
@ -361,6 +378,7 @@ const Event = (props) => {
times={times} times={times}
timeLabels={timeLabels} timeLabels={timeLabels}
dates={dates} dates={dates}
isSpecificDates={!!dates.length && dates[0].length === 8}
people={people.filter(p => p.availability.length > 0)} people={people.filter(p => p.availability.length > 0)}
min={min} min={min}
max={max} max={max}
@ -375,10 +393,13 @@ const Event = (props) => {
times={times} times={times}
timeLabels={timeLabels} timeLabels={timeLabels}
dates={dates} dates={dates}
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.map(date => dayjs.tz(date, 'HHmm-DDMMYYYY', timezone).utc().format('HHmm-DDMMYYYY')); 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 }); setUser({ ...user, availability });
try { try {
await api.patch(`/event/${id}/people/${user.name}`, { await api.patch(`/event/${id}/people/${user.name}`, {

View file

@ -82,6 +82,7 @@ const Home = () => {
if (dates.length === 0) { if (dates.length === 0) {
return setError(`You haven't selected any dates!`); return setError(`You haven't selected any dates!`);
} }
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
if (start === end) { if (start === end) {
return setError(`The start and end times can't be the same`); return setError(`The start and end times can't be the same`);
} }
@ -89,23 +90,31 @@ const Home = () => {
let times = dates.reduce((times, date) => { let times = dates.reduce((times, date) => {
let day = []; let day = [];
for (let i = start; i < (start > end ? 24 : end); i++) { for (let i = start; i < (start > end ? 24 : end); i++) {
if (isSpecificDates) {
day.push( day.push(
dayjs.tz(date, 'DDMMYYYY', data.timezone) dayjs.tz(date, 'DDMMYYYY', data.timezone)
.hour(i) .hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
.minute(0)
.utc()
.format('HHmm-DDMMYYYY')
); );
} else {
day.push(
dayjs().tz(data.timezone)
.day(date).hour(i).minute(0).utc().format('HHmm-d')
);
}
} }
if (start > end) { if (start > end) {
for (let i = 0; i < end; i++) { for (let i = 0; i < end; i++) {
if (isSpecificDates) {
day.push( day.push(
dayjs.tz(date, 'DDMMYYYY', data.timezone) dayjs.tz(date, 'DDMMYYYY', data.timezone)
.hour(i) .hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
.minute(0)
.utc()
.format('HHmm-DDMMYYYY')
); );
} else {
day.push(
dayjs().tz(data.timezone)
.day(date).hour(i).minute(0).utc().format('HHmm-d')
);
}
} }
} }
return [...times, ...day]; return [...times, ...day];