diff --git a/crabfit-backend/cron.yaml b/crabfit-backend/cron.yaml
index a999bc7..5f7911d 100644
--- a/crabfit-backend/cron.yaml
+++ b/crabfit-backend/cron.yaml
@@ -7,3 +7,7 @@ cron:
url: /tasks/legacyCleanup
schedule: every tuesday 09:00
target: api
+ - description: "remove people with an event id that no longer exists"
+ url: /tasks/removeOrphans
+ schedule: 1st wednesday of month 09:00
+ target: api
diff --git a/crabfit-backend/index.js b/crabfit-backend/index.js
index 7dc0b21..47f3dab 100644
--- a/crabfit-backend/index.js
+++ b/crabfit-backend/index.js
@@ -16,6 +16,7 @@ const updatePerson = require('./routes/updatePerson');
const taskCleanup = require('./routes/taskCleanup');
const taskLegacyCleanup = require('./routes/taskLegacyCleanup');
+const taskRemoveOrphans = require('./routes/taskRemoveOrphans');
const app = express();
const port = 8080;
@@ -53,6 +54,7 @@ app.patch('/event/:eventId/people/:personName', updatePerson);
// Tasks
app.get('/tasks/cleanup', taskCleanup);
app.get('/tasks/legacyCleanup', taskLegacyCleanup);
+app.get('/tasks/removeOrphans', taskRemoveOrphans);
app.listen(port, () => {
console.log(`Crabfit API listening at http://localhost:${port} in ${process.env.NODE_ENV === 'production' ? 'prod' : 'dev'} mode`)
diff --git a/crabfit-backend/routes/taskCleanup.js b/crabfit-backend/routes/taskCleanup.js
index 28182e5..fa87ae9 100644
--- a/crabfit-backend/routes/taskCleanup.js
+++ b/crabfit-backend/routes/taskCleanup.js
@@ -1,6 +1,10 @@
const dayjs = require('dayjs');
module.exports = async (req, res) => {
+ if (req.header('X-Appengine-Cron') === undefined) {
+ return res.status(400).send('This task can only be run from a cron job');
+ }
+
const threeMonthsAgo = dayjs().subtract(3, 'month').unix();
console.log(`Running cleanup task at ${dayjs().format('h:mma D MMM YYYY')}`);
diff --git a/crabfit-backend/routes/taskLegacyCleanup.js b/crabfit-backend/routes/taskLegacyCleanup.js
index cbb3e6e..1d7a64d 100644
--- a/crabfit-backend/routes/taskLegacyCleanup.js
+++ b/crabfit-backend/routes/taskLegacyCleanup.js
@@ -1,6 +1,10 @@
const dayjs = require('dayjs');
module.exports = async (req, res) => {
+ if (req.header('X-Appengine-Cron') === undefined) {
+ return res.status(400).send('This task can only be run from a cron job');
+ }
+
const threeMonthsAgo = dayjs().subtract(3, 'month').unix();
console.log(`Running LEGACY cleanup task at ${dayjs().format('h:mma D MMM YYYY')}`);
diff --git a/crabfit-backend/routes/taskRemoveOrphans.js b/crabfit-backend/routes/taskRemoveOrphans.js
new file mode 100644
index 0000000..320d276
--- /dev/null
+++ b/crabfit-backend/routes/taskRemoveOrphans.js
@@ -0,0 +1,46 @@
+const dayjs = require('dayjs');
+
+module.exports = async (req, res) => {
+ if (req.header('X-Appengine-Cron') === undefined) {
+ return res.status(400).send('This task can only be run from a cron job');
+ }
+
+ const threeMonthsAgo = dayjs().subtract(3, 'month').unix();
+
+ console.log(`Running orphan removal task at ${dayjs().format('h:mma D MMM YYYY')}`);
+
+ try {
+ // Fetch people that are older than 3 months
+ const peopleQuery = req.datastore.createQuery(req.types.person).filter('created', '<', threeMonthsAgo);
+ let oldPeople = (await req.datastore.runQuery(peopleQuery))[0];
+
+ if (oldPeople && oldPeople.length > 0) {
+ console.log(`Found ${oldPeople.length} people older than 3 months, checking for events`);
+
+ // Fetch events linked to the people discovered
+ let peopleWithoutEvents = 0;
+ await Promise.all(oldPeople.map(async (person) => {
+ let event = (await req.datastore.get(req.datastore.key([req.types.event, person.eventId])))[0];
+
+ if (!event) {
+ peopleWithoutEvents++;
+ await req.datastore.delete(person[req.datastore.KEY]);
+ }
+ }));
+
+ if (peopleWithoutEvents > 0) {
+ console.log(`Orphan removal successful: ${oldEventIds.length} events and ${peopleDiscovered} people removed`);
+ res.sendStatus(200);
+ } else {
+ console.log(`Found 0 people without events, ending orphan removal`);
+ res.sendStatus(404);
+ }
+ } else {
+ console.log(`Found 0 people older than 3 months, ending orphan removal`);
+ res.sendStatus(404);
+ }
+ } catch (e) {
+ console.error(e);
+ res.sendStatus(404);
+ }
+};
diff --git a/crabfit-backend/swagger.yaml b/crabfit-backend/swagger.yaml
index 7e15592..ad68a66 100644
--- a/crabfit-backend/swagger.yaml
+++ b/crabfit-backend/swagger.yaml
@@ -243,3 +243,16 @@ paths:
description: "Not found"
400:
description: "Not called from a cron job"
+ "/tasks/removeOrphans":
+ get:
+ summary: "Deletes people if the event they were created under no longer exists"
+ operationId: "taskRemoveOrphans"
+ tags:
+ - tasks
+ responses:
+ 200:
+ description: "OK"
+ 404:
+ description: "Not found"
+ 400:
+ description: "Not called from a cron job"
diff --git a/crabfit-browser-extension/manifest.json b/crabfit-browser-extension/manifest.json
index ed47952..c56c83d 100644
--- a/crabfit-browser-extension/manifest.json
+++ b/crabfit-browser-extension/manifest.json
@@ -1,7 +1,7 @@
{
"name": "Crab Fit",
"description": "Enter your availability to find a time that works for everyone!",
- "version": "1.1",
+ "version": "1.2",
"manifest_version": 2,
"author": "Ben Grant",
diff --git a/crabfit-browser-extension/res/icon128.png b/crabfit-browser-extension/res/icon128.png
index 6b9643d..4ee617e 100644
Binary files a/crabfit-browser-extension/res/icon128.png and b/crabfit-browser-extension/res/icon128.png differ
diff --git a/crabfit-browser-extension/res/icon16.png b/crabfit-browser-extension/res/icon16.png
index 9c24490..24e6b59 100644
Binary files a/crabfit-browser-extension/res/icon16.png and b/crabfit-browser-extension/res/icon16.png differ
diff --git a/crabfit-browser-extension/res/icon32.png b/crabfit-browser-extension/res/icon32.png
index 11987f1..9995d95 100644
Binary files a/crabfit-browser-extension/res/icon32.png and b/crabfit-browser-extension/res/icon32.png differ
diff --git a/crabfit-browser-extension/res/icon48.png b/crabfit-browser-extension/res/icon48.png
index 11650f4..36bfbf8 100644
Binary files a/crabfit-browser-extension/res/icon48.png and b/crabfit-browser-extension/res/icon48.png differ
diff --git a/crabfit-frontend/public/favicon.ico b/crabfit-frontend/public/favicon.ico
index bf79c38..26db6ba 100644
Binary files a/crabfit-frontend/public/favicon.ico and b/crabfit-frontend/public/favicon.ico differ
diff --git a/crabfit-frontend/public/index.css b/crabfit-frontend/public/index.css
index 399b4a0..80d01b8 100644
--- a/crabfit-frontend/public/index.css
+++ b/crabfit-frontend/public/index.css
@@ -1,7 +1,7 @@
@font-face {
- font-family: Karla;
- src: url('fonts/karla-variable.ttf') format('truetype');
- font-weight: 1 999;
+ font-family: Karla;
+ src: url('fonts/karla-variable.ttf') format('truetype');
+ font-weight: 1 999;
}
@font-face {
diff --git a/crabfit-frontend/public/index.html b/crabfit-frontend/public/index.html
index 4ccb9ac..2352c9d 100644
--- a/crabfit-frontend/public/index.html
+++ b/crabfit-frontend/public/index.html
@@ -5,10 +5,10 @@
-
+
-
-
-
+
+
+
-
+
Crab Fit
diff --git a/crabfit-frontend/public/logo-icon.png b/crabfit-frontend/public/logo-icon.png
index 40961ea..729153e 100644
Binary files a/crabfit-frontend/public/logo-icon.png and b/crabfit-frontend/public/logo-icon.png differ
diff --git a/crabfit-frontend/public/logo192.png b/crabfit-frontend/public/logo192.png
index 70f0a2c..f80366e 100644
Binary files a/crabfit-frontend/public/logo192.png and b/crabfit-frontend/public/logo192.png differ
diff --git a/crabfit-frontend/public/logo512.png b/crabfit-frontend/public/logo512.png
index 82acb67..4ebd853 100644
Binary files a/crabfit-frontend/public/logo512.png and b/crabfit-frontend/public/logo512.png differ
diff --git a/crabfit-frontend/src/App.tsx b/crabfit-frontend/src/App.tsx
index 7a4b0aa..0154143 100644
--- a/crabfit-frontend/src/App.tsx
+++ b/crabfit-frontend/src/App.tsx
@@ -20,8 +20,8 @@ const wb = new Workbox('sw.js');
const App = () => {
const colortheme = useSettingsStore(state => state.theme);
- const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
- const [isDark, setIsDark] = useState(darkQuery.matches);
+ const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ const [isDark, setIsDark] = useState(darkQuery.matches);
const [offline, setOffline] = useState(!window.navigator.onLine);
const [eggCount, setEggCount] = useState(0);
@@ -46,7 +46,7 @@ const App = () => {
[eggCount, eggKey]
);
- darkQuery.addListener(e => colortheme === 'System' && setIsDark(e.matches));
+ darkQuery.addListener(e => colortheme === 'System' && setIsDark(e.matches));
useEffect(() => {
const onOffline = () => setOffline(true);
@@ -87,56 +87,57 @@ const App = () => {
}, [colortheme, darkQuery.matches]);
return (
-
-
- ({
- html: {
- scrollBehavior: 'smooth',
- },
- body: {
- backgroundColor: theme.background,
- color: theme.text,
- fontFamily: `'Karla', sans-serif`,
- fontWeight: theme.mode === 'dark' ? 500 : 600,
- margin: 0,
- },
- a: {
- color: theme.primary,
- },
- '*::-webkit-scrollbar': {
- width: 16,
- height: 16,
- },
- '*::-webkit-scrollbar-track': {
- background: `${theme.primaryBackground}`,
- },
- '*::-webkit-scrollbar-thumb': {
- borderRadius: 100,
- border: `4px solid ${theme.primaryBackground}`,
- width: 12,
- background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}AA`,
- },
- '*::-webkit-scrollbar-thumb:hover': {
- background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}CC`,
- },
- '*::-webkit-scrollbar-thumb:active': {
- background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}`,
- },
- })}
- />
+
+
+ ({
+ html: {
+ scrollBehavior: 'smooth',
+ '-webkit-print-color-adjust': 'exact',
+ },
+ body: {
+ backgroundColor: theme.background,
+ color: theme.text,
+ fontFamily: `'Karla', sans-serif`,
+ fontWeight: theme.mode === 'dark' ? 500 : 600,
+ margin: 0,
+ },
+ a: {
+ color: theme.primary,
+ },
+ '*::-webkit-scrollbar': {
+ width: 16,
+ height: 16,
+ },
+ '*::-webkit-scrollbar-track': {
+ background: `${theme.primaryBackground}`,
+ },
+ '*::-webkit-scrollbar-thumb': {
+ borderRadius: 100,
+ border: `4px solid ${theme.primaryBackground}`,
+ width: 12,
+ background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}AA`,
+ },
+ '*::-webkit-scrollbar-thumb:hover': {
+ background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}CC`,
+ },
+ '*::-webkit-scrollbar-thumb:active': {
+ background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}`,
+ },
+ })}
+ />
}>
-
- (
+
+ (
}>
)} />
- (
+ (
}>
@@ -146,17 +147,17 @@ const App = () => {
)} />
- (
+ (
}>
)} />
- (
+ (
}>
)} />
-
+
{updateAvailable && (
}>
@@ -165,8 +166,8 @@ const App = () => {
)}
{eggVisible && setEggVisible(false)} />}
-
-
+
+
);
}
diff --git a/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx b/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx
index b1a93d7..165ed5b 100644
--- a/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx
+++ b/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx
@@ -9,17 +9,17 @@ import dayjs_timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import {
- Wrapper,
+ Wrapper,
ScrollWrapper,
- Container,
- Date,
- Times,
- DateLabel,
- DayLabel,
- Spacer,
- TimeLabels,
- TimeLabel,
- TimeSpace,
+ Container,
+ Date,
+ Times,
+ DateLabel,
+ DayLabel,
+ Spacer,
+ TimeLabels,
+ TimeLabel,
+ TimeSpace,
StyledMain,
} from 'components/AvailabilityViewer/availabilityViewerStyle';
import { Time } from './availabilityEditorStyle';
@@ -37,34 +37,34 @@ dayjs.extend(utc);
dayjs.extend(dayjs_timezone);
const AvailabilityEditor = ({
- times,
- timeLabels,
- dates,
+ times,
+ timeLabels,
+ dates,
timezone,
isSpecificDates,
- value = [],
- onChange,
- ...props
+ value = [],
+ onChange,
+ ...props
}) => {
const { t } = useTranslation('event');
const locale = useLocaleUpdateStore(state => state.locale);
- const [selectingTimes, _setSelectingTimes] = useState([]);
- const staticSelectingTimes = useRef([]);
- const setSelectingTimes = newTimes => {
- staticSelectingTimes.current = newTimes;
- _setSelectingTimes(newTimes);
- };
+ const [selectingTimes, _setSelectingTimes] = useState([]);
+ const staticSelectingTimes = useRef([]);
+ const setSelectingTimes = newTimes => {
+ staticSelectingTimes.current = newTimes;
+ _setSelectingTimes(newTimes);
+ };
- const startPos = useRef({});
- const staticMode = useRef(null);
- const [mode, _setMode] = useState(staticMode.current);
- const setMode = newMode => {
- staticMode.current = newMode;
- _setMode(newMode);
- };
+ const startPos = useRef({});
+ const staticMode = useRef(null);
+ const [mode, _setMode] = useState(staticMode.current);
+ const setMode = newMode => {
+ staticMode.current = newMode;
+ _setMode(newMode);
+ };
- return (
+ return (
<>
{t('event:you.info')}
@@ -98,89 +98,89 @@ const AvailabilityEditor = ({
)}
-
+
-
-
- {!!timeLabels.length && timeLabels.map((label, i) =>
-
- {label.label?.length !== '' && {label.label} }
-
- )}
-
- {dates.map((date, x) => {
- 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 (
-
-
- {isSpecificDates && {parsedDate.format('MMM D')} }
- {parsedDate.format('ddd')}
+
+
+ {!!timeLabels.length && timeLabels.map((label, i) =>
+
+ {label.label?.length !== '' && {label.label} }
+
+ )}
+
+ {dates.map((date, x) => {
+ 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 (
+
+
+ {isSpecificDates && {parsedDate.format('MMM D')} }
+ {parsedDate.format('ddd')}
1}
>
- {timeLabels.map((timeLabel, y) => {
- if (!timeLabel.time) return null;
- if (!times.includes(`${timeLabel.time}-${date}`)) {
- return (
-
- );
- }
- const time = `${timeLabel.time}-${date}`;
+ {timeLabels.map((timeLabel, y) => {
+ if (!timeLabel.time) return null;
+ if (!times.includes(`${timeLabel.time}-${date}`)) {
+ return (
+
+ );
+ }
+ const time = `${timeLabel.time}-${date}`;
- return (
- {
- e.preventDefault();
- startPos.current = {x, y};
- setMode(value.includes(time) ? 'remove' : 'add');
- setSelectingTimes([time]);
- e.currentTarget.releasePointerCapture(e.pointerId);
+ return (
+ {
+ e.preventDefault();
+ startPos.current = {x, y};
+ setMode(value.includes(time) ? 'remove' : 'add');
+ setSelectingTimes([time]);
+ e.currentTarget.releasePointerCapture(e.pointerId);
- document.addEventListener('pointerup', () => {
- if (staticMode.current === 'add') {
- onChange([...value, ...staticSelectingTimes.current]);
- } else if (staticMode.current === 'remove') {
- onChange(value.filter(t => !staticSelectingTimes.current.includes(t)));
- }
- 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});
- }
- }
- setSelectingTimes(found.filter(d => timeLabels[d.y].time?.length === 4).map(d => `${timeLabels[d.y].time}-${dates[d.x]}`));
- }
- }}
- />
- );
- })}
-
-
- {last && dates.length !== x+1 && (
-
- )}
-
- );
- })}
-
+ document.addEventListener('pointerup', () => {
+ if (staticMode.current === 'add') {
+ onChange([...value, ...staticSelectingTimes.current]);
+ } else if (staticMode.current === 'remove') {
+ onChange(value.filter(t => !staticSelectingTimes.current.includes(t)));
+ }
+ 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});
+ }
+ }
+ setSelectingTimes(found.filter(d => timeLabels[d.y].time?.length === 4).map(d => `${timeLabels[d.y].time}-${dates[d.x]}`));
+ }
+ }}
+ />
+ );
+ })}
+
+
+ {last && dates.length !== x+1 && (
+
+ )}
+
+ );
+ })}
+
-
+
>
- );
+ );
};
export default AvailabilityEditor;
diff --git a/crabfit-frontend/src/components/AvailabilityEditor/availabilityEditorStyle.ts b/crabfit-frontend/src/components/AvailabilityEditor/availabilityEditorStyle.ts
index 417ae33..65cb48e 100644
--- a/crabfit-frontend/src/components/AvailabilityEditor/availabilityEditorStyle.ts
+++ b/crabfit-frontend/src/components/AvailabilityEditor/availabilityEditorStyle.ts
@@ -1,24 +1,24 @@
import styled from '@emotion/styled';
export const Time = styled.div`
- height: 10px;
- touch-action: none;
+ height: 10px;
+ touch-action: none;
transition: background-color .1s;
${props => props.time.slice(2, 4) === '00' && `
- border-top: 2px solid ${props.theme.text};
- `}
- ${props => props.time.slice(2, 4) !== '00' && `
- border-top: 2px solid transparent;
- `}
- ${props => props.time.slice(2, 4) === '30' && `
- border-top: 2px dotted ${props.theme.text};
- `}
+ border-top: 2px solid ${props.theme.text};
+ `}
+ ${props => props.time.slice(2, 4) !== '00' && `
+ border-top: 2px solid transparent;
+ `}
+ ${props => props.time.slice(2, 4) === '30' && `
+ border-top: 2px dotted ${props.theme.text};
+ `}
- ${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
- background-color: ${props.theme.primary};
- `};
- ${props => props.mode === 'remove' && props.selecting && `
- background-color: ${props.theme.background};
- `};
+ ${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
+ background-color: ${props.theme.primary};
+ `};
+ ${props => props.mode === 'remove' && props.selecting && `
+ background-color: ${props.theme.background};
+ `};
`;
diff --git a/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx b/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx
index 02564f3..579a074 100644
--- a/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx
+++ b/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx
@@ -7,28 +7,29 @@ import relativeTime from 'dayjs/plugin/relativeTime';
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
-import { Legend, Center } from 'components';
+import { Legend } from 'components';
import {
- Wrapper,
- ScrollWrapper,
- Container,
- Date,
- Times,
- DateLabel,
- DayLabel,
- Time,
- Spacer,
- Tooltip,
- TooltipTitle,
- TooltipDate,
- TooltipContent,
+ Wrapper,
+ ScrollWrapper,
+ Container,
+ Date,
+ Times,
+ DateLabel,
+ DayLabel,
+ Time,
+ Spacer,
+ Tooltip,
+ TooltipTitle,
+ TooltipDate,
+ TooltipContent,
TooltipPerson,
- TimeLabels,
- TimeLabel,
- TimeSpace,
+ TimeLabels,
+ TimeLabel,
+ TimeSpace,
People,
Person,
StyledMain,
+ Info,
} from './availabilityViewerStyle';
import locales from 'res/dayjs_locales';
@@ -38,16 +39,16 @@ dayjs.extend(customParseFormat);
dayjs.extend(relativeTime);
const AvailabilityViewer = ({
- times,
- timeLabels,
- dates,
+ times,
+ timeLabels,
+ dates,
isSpecificDates,
- people = [],
- min = 0,
- max = 0,
- ...props
+ people = [],
+ min = 0,
+ max = 0,
+ ...props
}) => {
- const [tooltip, setTooltip] = useState(null);
+ const [tooltip, setTooltip] = useState(null);
const timeFormat = useSettingsStore(state => state.timeFormat);
const highlight = useSettingsStore(state => state.highlight);
const [filteredPeople, setFilteredPeople] = useState([]);
@@ -153,7 +154,7 @@ const AvailabilityViewer = ({
times,
]);
- return (
+ return (
<>
p.availability.length > 0).length}
onSegmentFocus={count => setFocusCount(count)}
/>
- {t('event:group.info1')}
+ {t('event:group.info1')}
{people.length > 1 && (
<>
- {t('event:group.info2')}
+ {t('event:group.info2')}
{people.map((person, i) =>
-
-
- {heatmap}
+
+
+ {heatmap}
- {tooltip && (
-
- {tooltip.available}
- {tooltip.date}
+ {tooltip && (
+
+ {tooltip.available}
+ {tooltip.date}
{!!filteredPeople.length && (
-
+
{tooltip.people.map(person =>
{person}
)}
@@ -215,12 +216,12 @@ const AvailabilityViewer = ({
)}
)}
-
- )}
+
+ )}
-
+
>
- );
+ );
};
export default AvailabilityViewer;
diff --git a/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts b/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts
index 330b3cc..c4a98b0 100644
--- a/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts
+++ b/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts
@@ -1,8 +1,8 @@
import styled from '@emotion/styled';
export const Wrapper = styled.div`
- overflow-y: visible;
- margin: 20px 0;
+ overflow-y: visible;
+ margin: 20px 0;
position: relative;
`;
@@ -11,30 +11,30 @@ export const ScrollWrapper = styled.div`
`;
export const Container = styled.div`
- display: inline-flex;
- box-sizing: border-box;
+ display: inline-flex;
+ box-sizing: border-box;
min-width: 100%;
- align-items: flex-end;
- justify-content: center;
- padding: 0 calc(calc(100% - 600px) / 2);
+ align-items: flex-end;
+ justify-content: center;
+ padding: 0 calc(calc(100% - 600px) / 2);
- @media (max-width: 660px) {
- padding: 0 30px;
- }
+ @media (max-width: 660px) {
+ padding: 0 30px;
+ }
`;
export const Date = styled.div`
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
- width: 60px;
- min-width: 60px;
- margin-bottom: 10px;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ width: 60px;
+ min-width: 60px;
+ margin-bottom: 10px;
`;
export const Times = styled.div`
- display: flex;
- flex-direction: column;
+ display: flex;
+ flex-direction: column;
border-bottom: 2px solid ${props => props.theme.text};
border-left: 1px solid ${props => props.theme.text};
@@ -57,44 +57,44 @@ export const Times = styled.div`
`;
export const DateLabel = styled.label`
- display: block;
- font-size: 12px;
- text-align: center;
- user-select: none;
+ display: block;
+ font-size: 12px;
+ text-align: center;
+ user-select: none;
`;
export const DayLabel = styled.label`
- display: block;
- font-size: 15px;
- text-align: center;
- user-select: none;
+ display: block;
+ font-size: 15px;
+ text-align: center;
+ user-select: none;
`;
export const Time = styled.div`
- height: 10px;
+ height: 10px;
background-origin: border-box;
transition: background-color .1s;
- ${props => props.time.slice(2, 4) === '00' && `
- border-top: 2px solid ${props.theme.text};
- `}
- ${props => props.time.slice(2, 4) !== '00' && `
- border-top: 2px solid transparent;
- `}
- ${props => props.time.slice(2, 4) === '30' && `
- border-top: 2px dotted ${props.theme.text};
- `}
+ ${props => props.time.slice(2, 4) === '00' && `
+ border-top: 2px solid ${props.theme.text};
+ `}
+ ${props => props.time.slice(2, 4) !== '00' && `
+ border-top: 2px solid transparent;
+ `}
+ ${props => props.time.slice(2, 4) === '30' && `
+ border-top: 2px dotted ${props.theme.text};
+ `}
background-color: ${props => `${props.theme.primary}${Math.round((props.peopleCount/props.maxPeople)*255).toString(16)}`};
${props => props.highlight && props.peopleCount === props.maxPeople && props.peopleCount > 0 && `
background-image: repeating-linear-gradient(
45deg,
- transparent,
- transparent 4.3px,
- ${props.theme.primaryDark} 4.3px,
- ${props.theme.primaryDark} 8.6px
- );
+ transparent,
+ transparent 4.3px,
+ ${props.theme.primaryDark} 4.3px,
+ ${props.theme.primaryDark} 8.6px
+ );
`}
@media (prefers-reduced-motion: reduce) {
@@ -103,40 +103,40 @@ export const Time = styled.div`
`;
export const Spacer = styled.div`
- width: 12px;
- flex-shrink: 0;
+ width: 12px;
+ flex-shrink: 0;
`;
export const Tooltip = styled.div`
- position: absolute;
- top: ${props => props.y}px;
- left: ${props => props.x}px;
- transform: translateX(-50%);
- border: 1px solid ${props => props.theme.text};
- border-radius: 3px;
- padding: 4px 8px;
- background-color: ${props => props.theme.background}${props => props.theme.mode === 'light' ? 'EE' : 'DD'};
- max-width: 200px;
- pointer-events: none;
+ position: absolute;
+ top: ${props => props.y}px;
+ left: ${props => props.x}px;
+ transform: translateX(-50%);
+ border: 1px solid ${props => props.theme.text};
+ border-radius: 3px;
+ padding: 4px 8px;
+ background-color: ${props => props.theme.background}${props => props.theme.mode === 'light' ? 'EE' : 'DD'};
+ max-width: 200px;
+ pointer-events: none;
z-index: 100;
user-select: none;
`;
export const TooltipTitle = styled.span`
- font-size: 15px;
- display: block;
- font-weight: 700;
+ font-size: 15px;
+ display: block;
+ font-weight: 700;
`;
export const TooltipDate = styled.span`
- font-size: 13px;
- display: block;
- opacity: .8;
- font-weight: 600;
+ font-size: 13px;
+ display: block;
+ opacity: .8;
+ font-weight: 600;
`;
export const TooltipContent = styled.div`
- font-size: 13px;
+ font-size: 13px;
padding: 4px 0;
`;
@@ -154,38 +154,38 @@ export const TooltipPerson = styled.span`
`;
export const TimeLabels = styled.div`
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
- width: 40px;
- padding-right: 6px;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ width: 40px;
+ padding-right: 6px;
`;
export const TimeSpace = styled.div`
- height: 10px;
- position: relative;
- border-top: 2px solid transparent;
+ height: 10px;
+ position: relative;
+ border-top: 2px solid transparent;
&.timespace {
background-origin: border-box;
background-image: repeating-linear-gradient(
45deg,
- transparent,
- transparent 4.3px,
- ${props => props.theme.loading} 4.3px,
- ${props => props.theme.loading} 8.6px
- );
+ transparent,
+ transparent 4.3px,
+ ${props => props.theme.loading} 4.3px,
+ ${props => props.theme.loading} 8.6px
+ );
}
`;
export const TimeLabel = styled.label`
- display: block;
- position: absolute;
- top: -.7em;
- font-size: 12px;
- text-align: right;
- user-select: none;
- width: 100%;
+ display: block;
+ position: absolute;
+ top: -.7em;
+ font-size: 12px;
+ text-align: right;
+ user-select: none;
+ width: 100%;
`;
export const StyledMain = styled.div`
@@ -220,3 +220,12 @@ export const Person = styled.button`
border-color: ${props.theme.primary};
`}
`;
+
+export const Info = styled.span`
+ display: block;
+ text-align: center;
+
+ @media print {
+ display: none;
+ }
+`;
diff --git a/crabfit-frontend/src/components/Button/Button.tsx b/crabfit-frontend/src/components/Button/Button.tsx
index 64e788d..e7a067b 100644
--- a/crabfit-frontend/src/components/Button/Button.tsx
+++ b/crabfit-frontend/src/components/Button/Button.tsx
@@ -1,7 +1,7 @@
import { Pressable } from './buttonStyle';
const Button = ({ href, type = 'button', icon, children, ...props }) => (
- props.isLoading && `
- color: transparent;
- cursor: wait;
+ color: transparent;
+ cursor: wait;
& img {
opacity: 0;
}
- @keyframes load {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
- }
+ @keyframes load {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+ }
- &:after {
- content: '';
- position: absolute;
- top: calc(50% - 12px);
- left: calc(50% - 12px);
- height: 18px;
- width: 18px;
- border: 3px solid ${props.primaryColor ? '#FFF' : props.theme.background};
- border-left-color: transparent;
- border-radius: 100px;
- animation: load .5s linear infinite;
- }
+ &:after {
+ content: '';
+ position: absolute;
+ top: calc(50% - 12px);
+ left: calc(50% - 12px);
+ height: 18px;
+ width: 18px;
+ border: 3px solid ${props.primaryColor ? '#FFF' : props.theme.background};
+ border-left-color: transparent;
+ border-radius: 100px;
+ animation: load .5s linear infinite;
+ }
@media (prefers-reduced-motion: reduce) {
&:after {
@@ -106,7 +106,7 @@ export const Pressable = styled.button`
justify-content: center;
}
}
- `}
+ `}
${props => props.secondary && `
background: transparent;
@@ -121,4 +121,14 @@ export const Pressable = styled.button`
transform: none;
}
`}
+
+ @media print {
+ ${props => !props.secondary && `
+ box-shadow: 0 4px 0 0 ${props.secondaryColor || props.theme.primaryDark};
+ `}
+
+ &::before {
+ display: none;
+ }
+ }
`;
diff --git a/crabfit-frontend/src/components/CalendarField/CalendarField.tsx b/crabfit-frontend/src/components/CalendarField/CalendarField.tsx
index 1927aa6..e90012c 100644
--- a/crabfit-frontend/src/components/CalendarField/CalendarField.tsx
+++ b/crabfit-frontend/src/components/CalendarField/CalendarField.tsx
@@ -9,14 +9,14 @@ import { Button, ToggleField } from 'components';
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
import {
- Wrapper,
- StyledLabel,
- StyledSubLabel,
- CalendarHeader,
- CalendarDays,
- CalendarBody,
- Date,
- Day,
+ Wrapper,
+ StyledLabel,
+ StyledSubLabel,
+ CalendarHeader,
+ CalendarDays,
+ CalendarBody,
+ Date,
+ Day,
} from './calendarFieldStyle';
dayjs.extend(isToday);
@@ -24,90 +24,90 @@ dayjs.extend(localeData);
dayjs.extend(updateLocale);
const calculateMonth = (month, year, weekStart) => {
- const date = dayjs().month(month).year(year);
- const daysInMonth = date.daysInMonth();
- const daysBefore = date.date(1).day() - weekStart;
- const daysAfter = 6 - date.date(daysInMonth).day() + weekStart;
+ const date = dayjs().month(month).year(year);
+ const daysInMonth = date.daysInMonth();
+ const daysBefore = date.date(1).day() - weekStart;
+ const daysAfter = 6 - date.date(daysInMonth).day() + weekStart;
- let dates = [];
- let curDate = date.date(1).subtract(daysBefore, 'day');
- let y = 0;
- let x = 0;
- for (let i = 0; i < daysBefore + daysInMonth + daysAfter; i++) {
- if (x === 0) dates[y] = [];
- dates[y][x] = curDate.clone();
- curDate = curDate.add(1, 'day');
- x++;
- if (x > 6) {
- x = 0;
- y++;
- }
- }
+ let dates = [];
+ let curDate = date.date(1).subtract(daysBefore, 'day');
+ let y = 0;
+ let x = 0;
+ for (let i = 0; i < daysBefore + daysInMonth + daysAfter; i++) {
+ if (x === 0) dates[y] = [];
+ dates[y][x] = curDate.clone();
+ curDate = curDate.add(1, 'day');
+ x++;
+ if (x > 6) {
+ x = 0;
+ y++;
+ }
+ }
- return dates;
+ return dates;
};
const CalendarField = forwardRef(({
- label,
- subLabel,
- id,
+ label,
+ subLabel,
+ id,
setValue,
- ...props
+ ...props
}, ref) => {
const weekStart = useSettingsStore(state => state.weekStart);
const locale = useLocaleUpdateStore(state => state.locale);
const { t } = useTranslation('home');
- const [type, setType] = useState(0);
+ 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());
+ const [month, setMonth] = useState(dayjs().month());
+ const [year, setYear] = useState(dayjs().year());
- const [selectedDates, setSelectedDates] = useState([]);
- const [selectingDates, _setSelectingDates] = useState([]);
- const staticSelectingDates = useRef([]);
- const setSelectingDates = newDates => {
- staticSelectingDates.current = newDates;
- _setSelectingDates(newDates);
- };
+ const [selectedDates, setSelectedDates] = useState([]);
+ const [selectingDates, _setSelectingDates] = useState([]);
+ const staticSelectingDates = useRef([]);
+ const setSelectingDates = newDates => {
+ staticSelectingDates.current = newDates;
+ _setSelectingDates(newDates);
+ };
- const [selectedDays, setSelectedDays] = useState([]);
- const [selectingDays, _setSelectingDays] = useState([]);
- const staticSelectingDays = useRef([]);
- const setSelectingDays = newDays => {
- staticSelectingDays.current = newDays;
- _setSelectingDays(newDays);
- };
+ 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);
- const setMode = newMode => {
- staticMode.current = newMode;
- _setMode(newMode);
- };
+ const startPos = useRef({});
+ const staticMode = useRef(null);
+ const [mode, _setMode] = useState(staticMode.current);
+ const setMode = newMode => {
+ staticMode.current = newMode;
+ _setMode(newMode);
+ };
useEffect(() => setValue(props.name, type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)), [type, selectedDays, selectedDates, setValue, props.name]);
- useEffect(() => {
+ useEffect(() => {
if (dayjs.Ls.hasOwnProperty(locale) && weekStart !== dayjs.Ls[locale].weekStart) {
dayjs.updateLocale(locale, { weekStart });
}
- setDates(calculateMonth(month, year, weekStart));
- }, [weekStart, month, year, locale]);
+ setDates(calculateMonth(month, year, weekStart));
+ }, [weekStart, month, year, locale]);
- return (
-
- {label && {label} }
- {subLabel && {subLabel} }
-
+ {label && {label} }
+ {subLabel && {subLabel} }
+
+ value={type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)}
+ {...props}
+ />
-
- {
- if (month-1 < 0) {
- setYear(year-1);
- setMonth(11);
- } else {
- setMonth(month-1);
- }
- }}
- ><
- {dayjs.months()[month]} {year}
- {
- if (month+1 > 11) {
- setYear(year+1);
- setMonth(0);
- } else {
- setMonth(month+1);
- }
- }}
- >>
-
+
+ {
+ if (month-1 < 0) {
+ setYear(year-1);
+ setMonth(11);
+ } else {
+ setMonth(month-1);
+ }
+ }}
+ ><
+ {dayjs.months()[month]} {year}
+ {
+ if (month+1 > 11) {
+ setYear(year+1);
+ setMonth(0);
+ } else {
+ setMonth(month+1);
+ }
+ }}
+ >>
+
-
- {(weekStart ? [...dayjs.weekdaysShort().filter((_,i) => i !== 0), dayjs.weekdaysShort()[0]] : dayjs.weekdaysShort()).map(name =>
- {name}
- )}
-
-
- {dates.length > 0 && dates.map((dateRow, y) =>
- dateRow.map((date, x) =>
-
+ {(weekStart ? [...dayjs.weekdaysShort().filter((_,i) => i !== 0), dayjs.weekdaysShort()[0]] : dayjs.weekdaysShort()).map(name =>
+ {name}
+ )}
+
+
+ {dates.length > 0 && dates.map((dateRow, y) =>
+ dateRow.map((date, x) =>
+ {
if (e.key === ' ' || e.key === 'Enter') {
@@ -176,37 +176,37 @@ const CalendarField = forwardRef(({
}
}
}}
- onPointerDown={e => {
- startPos.current = {x, y};
- setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
- setSelectingDates([date]);
- e.currentTarget.releasePointerCapture(e.pointerId);
+ onPointerDown={e => {
+ 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()}
- )
- )}
-
+ 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()}
+ )
+ )}
+
>
) : (
@@ -257,8 +257,8 @@ const CalendarField = forwardRef(({
)}
)}
-
- );
+
+ );
});
export default CalendarField;
diff --git a/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts b/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts
index 7dcee99..fe6c4b8 100644
--- a/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts
+++ b/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts
@@ -1,68 +1,68 @@
import styled from '@emotion/styled';
export const Wrapper = styled.div`
- margin: 30px 0;
+ margin: 30px 0;
`;
export const StyledLabel = styled.label`
- display: block;
- padding-bottom: 4px;
- font-size: 18px;
+ display: block;
+ padding-bottom: 4px;
+ font-size: 18px;
`;
export const StyledSubLabel = styled.label`
- display: block;
- font-size: 13px;
- opacity: .6;
+ display: block;
+ font-size: 13px;
+ opacity: .6;
`;
export const CalendarHeader = styled.div`
- display: flex;
- align-items: center;
- justify-content: space-between;
- user-select: none;
- padding: 6px 0;
- font-size: 1.2em;
- font-weight: bold;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ user-select: none;
+ padding: 6px 0;
+ font-size: 1.2em;
+ font-weight: bold;
`;
export const CalendarDays = styled.div`
- display: grid;
- grid-template-columns: repeat(7, 1fr);
- grid-gap: 2px;
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ grid-gap: 2px;
`;
export const Day = styled.div`
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 3px 0;
- font-weight: bold;
- user-select: none;
- opacity: .7;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 3px 0;
+ font-weight: bold;
+ user-select: none;
+ opacity: .7;
- @media (max-width: 350px) {
- font-size: 12px;
- }
+ @media (max-width: 350px) {
+ font-size: 12px;
+ }
`;
export const CalendarBody = styled.div`
- display: grid;
- grid-template-columns: repeat(7, 1fr);
- grid-gap: 2px;
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ grid-gap: 2px;
- & button:first-of-type {
- border-top-left-radius: 3px;
- }
- & button:nth-of-type(7) {
- border-top-right-radius: 3px;
- }
- & button:nth-last-of-type(7) {
- border-bottom-left-radius: 3px;
- }
- & button:last-of-type {
- border-bottom-right-radius: 3px;
- }
+ & button:first-of-type {
+ border-top-left-radius: 3px;
+ }
+ & button:nth-of-type(7) {
+ border-top-right-radius: 3px;
+ }
+ & button:nth-last-of-type(7) {
+ border-bottom-left-radius: 3px;
+ }
+ & button:last-of-type {
+ border-bottom-right-radius: 3px;
+ }
`;
export const Date = styled.button`
@@ -77,28 +77,28 @@ export const Date = styled.button`
transition: none;
}
- background-color: ${props => props.theme.primaryBackground};
- border: 1px solid ${props => props.theme.primary};
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 10px 0;
- user-select: none;
- touch-action: none;
+ background-color: ${props => props.theme.primaryBackground};
+ border: 1px solid ${props => props.theme.primary};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 10px 0;
+ user-select: none;
+ touch-action: none;
- ${props => props.otherMonth && `
- color: ${props.theme.mode === 'light' ? props.theme.primaryLight : props.theme.primaryDark};
- `}
- ${props => props.isToday && `
- font-weight: 900;
- color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
- `}
- ${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
- color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};
- background-color: ${props.theme.primary};
- `}
- ${props => props.mode === 'remove' && props.selecting && `
- background-color: ${props.theme.primaryBackground};
- color: ${props.isToday ? props.theme.primaryDark : (props.otherMonth ? props.theme.primaryLight : 'inherit')};
- `}
+ ${props => props.otherMonth && `
+ color: ${props.theme.mode === 'light' ? props.theme.primaryLight : props.theme.primaryDark};
+ `}
+ ${props => props.isToday && `
+ font-weight: 900;
+ color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
+ `}
+ ${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
+ color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};
+ background-color: ${props.theme.primary};
+ `}
+ ${props => props.mode === 'remove' && props.selecting && `
+ background-color: ${props.theme.primaryBackground};
+ color: ${props.isToday ? props.theme.primaryDark : (props.otherMonth ? props.theme.primaryLight : 'inherit')};
+ `}
`;
diff --git a/crabfit-frontend/src/components/Center/Center.ts b/crabfit-frontend/src/components/Center/Center.ts
index 0d691d0..bd722ff 100644
--- a/crabfit-frontend/src/components/Center/Center.ts
+++ b/crabfit-frontend/src/components/Center/Center.ts
@@ -1,9 +1,9 @@
import styled from '@emotion/styled';
const Center = styled.div`
- display: flex;
- align-items: center;
- justify-content: center;
+ display: flex;
+ align-items: center;
+ justify-content: center;
`;
export default Center;
diff --git a/crabfit-frontend/src/components/Donate/Donate.tsx b/crabfit-frontend/src/components/Donate/Donate.tsx
index 85ef132..1a2d0ef 100644
--- a/crabfit-frontend/src/components/Donate/Donate.tsx
+++ b/crabfit-frontend/src/components/Donate/Donate.tsx
@@ -96,9 +96,9 @@ const Donate = () => {
};
return (
-
-
+ {
if (closed) {
@@ -125,7 +125,7 @@ const Donate = () => {
role="button"
aria-expanded={isOpen ? 'true' : 'false'}
style={{ whiteSpace: 'nowrap' }}
- >{t('donate.button')}
+ >{t('donate.button')}
{
{t('donate.options.$10')}
{t('donate.options.choose')}
-
+
);
}
diff --git a/crabfit-frontend/src/components/Donate/donateStyle.ts b/crabfit-frontend/src/components/Donate/donateStyle.ts
index ad32073..0353637 100644
--- a/crabfit-frontend/src/components/Donate/donateStyle.ts
+++ b/crabfit-frontend/src/components/Donate/donateStyle.ts
@@ -7,7 +7,7 @@ export const Wrapper = styled.div`
`;
export const Options = styled.div`
- position: absolute;
+ position: absolute;
bottom: calc(100% + 20px);
right: 0;
background-color: ${props => props.theme.background};
diff --git a/crabfit-frontend/src/components/Egg/Egg.tsx b/crabfit-frontend/src/components/Egg/Egg.tsx
index afa2e82..0d65643 100644
--- a/crabfit-frontend/src/components/Egg/Egg.tsx
+++ b/crabfit-frontend/src/components/Egg/Egg.tsx
@@ -7,14 +7,14 @@ const Egg = ({ eggKey, onClose }) => {
const [isLoading, setIsLoading] = useState(true);
return (
- onClose()}>
- onClose()}>
+ setIsLoading(true)}
onLoad={() => setIsLoading(false)}
/>
{isLoading && }
-
+
);
}
diff --git a/crabfit-frontend/src/components/Egg/eggStyle.ts b/crabfit-frontend/src/components/Egg/eggStyle.ts
index 19ac1f3..2aebe29 100644
--- a/crabfit-frontend/src/components/Egg/eggStyle.ts
+++ b/crabfit-frontend/src/components/Egg/eggStyle.ts
@@ -1,7 +1,7 @@
import styled from '@emotion/styled';
export const Wrapper = styled.div`
- position: fixed;
+ position: fixed;
background: rgba(0,0,0,.6);
top: 0;
left: 0;
@@ -17,7 +17,7 @@ export const Wrapper = styled.div`
`;
export const Image = styled.img`
- max-width: 80%;
- max-height: 80%;
+ max-width: 80%;
+ max-height: 80%;
position: absolute;
`;
diff --git a/crabfit-frontend/src/components/Error/Error.tsx b/crabfit-frontend/src/components/Error/Error.tsx
index 0d51039..0d103a0 100644
--- a/crabfit-frontend/src/components/Error/Error.tsx
+++ b/crabfit-frontend/src/components/Error/Error.tsx
@@ -1,17 +1,17 @@
import { Wrapper, CloseButton } from './errorStyle';
const Error = ({
- children,
- onClose,
+ children,
+ onClose,
open = true,
- ...props
+ ...props
}) => (
-
- {children}
-
-
-
-
+
+ {children}
+
+
+
+
);
export default Error;
diff --git a/crabfit-frontend/src/components/Error/errorStyle.ts b/crabfit-frontend/src/components/Error/errorStyle.ts
index 59f43df..201001e 100644
--- a/crabfit-frontend/src/components/Error/errorStyle.ts
+++ b/crabfit-frontend/src/components/Error/errorStyle.ts
@@ -1,14 +1,14 @@
import styled from '@emotion/styled';
export const Wrapper = styled.div`
- border-radius: 3px;
- background-color: ${props => props.theme.error};
- color: #FFFFFF;
- padding: 0 16px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-size: 18px;
+ border-radius: 3px;
+ background-color: ${props => props.theme.error};
+ color: #FFFFFF;
+ padding: 0 16px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 18px;
opacity: 0;
max-height: 0;
margin: 0;
@@ -30,14 +30,14 @@ export const Wrapper = styled.div`
`;
export const CloseButton = styled.button`
- border: 0;
- background: none;
- height: 30px;
- width: 30px;
- cursor: pointer;
- color: inherit;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-left: 16px;
+ border: 0;
+ background: none;
+ height: 30px;
+ width: 30px;
+ cursor: pointer;
+ color: inherit;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: 16px;
`;
diff --git a/crabfit-frontend/src/components/Footer/footerStyle.ts b/crabfit-frontend/src/components/Footer/footerStyle.ts
index 4e04db7..009cd12 100644
--- a/crabfit-frontend/src/components/Footer/footerStyle.ts
+++ b/crabfit-frontend/src/components/Footer/footerStyle.ts
@@ -1,12 +1,12 @@
import styled from '@emotion/styled';
export const Wrapper = styled.footer`
- width: 600px;
- margin: 20px auto;
- max-width: calc(100% - 60px);
- display: flex;
- align-items: center;
- justify-content: space-between;
+ width: 600px;
+ margin: 20px auto;
+ max-width: calc(100% - 60px);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
${props => props.small && `
margin: 60px auto 0;
@@ -19,4 +19,8 @@ export const Wrapper = styled.footer`
margin-bottom: 20px;
}
`}
+
+ @media print {
+ display: none;
+ }
`;
diff --git a/crabfit-frontend/src/components/GoogleCalendar/GoogleCalendar.tsx b/crabfit-frontend/src/components/GoogleCalendar/GoogleCalendar.tsx
index 335f779..5b6f871 100644
--- a/crabfit-frontend/src/components/GoogleCalendar/GoogleCalendar.tsx
+++ b/crabfit-frontend/src/components/GoogleCalendar/GoogleCalendar.tsx
@@ -93,11 +93,11 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
}
}, [signedIn]);
- return (
+ return (
<>
{!signedIn ? (
- signIn()}
isLoading={signedIn === undefined}
primaryColor="#4286F5"
@@ -161,7 +161,7 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
)}
>
- );
+ );
};
export default GoogleCalendar;
diff --git a/crabfit-frontend/src/components/Legend/Legend.tsx b/crabfit-frontend/src/components/Legend/Legend.tsx
index 2760ded..69eae02 100644
--- a/crabfit-frontend/src/components/Legend/Legend.tsx
+++ b/crabfit-frontend/src/components/Legend/Legend.tsx
@@ -3,46 +3,46 @@ import { useSettingsStore } from 'stores';
import { useTranslation } from 'react-i18next';
import {
- Wrapper,
- Label,
- Bar,
- Grade,
+ Wrapper,
+ Label,
+ Bar,
+ Grade,
} from './legendStyle';
const Legend = ({
- min,
- max,
- total,
+ min,
+ max,
+ total,
onSegmentFocus,
- ...props
+ ...props
}) => {
- const theme = useTheme();
+ const theme = useTheme();
const { t } = useTranslation('event');
const highlight = useSettingsStore(state => state.highlight);
const setHighlight = useSettingsStore(state => state.setHighlight);
- return (
-
- {min}/{total} {t('event:available')}
+ return (
+
+ {min}/{total} {t('event:available')}
- onSegmentFocus(null)}
onClick={() => setHighlight(!highlight)}
title={t('event:group.legend_tooltip')}
>
- {[...Array(max+1-min).keys()].map(i => i+min).map(i =>
- i+min).map(i =>
+ 0}
onMouseOver={() => onSegmentFocus(i)}
/>
- )}
-
+ )}
+
- {max}/{total} {t('event:available')}
-
- );
+ {max}/{total} {t('event:available')}
+
+ );
};
export default Legend;
diff --git a/crabfit-frontend/src/components/Legend/legendStyle.ts b/crabfit-frontend/src/components/Legend/legendStyle.ts
index 7ca7f67..ce5ba62 100644
--- a/crabfit-frontend/src/components/Legend/legendStyle.ts
+++ b/crabfit-frontend/src/components/Legend/legendStyle.ts
@@ -1,52 +1,52 @@
import styled from '@emotion/styled';
export const Wrapper = styled.div`
- margin: 10px 0;
- display: flex;
- align-items: center;
- justify-content: center;
+ margin: 10px 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
- & label:last-of-type {
- text-align: right;
- }
+ & label:last-of-type {
+ text-align: right;
+ }
- @media (max-width: 400px) {
- display: block;
- }
+ @media (max-width: 400px) {
+ display: block;
+ }
`;
export const Label = styled.label`
- display: block;
- font-size: 14px;
- text-align: left;
+ display: block;
+ font-size: 14px;
+ text-align: left;
`;
export const Bar = styled.div`
- display: flex;
- width: 40%;
- height: 20px;
- border-radius: 3px;
- overflow: hidden;
- margin: 0 8px;
- border: 1px solid ${props => props.theme.text};
+ display: flex;
+ width: 40%;
+ height: 20px;
+ border-radius: 3px;
+ overflow: hidden;
+ margin: 0 8px;
+ border: 1px solid ${props => props.theme.text};
- @media (max-width: 400px) {
- width: 100%;
- margin: 8px 0;
- }
+ @media (max-width: 400px) {
+ width: 100%;
+ margin: 8px 0;
+ }
`;
export const Grade = styled.div`
- flex: 1;
- background-color: ${props => props.color};
+ flex: 1;
+ background-color: ${props => props.color};
${props => props.highlight && `
background-image: repeating-linear-gradient(
45deg,
- ${props.theme.primary},
- ${props.theme.primary} 4.5px,
- ${props.theme.primaryDark} 4.5px,
- ${props.theme.primaryDark} 9px
- );
+ ${props.theme.primary},
+ ${props.theme.primary} 4.5px,
+ ${props.theme.primaryDark} 4.5px,
+ ${props.theme.primaryDark} 9px
+ );
`}
`;
diff --git a/crabfit-frontend/src/components/Logo/logoStyle.ts b/crabfit-frontend/src/components/Logo/logoStyle.ts
index d9b13da..e6c3bdd 100644
--- a/crabfit-frontend/src/components/Logo/logoStyle.ts
+++ b/crabfit-frontend/src/components/Logo/logoStyle.ts
@@ -7,7 +7,7 @@ export const Wrapper = styled.div`
`;
export const A = styled.a`
- text-decoration: none;
+ text-decoration: none;
@keyframes jelly {
from,to {
@@ -35,31 +35,35 @@ export const A = styled.a`
`;
export const Top = styled.div`
- display: inline-flex;
+ display: inline-flex;
justify-content: center;
align-items: center;
`;
export const Image = styled.img`
- width: 2.5rem;
- margin-right: 16px;
+ width: 2.5rem;
+ margin-right: 16px;
`;
export const Title = styled.span`
- display: block;
- font-size: 2rem;
- color: ${props => props.theme.primary};
- font-family: 'Molot', sans-serif;
- font-weight: 400;
- text-shadow: 0 2px 0 ${props => props.theme.primaryDark};
- line-height: 1em;
+ display: block;
+ font-size: 2rem;
+ color: ${props => props.theme.primary};
+ font-family: 'Molot', sans-serif;
+ font-weight: 400;
+ text-shadow: 0 2px 0 ${props => props.theme.primaryDark};
+ line-height: 1em;
`;
export const Tagline = styled.span`
- text-decoration: underline;
+ text-decoration: underline;
font-size: 14px;
padding-top: 2px;
display: flex;
align-items: center;
justify-content: center;
+
+ @media print {
+ display: none;
+ }
`;
diff --git a/crabfit-frontend/src/components/OutlookCalendar/OutlookCalendar.tsx b/crabfit-frontend/src/components/OutlookCalendar/OutlookCalendar.tsx
index 05242ee..17b3f2a 100644
--- a/crabfit-frontend/src/components/OutlookCalendar/OutlookCalendar.tsx
+++ b/crabfit-frontend/src/components/OutlookCalendar/OutlookCalendar.tsx
@@ -160,11 +160,11 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
// eslint-disable-next-line
}, [client]);
- return (
+ return (
<>
{!client ? (
- signIn()}
isLoading={client === undefined}
primaryColor="#0364B9"
@@ -228,7 +228,7 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
)}
>
- );
+ );
};
export default OutlookCalendar;
diff --git a/crabfit-frontend/src/components/Recents/Recents.tsx b/crabfit-frontend/src/components/Recents/Recents.tsx
index 035ac99..451e620 100644
--- a/crabfit-frontend/src/components/Recents/Recents.tsx
+++ b/crabfit-frontend/src/components/Recents/Recents.tsx
@@ -4,7 +4,7 @@ import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { AboutSection, StyledMain } from '../../pages/Home/homeStyle';
-import { Recent } from './recentsStyle';
+import { Wrapper, Recent } from './recentsStyle';
dayjs.extend(relativeTime);
@@ -14,17 +14,19 @@ const Recents = ({ target }) => {
const { t } = useTranslation(['home', 'common']);
return !!recents.length && (
-
-
- {t('home:recently_visited')}
- {recents.map(event => (
-
- {event.name}
- {t('common:created', { date: dayjs.unix(event.created).fromNow() })}
-
- ))}
-
-
+
+
+
+ {t('home:recently_visited')}
+ {recents.map(event => (
+
+ {event.name}
+ {t('common:created', { date: dayjs.unix(event.created).fromNow() })}
+
+ ))}
+
+
+
);
};
diff --git a/crabfit-frontend/src/components/Recents/recentsStyle.ts b/crabfit-frontend/src/components/Recents/recentsStyle.ts
index 4d1e465..dca0b6c 100644
--- a/crabfit-frontend/src/components/Recents/recentsStyle.ts
+++ b/crabfit-frontend/src/components/Recents/recentsStyle.ts
@@ -1,7 +1,13 @@
import styled from '@emotion/styled';
+export const Wrapper = styled.div`
+ @media print {
+ display: none;
+ }
+`;
+
export const Recent = styled.a`
- text-decoration: none;
+ text-decoration: none;
display: flex;
align-items: center;
justify-content: space-between;
diff --git a/crabfit-frontend/src/components/SelectField/SelectField.tsx b/crabfit-frontend/src/components/SelectField/SelectField.tsx
index 8771c72..a036e71 100644
--- a/crabfit-frontend/src/components/SelectField/SelectField.tsx
+++ b/crabfit-frontend/src/components/SelectField/SelectField.tsx
@@ -1,43 +1,43 @@
import { forwardRef } from 'react';
import {
- Wrapper,
- StyledLabel,
- StyledSubLabel,
- StyledSelect,
+ Wrapper,
+ StyledLabel,
+ StyledSubLabel,
+ StyledSelect,
} from './selectFieldStyle';
const SelectField = forwardRef(({
- label,
- subLabel,
- id,
- options = [],
- inline = false,
- small = false,
- defaultOption,
- ...props
+ label,
+ subLabel,
+ id,
+ options = [],
+ inline = false,
+ small = false,
+ defaultOption,
+ ...props
}, ref) => (
-
- {label && {label} }
- {subLabel && {subLabel} }
+
+ {label && {label} }
+ {subLabel && {subLabel} }
-
- {defaultOption && {defaultOption} }
- {Array.isArray(options) ? (
+ {...props}
+ >
+ {defaultOption && {defaultOption} }
+ {Array.isArray(options) ? (
options.map(value =>
- {value}
- )
+ {value}
+ )
) : (
Object.entries(options).map(([key, value]) =>
- {value}
- )
+ {value}
+ )
)}
-
-
+
+
));
export default SelectField;
diff --git a/crabfit-frontend/src/components/SelectField/selectFieldStyle.ts b/crabfit-frontend/src/components/SelectField/selectFieldStyle.ts
index 2383a85..c921f06 100644
--- a/crabfit-frontend/src/components/SelectField/selectFieldStyle.ts
+++ b/crabfit-frontend/src/components/SelectField/selectFieldStyle.ts
@@ -1,60 +1,60 @@
import styled from '@emotion/styled';
export const Wrapper = styled.div`
- margin: 30px 0;
+ margin: 30px 0;
- ${props => props.inline && `
- margin: 0;
- `}
- ${props => props.small && `
- margin: 10px 0;
- `}
+ ${props => props.inline && `
+ margin: 0;
+ `}
+ ${props => props.small && `
+ margin: 10px 0;
+ `}
`;
export const StyledLabel = styled.label`
- display: block;
- padding-bottom: 4px;
- font-size: 18px;
+ display: block;
+ padding-bottom: 4px;
+ font-size: 18px;
- ${props => props.inline && `
- font-size: 16px;
- `}
- ${props => props.small && `
- font-size: .9rem;
- `}
+ ${props => props.inline && `
+ font-size: 16px;
+ `}
+ ${props => props.small && `
+ font-size: .9rem;
+ `}
`;
export const StyledSubLabel = styled.label`
- display: block;
- padding-bottom: 6px;
- font-size: 13px;
- opacity: .6;
+ display: block;
+ padding-bottom: 6px;
+ font-size: 13px;
+ opacity: .6;
`;
export const StyledSelect = styled.select`
- width: 100%;
- box-sizing: border-box;
- font: inherit;
- background: ${props => props.theme.primaryBackground};
- color: inherit;
- padding: 10px 14px;
- border: 1px solid ${props => props.theme.primary};
- box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
- border-radius: 3px;
- outline: none;
- transition: border-color .15s, box-shadow .15s;
+ width: 100%;
+ box-sizing: border-box;
+ font: inherit;
+ background: ${props => props.theme.primaryBackground};
+ color: inherit;
+ padding: 10px 14px;
+ border: 1px solid ${props => props.theme.primary};
+ box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
+ border-radius: 3px;
+ outline: none;
+ transition: border-color .15s, box-shadow .15s;
appearance: none;
background-image: url("data:image/svg+xml, encodeURIComponent(props.theme.primary)};font-size:60px;display:flex;align-items:center;justify-content:center;height:100%25;width:100%25%22>â–¼
");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 1em;
- &:focus {
- border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
- box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
- }
+ &:focus {
+ border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
+ box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
+ }
${props => props.small && `
padding: 6px 8px;
- `}
+ `}
`;
diff --git a/crabfit-frontend/src/components/Settings/settingsStyle.ts b/crabfit-frontend/src/components/Settings/settingsStyle.ts
index 0f42b74..952a6c0 100644
--- a/crabfit-frontend/src/components/Settings/settingsStyle.ts
+++ b/crabfit-frontend/src/components/Settings/settingsStyle.ts
@@ -1,15 +1,15 @@
import styled from '@emotion/styled';
export const OpenButton = styled.button`
- border: 0;
- background: none;
- height: 50px;
- width: 50px;
- cursor: pointer;
- color: inherit;
- display: flex;
- align-items: center;
- justify-content: center;
+ border: 0;
+ background: none;
+ height: 50px;
+ width: 50px;
+ cursor: pointer;
+ color: inherit;
+ display: flex;
+ align-items: center;
+ justify-content: center;
position: absolute;
top: 12px;
right: 12px;
@@ -33,10 +33,13 @@ export const OpenButton = styled.button`
@media (prefers-reduced-motion: reduce) {
transition: none;
}
+ @media print {
+ display: none;
+ }
`;
export const Cover = styled.div`
- position: fixed;
+ position: fixed;
top: 0;
left: 0;
right: 0;
@@ -50,7 +53,7 @@ export const Cover = styled.div`
`;
export const Modal = styled.div`
- position: absolute;
+ position: absolute;
top: 70px;
right: 12px;
background-color: ${props => props.theme.background};
@@ -81,10 +84,13 @@ export const Modal = styled.div`
@media (prefers-reduced-motion: reduce) {
transition: none;
}
+ @media print {
+ display: none;
+ }
`;
export const Heading = styled.span`
- font-size: 1.5rem;
+ font-size: 1.5rem;
display: block;
margin: 6px 0;
line-height: 1em;
diff --git a/crabfit-frontend/src/components/TextField/TextField.tsx b/crabfit-frontend/src/components/TextField/TextField.tsx
index e096583..68dbc2b 100644
--- a/crabfit-frontend/src/components/TextField/TextField.tsx
+++ b/crabfit-frontend/src/components/TextField/TextField.tsx
@@ -1,23 +1,23 @@
import { forwardRef } from 'react';
import {
- Wrapper,
- StyledLabel,
- StyledSubLabel,
- StyledInput,
+ Wrapper,
+ StyledLabel,
+ StyledSubLabel,
+ StyledInput,
} from './textFieldStyle';
const TextField = forwardRef(({
- label,
- subLabel,
- id,
- inline = false,
- ...props
+ label,
+ subLabel,
+ id,
+ inline = false,
+ ...props
}, ref) => (
-
- {label && {label} }
- {subLabel && {subLabel} }
-
-
+
+ {label && {label} }
+ {subLabel && {subLabel} }
+
+
));
export default TextField;
diff --git a/crabfit-frontend/src/components/TextField/textFieldStyle.ts b/crabfit-frontend/src/components/TextField/textFieldStyle.ts
index de2bc5f..9fe8315 100644
--- a/crabfit-frontend/src/components/TextField/textFieldStyle.ts
+++ b/crabfit-frontend/src/components/TextField/textFieldStyle.ts
@@ -1,46 +1,46 @@
import styled from '@emotion/styled';
export const Wrapper = styled.div`
- margin: 30px 0;
+ margin: 30px 0;
- ${props => props.inline && `
- margin: 0;
- `}
+ ${props => props.inline && `
+ margin: 0;
+ `}
`;
export const StyledLabel = styled.label`
- display: block;
- padding-bottom: 4px;
- font-size: 18px;
+ display: block;
+ padding-bottom: 4px;
+ font-size: 18px;
- ${props => props.inline && `
- font-size: 16px;
- `}
+ ${props => props.inline && `
+ font-size: 16px;
+ `}
`;
export const StyledSubLabel = styled.label`
- display: block;
- padding-bottom: 6px;
- font-size: 13px;
- opacity: .6;
+ display: block;
+ padding-bottom: 6px;
+ font-size: 13px;
+ opacity: .6;
`;
export const StyledInput = styled.input`
- width: 100%;
- box-sizing: border-box;
- font: inherit;
- background: ${props => props.theme.primaryBackground};
- color: inherit;
- padding: 10px 14px;
- border: 1px solid ${props => props.theme.primary};
- box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
- border-radius: 3px;
- font-size: 18px;
- outline: none;
- transition: border-color .15s, box-shadow .15s;
+ width: 100%;
+ box-sizing: border-box;
+ font: inherit;
+ background: ${props => props.theme.primaryBackground};
+ color: inherit;
+ padding: 10px 14px;
+ border: 1px solid ${props => props.theme.primary};
+ box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
+ border-radius: 3px;
+ font-size: 18px;
+ outline: none;
+ transition: border-color .15s, box-shadow .15s;
- &:focus {
- border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
- box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
- }
+ &:focus {
+ border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
+ box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
+ }
`;
diff --git a/crabfit-frontend/src/components/TimeRangeField/TimeRangeField.tsx b/crabfit-frontend/src/components/TimeRangeField/TimeRangeField.tsx
index c45e429..b176cd6 100644
--- a/crabfit-frontend/src/components/TimeRangeField/TimeRangeField.tsx
+++ b/crabfit-frontend/src/components/TimeRangeField/TimeRangeField.tsx
@@ -4,94 +4,94 @@ import dayjs from 'dayjs';
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
import {
- Wrapper,
- StyledLabel,
- StyledSubLabel,
- Range,
- Handle,
- Selected,
+ Wrapper,
+ StyledLabel,
+ StyledSubLabel,
+ Range,
+ Handle,
+ Selected,
} from './timeRangeFieldStyle';
const times = ['00','01','02','03','04','05','06','07','08','09','10','11','12','13','14','15','16','17','18','19','20','21','22','23','24'];
const TimeRangeField = forwardRef(({
- label,
- subLabel,
- id,
+ label,
+ subLabel,
+ id,
setValue,
- ...props
+ ...props
}, ref) => {
const timeFormat = useSettingsStore(state => state.timeFormat);
const locale = useLocaleUpdateStore(state => state.locale);
- const [start, setStart] = useState(9);
- const [end, setEnd] = useState(17);
+ const [start, setStart] = useState(9);
+ const [end, setEnd] = useState(17);
- const isStartMoving = useRef(false);
- const isEndMoving = useRef(false);
- const rangeRef = useRef();
- const rangeRect = useRef();
+ const isStartMoving = useRef(false);
+ const isEndMoving = useRef(false);
+ const rangeRef = useRef();
+ const rangeRect = useRef();
- useEffect(() => {
- if (rangeRef.current) {
- rangeRect.current = rangeRef.current.getBoundingClientRect();
- }
- }, [rangeRef]);
+ useEffect(() => {
+ if (rangeRef.current) {
+ rangeRect.current = rangeRef.current.getBoundingClientRect();
+ }
+ }, [rangeRef]);
useEffect(() => setValue(props.name, JSON.stringify({start, end})), [start, end, setValue, props.name]);
- const handleMouseMove = e => {
- if (isStartMoving.current || isEndMoving.current) {
- let step = Math.round(((e.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
- if (step < 0) step = 0;
- if (step > 24) step = 24;
- step = Math.abs(step);
+ const handleMouseMove = e => {
+ if (isStartMoving.current || isEndMoving.current) {
+ let step = Math.round(((e.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
+ if (step < 0) step = 0;
+ if (step > 24) step = 24;
+ step = Math.abs(step);
- if (isStartMoving.current) {
- setStart(step);
- } else if (isEndMoving.current) {
- setEnd(step);
- }
- }
- };
+ if (isStartMoving.current) {
+ setStart(step);
+ } else if (isEndMoving.current) {
+ setEnd(step);
+ }
+ }
+ };
- return (
-
- {label && {label} }
- {subLabel && {subLabel} }
-
+ {label && {label} }
+ {subLabel && {subLabel} }
+
+ {...props}
+ />
-
- end ? 24 : end} />
- {start > end && end ? 0 : start} end={end} />}
-
+ end ? 24 : end} />
+ {start > end && end ? 0 : start} end={end} />}
+ {
- document.addEventListener('mousemove', handleMouseMove);
- isStartMoving.current = true;
+ onMouseDown={() => {
+ document.addEventListener('mousemove', handleMouseMove);
+ isStartMoving.current = true;
- document.addEventListener('mouseup', () => {
- isStartMoving.current = false;
- document.removeEventListener('mousemove', handleMouseMove);
- }, { once: true });
- }}
- onTouchMove={(e) => {
- const touch = e.targetTouches[0];
+ document.addEventListener('mouseup', () => {
+ isStartMoving.current = false;
+ document.removeEventListener('mousemove', handleMouseMove);
+ }, { once: true });
+ }}
+ onTouchMove={(e) => {
+ const touch = e.targetTouches[0];
- let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
- if (step < 0) step = 0;
- if (step > 24) step = 24;
- step = Math.abs(step);
- setStart(step);
- }}
+ let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
+ if (step < 0) step = 0;
+ if (step > 24) step = 24;
+ step = Math.abs(step);
+ setStart(step);
+ }}
tabIndex="0"
onKeyDown={e => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
@@ -103,29 +103,29 @@ const TimeRangeField = forwardRef(({
setStart(Math.min(start+1, 24));
}
}}
- />
-
+ {
- document.addEventListener('mousemove', handleMouseMove);
- isEndMoving.current = true;
+ onMouseDown={() => {
+ document.addEventListener('mousemove', handleMouseMove);
+ isEndMoving.current = true;
- document.addEventListener('mouseup', () => {
- isEndMoving.current = false;
- document.removeEventListener('mousemove', handleMouseMove);
- }, { once: true });
- }}
- onTouchMove={(e) => {
- const touch = e.targetTouches[0];
+ document.addEventListener('mouseup', () => {
+ isEndMoving.current = false;
+ document.removeEventListener('mousemove', handleMouseMove);
+ }, { once: true });
+ }}
+ onTouchMove={(e) => {
+ const touch = e.targetTouches[0];
- let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
- if (step < 0) step = 0;
- if (step > 24) step = 24;
- step = Math.abs(step);
- setEnd(step);
- }}
+ let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
+ if (step < 0) step = 0;
+ if (step > 24) step = 24;
+ step = Math.abs(step);
+ setEnd(step);
+ }}
tabIndex="0"
onKeyDown={e => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
@@ -137,10 +137,10 @@ const TimeRangeField = forwardRef(({
setEnd(Math.min(end+1, 24));
}
}}
- />
-
-
- );
+ />
+
+
+ );
});
export default TimeRangeField;
diff --git a/crabfit-frontend/src/components/TimeRangeField/timeRangeFieldStyle.ts b/crabfit-frontend/src/components/TimeRangeField/timeRangeFieldStyle.ts
index 447d4dc..03869f7 100644
--- a/crabfit-frontend/src/components/TimeRangeField/timeRangeFieldStyle.ts
+++ b/crabfit-frontend/src/components/TimeRangeField/timeRangeFieldStyle.ts
@@ -1,82 +1,82 @@
import styled from '@emotion/styled';
export const Wrapper = styled.div`
- margin: 30px 0;
+ margin: 30px 0;
`;
export const StyledLabel = styled.label`
- display: block;
- padding-bottom: 4px;
- font-size: 18px;
+ display: block;
+ padding-bottom: 4px;
+ font-size: 18px;
`;
export const StyledSubLabel = styled.label`
- display: block;
- padding-bottom: 6px;
- font-size: 13px;
- opacity: .6;
+ display: block;
+ padding-bottom: 6px;
+ font-size: 13px;
+ opacity: .6;
`;
export const Range = styled.div`
- user-select: none;
- background-color: ${props => props.theme.primaryBackground};
- border: 1px solid ${props => props.theme.primary};
- border-radius: 3px;
- height: 50px;
- position: relative;
- margin: 38px 6px 18px;
+ user-select: none;
+ background-color: ${props => props.theme.primaryBackground};
+ border: 1px solid ${props => props.theme.primary};
+ border-radius: 3px;
+ height: 50px;
+ position: relative;
+ margin: 38px 6px 18px;
`;
export const Handle = styled.div`
- height: calc(100% + 20px);
- width: 20px;
- border: 1px solid ${props => props.theme.primary};
- background-color: ${props => props.theme.primaryLight};
- border-radius: 3px;
- position: absolute;
- top: -10px;
- left: calc(${props => props.value * 4.1666666666666666}% - 11px);
- cursor: ew-resize;
- touch-action: none;
+ height: calc(100% + 20px);
+ width: 20px;
+ border: 1px solid ${props => props.theme.primary};
+ background-color: ${props => props.theme.primaryLight};
+ border-radius: 3px;
+ position: absolute;
+ top: -10px;
+ left: calc(${props => props.value * 4.1666666666666666}% - 11px);
+ cursor: ew-resize;
+ touch-action: none;
transition: left .1s;
@media (prefers-reduced-motion: reduce) {
transition: none;
}
- &:after {
- content: '|||';
- font-size: 8px;
- position: absolute;
- top: 0;
- left: 0;
- height: 100%;
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- color: ${props => props.theme.primaryDark};
- }
+ &:after {
+ content: '|||';
+ font-size: 8px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: ${props => props.theme.primaryDark};
+ }
- &:before {
- content: '${props => props.label}';
- position: absolute;
- bottom: calc(100% + 8px);
- text-align: center;
- left: 50%;
- transform: translateX(-50%);
+ &:before {
+ content: '${props => props.label}';
+ position: absolute;
+ bottom: calc(100% + 8px);
+ text-align: center;
+ left: 50%;
+ transform: translateX(-50%);
white-space: nowrap;
${props => props.extraPadding}
- }
+ }
`;
export const Selected = styled.div`
- position: absolute;
- height: 100%;
- left: ${props => props.start * 4.1666666666666666}%;
- right: calc(100% - ${props => props.end * 4.1666666666666666}%);
- top: 0;
- background-color: ${props => props.theme.primary};
- border-radius: 2px;
+ position: absolute;
+ height: 100%;
+ left: ${props => props.start * 4.1666666666666666}%;
+ right: calc(100% - ${props => props.end * 4.1666666666666666}%);
+ top: 0;
+ background-color: ${props => props.theme.primary};
+ border-radius: 2px;
transition: left .1s, right .1s;
@media (prefers-reduced-motion: reduce) {
transition: none;
diff --git a/crabfit-frontend/src/components/ToggleField/ToggleField.tsx b/crabfit-frontend/src/components/ToggleField/ToggleField.tsx
index 3e83a65..2894a0d 100644
--- a/crabfit-frontend/src/components/ToggleField/ToggleField.tsx
+++ b/crabfit-frontend/src/components/ToggleField/ToggleField.tsx
@@ -1,25 +1,25 @@
import {
- Wrapper,
+ Wrapper,
ToggleContainer,
- StyledLabel,
- Option,
- HiddenInput,
+ StyledLabel,
+ Option,
+ HiddenInput,
LabelButton,
} from './toggleFieldStyle';
const ToggleField = ({
- label,
- id,
+ label,
+ id,
name,
title = '',
- options = [],
+ options = [],
value,
onChange,
inputRef,
- ...props
+ ...props
}) => (
-
- {label && {label} {title !== '' && } }
+
+ {label && {label} {title !== '' && } }
{Object.entries(options).map(([key, label]) =>
@@ -37,7 +37,7 @@ const ToggleField = ({
)}
-
+
);
export default ToggleField;
diff --git a/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts b/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts
index f2b7e0e..e3e7398 100644
--- a/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts
+++ b/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts
@@ -1,11 +1,11 @@
import styled from '@emotion/styled';
export const Wrapper = styled.div`
- margin: 10px 0;
+ margin: 10px 0;
`;
export const ToggleContainer = styled.div`
- display: flex;
+ display: flex;
border: 1px solid ${props => props.theme.primary};
border-radius: 3px;
overflow: hidden;
@@ -29,9 +29,9 @@ export const ToggleContainer = styled.div`
`;
export const StyledLabel = styled.label`
- display: block;
- padding-bottom: 4px;
- font-size: .9rem;
+ display: block;
+ padding-bottom: 4px;
+ font-size: .9rem;
& svg {
height: 1em;
@@ -41,7 +41,7 @@ export const StyledLabel = styled.label`
`;
export const Option = styled.div`
- flex: 1;
+ flex: 1;
position: relative;
`;
diff --git a/crabfit-frontend/src/components/UpdateDialog/UpdateDialog.tsx b/crabfit-frontend/src/components/UpdateDialog/UpdateDialog.tsx
index f087183..4f594af 100644
--- a/crabfit-frontend/src/components/UpdateDialog/UpdateDialog.tsx
+++ b/crabfit-frontend/src/components/UpdateDialog/UpdateDialog.tsx
@@ -10,14 +10,14 @@ const UpdateDialog = ({ onClose }) => {
const { t } = useTranslation('common');
return (
-
+
{t('common:update.heading')}
{t('common:update.body')}
{t('common:update.buttons.close')}
- window.location.reload()}>{t('common:update.buttons.reload')}
+ window.location.reload()}>{t('common:update.buttons.reload')}
-
+
);
}
diff --git a/crabfit-frontend/src/components/UpdateDialog/updateDialogStyle.ts b/crabfit-frontend/src/components/UpdateDialog/updateDialogStyle.ts
index 139f211..1e22753 100644
--- a/crabfit-frontend/src/components/UpdateDialog/updateDialogStyle.ts
+++ b/crabfit-frontend/src/components/UpdateDialog/updateDialogStyle.ts
@@ -13,7 +13,7 @@ export const Wrapper = styled.div`
border-radius: 3px;
width: 400px;
box-sizing: border-box;
- max-width: calc(100% - 20px);
+ max-width: calc(100% - 40px);
box-shadow: 0 3px 6px 0 rgba(0,0,0,.3);
& h2 {
@@ -31,4 +31,5 @@ export const ButtonWrapper = styled.div`
align-items: center;
justify-content: flex-end;
gap: 16px;
+ flex-wrap: wrap;
`;
diff --git a/crabfit-frontend/src/pages/Create/Create.tsx b/crabfit-frontend/src/pages/Create/Create.tsx
index d84e2a9..f63213f 100644
--- a/crabfit-frontend/src/pages/Create/Create.tsx
+++ b/crabfit-frontend/src/pages/Create/Create.tsx
@@ -9,21 +9,21 @@ import timezone from 'dayjs/plugin/timezone';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import {
- TextField,
- CalendarField,
- TimeRangeField,
- SelectField,
- Button,
- Error,
+ TextField,
+ CalendarField,
+ TimeRangeField,
+ SelectField,
+ Button,
+ Error,
Recents,
Footer,
} from 'components';
import {
- StyledMain,
- CreateForm,
- TitleSmall,
- TitleLarge,
+ StyledMain,
+ CreateForm,
+ TitleSmall,
+ TitleLarge,
P,
OfflineMessage,
ShareInfo,
@@ -39,14 +39,14 @@ dayjs.extend(timezone);
dayjs.extend(customParseFormat);
const Create = ({ offline }) => {
- const { register, handleSubmit, setValue } = useForm({
- defaultValues: {
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
- },
- });
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState(null);
- const [createdEvent, setCreatedEvent] = useState(null);
+ const { register, handleSubmit, setValue } = useForm({
+ defaultValues: {
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+ },
+ });
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [createdEvent, setCreatedEvent] = useState(null);
const [copied, setCopied] = useState(null);
const [showFooter, setShowFooter] = useState(true);
@@ -55,11 +55,11 @@ const Create = ({ offline }) => {
const addRecent = useRecentsStore(state => state.addRecent);
- useEffect(() => {
+ useEffect(() => {
if (window.self === window.top) {
push('/');
}
- document.title = 'Create a Crab Fit';
+ document.title = 'Create a Crab Fit';
if (window.parent) {
window.parent.postMessage('crabfit-create', '*');
@@ -71,67 +71,67 @@ const Create = ({ offline }) => {
once: true
});
}
- }, [push]);
+ }, [push]);
- const onSubmit = async data => {
- setIsLoading(true);
- setError(null);
- try {
- const { start, end } = JSON.parse(data.times);
- const dates = JSON.parse(data.dates);
+ const onSubmit = async data => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ const { start, end } = JSON.parse(data.times);
+ const dates = JSON.parse(data.dates);
- if (dates.length === 0) {
- return setError(t('home:form.errors.no_dates'));
- }
+ if (dates.length === 0) {
+ return setError(t('home:form.errors.no_dates'));
+ }
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
- if (start === end) {
- return setError(t('home:form.errors.same_times'));
- }
+ if (start === end) {
+ return setError(t('home:form.errors.same_times'));
+ }
- let times = dates.reduce((times, date) => {
- let day = [];
- for (let i = start; i < (start > end ? 24 : end); i++) {
+ let times = dates.reduce((times, date) => {
+ let day = [];
+ for (let i = start; i < (start > end ? 24 : end); i++) {
if (isSpecificDates) {
- day.push(
- dayjs.tz(date, 'DDMMYYYY', data.timezone)
- .hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
- );
+ 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++) {
+ }
+ if (start > end) {
+ for (let i = 0; i < end; i++) {
if (isSpecificDates) {
- day.push(
- dayjs.tz(date, 'DDMMYYYY', data.timezone)
- .hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
- );
+ 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];
- }, []);
+ }
+ }
+ return [...times, ...day];
+ }, []);
- if (times.length === 0) {
- return setError(t('home:form.errors.no_time'));
- }
+ if (times.length === 0) {
+ return setError(t('home:form.errors.no_time'));
+ }
- const response = await api.post('/event', {
- event: {
- name: data.name,
- times: times,
+ const response = await api.post('/event', {
+ event: {
+ name: data.name,
+ times: times,
timezone: data.timezone,
- },
- });
+ },
+ });
setCreatedEvent(response.data);
addRecent({
id: response.data.id,
@@ -141,19 +141,19 @@ const Create = ({ offline }) => {
gtag('event', 'create_event', {
'event_category': 'create',
});
- } catch (e) {
- setError(t('home:form.errors.unknown'));
- console.error(e);
- } finally {
- setIsLoading(false);
- }
- };
+ } catch (e) {
+ setError(t('home:form.errors.unknown'));
+ console.error(e);
+ } finally {
+ setIsLoading(false);
+ }
+ };
- return (
- <>
-
- {t('home:create')}
- CRAB FIT
+ return (
+ <>
+
+ {t('home:create')}
+ CRAB FIT
{createdEvent ? (
@@ -173,10 +173,10 @@ const Create = ({ offline }) => {
}
title={!!navigator.clipboard ? t('event:nav.title') : ''}
>{copied ?? `https://crab.fit/${createdEvent?.id}`}
-
+
{/* eslint-disable-next-line */}
- Click the link above to copy it to your clipboard, or share via gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: createdEvent?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${createdEvent?.id}`)}`} target="_blank">email .
-
+ Click the link above to copy it to your clipboard, or share via gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: createdEvent?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${createdEvent?.id}`)}`} target="_blank">email .
+
{showFooter && }
@@ -191,52 +191,52 @@ const Create = ({ offline }) => {
{t('home:offline')}
) : (
-
-
+
+
-
+ />
-
+ />
-
+ defaultOption={t('home:form.timezone.defaultOption')}
+ />
- setError(null)}>{error}
+ setError(null)}>{error}
- {t('home:form.button')}
-
+ {t('home:form.button')}
+
)}
-
+
>
)}
- >
- );
+ >
+ );
};
export default Create;
diff --git a/crabfit-frontend/src/pages/Create/createStyle.ts b/crabfit-frontend/src/pages/Create/createStyle.ts
index d7d2e9c..6b3d021 100644
--- a/crabfit-frontend/src/pages/Create/createStyle.ts
+++ b/crabfit-frontend/src/pages/Create/createStyle.ts
@@ -1,9 +1,9 @@
import styled from '@emotion/styled';
export const StyledMain = styled.div`
- width: 600px;
- margin: 10px auto;
- max-width: calc(100% - 30px);
+ width: 600px;
+ margin: 10px auto;
+ max-width: calc(100% - 30px);
`;
export const CreateForm = styled.form`
@@ -11,43 +11,43 @@ export const CreateForm = styled.form`
`;
export const TitleSmall = styled.span`
- display: block;
- margin: 0;
- font-size: 2rem;
- text-align: center;
- font-family: 'Samurai Bob', sans-serif;
- font-weight: 400;
- color: ${props => props.theme.primaryDark};
- line-height: 1em;
+ display: block;
+ margin: 0;
+ font-size: 2rem;
+ text-align: center;
+ font-family: 'Samurai Bob', sans-serif;
+ font-weight: 400;
+ color: ${props => props.theme.primaryDark};
+ line-height: 1em;
text-transform: uppercase;
`;
export const TitleLarge = styled.h1`
- margin: 0;
- font-size: 2rem;
- text-align: center;
- color: ${props => props.theme.primary};
- font-family: 'Molot', sans-serif;
- font-weight: 400;
- text-shadow: 0 3px 0 ${props => props.theme.primaryDark};
- line-height: 1em;
+ margin: 0;
+ font-size: 2rem;
+ text-align: center;
+ color: ${props => props.theme.primary};
+ font-family: 'Molot', sans-serif;
+ font-weight: 400;
+ text-shadow: 0 3px 0 ${props => props.theme.primaryDark};
+ line-height: 1em;
text-transform: uppercase;
`;
export const P = styled.p`
- font-weight: 500;
- line-height: 1.6em;
+ font-weight: 500;
+ line-height: 1.6em;
`;
export const OfflineMessage = styled.div`
- text-align: center;
+ text-align: center;
margin: 50px 0 20px;
`;
export const ShareInfo = styled.p`
- margin: 6px 0;
- text-align: center;
- font-size: 15px;
+ margin: 6px 0;
+ text-align: center;
+ font-size: 15px;
padding: 10px 0;
${props => props.onClick && `
diff --git a/crabfit-frontend/src/pages/Event/Event.tsx b/crabfit-frontend/src/pages/Event/Event.tsx
index c9dfa4b..1a63e58 100644
--- a/crabfit-frontend/src/pages/Event/Event.tsx
+++ b/crabfit-frontend/src/pages/Event/Event.tsx
@@ -9,27 +9,27 @@ import customParseFormat from 'dayjs/plugin/customParseFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
import {
- Footer,
- TextField,
- SelectField,
- Button,
- AvailabilityViewer,
- AvailabilityEditor,
- Error,
+ Footer,
+ TextField,
+ SelectField,
+ Button,
+ AvailabilityViewer,
+ AvailabilityEditor,
+ Error,
Logo,
} from 'components';
import { StyledMain } from '../Home/homeStyle';
import {
- EventName,
+ EventName,
EventDate,
- LoginForm,
- LoginSection,
- Info,
- ShareInfo,
- Tabs,
- Tab,
+ LoginForm,
+ LoginSection,
+ Info,
+ ShareInfo,
+ Tabs,
+ Tab,
} from './eventStyle';
import api from 'services';
@@ -47,94 +47,98 @@ const Event = (props) => {
const weekStart = useSettingsStore(state => state.weekStart);
const addRecent = useRecentsStore(state => state.addRecent);
+ const removeRecent = useRecentsStore(state => state.removeRecent);
const locale = useLocaleUpdateStore(state => state.locale);
const { t } = useTranslation(['common', 'event']);
- const { register, handleSubmit, setFocus, reset } = useForm();
- const { id } = props.match.params;
+ const { register, handleSubmit, setFocus, reset } = 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 [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 [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);
+ const [copied, setCopied] = useState(null);
- useEffect(() => {
- const fetchEvent = async () => {
- try {
- const response = await api.get(`/event/${id}`);
+ useEffect(() => {
+ const fetchEvent = async () => {
+ try {
+ const response = await api.get(`/event/${id}`);
- setEvent(response.data);
+ 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);
- }
- };
+ document.title = `${response.data.name} | Crab Fit`;
+ } catch (e) {
+ console.error(e);
+ if (e.status === 404) {
+ removeRecent(id);
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ };
- fetchEvent();
- }, [id, addRecent]);
+ fetchEvent();
+ }, [id, addRecent, removeRecent]);
- 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)
+ 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);
- }
- }
+ }));
+ setPeople(adjustedPeople);
+ } catch (e) {
+ console.error(e);
+ }
+ }
- if (tab === 'group') {
- fetchPeople();
- }
- }, [tab, id, timezone]);
+ if (tab === 'group') {
+ fetchPeople();
+ }
+ }, [tab, id, timezone]);
- // Convert to timezone and expand minute segments
- useEffect(() => {
- if (event) {
+ // Convert to timezone and expand minute segments
+ useEffect(() => {
+ if (event) {
const isSpecificDates = event.times[0].length === 13;
- setTimes(event.times.reduce(
- (allTimes, time) => {
+ 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) => {
+ 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 {
@@ -142,154 +146,154 @@ const Event = (props) => {
.diff(dayjs(b, 'HHmm').day((parseInt(b.substring(5))-weekStart % 7 + 7) % 7));
}
}));
- }
- }, [event, timezone, weekStart]);
+ }
+ }, [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 && !!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 }];
- }, []));
+ 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]);
+ 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)
+ 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);
- }
- };
+ };
+ setUser(adjustedUser);
+ } catch (e) {
+ console.log(e);
+ }
+ };
- if (user) {
- fetchUser();
- }
- // eslint-disable-next-line
- }, [timezone]);
+ if (user) {
+ fetchUser();
+ }
+ // eslint-disable-next-line
+ }, [timezone]);
- const onSubmit = async data => {
+ const onSubmit = async data => {
if (!data.name || data.name.length === 0) {
setFocus('name');
return setError(t('event:form.errors.name_required'));
}
- setIsLoginLoading(true);
- setError(null);
+ 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)
+ 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);
+ };
+ 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',
});
reset();
- }
- };
+ }
+ };
- return (
- <>
-
-
+ return (
+ <>
+
+
- {(!!event || isLoading) ? (
- <>
- {event?.name}
+ {(!!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'));
@@ -302,81 +306,81 @@ const Event = (props) => {
}
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 .
- }
-
- >
- ) : (
+
+ {!!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:offline.title')}
+
+
) : (
-
- {t('event:error.title')}
- {t('event:error.body')}
-
+
+ {t('event:error.title')}
+ {t('event:error.body')}
+
)
- )}
-
+ )}
+
- {(!!event || isLoading) && (
- <>
-
-
- {user ? (
+ {(!!event || isLoading) && (
+ <>
+
+
+ {user ? (
-
{t('event:form.signed_in', { name: user.name })}
+ {t('event:form.signed_in', { name: user.name })}
{
setTab('group');
setUser(null);
setPassword(null);
}}>{t('event:form.logout_button')}
- ) : (
- <>
- {t('event:form.signed_out')}
-
-
+ ) : (
+ <>
+ {t('event:form.signed_out')}
+
+
-
+
- {t('event:form.button')}
-
- setError(null)}>{error}
- {t('event:form.info')}
- >
- )}
+ {t('event:form.button')}
+
+ setError(null)}>{error}
+ {t('event:form.info')}
+ >
+ )}
- setTimezone(event.currentTarget.value)}
- options={timezones}
- />
+ 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();
@@ -395,84 +399,84 @@ const Event = (props) => {
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
}}>Click here to use it.
)}
-
-
+
+
-
-
- {
- e.preventDefault();
- if (user) {
- setTab('you');
- } else {
+
+
+ {
+ e.preventDefault();
+ if (user) {
+ setTab('you');
+ } else {
setFocus('name');
}
- }}
- 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')}
-
-
+ }}
+ 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}
- />
-
- ) : (
-
- p.availability.length > 0)}
+ min={min}
+ max={max}
+ />
+
+ ) : (
+
+ {
- const oldAvailability = [...user.availability];
- const utcAvailability = (!!availability.length && availability[0].length === 13)
+ value={user.availability}
+ onChange={async availability => {
+ 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 });
- }
- }}
- />
-
- )}
- >
- )}
+ 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;
diff --git a/crabfit-frontend/src/pages/Event/eventStyle.ts b/crabfit-frontend/src/pages/Event/eventStyle.ts
index 9a87bcc..c554204 100644
--- a/crabfit-frontend/src/pages/Event/eventStyle.ts
+++ b/crabfit-frontend/src/pages/Event/eventStyle.ts
@@ -1,21 +1,21 @@
import styled from '@emotion/styled';
export const EventName = styled.h1`
- text-align: center;
- font-weight: 800;
- margin: 20px 0 5px;
+ text-align: center;
+ font-weight: 800;
+ margin: 20px 0 5px;
- ${props => props.isLoading && `
- &:after {
- content: '';
- display: inline-block;
- height: 1em;
- width: 400px;
- max-width: 100%;
- background-color: ${props.theme.loading};
- border-radius: 3px;
- }
- `}
+ ${props => props.isLoading && `
+ &:after {
+ content: '';
+ display: inline-block;
+ height: 1em;
+ width: 400px;
+ max-width: 100%;
+ background-color: ${props.theme.loading};
+ border-radius: 3px;
+ }
+ `}
`;
export const EventDate = styled.span`
@@ -28,63 +28,73 @@ export const EventDate = styled.span`
letter-spacing: .01em;
${props => props.isLoading && `
- &:after {
- content: '';
- display: inline-block;
- height: 1em;
- width: 200px;
- max-width: 100%;
- background-color: ${props.theme.loading};
- border-radius: 3px;
- }
- `}
+ &:after {
+ content: '';
+ display: inline-block;
+ height: 1em;
+ width: 200px;
+ max-width: 100%;
+ background-color: ${props.theme.loading};
+ border-radius: 3px;
+ }
+ `}
+
+ @media print {
+ &::after {
+ content: ' - ' attr(title);
+ }
+ }
`;
export const LoginForm = styled.form`
- display: grid;
- grid-template-columns: 1fr 1fr auto;
- align-items: flex-end;
- grid-gap: 18px;
+ display: grid;
+ grid-template-columns: 1fr 1fr auto;
+ align-items: flex-end;
+ grid-gap: 18px;
- @media (max-width: 500px) {
- grid-template-columns: 1fr 1fr;
- }
- @media (max-width: 400px) {
- grid-template-columns: 1fr;
+ @media (max-width: 500px) {
+ grid-template-columns: 1fr 1fr;
+ }
+ @media (max-width: 400px) {
+ grid-template-columns: 1fr;
& div:last-child {
--btn-width: 100%;
}
- }
+ }
`;
export const LoginSection = styled.section`
- background-color: ${props => props.theme.primaryBackground};
- padding: 10px 0;
+ background-color: ${props => props.theme.primaryBackground};
+ padding: 10px 0;
+
+ @media print {
+ display: none;
+ }
`;
export const Info = styled.p`
- margin: 18px 0;
- opacity: .6;
- font-size: 12px;
+ margin: 18px 0;
+ opacity: .6;
+ font-size: 12px;
`;
export const ShareInfo = styled.p`
- margin: 6px 0;
- text-align: center;
- font-size: 15px;
+ margin: 6px 0;
+ text-align: center;
+ font-size: 15px;
- ${props => props.isLoading && `
- &:after {
- content: '';
- display: inline-block;
- height: 1em;
- width: 300px;
- max-width: 100%;
- background-color: ${props.theme.loading};
- border-radius: 3px;
- }
- `}
+ ${props => props.isLoading && `
+ &:after {
+ content: '';
+ display: inline-block;
+ height: 1em;
+ width: 300px;
+ max-width: 100%;
+ background-color: ${props.theme.loading};
+ border-radius: 3px;
+ }
+ `}
${props => props.onClick && `
cursor: pointer;
@@ -93,36 +103,46 @@ export const ShareInfo = styled.p`
color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
}
`}
+
+ @media print {
+ &.instructions {
+ display: none;
+ }
+ }
`;
export const Tabs = styled.div`
- display: flex;
- align-items: center;
- justify-content: center;
- margin: 30px 0 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 30px 0 20px;
+
+ @media print {
+ display: none;
+ }
`;
export const Tab = styled.a`
- user-select: none;
- text-decoration: none;
- display: block;
- color: ${props => props.theme.text};
- padding: 8px 18px;
- background-color: ${props => props.theme.primaryBackground};
- border: 1px solid ${props => props.theme.primary};
- border-bottom: 0;
- margin: 0 4px;
- border-top-left-radius: 5px;
- border-top-right-radius: 5px;
+ user-select: none;
+ text-decoration: none;
+ display: block;
+ color: ${props => props.theme.text};
+ padding: 8px 18px;
+ background-color: ${props => props.theme.primaryBackground};
+ border: 1px solid ${props => props.theme.primary};
+ border-bottom: 0;
+ margin: 0 4px;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
- ${props => props.selected && `
- color: #FFF;
- background-color: ${props.theme.primary};
- border-color: ${props.theme.primary};
- `}
+ ${props => props.selected && `
+ color: #FFF;
+ background-color: ${props.theme.primary};
+ border-color: ${props.theme.primary};
+ `}
- ${props => props.disabled && `
- opacity: .5;
- cursor: not-allowed;
- `}
+ ${props => props.disabled && `
+ opacity: .5;
+ cursor: not-allowed;
+ `}
`;
diff --git a/crabfit-frontend/src/pages/Help/Help.tsx b/crabfit-frontend/src/pages/Help/Help.tsx
index 4196595..b8817a7 100644
--- a/crabfit-frontend/src/pages/Help/Help.tsx
+++ b/crabfit-frontend/src/pages/Help/Help.tsx
@@ -3,41 +3,42 @@ import { Link, useHistory } from 'react-router-dom';
import { useTranslation, Trans } from 'react-i18next';
import {
- Button,
- Center,
- Footer,
+ Button,
+ Center,
+ Footer,
AvailabilityViewer,
Logo,
} from 'components';
import {
StyledMain,
- AboutSection,
- P,
+ AboutSection,
+ P,
} from '../Home/homeStyle';
import {
Step,
FakeCalendar,
FakeTimeRange,
+ ButtonArea,
} from './helpStyle';
const Help = () => {
const { push } = useHistory();
const { t } = useTranslation(['common', 'help']);
- useEffect(() => {
- document.title = t('help:name');
- }, [t]);
+ useEffect(() => {
+ document.title = t('help:name');
+ }, [t]);
- return (
- <>
-
+ return (
+ <>
+
- {t('help:name')}
+ {t('help:name')}
{t('help:p1')}
{t('help:p2')}
@@ -80,17 +81,19 @@ const Help = () => {
min={0}
max={5}
/>
-
+
-
-
- push('/')}>{t('common:cta')}
-
-
+
+
+
+ push('/')}>{t('common:cta')}
+
+
+
-
- >
- );
+
+ >
+ );
};
export default Help;
diff --git a/crabfit-frontend/src/pages/Help/helpStyle.ts b/crabfit-frontend/src/pages/Help/helpStyle.ts
index f0f93b8..91bd97f 100644
--- a/crabfit-frontend/src/pages/Help/helpStyle.ts
+++ b/crabfit-frontend/src/pages/Help/helpStyle.ts
@@ -1,43 +1,43 @@
import styled from '@emotion/styled';
export const Step = styled.h2`
- text-decoration-color: ${props => props.theme.primary};
+ text-decoration-color: ${props => props.theme.primary};
text-decoration-style: solid;
text-decoration-line: underline;
margin-top: 30px;
`;
export const FakeCalendar = styled.div`
- user-select: none;
+ user-select: none;
& div {
display: grid;
- grid-template-columns: repeat(7, 1fr);
- grid-gap: 2px;
+ grid-template-columns: repeat(7, 1fr);
+ grid-gap: 2px;
}
& .days span {
display: flex;
- align-items: center;
- justify-content: center;
- padding: 3px 0;
- font-weight: bold;
- user-select: none;
- opacity: .7;
+ align-items: center;
+ justify-content: center;
+ padding: 3px 0;
+ font-weight: bold;
+ user-select: none;
+ opacity: .7;
@media (max-width: 350px) {
- font-size: 12px;
- }
+ font-size: 12px;
+ }
}
& .dates span {
background-color: ${props => props.theme.primaryBackground};
- border: 1px solid ${props => props.theme.primary};
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 10px 0;
+ border: 1px solid ${props => props.theme.primary};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 10px 0;
&.selected {
color: #FFF;
- background-color: ${props => props.theme.primary};
+ background-color: ${props => props.theme.primary};
}
}
& .dates span:first-of-type {
@@ -51,45 +51,45 @@ export const FakeCalendar = styled.div`
`;
export const FakeTimeRange = styled.div`
- user-select: none;
+ user-select: none;
background-color: ${props => props.theme.primaryBackground};
- border: 1px solid ${props => props.theme.primary};
- border-radius: 3px;
- height: 50px;
- position: relative;
- margin: 38px 6px 18px;
+ border: 1px solid ${props => props.theme.primary};
+ border-radius: 3px;
+ height: 50px;
+ position: relative;
+ margin: 38px 6px 18px;
& div {
height: calc(100% + 20px);
- width: 20px;
- border: 1px solid ${props => props.theme.primary};
- background-color: ${props => props.theme.primaryLight};
- border-radius: 3px;
- position: absolute;
- top: -10px;
+ width: 20px;
+ border: 1px solid ${props => props.theme.primary};
+ background-color: ${props => props.theme.primaryLight};
+ border-radius: 3px;
+ position: absolute;
+ top: -10px;
- &:after {
- content: '|||';
- font-size: 8px;
- position: absolute;
- top: 0;
- left: 0;
- height: 100%;
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- color: ${props => props.theme.primaryDark};
- }
+ &:after {
+ content: '|||';
+ font-size: 8px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: ${props => props.theme.primaryDark};
+ }
- &:before {
- content: attr(data-label);
- position: absolute;
- bottom: calc(100% + 8px);
- text-align: center;
- left: 50%;
- transform: translateX(-50%);
- }
+ &:before {
+ content: attr(data-label);
+ position: absolute;
+ bottom: calc(100% + 8px);
+ text-align: center;
+ left: 50%;
+ transform: translateX(-50%);
+ }
}
& .start {
left: calc(${11 * 4.1666666666666666}% - 11px);
@@ -100,11 +100,17 @@ export const FakeTimeRange = styled.div`
&:before {
content: '';
position: absolute;
- height: 100%;
- left: ${11 * 4.1666666666666666}%;
- right: calc(100% - ${17 * 4.1666666666666666}%);
- top: 0;
- background-color: ${props => props.theme.primary};
- border-radius: 2px;
+ height: 100%;
+ left: ${11 * 4.1666666666666666}%;
+ right: calc(100% - ${17 * 4.1666666666666666}%);
+ top: 0;
+ background-color: ${props => props.theme.primary};
+ border-radius: 2px;
+ }
+`;
+
+export const ButtonArea = styled.div`
+ @media print {
+ display: none;
}
`;
diff --git a/crabfit-frontend/src/pages/Home/Home.tsx b/crabfit-frontend/src/pages/Home/Home.tsx
index c434b9e..3055859 100644
--- a/crabfit-frontend/src/pages/Home/Home.tsx
+++ b/crabfit-frontend/src/pages/Home/Home.tsx
@@ -9,36 +9,37 @@ import timezone from 'dayjs/plugin/timezone';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import {
- TextField,
- CalendarField,
- TimeRangeField,
- SelectField,
- Button,
- Center,
- Error,
+ TextField,
+ CalendarField,
+ TimeRangeField,
+ SelectField,
+ Button,
+ Center,
+ Error,
Footer,
Recents,
} from 'components';
import {
- StyledMain,
- CreateForm,
- TitleSmall,
- TitleLarge,
- Logo,
- Links,
- AboutSection,
- P,
- Stats,
- Stat,
- StatNumber,
- StatLabel,
+ StyledMain,
+ CreateForm,
+ TitleSmall,
+ TitleLarge,
+ Logo,
+ Links,
+ AboutSection,
+ P,
+ Stats,
+ Stat,
+ StatNumber,
+ StatLabel,
OfflineMessage,
ButtonArea,
} from './homeStyle';
import api from 'services';
import { detect_browser } from 'utils';
+import { useTWAStore } from 'stores';
import logo from 'res/logo.svg';
import timezones from 'res/timezones.json';
@@ -48,119 +49,120 @@ dayjs.extend(timezone);
dayjs.extend(customParseFormat);
const Home = ({ offline }) => {
- const { register, handleSubmit, setValue } = useForm({
- defaultValues: {
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
- },
- });
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState(null);
- const [stats, setStats] = useState({
- eventCount: null,
- personCount: null,
- version: 'loading...',
- });
+ const { register, handleSubmit, setValue } = useForm({
+ defaultValues: {
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+ },
+ });
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [stats, setStats] = useState({
+ eventCount: null,
+ personCount: null,
+ version: 'loading...',
+ });
const [browser, setBrowser] = useState(undefined);
- const { push } = useHistory();
+ const { push } = useHistory();
const { t } = useTranslation(['common', 'home']);
+ const isTWA = useTWAStore(state => state.TWA);
- useEffect(() => {
- const fetch = async () => {
- try {
- const response = await api.get('/stats');
- setStats(response.data);
- } catch (e) {
- console.error(e);
- }
- };
+ useEffect(() => {
+ const fetch = async () => {
+ try {
+ const response = await api.get('/stats');
+ setStats(response.data);
+ } catch (e) {
+ console.error(e);
+ }
+ };
- fetch();
- document.title = 'Crab Fit';
+ fetch();
+ document.title = 'Crab Fit';
setBrowser(detect_browser());
- }, []);
+ }, []);
- const onSubmit = async data => {
- setIsLoading(true);
- setError(null);
- try {
- const { start, end } = JSON.parse(data.times);
- const dates = JSON.parse(data.dates);
+ const onSubmit = async data => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ const { start, end } = JSON.parse(data.times);
+ const dates = JSON.parse(data.dates);
- if (dates.length === 0) {
- return setError(t('home:form.errors.no_dates'));
- }
+ if (dates.length === 0) {
+ return setError(t('home:form.errors.no_dates'));
+ }
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
- if (start === end) {
- return setError(t('home:form.errors.same_times'));
- }
+ if (start === end) {
+ return setError(t('home:form.errors.same_times'));
+ }
- let times = dates.reduce((times, date) => {
- let day = [];
- for (let i = start; i < (start > end ? 24 : end); i++) {
+ let times = dates.reduce((times, date) => {
+ let day = [];
+ for (let i = start; i < (start > end ? 24 : end); i++) {
if (isSpecificDates) {
- day.push(
- dayjs.tz(date, 'DDMMYYYY', data.timezone)
- .hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
- );
+ 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++) {
+ }
+ if (start > end) {
+ for (let i = 0; i < end; i++) {
if (isSpecificDates) {
- day.push(
- dayjs.tz(date, 'DDMMYYYY', data.timezone)
- .hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
- );
+ 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];
- }, []);
+ }
+ }
+ return [...times, ...day];
+ }, []);
- if (times.length === 0) {
- return setError(t('home:form.errors.no_time'));
- }
+ if (times.length === 0) {
+ return setError(t('home:form.errors.no_time'));
+ }
- const response = await api.post('/event', {
- event: {
- name: data.name,
- times: times,
+ const response = await api.post('/event', {
+ event: {
+ name: data.name,
+ times: times,
timezone: data.timezone,
- },
- });
- push(`/${response.data.id}`);
+ },
+ });
+ push(`/${response.data.id}`);
gtag('event', 'create_event', {
'event_category': 'home',
});
- } catch (e) {
- setError(t('home:form.errors.unknown'));
- console.error(e);
- } finally {
- setIsLoading(false);
- }
- };
+ } catch (e) {
+ setError(t('home:form.errors.unknown'));
+ console.error(e);
+ } finally {
+ setIsLoading(false);
+ }
+ };
- return (
- <>
-
-
-
-
- {t('home:create')}
- CRAB FIT
-
- {t('home:nav.about')} / {t('home:nav.donate')}
-
+ return (
+ <>
+
+
+
+
+ {t('home:create')}
+ CRAB FIT
+
+ {t('home:nav.about')} / {t('home:nav.donate')}
+
@@ -172,107 +174,109 @@ const Home = ({ offline }) => {
{t('home:offline')}
) : (
-
-
+
+ />
-
+ />
-
+ />
-
+ defaultOption={t('home:form.timezone.defaultOption')}
+ />
- setError(null)}>{error}
+ setError(null)}>{error}
-
- {t('home:form.button')}
-
-
+
+ {t('home:form.button')}
+
+
)}
-
+
-
-
- {t('home:about.name')}
-
-
- {stats.eventCount ?? '350+'}
- {t('home:about.events')}
-
-
- {stats.personCount ?? '550+'}
- {t('home:about.availabilities')}
-
-
- Crab Fit helps you fit your event around everyone's schedules. Simply create an event above and send the link to everyone that is participating. Results update live and you will be able to see a heat-map of when everyone is free. Learn more about how to Crab Fit.
-
- {['chrome', 'firefox', 'safari'].includes(browser) && (
+
+
+ {t('home:about.name')}
+
+
+ {stats.eventCount ?? '350+'}
+ {t('home:about.events')}
+
+
+ {stats.personCount ?? '550+'}
+ {t('home:about.availabilities')}
+
+
+ Crab Fit helps you fit your event around everyone's schedules. Simply create an event above and send the link to everyone that is participating. Results update live and you will be able to see a heat-map of when everyone is free. Learn more about how to Crab Fit.
+ {isTWA !== true && (
+
+ {['chrome', 'firefox', 'safari'].includes(browser) && (
+ ,
+ firefox: ,
+ safari: ,
+ }[browser]}
+ onClick={() => gtag('event', `download_extension_${browser}`, { 'event_category': 'home'})}
+ target="_blank"
+ rel="noreferrer noopener"
+ secondary
+ >{{
+ chrome: t('home:about.chrome_extension'),
+ firefox: t('home:about.firefox_extension'),
+ safari: t('home:about.safari_extension'),
+ }[browser]}
+ )}
,
- firefox: ,
- safari: ,
- }[browser]}
- onClick={() => gtag('event', `download_extension_${browser}`, { 'event_category': 'home'})}
+ href="https://play.google.com/store/apps/details?id=fit.crab"
+ icon={ }
+ onClick={() => gtag('event', 'download_android_app', { 'event_category': 'home' })}
target="_blank"
rel="noreferrer noopener"
secondary
- >{{
- chrome: t('home:about.chrome_extension'),
- firefox: t('home:about.firefox_extension'),
- safari: t('home:about.safari_extension'),
- }[browser]}
- )}
- }
- onClick={() => gtag('event', 'download_android_app', { 'event_category': 'home' })}
- target="_blank"
- rel="noreferrer noopener"
- secondary
- >{t('home:about.android_app')}
-
- Created by Ben Grant , Crab Fit is the modern-day solution to your group event planning debates.
- The code for Crab Fit is open source, if you find any issues or want to contribute, you can visit the repository . By using Crab Fit you agree to the privacy policy.
+ >{t('home:about.android_app')}
+
+ )}
+ Created by Ben Grant , Crab Fit is the modern-day solution to your group event planning debates.
+ The code for Crab Fit is open source, if you find any issues or want to contribute, you can visit the repository . By using Crab Fit you agree to the privacy policy.
{t('home:about.content.p6')}
{t('home:about.content.p5')}
-
-
+
+
-
- >
- );
+
+ >
+ );
};
export default Home;
diff --git a/crabfit-frontend/src/pages/Home/homeStyle.ts b/crabfit-frontend/src/pages/Home/homeStyle.ts
index 70ca3dc..54e8cd7 100644
--- a/crabfit-frontend/src/pages/Home/homeStyle.ts
+++ b/crabfit-frontend/src/pages/Home/homeStyle.ts
@@ -1,9 +1,9 @@
import styled from '@emotion/styled';
export const StyledMain = styled.div`
- width: 600px;
- margin: 20px auto;
- max-width: calc(100% - 60px);
+ width: 600px;
+ margin: 20px auto;
+ max-width: calc(100% - 60px);
`;
export const CreateForm = styled.form`
@@ -11,14 +11,14 @@ export const CreateForm = styled.form`
`;
export const TitleSmall = styled.span`
- display: block;
- margin: 0;
- font-size: 3rem;
- text-align: center;
- font-family: 'Samurai Bob', sans-serif;
- font-weight: 400;
- color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
- line-height: 1em;
+ display: block;
+ margin: 0;
+ font-size: 3rem;
+ text-align: center;
+ font-family: 'Samurai Bob', sans-serif;
+ font-weight: 400;
+ color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
+ line-height: 1em;
text-transform: uppercase;
${props => !props.altChars && `
@@ -30,23 +30,23 @@ export const TitleSmall = styled.span`
`;
export const TitleLarge = styled.h1`
- margin: 0;
- font-size: 4rem;
- text-align: center;
- color: ${props => props.theme.primary};
- font-family: 'Molot', sans-serif;
- font-weight: 400;
- text-shadow: 0 4px 0 ${props => props.theme.primaryDark};
- line-height: 1em;
+ margin: 0;
+ font-size: 4rem;
+ text-align: center;
+ color: ${props => props.theme.primary};
+ font-family: 'Molot', sans-serif;
+ font-weight: 400;
+ text-shadow: 0 4px 0 ${props => props.theme.primaryDark};
+ line-height: 1em;
text-transform: uppercase;
- @media (max-width: 350px) {
- font-size: 3.5rem;
- }
+ @media (max-width: 350px) {
+ font-size: 3.5rem;
+ }
`;
export const Logo = styled.img`
- width: 80px;
+ width: 80px;
transition: transform .15s;
animation: jelly .5s 1 .05s;
user-select: none;
@@ -81,14 +81,14 @@ export const Logo = styled.img`
`;
export const Links = styled.nav`
- text-align: center;
- margin: 20px 0;
+ text-align: center;
+ margin: 20px 0;
`;
export const AboutSection = styled.section`
- margin: 30px 0 0;
- background-color: ${props => props.theme.primaryBackground};
- padding: 20px 0;
+ margin: 30px 0 0;
+ background-color: ${props => props.theme.primaryBackground};
+ padding: 20px 0;
& a {
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
@@ -96,42 +96,42 @@ export const AboutSection = styled.section`
`;
export const P = styled.p`
- font-weight: 500;
- line-height: 1.6em;
+ font-weight: 500;
+ line-height: 1.6em;
`;
export const Stats = styled.div`
- display: flex;
- justify-content: space-around;
- align-items: flex-start;
- flex-wrap: wrap;
+ display: flex;
+ justify-content: space-around;
+ align-items: flex-start;
+ flex-wrap: wrap;
`;
export const Stat = styled.div`
- text-align: center;
- padding: 0 6px;
- min-width: 160px;
- margin: 10px 0;
+ text-align: center;
+ padding: 0 6px;
+ min-width: 160px;
+ margin: 10px 0;
`;
export const StatNumber = styled.span`
- display: block;
- font-weight: 900;
- color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
- font-size: 2em;
+ display: block;
+ font-weight: 900;
+ color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
+ font-size: 2em;
`;
export const StatLabel = styled.span`
- display: block;
+ display: block;
`;
export const OfflineMessage = styled.div`
- text-align: center;
+ text-align: center;
margin: 50px 0 20px;
`;
export const ButtonArea = styled.div`
- display: flex;
+ display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
diff --git a/crabfit-frontend/src/pages/Privacy/Privacy.tsx b/crabfit-frontend/src/pages/Privacy/Privacy.tsx
index 3717c96..2b1fd18 100644
--- a/crabfit-frontend/src/pages/Privacy/Privacy.tsx
+++ b/crabfit-frontend/src/pages/Privacy/Privacy.tsx
@@ -3,18 +3,18 @@ import { useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
- Button,
- Center,
- Footer,
+ Button,
+ Center,
+ Footer,
Logo,
} from 'components';
import {
- StyledMain,
- AboutSection,
- P,
+ StyledMain,
+ AboutSection,
+ P,
} from '../Home/homeStyle';
-import { Note } from './privacyStyle';
+import { Note, ButtonArea } from './privacyStyle';
const translationDisclaimer = 'While the translated document is provided for your convenience, the English version as displayed at https://crab.fit is legally binding.';
@@ -24,20 +24,20 @@ const Privacy = () => {
const contentRef = useRef();
const [content, setContent] = useState('');
- useEffect(() => {
- document.title = `${t('privacy:name')} - Crab Fit`;
- }, [t]);
+ useEffect(() => {
+ document.title = `${t('privacy:name')} - Crab Fit`;
+ }, [t]);
useEffect(() => setContent(contentRef.current?.innerText || ''), [contentRef]);
- return (
- <>
-
+ return (
+ <>
+
- {t('privacy:name')}
+ {t('privacy:name')}
{!i18n.language.startsWith('en') && (
@@ -58,9 +58,9 @@ const Privacy = () => {
Information Collection and Use
The Service uses third party services that may collect information used to identify you.
Links to privacy policies of the third party service providers used by the Service:
-
+
Log Data
When you use the Service, in the case of an error, data and information is collected to improve the Service, which may include your IP address, device name, operating system version, app configuration and the time and date of the error.
@@ -71,12 +71,12 @@ const Privacy = () => {
Service Providers
Third-party companies may be employed for the following reasons:
-
+
To facilitate the Service
To provide the Service on our behalf
To perform Service-related services
To assist in analyzing how the Service is used
-
+
To perform these tasks, the third parties may have access to your Personal Information, but are obligated not to disclose or use this information for any purpose except the above.
Security
@@ -98,15 +98,17 @@ const Privacy = () => {
-
-
- push('/')}>{t('common:cta')}
-
-
+
+
+
+ push('/')}>{t('common:cta')}
+
+
+
-
- >
- );
+
+ >
+ );
};
export default Privacy;
diff --git a/crabfit-frontend/src/pages/Privacy/privacyStyle.ts b/crabfit-frontend/src/pages/Privacy/privacyStyle.ts
index 7597810..5a1c3cd 100644
--- a/crabfit-frontend/src/pages/Privacy/privacyStyle.ts
+++ b/crabfit-frontend/src/pages/Privacy/privacyStyle.ts
@@ -7,8 +7,16 @@ export const Note = styled.p`
padding: 12px 16px;
margin: 16px 0;
box-sizing: border-box;
+ font-weight: 500;
+ line-height: 1.6em;
& a {
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
}
`;
+
+export const ButtonArea = styled.div`
+ @media print {
+ display: none;
+ }
+`;
diff --git a/crabfit-frontend/src/res/google.svg b/crabfit-frontend/src/res/google.svg
index 3b71e33..ba03d4e 100644
--- a/crabfit-frontend/src/res/google.svg
+++ b/crabfit-frontend/src/res/google.svg
@@ -1,9 +1,9 @@
+ c-329.4,0-609-217.3-708.7-517.7h0l0,0v0c-26.3-77.5-41.5-160.6-41.5-246.4c0-85.8,15.2-168.9,40.1-246.4v0
+ c101-300.4,380.6-517.7,710.1-517.7v0c233.9,0,391.7,101,481.7,185.5l351.6-343.3C1863.2,123.2,1582.2,0,1245.8,0
+ C758.6,0,337.8,279.6,133,686.5h0h0C48.6,855.4,0.1,1045,0.1,1245.7S48.6,1636,133,1804.9h0l0,0
+ c204.8,406.9,625.6,686.5,1112.8,686.5c336.3,0,618.7-110.7,824.9-301.7l0,0h0c235.3-217.3,370.9-537,370.9-916.3
+ c0-102.4-8.3-177.2-26.3-254.7H1245.8z"/>
diff --git a/crabfit-frontend/src/res/logo.svg b/crabfit-frontend/src/res/logo.svg
index 87c2c60..5d66013 100644
--- a/crabfit-frontend/src/res/logo.svg
+++ b/crabfit-frontend/src/res/logo.svg
@@ -1,43 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/crabfit-frontend/src/res/timezones.json b/crabfit-frontend/src/res/timezones.json
index b58f2e5..8026226 100644
--- a/crabfit-frontend/src/res/timezones.json
+++ b/crabfit-frontend/src/res/timezones.json
@@ -1,596 +1,596 @@
[
- "Africa/Abidjan",
- "Africa/Accra",
- "Africa/Addis_Ababa",
- "Africa/Algiers",
- "Africa/Asmara",
- "Africa/Asmera",
- "Africa/Bamako",
- "Africa/Bangui",
- "Africa/Banjul",
- "Africa/Bissau",
- "Africa/Blantyre",
- "Africa/Brazzaville",
- "Africa/Bujumbura",
- "Africa/Cairo",
- "Africa/Casablanca",
- "Africa/Ceuta",
- "Africa/Conakry",
- "Africa/Dakar",
- "Africa/Dar_es_Salaam",
- "Africa/Djibouti",
- "Africa/Douala",
- "Africa/El_Aaiun",
- "Africa/Freetown",
- "Africa/Gaborone",
- "Africa/Harare",
- "Africa/Johannesburg",
- "Africa/Juba",
- "Africa/Kampala",
- "Africa/Khartoum",
- "Africa/Kigali",
- "Africa/Kinshasa",
- "Africa/Lagos",
- "Africa/Libreville",
- "Africa/Lome",
- "Africa/Luanda",
- "Africa/Lubumbashi",
- "Africa/Lusaka",
- "Africa/Malabo",
- "Africa/Maputo",
- "Africa/Maseru",
- "Africa/Mbabane",
- "Africa/Mogadishu",
- "Africa/Monrovia",
- "Africa/Nairobi",
- "Africa/Ndjamena",
- "Africa/Niamey",
- "Africa/Nouakchott",
- "Africa/Ouagadougou",
- "Africa/Porto-Novo",
- "Africa/Sao_Tome",
- "Africa/Timbuktu",
- "Africa/Tripoli",
- "Africa/Tunis",
- "Africa/Windhoek",
- "America/Adak",
- "America/Anchorage",
- "America/Anguilla",
- "America/Antigua",
- "America/Araguaina",
- "America/Argentina/Buenos_Aires",
- "America/Argentina/Catamarca",
- "America/Argentina/ComodRivadavia",
- "America/Argentina/Cordoba",
- "America/Argentina/Jujuy",
- "America/Argentina/La_Rioja",
- "America/Argentina/Mendoza",
- "America/Argentina/Rio_Gallegos",
- "America/Argentina/Salta",
- "America/Argentina/San_Juan",
- "America/Argentina/San_Luis",
- "America/Argentina/Tucuman",
- "America/Argentina/Ushuaia",
- "America/Aruba",
- "America/Asuncion",
- "America/Atikokan",
- "America/Atka",
- "America/Bahia",
- "America/Bahia_Banderas",
- "America/Barbados",
- "America/Belem",
- "America/Belize",
- "America/Blanc-Sablon",
- "America/Boa_Vista",
- "America/Bogota",
- "America/Boise",
- "America/Buenos_Aires",
- "America/Cambridge_Bay",
- "America/Campo_Grande",
- "America/Cancun",
- "America/Caracas",
- "America/Catamarca",
- "America/Cayenne",
- "America/Cayman",
- "America/Chicago",
- "America/Chihuahua",
- "America/Coral_Harbour",
- "America/Cordoba",
- "America/Costa_Rica",
- "America/Creston",
- "America/Cuiaba",
- "America/Curacao",
- "America/Danmarkshavn",
- "America/Dawson",
- "America/Dawson_Creek",
- "America/Denver",
- "America/Detroit",
- "America/Dominica",
- "America/Edmonton",
- "America/Eirunepe",
- "America/El_Salvador",
- "America/Ensenada",
- "America/Fort_Nelson",
- "America/Fort_Wayne",
- "America/Fortaleza",
- "America/Glace_Bay",
- "America/Godthab",
- "America/Goose_Bay",
- "America/Grand_Turk",
- "America/Grenada",
- "America/Guadeloupe",
- "America/Guatemala",
- "America/Guayaquil",
- "America/Guyana",
- "America/Halifax",
- "America/Havana",
- "America/Hermosillo",
- "America/Indiana/Indianapolis",
- "America/Indiana/Knox",
- "America/Indiana/Marengo",
- "America/Indiana/Petersburg",
- "America/Indiana/Tell_City",
- "America/Indiana/Vevay",
- "America/Indiana/Vincennes",
- "America/Indiana/Winamac",
- "America/Indianapolis",
- "America/Inuvik",
- "America/Iqaluit",
- "America/Jamaica",
- "America/Jujuy",
- "America/Juneau",
- "America/Kentucky/Louisville",
- "America/Kentucky/Monticello",
- "America/Knox_IN",
- "America/Kralendijk",
- "America/La_Paz",
- "America/Lima",
- "America/Los_Angeles",
- "America/Louisville",
- "America/Lower_Princes",
- "America/Maceio",
- "America/Managua",
- "America/Manaus",
- "America/Marigot",
- "America/Martinique",
- "America/Matamoros",
- "America/Mazatlan",
- "America/Mendoza",
- "America/Menominee",
- "America/Merida",
- "America/Metlakatla",
- "America/Mexico_City",
- "America/Miquelon",
- "America/Moncton",
- "America/Monterrey",
- "America/Montevideo",
- "America/Montreal",
- "America/Montserrat",
- "America/Nassau",
- "America/New_York",
- "America/Nipigon",
- "America/Nome",
- "America/Noronha",
- "America/North_Dakota/Beulah",
- "America/North_Dakota/Center",
- "America/North_Dakota/New_Salem",
- "America/Nuuk",
- "America/Ojinaga",
- "America/Panama",
- "America/Pangnirtung",
- "America/Paramaribo",
- "America/Phoenix",
- "America/Port-au-Prince",
- "America/Port_of_Spain",
- "America/Porto_Acre",
- "America/Porto_Velho",
- "America/Puerto_Rico",
- "America/Punta_Arenas",
- "America/Rainy_River",
- "America/Rankin_Inlet",
- "America/Recife",
- "America/Regina",
- "America/Resolute",
- "America/Rio_Branco",
- "America/Rosario",
- "America/Santa_Isabel",
- "America/Santarem",
- "America/Santiago",
- "America/Santo_Domingo",
- "America/Sao_Paulo",
- "America/Scoresbysund",
- "America/Shiprock",
- "America/Sitka",
- "America/St_Barthelemy",
- "America/St_Johns",
- "America/St_Kitts",
- "America/St_Lucia",
- "America/St_Thomas",
- "America/St_Vincent",
- "America/Swift_Current",
- "America/Tegucigalpa",
- "America/Thule",
- "America/Thunder_Bay",
- "America/Tijuana",
- "America/Toronto",
- "America/Tortola",
- "America/Vancouver",
- "America/Virgin",
- "America/Whitehorse",
- "America/Winnipeg",
- "America/Yakutat",
- "America/Yellowknife",
- "Antarctica/Casey",
- "Antarctica/Davis",
- "Antarctica/DumontDUrville",
- "Antarctica/Macquarie",
- "Antarctica/Mawson",
- "Antarctica/McMurdo",
- "Antarctica/Palmer",
- "Antarctica/Rothera",
- "Antarctica/South_Pole",
- "Antarctica/Syowa",
- "Antarctica/Troll",
- "Antarctica/Vostok",
- "Arctic/Longyearbyen",
- "Asia/Aden",
- "Asia/Almaty",
- "Asia/Amman",
- "Asia/Anadyr",
- "Asia/Aqtau",
- "Asia/Aqtobe",
- "Asia/Ashgabat",
- "Asia/Ashkhabad",
- "Asia/Atyrau",
- "Asia/Baghdad",
- "Asia/Bahrain",
- "Asia/Baku",
- "Asia/Bangkok",
- "Asia/Barnaul",
- "Asia/Beirut",
- "Asia/Bishkek",
- "Asia/Brunei",
- "Asia/Calcutta",
- "Asia/Chita",
- "Asia/Choibalsan",
- "Asia/Chongqing",
- "Asia/Chungking",
- "Asia/Colombo",
- "Asia/Dacca",
- "Asia/Damascus",
- "Asia/Dhaka",
- "Asia/Dili",
- "Asia/Dubai",
- "Asia/Dushanbe",
- "Asia/Famagusta",
- "Asia/Gaza",
- "Asia/Harbin",
- "Asia/Hebron",
- "Asia/Ho_Chi_Minh",
- "Asia/Hong_Kong",
- "Asia/Hovd",
- "Asia/Irkutsk",
- "Asia/Istanbul",
- "Asia/Jakarta",
- "Asia/Jayapura",
- "Asia/Jerusalem",
- "Asia/Kabul",
- "Asia/Kamchatka",
- "Asia/Karachi",
- "Asia/Kashgar",
- "Asia/Kathmandu",
- "Asia/Katmandu",
- "Asia/Khandyga",
- "Asia/Kolkata",
- "Asia/Krasnoyarsk",
- "Asia/Kuala_Lumpur",
- "Asia/Kuching",
- "Asia/Kuwait",
- "Asia/Macao",
- "Asia/Macau",
- "Asia/Magadan",
- "Asia/Makassar",
- "Asia/Manila",
- "Asia/Muscat",
- "Asia/Nicosia",
- "Asia/Novokuznetsk",
- "Asia/Novosibirsk",
- "Asia/Omsk",
- "Asia/Oral",
- "Asia/Phnom_Penh",
- "Asia/Pontianak",
- "Asia/Pyongyang",
- "Asia/Qatar",
- "Asia/Qostanay",
- "Asia/Qyzylorda",
- "Asia/Rangoon",
- "Asia/Riyadh",
- "Asia/Saigon",
- "Asia/Sakhalin",
- "Asia/Samarkand",
- "Asia/Seoul",
- "Asia/Shanghai",
- "Asia/Singapore",
- "Asia/Srednekolymsk",
- "Asia/Taipei",
- "Asia/Tashkent",
- "Asia/Tbilisi",
- "Asia/Tehran",
- "Asia/Tel_Aviv",
- "Asia/Thimbu",
- "Asia/Thimphu",
- "Asia/Tokyo",
- "Asia/Tomsk",
- "Asia/Ujung_Pandang",
- "Asia/Ulaanbaatar",
- "Asia/Ulan_Bator",
- "Asia/Urumqi",
- "Asia/Ust-Nera",
- "Asia/Vientiane",
- "Asia/Vladivostok",
- "Asia/Yakutsk",
- "Asia/Yangon",
- "Asia/Yekaterinburg",
- "Asia/Yerevan",
- "Atlantic/Azores",
- "Atlantic/Bermuda",
- "Atlantic/Canary",
- "Atlantic/Cape_Verde",
- "Atlantic/Faeroe",
- "Atlantic/Faroe",
- "Atlantic/Jan_Mayen",
- "Atlantic/Madeira",
- "Atlantic/Reykjavik",
- "Atlantic/South_Georgia",
- "Atlantic/St_Helena",
- "Atlantic/Stanley",
- "Australia/ACT",
- "Australia/Adelaide",
- "Australia/Brisbane",
- "Australia/Broken_Hill",
- "Australia/Canberra",
- "Australia/Currie",
- "Australia/Darwin",
- "Australia/Eucla",
- "Australia/Hobart",
- "Australia/LHI",
- "Australia/Lindeman",
- "Australia/Lord_Howe",
- "Australia/Melbourne",
- "Australia/North",
- "Australia/NSW",
- "Australia/Perth",
- "Australia/Queensland",
- "Australia/South",
- "Australia/Sydney",
- "Australia/Tasmania",
- "Australia/Victoria",
- "Australia/West",
- "Australia/Yancowinna",
- "Brazil/Acre",
- "Brazil/DeNoronha",
- "Brazil/East",
- "Brazil/West",
- "Canada/Atlantic",
- "Canada/Central",
- "Canada/Eastern",
- "Canada/Mountain",
- "Canada/Newfoundland",
- "Canada/Pacific",
- "Canada/Saskatchewan",
- "Canada/Yukon",
- "CET",
- "Chile/Continental",
- "Chile/EasterIsland",
- "CST6CDT",
- "Cuba",
- "EET",
- "Egypt",
- "Eire",
- "EST",
- "EST5EDT",
- "Etc/GMT",
- "Etc/GMT+0",
- "Etc/GMT+1",
- "Etc/GMT+10",
- "Etc/GMT+11",
- "Etc/GMT+12",
- "Etc/GMT+2",
- "Etc/GMT+3",
- "Etc/GMT+4",
- "Etc/GMT+5",
- "Etc/GMT+6",
- "Etc/GMT+7",
- "Etc/GMT+8",
- "Etc/GMT+9",
- "Etc/GMT-0",
- "Etc/GMT-1",
- "Etc/GMT-10",
- "Etc/GMT-11",
- "Etc/GMT-12",
- "Etc/GMT-13",
- "Etc/GMT-14",
- "Etc/GMT-2",
- "Etc/GMT-3",
- "Etc/GMT-4",
- "Etc/GMT-5",
- "Etc/GMT-6",
- "Etc/GMT-7",
- "Etc/GMT-8",
- "Etc/GMT-9",
- "Etc/GMT0",
- "Etc/Greenwich",
- "Etc/UCT",
- "Etc/Universal",
- "Etc/UTC",
- "Etc/Zulu",
- "Europe/Amsterdam",
- "Europe/Andorra",
- "Europe/Astrakhan",
- "Europe/Athens",
- "Europe/Belfast",
- "Europe/Belgrade",
- "Europe/Berlin",
- "Europe/Bratislava",
- "Europe/Brussels",
- "Europe/Bucharest",
- "Europe/Budapest",
- "Europe/Busingen",
- "Europe/Chisinau",
- "Europe/Copenhagen",
- "Europe/Dublin",
- "Europe/Gibraltar",
- "Europe/Guernsey",
- "Europe/Helsinki",
- "Europe/Isle_of_Man",
- "Europe/Istanbul",
- "Europe/Jersey",
- "Europe/Kaliningrad",
- "Europe/Kiev",
- "Europe/Kirov",
- "Europe/Lisbon",
- "Europe/Ljubljana",
- "Europe/London",
- "Europe/Luxembourg",
- "Europe/Madrid",
- "Europe/Malta",
- "Europe/Mariehamn",
- "Europe/Minsk",
- "Europe/Monaco",
- "Europe/Moscow",
- "Europe/Nicosia",
- "Europe/Oslo",
- "Europe/Paris",
- "Europe/Podgorica",
- "Europe/Prague",
- "Europe/Riga",
- "Europe/Rome",
- "Europe/Samara",
- "Europe/San_Marino",
- "Europe/Sarajevo",
- "Europe/Saratov",
- "Europe/Simferopol",
- "Europe/Skopje",
- "Europe/Sofia",
- "Europe/Stockholm",
- "Europe/Tallinn",
- "Europe/Tirane",
- "Europe/Tiraspol",
- "Europe/Ulyanovsk",
- "Europe/Uzhgorod",
- "Europe/Vaduz",
- "Europe/Vatican",
- "Europe/Vienna",
- "Europe/Vilnius",
- "Europe/Volgograd",
- "Europe/Warsaw",
- "Europe/Zagreb",
- "Europe/Zaporozhye",
- "Europe/Zurich",
- "Factory",
- "GB",
- "GB-Eire",
- "GMT",
- "GMT+0",
- "GMT-0",
- "GMT0",
- "Greenwich",
- "Hongkong",
- "HST",
- "Iceland",
- "Indian/Antananarivo",
- "Indian/Chagos",
- "Indian/Christmas",
- "Indian/Cocos",
- "Indian/Comoro",
- "Indian/Kerguelen",
- "Indian/Mahe",
- "Indian/Maldives",
- "Indian/Mauritius",
- "Indian/Mayotte",
- "Indian/Reunion",
- "Iran",
- "Israel",
- "Jamaica",
- "Japan",
- "Kwajalein",
- "Libya",
- "MET",
- "Mexico/BajaNorte",
- "Mexico/BajaSur",
- "Mexico/General",
- "MST",
- "MST7MDT",
- "Navajo",
- "NZ",
- "NZ-CHAT",
- "Pacific/Apia",
- "Pacific/Auckland",
- "Pacific/Bougainville",
- "Pacific/Chatham",
- "Pacific/Chuuk",
- "Pacific/Easter",
- "Pacific/Efate",
- "Pacific/Enderbury",
- "Pacific/Fakaofo",
- "Pacific/Fiji",
- "Pacific/Funafuti",
- "Pacific/Galapagos",
- "Pacific/Gambier",
- "Pacific/Guadalcanal",
- "Pacific/Guam",
- "Pacific/Honolulu",
- "Pacific/Johnston",
- "Pacific/Kiritimati",
- "Pacific/Kosrae",
- "Pacific/Kwajalein",
- "Pacific/Majuro",
- "Pacific/Marquesas",
- "Pacific/Midway",
- "Pacific/Nauru",
- "Pacific/Niue",
- "Pacific/Norfolk",
- "Pacific/Noumea",
- "Pacific/Pago_Pago",
- "Pacific/Palau",
- "Pacific/Pitcairn",
- "Pacific/Pohnpei",
- "Pacific/Ponape",
- "Pacific/Port_Moresby",
- "Pacific/Rarotonga",
- "Pacific/Saipan",
- "Pacific/Samoa",
- "Pacific/Tahiti",
- "Pacific/Tarawa",
- "Pacific/Tongatapu",
- "Pacific/Truk",
- "Pacific/Wake",
- "Pacific/Wallis",
- "Pacific/Yap",
- "Poland",
- "Portugal",
- "PRC",
- "PST8PDT",
- "ROC",
- "ROK",
- "Singapore",
- "Turkey",
- "UCT",
- "Universal",
- "US/Alaska",
- "US/Aleutian",
- "US/Arizona",
- "US/Central",
- "US/East-Indiana",
- "US/Eastern",
- "US/Hawaii",
- "US/Indiana-Starke",
- "US/Michigan",
- "US/Mountain",
- "US/Pacific",
- "US/Samoa",
- "UTC",
- "W-SU",
- "WET",
- "Zulu"
+ "Africa/Abidjan",
+ "Africa/Accra",
+ "Africa/Addis_Ababa",
+ "Africa/Algiers",
+ "Africa/Asmara",
+ "Africa/Asmera",
+ "Africa/Bamako",
+ "Africa/Bangui",
+ "Africa/Banjul",
+ "Africa/Bissau",
+ "Africa/Blantyre",
+ "Africa/Brazzaville",
+ "Africa/Bujumbura",
+ "Africa/Cairo",
+ "Africa/Casablanca",
+ "Africa/Ceuta",
+ "Africa/Conakry",
+ "Africa/Dakar",
+ "Africa/Dar_es_Salaam",
+ "Africa/Djibouti",
+ "Africa/Douala",
+ "Africa/El_Aaiun",
+ "Africa/Freetown",
+ "Africa/Gaborone",
+ "Africa/Harare",
+ "Africa/Johannesburg",
+ "Africa/Juba",
+ "Africa/Kampala",
+ "Africa/Khartoum",
+ "Africa/Kigali",
+ "Africa/Kinshasa",
+ "Africa/Lagos",
+ "Africa/Libreville",
+ "Africa/Lome",
+ "Africa/Luanda",
+ "Africa/Lubumbashi",
+ "Africa/Lusaka",
+ "Africa/Malabo",
+ "Africa/Maputo",
+ "Africa/Maseru",
+ "Africa/Mbabane",
+ "Africa/Mogadishu",
+ "Africa/Monrovia",
+ "Africa/Nairobi",
+ "Africa/Ndjamena",
+ "Africa/Niamey",
+ "Africa/Nouakchott",
+ "Africa/Ouagadougou",
+ "Africa/Porto-Novo",
+ "Africa/Sao_Tome",
+ "Africa/Timbuktu",
+ "Africa/Tripoli",
+ "Africa/Tunis",
+ "Africa/Windhoek",
+ "America/Adak",
+ "America/Anchorage",
+ "America/Anguilla",
+ "America/Antigua",
+ "America/Araguaina",
+ "America/Argentina/Buenos_Aires",
+ "America/Argentina/Catamarca",
+ "America/Argentina/ComodRivadavia",
+ "America/Argentina/Cordoba",
+ "America/Argentina/Jujuy",
+ "America/Argentina/La_Rioja",
+ "America/Argentina/Mendoza",
+ "America/Argentina/Rio_Gallegos",
+ "America/Argentina/Salta",
+ "America/Argentina/San_Juan",
+ "America/Argentina/San_Luis",
+ "America/Argentina/Tucuman",
+ "America/Argentina/Ushuaia",
+ "America/Aruba",
+ "America/Asuncion",
+ "America/Atikokan",
+ "America/Atka",
+ "America/Bahia",
+ "America/Bahia_Banderas",
+ "America/Barbados",
+ "America/Belem",
+ "America/Belize",
+ "America/Blanc-Sablon",
+ "America/Boa_Vista",
+ "America/Bogota",
+ "America/Boise",
+ "America/Buenos_Aires",
+ "America/Cambridge_Bay",
+ "America/Campo_Grande",
+ "America/Cancun",
+ "America/Caracas",
+ "America/Catamarca",
+ "America/Cayenne",
+ "America/Cayman",
+ "America/Chicago",
+ "America/Chihuahua",
+ "America/Coral_Harbour",
+ "America/Cordoba",
+ "America/Costa_Rica",
+ "America/Creston",
+ "America/Cuiaba",
+ "America/Curacao",
+ "America/Danmarkshavn",
+ "America/Dawson",
+ "America/Dawson_Creek",
+ "America/Denver",
+ "America/Detroit",
+ "America/Dominica",
+ "America/Edmonton",
+ "America/Eirunepe",
+ "America/El_Salvador",
+ "America/Ensenada",
+ "America/Fort_Nelson",
+ "America/Fort_Wayne",
+ "America/Fortaleza",
+ "America/Glace_Bay",
+ "America/Godthab",
+ "America/Goose_Bay",
+ "America/Grand_Turk",
+ "America/Grenada",
+ "America/Guadeloupe",
+ "America/Guatemala",
+ "America/Guayaquil",
+ "America/Guyana",
+ "America/Halifax",
+ "America/Havana",
+ "America/Hermosillo",
+ "America/Indiana/Indianapolis",
+ "America/Indiana/Knox",
+ "America/Indiana/Marengo",
+ "America/Indiana/Petersburg",
+ "America/Indiana/Tell_City",
+ "America/Indiana/Vevay",
+ "America/Indiana/Vincennes",
+ "America/Indiana/Winamac",
+ "America/Indianapolis",
+ "America/Inuvik",
+ "America/Iqaluit",
+ "America/Jamaica",
+ "America/Jujuy",
+ "America/Juneau",
+ "America/Kentucky/Louisville",
+ "America/Kentucky/Monticello",
+ "America/Knox_IN",
+ "America/Kralendijk",
+ "America/La_Paz",
+ "America/Lima",
+ "America/Los_Angeles",
+ "America/Louisville",
+ "America/Lower_Princes",
+ "America/Maceio",
+ "America/Managua",
+ "America/Manaus",
+ "America/Marigot",
+ "America/Martinique",
+ "America/Matamoros",
+ "America/Mazatlan",
+ "America/Mendoza",
+ "America/Menominee",
+ "America/Merida",
+ "America/Metlakatla",
+ "America/Mexico_City",
+ "America/Miquelon",
+ "America/Moncton",
+ "America/Monterrey",
+ "America/Montevideo",
+ "America/Montreal",
+ "America/Montserrat",
+ "America/Nassau",
+ "America/New_York",
+ "America/Nipigon",
+ "America/Nome",
+ "America/Noronha",
+ "America/North_Dakota/Beulah",
+ "America/North_Dakota/Center",
+ "America/North_Dakota/New_Salem",
+ "America/Nuuk",
+ "America/Ojinaga",
+ "America/Panama",
+ "America/Pangnirtung",
+ "America/Paramaribo",
+ "America/Phoenix",
+ "America/Port-au-Prince",
+ "America/Port_of_Spain",
+ "America/Porto_Acre",
+ "America/Porto_Velho",
+ "America/Puerto_Rico",
+ "America/Punta_Arenas",
+ "America/Rainy_River",
+ "America/Rankin_Inlet",
+ "America/Recife",
+ "America/Regina",
+ "America/Resolute",
+ "America/Rio_Branco",
+ "America/Rosario",
+ "America/Santa_Isabel",
+ "America/Santarem",
+ "America/Santiago",
+ "America/Santo_Domingo",
+ "America/Sao_Paulo",
+ "America/Scoresbysund",
+ "America/Shiprock",
+ "America/Sitka",
+ "America/St_Barthelemy",
+ "America/St_Johns",
+ "America/St_Kitts",
+ "America/St_Lucia",
+ "America/St_Thomas",
+ "America/St_Vincent",
+ "America/Swift_Current",
+ "America/Tegucigalpa",
+ "America/Thule",
+ "America/Thunder_Bay",
+ "America/Tijuana",
+ "America/Toronto",
+ "America/Tortola",
+ "America/Vancouver",
+ "America/Virgin",
+ "America/Whitehorse",
+ "America/Winnipeg",
+ "America/Yakutat",
+ "America/Yellowknife",
+ "Antarctica/Casey",
+ "Antarctica/Davis",
+ "Antarctica/DumontDUrville",
+ "Antarctica/Macquarie",
+ "Antarctica/Mawson",
+ "Antarctica/McMurdo",
+ "Antarctica/Palmer",
+ "Antarctica/Rothera",
+ "Antarctica/South_Pole",
+ "Antarctica/Syowa",
+ "Antarctica/Troll",
+ "Antarctica/Vostok",
+ "Arctic/Longyearbyen",
+ "Asia/Aden",
+ "Asia/Almaty",
+ "Asia/Amman",
+ "Asia/Anadyr",
+ "Asia/Aqtau",
+ "Asia/Aqtobe",
+ "Asia/Ashgabat",
+ "Asia/Ashkhabad",
+ "Asia/Atyrau",
+ "Asia/Baghdad",
+ "Asia/Bahrain",
+ "Asia/Baku",
+ "Asia/Bangkok",
+ "Asia/Barnaul",
+ "Asia/Beirut",
+ "Asia/Bishkek",
+ "Asia/Brunei",
+ "Asia/Calcutta",
+ "Asia/Chita",
+ "Asia/Choibalsan",
+ "Asia/Chongqing",
+ "Asia/Chungking",
+ "Asia/Colombo",
+ "Asia/Dacca",
+ "Asia/Damascus",
+ "Asia/Dhaka",
+ "Asia/Dili",
+ "Asia/Dubai",
+ "Asia/Dushanbe",
+ "Asia/Famagusta",
+ "Asia/Gaza",
+ "Asia/Harbin",
+ "Asia/Hebron",
+ "Asia/Ho_Chi_Minh",
+ "Asia/Hong_Kong",
+ "Asia/Hovd",
+ "Asia/Irkutsk",
+ "Asia/Istanbul",
+ "Asia/Jakarta",
+ "Asia/Jayapura",
+ "Asia/Jerusalem",
+ "Asia/Kabul",
+ "Asia/Kamchatka",
+ "Asia/Karachi",
+ "Asia/Kashgar",
+ "Asia/Kathmandu",
+ "Asia/Katmandu",
+ "Asia/Khandyga",
+ "Asia/Kolkata",
+ "Asia/Krasnoyarsk",
+ "Asia/Kuala_Lumpur",
+ "Asia/Kuching",
+ "Asia/Kuwait",
+ "Asia/Macao",
+ "Asia/Macau",
+ "Asia/Magadan",
+ "Asia/Makassar",
+ "Asia/Manila",
+ "Asia/Muscat",
+ "Asia/Nicosia",
+ "Asia/Novokuznetsk",
+ "Asia/Novosibirsk",
+ "Asia/Omsk",
+ "Asia/Oral",
+ "Asia/Phnom_Penh",
+ "Asia/Pontianak",
+ "Asia/Pyongyang",
+ "Asia/Qatar",
+ "Asia/Qostanay",
+ "Asia/Qyzylorda",
+ "Asia/Rangoon",
+ "Asia/Riyadh",
+ "Asia/Saigon",
+ "Asia/Sakhalin",
+ "Asia/Samarkand",
+ "Asia/Seoul",
+ "Asia/Shanghai",
+ "Asia/Singapore",
+ "Asia/Srednekolymsk",
+ "Asia/Taipei",
+ "Asia/Tashkent",
+ "Asia/Tbilisi",
+ "Asia/Tehran",
+ "Asia/Tel_Aviv",
+ "Asia/Thimbu",
+ "Asia/Thimphu",
+ "Asia/Tokyo",
+ "Asia/Tomsk",
+ "Asia/Ujung_Pandang",
+ "Asia/Ulaanbaatar",
+ "Asia/Ulan_Bator",
+ "Asia/Urumqi",
+ "Asia/Ust-Nera",
+ "Asia/Vientiane",
+ "Asia/Vladivostok",
+ "Asia/Yakutsk",
+ "Asia/Yangon",
+ "Asia/Yekaterinburg",
+ "Asia/Yerevan",
+ "Atlantic/Azores",
+ "Atlantic/Bermuda",
+ "Atlantic/Canary",
+ "Atlantic/Cape_Verde",
+ "Atlantic/Faeroe",
+ "Atlantic/Faroe",
+ "Atlantic/Jan_Mayen",
+ "Atlantic/Madeira",
+ "Atlantic/Reykjavik",
+ "Atlantic/South_Georgia",
+ "Atlantic/St_Helena",
+ "Atlantic/Stanley",
+ "Australia/ACT",
+ "Australia/Adelaide",
+ "Australia/Brisbane",
+ "Australia/Broken_Hill",
+ "Australia/Canberra",
+ "Australia/Currie",
+ "Australia/Darwin",
+ "Australia/Eucla",
+ "Australia/Hobart",
+ "Australia/LHI",
+ "Australia/Lindeman",
+ "Australia/Lord_Howe",
+ "Australia/Melbourne",
+ "Australia/North",
+ "Australia/NSW",
+ "Australia/Perth",
+ "Australia/Queensland",
+ "Australia/South",
+ "Australia/Sydney",
+ "Australia/Tasmania",
+ "Australia/Victoria",
+ "Australia/West",
+ "Australia/Yancowinna",
+ "Brazil/Acre",
+ "Brazil/DeNoronha",
+ "Brazil/East",
+ "Brazil/West",
+ "Canada/Atlantic",
+ "Canada/Central",
+ "Canada/Eastern",
+ "Canada/Mountain",
+ "Canada/Newfoundland",
+ "Canada/Pacific",
+ "Canada/Saskatchewan",
+ "Canada/Yukon",
+ "CET",
+ "Chile/Continental",
+ "Chile/EasterIsland",
+ "CST6CDT",
+ "Cuba",
+ "EET",
+ "Egypt",
+ "Eire",
+ "EST",
+ "EST5EDT",
+ "Etc/GMT",
+ "Etc/GMT+0",
+ "Etc/GMT+1",
+ "Etc/GMT+10",
+ "Etc/GMT+11",
+ "Etc/GMT+12",
+ "Etc/GMT+2",
+ "Etc/GMT+3",
+ "Etc/GMT+4",
+ "Etc/GMT+5",
+ "Etc/GMT+6",
+ "Etc/GMT+7",
+ "Etc/GMT+8",
+ "Etc/GMT+9",
+ "Etc/GMT-0",
+ "Etc/GMT-1",
+ "Etc/GMT-10",
+ "Etc/GMT-11",
+ "Etc/GMT-12",
+ "Etc/GMT-13",
+ "Etc/GMT-14",
+ "Etc/GMT-2",
+ "Etc/GMT-3",
+ "Etc/GMT-4",
+ "Etc/GMT-5",
+ "Etc/GMT-6",
+ "Etc/GMT-7",
+ "Etc/GMT-8",
+ "Etc/GMT-9",
+ "Etc/GMT0",
+ "Etc/Greenwich",
+ "Etc/UCT",
+ "Etc/Universal",
+ "Etc/UTC",
+ "Etc/Zulu",
+ "Europe/Amsterdam",
+ "Europe/Andorra",
+ "Europe/Astrakhan",
+ "Europe/Athens",
+ "Europe/Belfast",
+ "Europe/Belgrade",
+ "Europe/Berlin",
+ "Europe/Bratislava",
+ "Europe/Brussels",
+ "Europe/Bucharest",
+ "Europe/Budapest",
+ "Europe/Busingen",
+ "Europe/Chisinau",
+ "Europe/Copenhagen",
+ "Europe/Dublin",
+ "Europe/Gibraltar",
+ "Europe/Guernsey",
+ "Europe/Helsinki",
+ "Europe/Isle_of_Man",
+ "Europe/Istanbul",
+ "Europe/Jersey",
+ "Europe/Kaliningrad",
+ "Europe/Kiev",
+ "Europe/Kirov",
+ "Europe/Lisbon",
+ "Europe/Ljubljana",
+ "Europe/London",
+ "Europe/Luxembourg",
+ "Europe/Madrid",
+ "Europe/Malta",
+ "Europe/Mariehamn",
+ "Europe/Minsk",
+ "Europe/Monaco",
+ "Europe/Moscow",
+ "Europe/Nicosia",
+ "Europe/Oslo",
+ "Europe/Paris",
+ "Europe/Podgorica",
+ "Europe/Prague",
+ "Europe/Riga",
+ "Europe/Rome",
+ "Europe/Samara",
+ "Europe/San_Marino",
+ "Europe/Sarajevo",
+ "Europe/Saratov",
+ "Europe/Simferopol",
+ "Europe/Skopje",
+ "Europe/Sofia",
+ "Europe/Stockholm",
+ "Europe/Tallinn",
+ "Europe/Tirane",
+ "Europe/Tiraspol",
+ "Europe/Ulyanovsk",
+ "Europe/Uzhgorod",
+ "Europe/Vaduz",
+ "Europe/Vatican",
+ "Europe/Vienna",
+ "Europe/Vilnius",
+ "Europe/Volgograd",
+ "Europe/Warsaw",
+ "Europe/Zagreb",
+ "Europe/Zaporozhye",
+ "Europe/Zurich",
+ "Factory",
+ "GB",
+ "GB-Eire",
+ "GMT",
+ "GMT+0",
+ "GMT-0",
+ "GMT0",
+ "Greenwich",
+ "Hongkong",
+ "HST",
+ "Iceland",
+ "Indian/Antananarivo",
+ "Indian/Chagos",
+ "Indian/Christmas",
+ "Indian/Cocos",
+ "Indian/Comoro",
+ "Indian/Kerguelen",
+ "Indian/Mahe",
+ "Indian/Maldives",
+ "Indian/Mauritius",
+ "Indian/Mayotte",
+ "Indian/Reunion",
+ "Iran",
+ "Israel",
+ "Jamaica",
+ "Japan",
+ "Kwajalein",
+ "Libya",
+ "MET",
+ "Mexico/BajaNorte",
+ "Mexico/BajaSur",
+ "Mexico/General",
+ "MST",
+ "MST7MDT",
+ "Navajo",
+ "NZ",
+ "NZ-CHAT",
+ "Pacific/Apia",
+ "Pacific/Auckland",
+ "Pacific/Bougainville",
+ "Pacific/Chatham",
+ "Pacific/Chuuk",
+ "Pacific/Easter",
+ "Pacific/Efate",
+ "Pacific/Enderbury",
+ "Pacific/Fakaofo",
+ "Pacific/Fiji",
+ "Pacific/Funafuti",
+ "Pacific/Galapagos",
+ "Pacific/Gambier",
+ "Pacific/Guadalcanal",
+ "Pacific/Guam",
+ "Pacific/Honolulu",
+ "Pacific/Johnston",
+ "Pacific/Kiritimati",
+ "Pacific/Kosrae",
+ "Pacific/Kwajalein",
+ "Pacific/Majuro",
+ "Pacific/Marquesas",
+ "Pacific/Midway",
+ "Pacific/Nauru",
+ "Pacific/Niue",
+ "Pacific/Norfolk",
+ "Pacific/Noumea",
+ "Pacific/Pago_Pago",
+ "Pacific/Palau",
+ "Pacific/Pitcairn",
+ "Pacific/Pohnpei",
+ "Pacific/Ponape",
+ "Pacific/Port_Moresby",
+ "Pacific/Rarotonga",
+ "Pacific/Saipan",
+ "Pacific/Samoa",
+ "Pacific/Tahiti",
+ "Pacific/Tarawa",
+ "Pacific/Tongatapu",
+ "Pacific/Truk",
+ "Pacific/Wake",
+ "Pacific/Wallis",
+ "Pacific/Yap",
+ "Poland",
+ "Portugal",
+ "PRC",
+ "PST8PDT",
+ "ROC",
+ "ROK",
+ "Singapore",
+ "Turkey",
+ "UCT",
+ "Universal",
+ "US/Alaska",
+ "US/Aleutian",
+ "US/Arizona",
+ "US/Central",
+ "US/East-Indiana",
+ "US/Eastern",
+ "US/Hawaii",
+ "US/Indiana-Starke",
+ "US/Michigan",
+ "US/Mountain",
+ "US/Pacific",
+ "US/Samoa",
+ "UTC",
+ "W-SU",
+ "WET",
+ "Zulu"
]
diff --git a/crabfit-frontend/src/services/index.js b/crabfit-frontend/src/services/index.js
index dfda193..ee4772c 100644
--- a/crabfit-frontend/src/services/index.js
+++ b/crabfit-frontend/src/services/index.js
@@ -1,45 +1,45 @@
import axios from 'axios';
export const instance = axios.create({
- baseURL: process.env.NODE_ENV === 'production' ? 'https://api-dot-crabfit.uc.r.appspot.com' : 'http://localhost:8080',
- timeout: 1000 * 300,
- headers: {
- 'Content-Type': 'application/json',
- },
+ baseURL: process.env.NODE_ENV === 'production' ? 'https://api-dot-crabfit.uc.r.appspot.com' : 'http://localhost:8080',
+ timeout: 1000 * 300,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
});
const handleError = error => {
- if (error.response && error.response.status) {
- console.log('[Error handler] res:', error.response);
- }
- return Promise.reject(error.response);
+ if (error.response && error.response.status) {
+ console.log('[Error handler] res:', error.response);
+ }
+ return Promise.reject(error.response);
};
const api = {
- get: async (endpoint, data) => {
- try {
- const response = await instance.get(endpoint, data);
- return Promise.resolve(response);
- } catch (error) {
- return handleError(error);
- }
- },
- post: async (endpoint, data, options = {}) => {
- try {
- const response = await instance.post(endpoint, data, options);
- return Promise.resolve(response);
- } catch (error) {
- return handleError(error);
- }
- },
- patch: async (endpoint, data) => {
- try {
- const response = await instance.patch(endpoint, data);
- return Promise.resolve(response);
- } catch (error) {
- return handleError(error);
- }
- },
+ get: async (endpoint, data) => {
+ try {
+ const response = await instance.get(endpoint, data);
+ return Promise.resolve(response);
+ } catch (error) {
+ return handleError(error);
+ }
+ },
+ post: async (endpoint, data, options = {}) => {
+ try {
+ const response = await instance.post(endpoint, data, options);
+ return Promise.resolve(response);
+ } catch (error) {
+ return handleError(error);
+ }
+ },
+ patch: async (endpoint, data) => {
+ try {
+ const response = await instance.patch(endpoint, data);
+ return Promise.resolve(response);
+ } catch (error) {
+ return handleError(error);
+ }
+ },
};
export default api;
diff --git a/crabfit-frontend/src/theme/index.ts b/crabfit-frontend/src/theme/index.ts
index b102ecc..d3c434c 100644
--- a/crabfit-frontend/src/theme/index.ts
+++ b/crabfit-frontend/src/theme/index.ts
@@ -1,26 +1,26 @@
const theme = {
- light: {
- mode: 'light',
- background: '#FFFFFF',
- text: '#000000',
- primary: '#F79E00',
- primaryDark: '#F48600',
- primaryLight: '#F4BB60',
- primaryBackground: '#FEF2DD',
- error: '#D32F2F',
- loading: '#DDDDDD',
- },
- dark: {
- mode: 'dark',
- background: '#111111',
- text: '#DDDDDD',
- primary: '#F79E00',
- primaryDark: '#CC7313',
- primaryLight: '#F4BB60',
- primaryBackground: '#30240F',
- error: '#E53935',
- loading: '#444444',
- },
+ light: {
+ mode: 'light',
+ background: '#FFFFFF',
+ text: '#000000',
+ primary: '#F79E00',
+ primaryDark: '#F48600',
+ primaryLight: '#F4BB60',
+ primaryBackground: '#FEF2DD',
+ error: '#D32F2F',
+ loading: '#DDDDDD',
+ },
+ dark: {
+ mode: 'dark',
+ background: '#111111',
+ text: '#DDDDDD',
+ primary: '#F79E00',
+ primaryDark: '#CC7313',
+ primaryLight: '#F4BB60',
+ primaryBackground: '#30240F',
+ error: '#E53935',
+ loading: '#444444',
+ },
};
export default theme;