diff --git a/crabfit-frontend/src/App.tsx b/crabfit-frontend/src/App.tsx
index 74cdd38..e7fd787 100644
--- a/crabfit-frontend/src/App.tsx
+++ b/crabfit-frontend/src/App.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, Suspense, lazy } from 'react';
import {
BrowserRouter,
Switch,
@@ -6,14 +6,13 @@ import {
} from 'react-router-dom';
import { ThemeProvider, Global } from '@emotion/react';
-import { Settings } from 'components';
-import {
- Home,
- Event,
-} from 'pages';
+import { Settings, Loading } from 'components';
import theme from 'theme';
+const Home = lazy(() => import('pages/Home/Home'));
+const Event = lazy(() => import('pages/Event/Event'));
+
const App = () => {
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
const [isDark, setIsDark] = useState(darkQuery.matches);
@@ -61,8 +60,16 @@ const App = () => {
})}
/>
-
-
+ (
+ }>
+
+
+ )} />
+ (
+ }>
+
+
+ )} />
diff --git a/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx b/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx
index 621f1e0..927799f 100644
--- a/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx
+++ b/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx
@@ -24,6 +24,7 @@ const AvailabilityEditor = ({
times,
timeLabels,
dates,
+ isSpecificDates,
value = [],
onChange,
...props
@@ -54,12 +55,12 @@ const AvailabilityEditor = ({
)}
{dates.map((date, x) => {
- const parsedDate = dayjs(date, 'DDMMYYYY');
- const last = dates.length === x+1 || dayjs(dates[x+1], 'DDMMYYYY').diff(parsedDate, 'day') > 1;
+ const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
+ const last = dates.length === x+1 || (isSpecificDates ? dayjs(dates[x+1], 'DDMMYYYY') : dayjs().day(dates[x+1])).diff(parsedDate, 'day') > 1;
return (
- {parsedDate.format('MMM D')}
+ {isSpecificDates && {parsedDate.format('MMM D')}}
{parsedDate.format('ddd')}
diff --git a/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx b/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx
index 1a3e77e..37a6ac1 100644
--- a/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx
+++ b/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx
@@ -3,6 +3,8 @@ import dayjs from 'dayjs';
import localeData from 'dayjs/plugin/localeData';
import customParseFormat from 'dayjs/plugin/customParseFormat';
+import { useSettingsStore } from 'stores';
+
import {
Wrapper,
Container,
@@ -28,12 +30,14 @@ const AvailabilityViewer = ({
times,
timeLabels,
dates,
+ isSpecificDates,
people = [],
min = 0,
max = 0,
...props
}) => {
const [tooltip, setTooltip] = useState(null);
+ const timeFormat = useSettingsStore(state => state.timeFormat);
return (
@@ -46,12 +50,12 @@ const AvailabilityViewer = ({
)}
{dates.map((date, i) => {
- const parsedDate = dayjs(date, 'DDMMYYYY');
- const last = dates.length === i+1 || dayjs(dates[i+1], 'DDMMYYYY').diff(parsedDate, 'day') > 1;
+ const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
+ const last = dates.length === i+1 || (isSpecificDates ? dayjs(dates[i+1], 'DDMMYYYY') : dayjs().day(dates[i+1])).diff(parsedDate, 'day') > 1;
return (
- {parsedDate.format('MMM D')}
+ {isSpecificDates && {parsedDate.format('MMM D')}}
{parsedDate.format('ddd')}
@@ -76,11 +80,12 @@ const AvailabilityViewer = ({
minPeople={min}
onMouseEnter={(e) => {
const cellBox = e.currentTarget.getBoundingClientRect();
+ const timeText = timeFormat === '12h' ? 'h:mma' : 'HH:mm';
setTooltip({
x: Math.round(cellBox.x + cellBox.width/2),
y: Math.round(cellBox.y + cellBox.height)+6,
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(', '),
});
}}
diff --git a/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts b/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts
index 4023a30..1abf22a 100644
--- a/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts
+++ b/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts
@@ -80,7 +80,7 @@ export const Tooltip = styled.div`
border: 1px solid ${props => props.theme.text};
border-radius: 3px;
padding: 4px 8px;
- background-color: ${props => props.theme.background}99;
+ background-color: ${props => props.theme.background}DD;
max-width: 200px;
pointer-events: none;
`;
diff --git a/crabfit-frontend/src/components/CalendarField/CalendarField.tsx b/crabfit-frontend/src/components/CalendarField/CalendarField.tsx
index dea287e..b46e631 100644
--- a/crabfit-frontend/src/components/CalendarField/CalendarField.tsx
+++ b/crabfit-frontend/src/components/CalendarField/CalendarField.tsx
@@ -4,7 +4,7 @@ import isToday from 'dayjs/plugin/isToday';
import localeData from 'dayjs/plugin/localeData';
import updateLocale from 'dayjs/plugin/updateLocale';
-import { Button } from 'components';
+import { Button, ToggleField } from 'components';
import { useSettingsStore } from 'stores';
import {
@@ -55,7 +55,9 @@ const CalendarField = ({
}) => {
const weekStart = useSettingsStore(state => state.weekStart);
- const [dates, setDates] = useState(calculateMonth(dayjs().month(), dayjs().year(), weekStart));
+ const [type, setType] = useState(0);
+
+ const [dates, setDates] = useState(calculateMonth(dayjs().month(), dayjs().year(), weekStart));
const [month, setMonth] = useState(dayjs().month());
const [year, setYear] = useState(dayjs().year());
@@ -67,6 +69,14 @@ const CalendarField = ({
_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 staticMode = useRef(null);
const [mode, _setMode] = useState(staticMode.current);
@@ -93,89 +103,139 @@ const CalendarField = ({
id={id}
type="hidden"
ref={register}
- value={JSON.stringify(selectedDates)}
+ value={type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)}
{...props}
/>
-
-
- {dayjs.months()[month]} {year}
-
-
+ setType(value === 'Specific dates' ? 0 : 1)}
+ />
-
- {dayjs.weekdaysShort().map((name, i) =>
- {name}
- )}
-
-
- {dates.length > 0 && dates.map((dateRow, y) =>
- dateRow.map((date, x) =>
- {
- startPos.current = {x, y};
- setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
- setSelectingDates([date]);
- e.currentTarget.releasePointerCapture(e.pointerId);
+ {type === 0 ? (
+ <>
+
+
+ {dayjs.months()[month]} {year}
+
+
- document.addEventListener('pointerup', () => {
- if (staticMode.current === 'add') {
- setSelectedDates([...selectedDates, ...staticSelectingDates.current.map(d => d.format('DDMMYYYY'))]);
- } else if (staticMode.current === 'remove') {
- const toRemove = staticSelectingDates.current.map(d => d.format('DDMMYYYY'));
- setSelectedDates(selectedDates.filter(d => !toRemove.includes(d)));
- }
- setMode(null);
- }, { once: true });
- }}
- onPointerEnter={() => {
- if (staticMode.current) {
- let found = [];
- for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
- for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
- found.push({y: cy, x: cx});
- }
- }
- setSelectingDates(found.map(d => dates[d.y][d.x]));
- }
- }}
- >{date.date()}
- )
- )}
-
+
+ {dayjs.weekdaysShort().map(name =>
+ {name}
+ )}
+
+
+ {dates.length > 0 && dates.map((dateRow, y) =>
+ dateRow.map((date, x) =>
+ {
+ startPos.current = {x, y};
+ setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
+ setSelectingDates([date]);
+ e.currentTarget.releasePointerCapture(e.pointerId);
+
+ document.addEventListener('pointerup', () => {
+ if (staticMode.current === 'add') {
+ setSelectedDates([...selectedDates, ...staticSelectingDates.current.map(d => d.format('DDMMYYYY'))]);
+ } else if (staticMode.current === 'remove') {
+ const toRemove = staticSelectingDates.current.map(d => d.format('DDMMYYYY'));
+ setSelectedDates(selectedDates.filter(d => !toRemove.includes(d)));
+ }
+ setMode(null);
+ }, { once: true });
+ }}
+ onPointerEnter={() => {
+ if (staticMode.current) {
+ let found = [];
+ for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
+ for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
+ found.push({y: cy, x: cx});
+ }
+ }
+ setSelectingDates(found.map(d => dates[d.y][d.x]));
+ }
+ }}
+ >{date.date()}
+ )
+ )}
+
+ >
+ ) : (
+
+ {dayjs.weekdaysShort().map((name, i) =>
+ {
+ 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}
+ )}
+
+ )}
);
};
diff --git a/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts b/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts
index e415def..b664e32 100644
--- a/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts
+++ b/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts
@@ -12,7 +12,6 @@ export const StyledLabel = styled.label`
export const StyledSubLabel = styled.label`
display: block;
- padding-bottom: 6px;
font-size: 13px;
opacity: .6;
`;
diff --git a/crabfit-frontend/src/components/Loading/Loading.tsx b/crabfit-frontend/src/components/Loading/Loading.tsx
new file mode 100644
index 0000000..23606b7
--- /dev/null
+++ b/crabfit-frontend/src/components/Loading/Loading.tsx
@@ -0,0 +1,12 @@
+import {
+ Wrapper,
+ Loader,
+} from './loadingStyle';
+
+const Loading = () => (
+
+
+
+);
+
+export default Loading;
diff --git a/crabfit-frontend/src/components/Loading/loadingStyle.ts b/crabfit-frontend/src/components/Loading/loadingStyle.ts
new file mode 100644
index 0000000..44bd8bb
--- /dev/null
+++ b/crabfit-frontend/src/components/Loading/loadingStyle.ts
@@ -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;
+`;
diff --git a/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts b/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts
index a1e91aa..c33601c 100644
--- a/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts
+++ b/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts
@@ -26,7 +26,6 @@ export const HiddenInput = styled.input`
width: 0;
position: absolute;
right: -1000px;
- top: 0;
&:checked + label {
color: ${props => props.theme.background};
@@ -36,8 +35,12 @@ export const HiddenInput = styled.input`
export const LabelButton = styled.label`
padding: 6px;
- display: block;
+ display: flex;
text-align: center;
cursor: pointer;
user-select: none;
+ height: 100%;
+ box-sizing: border-box;
+ align-items: center;
+ justify-content: center;
`;
diff --git a/crabfit-frontend/src/components/index.ts b/crabfit-frontend/src/components/index.ts
index 2cff948..dc7571c 100644
--- a/crabfit-frontend/src/components/index.ts
+++ b/crabfit-frontend/src/components/index.ts
@@ -9,6 +9,7 @@ export { default as Legend } from './Legend/Legend';
export { default as AvailabilityViewer } from './AvailabilityViewer/AvailabilityViewer';
export { default as AvailabilityEditor } from './AvailabilityEditor/AvailabilityEditor';
export { default as Error } from './Error/Error';
+export { default as Loading } from './Loading/Loading';
export { default as Center } from './Center/Center';
export { default as Donate } from './Donate/Donate';
diff --git a/crabfit-frontend/src/pages/Event/Event.tsx b/crabfit-frontend/src/pages/Event/Event.tsx
index b7a9392..d000fe0 100644
--- a/crabfit-frontend/src/pages/Event/Event.tsx
+++ b/crabfit-frontend/src/pages/Event/Event.tsx
@@ -45,6 +45,7 @@ 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;
@@ -87,7 +88,9 @@ const Event = (props) => {
const response = await api.get(`/event/${id}/people`);
const adjustedPeople = response.data.people.map(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);
} catch (e) {
@@ -103,21 +106,32 @@ const Event = (props) => {
// 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 = 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 [
...allTimes,
- date.minute(0).format('HHmm-DDMMYYYY'),
- date.minute(15).format('HHmm-DDMMYYYY'),
- date.minute(30).format('HHmm-DDMMYYYY'),
- date.minute(45).format('HHmm-DDMMYYYY'),
+ date.minute(0).format(format),
+ date.minute(15).format(format),
+ date.minute(30).format(format),
+ 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(() => {
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 adjustedUser = {
...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);
} catch (e) {
@@ -192,7 +208,6 @@ const Event = (props) => {
};
if (user) {
- console.log('FETCHING', timezone);
fetchUser();
}
// eslint-disable-next-line
@@ -210,7 +225,9 @@ const Event = (props) => {
setPassword(data.password);
const adjustedUser = {
...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);
setTab('you');
@@ -361,6 +378,7 @@ const Event = (props) => {
times={times}
timeLabels={timeLabels}
dates={dates}
+ isSpecificDates={!!dates.length && dates[0].length === 8}
people={people.filter(p => p.availability.length > 0)}
min={min}
max={max}
@@ -375,10 +393,13 @@ const Event = (props) => {
times={times}
timeLabels={timeLabels}
dates={dates}
+ isSpecificDates={!!dates.length && dates[0].length === 8}
value={user.availability}
onChange={async 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 });
try {
await api.patch(`/event/${id}/people/${user.name}`, {
diff --git a/crabfit-frontend/src/pages/Home/Home.tsx b/crabfit-frontend/src/pages/Home/Home.tsx
index 4e718a3..c64f749 100644
--- a/crabfit-frontend/src/pages/Home/Home.tsx
+++ b/crabfit-frontend/src/pages/Home/Home.tsx
@@ -82,6 +82,7 @@ const Home = () => {
if (dates.length === 0) {
return setError(`You haven't selected any dates!`);
}
+ const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
if (start === end) {
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 day = [];
for (let i = start; i < (start > end ? 24 : end); i++) {
- day.push(
- dayjs.tz(date, 'DDMMYYYY', data.timezone)
- .hour(i)
- .minute(0)
- .utc()
- .format('HHmm-DDMMYYYY')
- );
+ if (isSpecificDates) {
+ day.push(
+ dayjs.tz(date, 'DDMMYYYY', data.timezone)
+ .hour(i).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) {
for (let i = 0; i < end; i++) {
- day.push(
- dayjs.tz(date, 'DDMMYYYY', data.timezone)
- .hour(i)
- .minute(0)
- .utc()
- .format('HHmm-DDMMYYYY')
- );
+ if (isSpecificDates) {
+ day.push(
+ dayjs.tz(date, 'DDMMYYYY', data.timezone)
+ .hour(i).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];