|
|
@ -7,3 +7,7 @@ cron:
|
||||||
url: /tasks/legacyCleanup
|
url: /tasks/legacyCleanup
|
||||||
schedule: every tuesday 09:00
|
schedule: every tuesday 09:00
|
||||||
target: api
|
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
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ const updatePerson = require('./routes/updatePerson');
|
||||||
|
|
||||||
const taskCleanup = require('./routes/taskCleanup');
|
const taskCleanup = require('./routes/taskCleanup');
|
||||||
const taskLegacyCleanup = require('./routes/taskLegacyCleanup');
|
const taskLegacyCleanup = require('./routes/taskLegacyCleanup');
|
||||||
|
const taskRemoveOrphans = require('./routes/taskRemoveOrphans');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 8080;
|
const port = 8080;
|
||||||
|
|
@ -53,6 +54,7 @@ app.patch('/event/:eventId/people/:personName', updatePerson);
|
||||||
// Tasks
|
// Tasks
|
||||||
app.get('/tasks/cleanup', taskCleanup);
|
app.get('/tasks/cleanup', taskCleanup);
|
||||||
app.get('/tasks/legacyCleanup', taskLegacyCleanup);
|
app.get('/tasks/legacyCleanup', taskLegacyCleanup);
|
||||||
|
app.get('/tasks/removeOrphans', taskRemoveOrphans);
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Crabfit API listening at http://localhost:${port} in ${process.env.NODE_ENV === 'production' ? 'prod' : 'dev'} mode`)
|
console.log(`Crabfit API listening at http://localhost:${port} in ${process.env.NODE_ENV === 'production' ? 'prod' : 'dev'} mode`)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
const dayjs = require('dayjs');
|
const dayjs = require('dayjs');
|
||||||
|
|
||||||
module.exports = async (req, res) => {
|
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();
|
const threeMonthsAgo = dayjs().subtract(3, 'month').unix();
|
||||||
|
|
||||||
console.log(`Running cleanup task at ${dayjs().format('h:mma D MMM YYYY')}`);
|
console.log(`Running cleanup task at ${dayjs().format('h:mma D MMM YYYY')}`);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
const dayjs = require('dayjs');
|
const dayjs = require('dayjs');
|
||||||
|
|
||||||
module.exports = async (req, res) => {
|
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();
|
const threeMonthsAgo = dayjs().subtract(3, 'month').unix();
|
||||||
|
|
||||||
console.log(`Running LEGACY cleanup task at ${dayjs().format('h:mma D MMM YYYY')}`);
|
console.log(`Running LEGACY cleanup task at ${dayjs().format('h:mma D MMM YYYY')}`);
|
||||||
|
|
|
||||||
46
crabfit-backend/routes/taskRemoveOrphans.js
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -243,3 +243,16 @@ paths:
|
||||||
description: "Not found"
|
description: "Not found"
|
||||||
400:
|
400:
|
||||||
description: "Not called from a cron job"
|
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"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "Crab Fit",
|
"name": "Crab Fit",
|
||||||
"description": "Enter your availability to find a time that works for everyone!",
|
"description": "Enter your availability to find a time that works for everyone!",
|
||||||
"version": "1.1",
|
"version": "1.2",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
|
|
||||||
"author": "Ben Grant",
|
"author": "Ben Grant",
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 620 B After Width: | Height: | Size: 416 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 841 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 104 KiB |
|
|
@ -1,7 +1,7 @@
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Karla;
|
font-family: Karla;
|
||||||
src: url('fonts/karla-variable.ttf') format('truetype');
|
src: url('fonts/karla-variable.ttf') format('truetype');
|
||||||
font-weight: 1 999;
|
font-weight: 1 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico">
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="theme-color" content="#F79E00">
|
<meta name="theme-color" content="#F79E00">
|
||||||
<meta
|
<meta
|
||||||
name="keywords"
|
name="keywords"
|
||||||
content="crab, fit, crabfit, schedule, availability, availabilities, when2meet, doodle, meet, plan, time, timezone"
|
content="crab, fit, crabfit, schedule, availability, availabilities, when2meet, doodle, meet, plan, time, timezone"
|
||||||
>
|
>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Enter your availability to find a time that works for everyone!"
|
content="Enter your availability to find a time that works for everyone!"
|
||||||
|
|
@ -17,11 +17,11 @@
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png">
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png">
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||||
|
|
||||||
<meta property="og:title" content="Crab Fit">
|
<meta property="og:title" content="Crab Fit">
|
||||||
<meta property="og:description" content="Enter your availability to find a time that works for everyone!">
|
<meta property="og:description" content="Enter your availability to find a time that works for everyone!">
|
||||||
<meta property="og:url" content="https://crab.fit">
|
<meta property="og:url" content="https://crab.fit">
|
||||||
|
|
||||||
<link rel="stylesheet" href="%PUBLIC_URL%/index.css">
|
<link rel="stylesheet" href="%PUBLIC_URL%/index.css">
|
||||||
|
|
||||||
<title>Crab Fit</title>
|
<title>Crab Fit</title>
|
||||||
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -20,8 +20,8 @@ const wb = new Workbox('sw.js');
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const colortheme = useSettingsStore(state => state.theme);
|
const colortheme = useSettingsStore(state => state.theme);
|
||||||
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
const [isDark, setIsDark] = useState(darkQuery.matches);
|
const [isDark, setIsDark] = useState(darkQuery.matches);
|
||||||
const [offline, setOffline] = useState(!window.navigator.onLine);
|
const [offline, setOffline] = useState(!window.navigator.onLine);
|
||||||
|
|
||||||
const [eggCount, setEggCount] = useState(0);
|
const [eggCount, setEggCount] = useState(0);
|
||||||
|
|
@ -46,7 +46,7 @@ const App = () => {
|
||||||
[eggCount, eggKey]
|
[eggCount, eggKey]
|
||||||
);
|
);
|
||||||
|
|
||||||
darkQuery.addListener(e => colortheme === 'System' && setIsDark(e.matches));
|
darkQuery.addListener(e => colortheme === 'System' && setIsDark(e.matches));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onOffline = () => setOffline(true);
|
const onOffline = () => setOffline(true);
|
||||||
|
|
@ -87,56 +87,57 @@ const App = () => {
|
||||||
}, [colortheme, darkQuery.matches]);
|
}, [colortheme, darkQuery.matches]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ThemeProvider theme={theme[isDark ? 'dark' : 'light']}>
|
<ThemeProvider theme={theme[isDark ? 'dark' : 'light']}>
|
||||||
<Global
|
<Global
|
||||||
styles={theme => ({
|
styles={theme => ({
|
||||||
html: {
|
html: {
|
||||||
scrollBehavior: 'smooth',
|
scrollBehavior: 'smooth',
|
||||||
},
|
'-webkit-print-color-adjust': 'exact',
|
||||||
body: {
|
},
|
||||||
backgroundColor: theme.background,
|
body: {
|
||||||
color: theme.text,
|
backgroundColor: theme.background,
|
||||||
fontFamily: `'Karla', sans-serif`,
|
color: theme.text,
|
||||||
fontWeight: theme.mode === 'dark' ? 500 : 600,
|
fontFamily: `'Karla', sans-serif`,
|
||||||
margin: 0,
|
fontWeight: theme.mode === 'dark' ? 500 : 600,
|
||||||
},
|
margin: 0,
|
||||||
a: {
|
},
|
||||||
color: theme.primary,
|
a: {
|
||||||
},
|
color: theme.primary,
|
||||||
'*::-webkit-scrollbar': {
|
},
|
||||||
width: 16,
|
'*::-webkit-scrollbar': {
|
||||||
height: 16,
|
width: 16,
|
||||||
},
|
height: 16,
|
||||||
'*::-webkit-scrollbar-track': {
|
},
|
||||||
background: `${theme.primaryBackground}`,
|
'*::-webkit-scrollbar-track': {
|
||||||
},
|
background: `${theme.primaryBackground}`,
|
||||||
'*::-webkit-scrollbar-thumb': {
|
},
|
||||||
borderRadius: 100,
|
'*::-webkit-scrollbar-thumb': {
|
||||||
border: `4px solid ${theme.primaryBackground}`,
|
borderRadius: 100,
|
||||||
width: 12,
|
border: `4px solid ${theme.primaryBackground}`,
|
||||||
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}AA`,
|
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:hover': {
|
||||||
},
|
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}CC`,
|
||||||
'*::-webkit-scrollbar-thumb:active': {
|
},
|
||||||
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}`,
|
'*::-webkit-scrollbar-thumb:active': {
|
||||||
},
|
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}`,
|
||||||
})}
|
},
|
||||||
/>
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<Settings />
|
<Settings />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" exact render={props => (
|
<Route path="/" exact render={props => (
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<Home offline={offline} {...props} />
|
<Home offline={offline} {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)} />
|
)} />
|
||||||
<Route path="/how-to" exact render={props => (
|
<Route path="/how-to" exact render={props => (
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<Help {...props} />
|
<Help {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
@ -146,17 +147,17 @@ const App = () => {
|
||||||
<Privacy {...props} />
|
<Privacy {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)} />
|
)} />
|
||||||
<Route path="/create" exact render={props => (
|
<Route path="/create" exact render={props => (
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<Create offline={offline} {...props} />
|
<Create offline={offline} {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)} />
|
)} />
|
||||||
<Route path="/:id" exact render={props => (
|
<Route path="/:id" exact render={props => (
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<Event offline={offline} {...props} />
|
<Event offline={offline} {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)} />
|
)} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
||||||
{updateAvailable && (
|
{updateAvailable && (
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
|
|
@ -165,8 +166,8 @@ const App = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{eggVisible && <Egg eggKey={eggKey} onClose={() => setEggVisible(false)} />}
|
{eggVisible && <Egg eggKey={eggKey} onClose={() => setEggVisible(false)} />}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,17 @@ import dayjs_timezone from 'dayjs/plugin/timezone';
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
ScrollWrapper,
|
ScrollWrapper,
|
||||||
Container,
|
Container,
|
||||||
Date,
|
Date,
|
||||||
Times,
|
Times,
|
||||||
DateLabel,
|
DateLabel,
|
||||||
DayLabel,
|
DayLabel,
|
||||||
Spacer,
|
Spacer,
|
||||||
TimeLabels,
|
TimeLabels,
|
||||||
TimeLabel,
|
TimeLabel,
|
||||||
TimeSpace,
|
TimeSpace,
|
||||||
StyledMain,
|
StyledMain,
|
||||||
} from 'components/AvailabilityViewer/availabilityViewerStyle';
|
} from 'components/AvailabilityViewer/availabilityViewerStyle';
|
||||||
import { Time } from './availabilityEditorStyle';
|
import { Time } from './availabilityEditorStyle';
|
||||||
|
|
@ -37,34 +37,34 @@ dayjs.extend(utc);
|
||||||
dayjs.extend(dayjs_timezone);
|
dayjs.extend(dayjs_timezone);
|
||||||
|
|
||||||
const AvailabilityEditor = ({
|
const AvailabilityEditor = ({
|
||||||
times,
|
times,
|
||||||
timeLabels,
|
timeLabels,
|
||||||
dates,
|
dates,
|
||||||
timezone,
|
timezone,
|
||||||
isSpecificDates,
|
isSpecificDates,
|
||||||
value = [],
|
value = [],
|
||||||
onChange,
|
onChange,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('event');
|
const { t } = useTranslation('event');
|
||||||
const locale = useLocaleUpdateStore(state => state.locale);
|
const locale = useLocaleUpdateStore(state => state.locale);
|
||||||
|
|
||||||
const [selectingTimes, _setSelectingTimes] = useState([]);
|
const [selectingTimes, _setSelectingTimes] = useState([]);
|
||||||
const staticSelectingTimes = useRef([]);
|
const staticSelectingTimes = useRef([]);
|
||||||
const setSelectingTimes = newTimes => {
|
const setSelectingTimes = newTimes => {
|
||||||
staticSelectingTimes.current = newTimes;
|
staticSelectingTimes.current = newTimes;
|
||||||
_setSelectingTimes(newTimes);
|
_setSelectingTimes(newTimes);
|
||||||
};
|
};
|
||||||
|
|
||||||
const startPos = useRef({});
|
const startPos = useRef({});
|
||||||
const staticMode = useRef(null);
|
const staticMode = useRef(null);
|
||||||
const [mode, _setMode] = useState(staticMode.current);
|
const [mode, _setMode] = useState(staticMode.current);
|
||||||
const setMode = newMode => {
|
const setMode = newMode => {
|
||||||
staticMode.current = newMode;
|
staticMode.current = newMode;
|
||||||
_setMode(newMode);
|
_setMode(newMode);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledMain>
|
<StyledMain>
|
||||||
<Center style={{textAlign: 'center'}}>{t('event:you.info')}</Center>
|
<Center style={{textAlign: 'center'}}>{t('event:you.info')}</Center>
|
||||||
|
|
@ -98,89 +98,89 @@ const AvailabilityEditor = ({
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Wrapper locale={locale}>
|
<Wrapper locale={locale}>
|
||||||
<ScrollWrapper>
|
<ScrollWrapper>
|
||||||
<Container>
|
<Container>
|
||||||
<TimeLabels>
|
<TimeLabels>
|
||||||
{!!timeLabels.length && timeLabels.map((label, i) =>
|
{!!timeLabels.length && timeLabels.map((label, i) =>
|
||||||
<TimeSpace key={i}>
|
<TimeSpace key={i}>
|
||||||
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
|
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
|
||||||
</TimeSpace>
|
</TimeSpace>
|
||||||
)}
|
)}
|
||||||
</TimeLabels>
|
</TimeLabels>
|
||||||
{dates.map((date, x) => {
|
{dates.map((date, x) => {
|
||||||
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
|
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;
|
const last = dates.length === x+1 || (isSpecificDates ? dayjs(dates[x+1], 'DDMMYYYY') : dayjs().day(dates[x+1])).diff(parsedDate, 'day') > 1;
|
||||||
return (
|
return (
|
||||||
<Fragment key={x}>
|
<Fragment key={x}>
|
||||||
<Date>
|
<Date>
|
||||||
{isSpecificDates && <DateLabel>{parsedDate.format('MMM D')}</DateLabel>}
|
{isSpecificDates && <DateLabel>{parsedDate.format('MMM D')}</DateLabel>}
|
||||||
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
|
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
|
||||||
|
|
||||||
<Times
|
<Times
|
||||||
borderRight={last}
|
borderRight={last}
|
||||||
borderLeft={x === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[x-1], 'DDMMYYYY') : dayjs().day(dates[x-1]), 'day') > 1}
|
borderLeft={x === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[x-1], 'DDMMYYYY') : dayjs().day(dates[x-1]), 'day') > 1}
|
||||||
>
|
>
|
||||||
{timeLabels.map((timeLabel, y) => {
|
{timeLabels.map((timeLabel, y) => {
|
||||||
if (!timeLabel.time) return null;
|
if (!timeLabel.time) return null;
|
||||||
if (!times.includes(`${timeLabel.time}-${date}`)) {
|
if (!times.includes(`${timeLabel.time}-${date}`)) {
|
||||||
return (
|
return (
|
||||||
<TimeSpace key={x+y} className='timespace' title={t('event:greyed_times')} />
|
<TimeSpace key={x+y} className='timespace' title={t('event:greyed_times')} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const time = `${timeLabel.time}-${date}`;
|
const time = `${timeLabel.time}-${date}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Time
|
<Time
|
||||||
key={x+y}
|
key={x+y}
|
||||||
time={time}
|
time={time}
|
||||||
className="time"
|
className="time"
|
||||||
selected={value.includes(time)}
|
selected={value.includes(time)}
|
||||||
selecting={selectingTimes.includes(time)}
|
selecting={selectingTimes.includes(time)}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
startPos.current = {x, y};
|
startPos.current = {x, y};
|
||||||
setMode(value.includes(time) ? 'remove' : 'add');
|
setMode(value.includes(time) ? 'remove' : 'add');
|
||||||
setSelectingTimes([time]);
|
setSelectingTimes([time]);
|
||||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||||
|
|
||||||
document.addEventListener('pointerup', () => {
|
document.addEventListener('pointerup', () => {
|
||||||
if (staticMode.current === 'add') {
|
if (staticMode.current === 'add') {
|
||||||
onChange([...value, ...staticSelectingTimes.current]);
|
onChange([...value, ...staticSelectingTimes.current]);
|
||||||
} else if (staticMode.current === 'remove') {
|
} else if (staticMode.current === 'remove') {
|
||||||
onChange(value.filter(t => !staticSelectingTimes.current.includes(t)));
|
onChange(value.filter(t => !staticSelectingTimes.current.includes(t)));
|
||||||
}
|
}
|
||||||
setMode(null);
|
setMode(null);
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
}}
|
}}
|
||||||
onPointerEnter={() => {
|
onPointerEnter={() => {
|
||||||
if (staticMode.current) {
|
if (staticMode.current) {
|
||||||
let found = [];
|
let found = [];
|
||||||
for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
|
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++) {
|
for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
|
||||||
found.push({y: cy, x: 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]}`));
|
setSelectingTimes(found.filter(d => timeLabels[d.y].time?.length === 4).map(d => `${timeLabels[d.y].time}-${dates[d.x]}`));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Times>
|
</Times>
|
||||||
</Date>
|
</Date>
|
||||||
{last && dates.length !== x+1 && (
|
{last && dates.length !== x+1 && (
|
||||||
<Spacer />
|
<Spacer />
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Container>
|
</Container>
|
||||||
</ScrollWrapper>
|
</ScrollWrapper>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AvailabilityEditor;
|
export default AvailabilityEditor;
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const Time = styled.div`
|
export const Time = styled.div`
|
||||||
height: 10px;
|
height: 10px;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
transition: background-color .1s;
|
transition: background-color .1s;
|
||||||
|
|
||||||
${props => props.time.slice(2, 4) === '00' && `
|
${props => props.time.slice(2, 4) === '00' && `
|
||||||
border-top: 2px solid ${props.theme.text};
|
border-top: 2px solid ${props.theme.text};
|
||||||
`}
|
`}
|
||||||
${props => props.time.slice(2, 4) !== '00' && `
|
${props => props.time.slice(2, 4) !== '00' && `
|
||||||
border-top: 2px solid transparent;
|
border-top: 2px solid transparent;
|
||||||
`}
|
`}
|
||||||
${props => props.time.slice(2, 4) === '30' && `
|
${props => props.time.slice(2, 4) === '30' && `
|
||||||
border-top: 2px dotted ${props.theme.text};
|
border-top: 2px dotted ${props.theme.text};
|
||||||
`}
|
`}
|
||||||
|
|
||||||
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
||||||
background-color: ${props.theme.primary};
|
background-color: ${props.theme.primary};
|
||||||
`};
|
`};
|
||||||
${props => props.mode === 'remove' && props.selecting && `
|
${props => props.mode === 'remove' && props.selecting && `
|
||||||
background-color: ${props.theme.background};
|
background-color: ${props.theme.background};
|
||||||
`};
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -7,28 +7,29 @@ import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
|
||||||
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
||||||
|
|
||||||
import { Legend, Center } from 'components';
|
import { Legend } from 'components';
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
ScrollWrapper,
|
ScrollWrapper,
|
||||||
Container,
|
Container,
|
||||||
Date,
|
Date,
|
||||||
Times,
|
Times,
|
||||||
DateLabel,
|
DateLabel,
|
||||||
DayLabel,
|
DayLabel,
|
||||||
Time,
|
Time,
|
||||||
Spacer,
|
Spacer,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipTitle,
|
TooltipTitle,
|
||||||
TooltipDate,
|
TooltipDate,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipPerson,
|
TooltipPerson,
|
||||||
TimeLabels,
|
TimeLabels,
|
||||||
TimeLabel,
|
TimeLabel,
|
||||||
TimeSpace,
|
TimeSpace,
|
||||||
People,
|
People,
|
||||||
Person,
|
Person,
|
||||||
StyledMain,
|
StyledMain,
|
||||||
|
Info,
|
||||||
} from './availabilityViewerStyle';
|
} from './availabilityViewerStyle';
|
||||||
|
|
||||||
import locales from 'res/dayjs_locales';
|
import locales from 'res/dayjs_locales';
|
||||||
|
|
@ -38,16 +39,16 @@ dayjs.extend(customParseFormat);
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const AvailabilityViewer = ({
|
const AvailabilityViewer = ({
|
||||||
times,
|
times,
|
||||||
timeLabels,
|
timeLabels,
|
||||||
dates,
|
dates,
|
||||||
isSpecificDates,
|
isSpecificDates,
|
||||||
people = [],
|
people = [],
|
||||||
min = 0,
|
min = 0,
|
||||||
max = 0,
|
max = 0,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const [tooltip, setTooltip] = useState(null);
|
const [tooltip, setTooltip] = useState(null);
|
||||||
const timeFormat = useSettingsStore(state => state.timeFormat);
|
const timeFormat = useSettingsStore(state => state.timeFormat);
|
||||||
const highlight = useSettingsStore(state => state.highlight);
|
const highlight = useSettingsStore(state => state.highlight);
|
||||||
const [filteredPeople, setFilteredPeople] = useState([]);
|
const [filteredPeople, setFilteredPeople] = useState([]);
|
||||||
|
|
@ -153,7 +154,7 @@ const AvailabilityViewer = ({
|
||||||
times,
|
times,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledMain>
|
<StyledMain>
|
||||||
<Legend
|
<Legend
|
||||||
|
|
@ -162,10 +163,10 @@ const AvailabilityViewer = ({
|
||||||
total={people.filter(p => p.availability.length > 0).length}
|
total={people.filter(p => p.availability.length > 0).length}
|
||||||
onSegmentFocus={count => setFocusCount(count)}
|
onSegmentFocus={count => setFocusCount(count)}
|
||||||
/>
|
/>
|
||||||
<Center style={{textAlign: 'center'}}>{t('event:group.info1')}</Center>
|
<Info>{t('event:group.info1')}</Info>
|
||||||
{people.length > 1 && (
|
{people.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<Center style={{textAlign: 'center'}}>{t('event:group.info2')}</Center>
|
<Info>{t('event:group.info2')}</Info>
|
||||||
<People>
|
<People>
|
||||||
{people.map((person, i) =>
|
{people.map((person, i) =>
|
||||||
<Person
|
<Person
|
||||||
|
|
@ -194,19 +195,19 @@ const AvailabilityViewer = ({
|
||||||
)}
|
)}
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
|
|
||||||
<Wrapper ref={wrapper}>
|
<Wrapper ref={wrapper}>
|
||||||
<ScrollWrapper>
|
<ScrollWrapper>
|
||||||
{heatmap}
|
{heatmap}
|
||||||
|
|
||||||
{tooltip && (
|
{tooltip && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
x={tooltip.x}
|
x={tooltip.x}
|
||||||
y={tooltip.y}
|
y={tooltip.y}
|
||||||
>
|
>
|
||||||
<TooltipTitle>{tooltip.available}</TooltipTitle>
|
<TooltipTitle>{tooltip.available}</TooltipTitle>
|
||||||
<TooltipDate>{tooltip.date}</TooltipDate>
|
<TooltipDate>{tooltip.date}</TooltipDate>
|
||||||
{!!filteredPeople.length && (
|
{!!filteredPeople.length && (
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{tooltip.people.map(person =>
|
{tooltip.people.map(person =>
|
||||||
<TooltipPerson key={person}>{person}</TooltipPerson>
|
<TooltipPerson key={person}>{person}</TooltipPerson>
|
||||||
)}
|
)}
|
||||||
|
|
@ -215,12 +216,12 @@ const AvailabilityViewer = ({
|
||||||
)}
|
)}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</ScrollWrapper>
|
</ScrollWrapper>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AvailabilityViewer;
|
export default AvailabilityViewer;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled.div`
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -11,30 +11,30 @@ export const ScrollWrapper = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Container = styled.div`
|
export const Container = styled.div`
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0 calc(calc(100% - 600px) / 2);
|
padding: 0 calc(calc(100% - 600px) / 2);
|
||||||
|
|
||||||
@media (max-width: 660px) {
|
@media (max-width: 660px) {
|
||||||
padding: 0 30px;
|
padding: 0 30px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Date = styled.div`
|
export const Date = styled.div`
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Times = styled.div`
|
export const Times = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
border-bottom: 2px solid ${props => props.theme.text};
|
border-bottom: 2px solid ${props => props.theme.text};
|
||||||
border-left: 1px 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`
|
export const DateLabel = styled.label`
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DayLabel = styled.label`
|
export const DayLabel = styled.label`
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Time = styled.div`
|
export const Time = styled.div`
|
||||||
height: 10px;
|
height: 10px;
|
||||||
background-origin: border-box;
|
background-origin: border-box;
|
||||||
transition: background-color .1s;
|
transition: background-color .1s;
|
||||||
|
|
||||||
${props => props.time.slice(2, 4) === '00' && `
|
${props => props.time.slice(2, 4) === '00' && `
|
||||||
border-top: 2px solid ${props.theme.text};
|
border-top: 2px solid ${props.theme.text};
|
||||||
`}
|
`}
|
||||||
${props => props.time.slice(2, 4) !== '00' && `
|
${props => props.time.slice(2, 4) !== '00' && `
|
||||||
border-top: 2px solid transparent;
|
border-top: 2px solid transparent;
|
||||||
`}
|
`}
|
||||||
${props => props.time.slice(2, 4) === '30' && `
|
${props => props.time.slice(2, 4) === '30' && `
|
||||||
border-top: 2px dotted ${props.theme.text};
|
border-top: 2px dotted ${props.theme.text};
|
||||||
`}
|
`}
|
||||||
|
|
||||||
background-color: ${props => `${props.theme.primary}${Math.round((props.peopleCount/props.maxPeople)*255).toString(16)}`};
|
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 && `
|
${props => props.highlight && props.peopleCount === props.maxPeople && props.peopleCount > 0 && `
|
||||||
background-image: repeating-linear-gradient(
|
background-image: repeating-linear-gradient(
|
||||||
45deg,
|
45deg,
|
||||||
transparent,
|
transparent,
|
||||||
transparent 4.3px,
|
transparent 4.3px,
|
||||||
${props.theme.primaryDark} 4.3px,
|
${props.theme.primaryDark} 4.3px,
|
||||||
${props.theme.primaryDark} 8.6px
|
${props.theme.primaryDark} 8.6px
|
||||||
);
|
);
|
||||||
`}
|
`}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|
@ -103,40 +103,40 @@ export const Time = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Spacer = styled.div`
|
export const Spacer = styled.div`
|
||||||
width: 12px;
|
width: 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Tooltip = styled.div`
|
export const Tooltip = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: ${props => props.y}px;
|
top: ${props => props.y}px;
|
||||||
left: ${props => props.x}px;
|
left: ${props => props.x}px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
border: 1px solid ${props => props.theme.text};
|
border: 1px solid ${props => props.theme.text};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
background-color: ${props => props.theme.background}${props => props.theme.mode === 'light' ? 'EE' : 'DD'};
|
background-color: ${props => props.theme.background}${props => props.theme.mode === 'light' ? 'EE' : 'DD'};
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TooltipTitle = styled.span`
|
export const TooltipTitle = styled.span`
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TooltipDate = styled.span`
|
export const TooltipDate = styled.span`
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
display: block;
|
display: block;
|
||||||
opacity: .8;
|
opacity: .8;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TooltipContent = styled.div`
|
export const TooltipContent = styled.div`
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -154,38 +154,38 @@ export const TooltipPerson = styled.span`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TimeLabels = styled.div`
|
export const TimeLabels = styled.div`
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TimeSpace = styled.div`
|
export const TimeSpace = styled.div`
|
||||||
height: 10px;
|
height: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-top: 2px solid transparent;
|
border-top: 2px solid transparent;
|
||||||
|
|
||||||
&.timespace {
|
&.timespace {
|
||||||
background-origin: border-box;
|
background-origin: border-box;
|
||||||
background-image: repeating-linear-gradient(
|
background-image: repeating-linear-gradient(
|
||||||
45deg,
|
45deg,
|
||||||
transparent,
|
transparent,
|
||||||
transparent 4.3px,
|
transparent 4.3px,
|
||||||
${props => props.theme.loading} 4.3px,
|
${props => props.theme.loading} 4.3px,
|
||||||
${props => props.theme.loading} 8.6px
|
${props => props.theme.loading} 8.6px
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TimeLabel = styled.label`
|
export const TimeLabel = styled.label`
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -.7em;
|
top: -.7em;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledMain = styled.div`
|
export const StyledMain = styled.div`
|
||||||
|
|
@ -220,3 +220,12 @@ export const Person = styled.button`
|
||||||
border-color: ${props.theme.primary};
|
border-color: ${props.theme.primary};
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const Info = styled.span`
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Pressable } from './buttonStyle';
|
import { Pressable } from './buttonStyle';
|
||||||
|
|
||||||
const Button = ({ href, type = 'button', icon, children, ...props }) => (
|
const Button = ({ href, type = 'button', icon, children, ...props }) => (
|
||||||
<Pressable
|
<Pressable
|
||||||
type={type}
|
type={type}
|
||||||
as={href ? 'a' : 'button'}
|
as={href ? 'a' : 'button'}
|
||||||
href={href}
|
href={href}
|
||||||
|
|
|
||||||
|
|
@ -60,34 +60,34 @@ export const Pressable = styled.button`
|
||||||
}
|
}
|
||||||
|
|
||||||
${props => props.isLoading && `
|
${props => props.isLoading && `
|
||||||
color: transparent;
|
color: transparent;
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
|
|
||||||
& img {
|
& img {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes load {
|
@keyframes load {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(50% - 12px);
|
top: calc(50% - 12px);
|
||||||
left: calc(50% - 12px);
|
left: calc(50% - 12px);
|
||||||
height: 18px;
|
height: 18px;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
border: 3px solid ${props.primaryColor ? '#FFF' : props.theme.background};
|
border: 3px solid ${props.primaryColor ? '#FFF' : props.theme.background};
|
||||||
border-left-color: transparent;
|
border-left-color: transparent;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
animation: load .5s linear infinite;
|
animation: load .5s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
&:after {
|
&:after {
|
||||||
|
|
@ -106,7 +106,7 @@ export const Pressable = styled.button`
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
|
||||||
${props => props.secondary && `
|
${props => props.secondary && `
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
@ -121,4 +121,14 @@ export const Pressable = styled.button`
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
${props => !props.secondary && `
|
||||||
|
box-shadow: 0 4px 0 0 ${props.secondaryColor || props.theme.primaryDark};
|
||||||
|
`}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@ import { Button, ToggleField } from 'components';
|
||||||
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
StyledLabel,
|
StyledLabel,
|
||||||
StyledSubLabel,
|
StyledSubLabel,
|
||||||
CalendarHeader,
|
CalendarHeader,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
CalendarBody,
|
CalendarBody,
|
||||||
Date,
|
Date,
|
||||||
Day,
|
Day,
|
||||||
} from './calendarFieldStyle';
|
} from './calendarFieldStyle';
|
||||||
|
|
||||||
dayjs.extend(isToday);
|
dayjs.extend(isToday);
|
||||||
|
|
@ -24,90 +24,90 @@ dayjs.extend(localeData);
|
||||||
dayjs.extend(updateLocale);
|
dayjs.extend(updateLocale);
|
||||||
|
|
||||||
const calculateMonth = (month, year, weekStart) => {
|
const calculateMonth = (month, year, weekStart) => {
|
||||||
const date = dayjs().month(month).year(year);
|
const date = dayjs().month(month).year(year);
|
||||||
const daysInMonth = date.daysInMonth();
|
const daysInMonth = date.daysInMonth();
|
||||||
const daysBefore = date.date(1).day() - weekStart;
|
const daysBefore = date.date(1).day() - weekStart;
|
||||||
const daysAfter = 6 - date.date(daysInMonth).day() + weekStart;
|
const daysAfter = 6 - date.date(daysInMonth).day() + weekStart;
|
||||||
|
|
||||||
let dates = [];
|
let dates = [];
|
||||||
let curDate = date.date(1).subtract(daysBefore, 'day');
|
let curDate = date.date(1).subtract(daysBefore, 'day');
|
||||||
let y = 0;
|
let y = 0;
|
||||||
let x = 0;
|
let x = 0;
|
||||||
for (let i = 0; i < daysBefore + daysInMonth + daysAfter; i++) {
|
for (let i = 0; i < daysBefore + daysInMonth + daysAfter; i++) {
|
||||||
if (x === 0) dates[y] = [];
|
if (x === 0) dates[y] = [];
|
||||||
dates[y][x] = curDate.clone();
|
dates[y][x] = curDate.clone();
|
||||||
curDate = curDate.add(1, 'day');
|
curDate = curDate.add(1, 'day');
|
||||||
x++;
|
x++;
|
||||||
if (x > 6) {
|
if (x > 6) {
|
||||||
x = 0;
|
x = 0;
|
||||||
y++;
|
y++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dates;
|
return dates;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CalendarField = forwardRef(({
|
const CalendarField = forwardRef(({
|
||||||
label,
|
label,
|
||||||
subLabel,
|
subLabel,
|
||||||
id,
|
id,
|
||||||
setValue,
|
setValue,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const weekStart = useSettingsStore(state => state.weekStart);
|
const weekStart = useSettingsStore(state => state.weekStart);
|
||||||
const locale = useLocaleUpdateStore(state => state.locale);
|
const locale = useLocaleUpdateStore(state => state.locale);
|
||||||
const { t } = useTranslation('home');
|
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 [dates, setDates] = useState(calculateMonth(dayjs().month(), dayjs().year(), weekStart));
|
||||||
const [month, setMonth] = useState(dayjs().month());
|
const [month, setMonth] = useState(dayjs().month());
|
||||||
const [year, setYear] = useState(dayjs().year());
|
const [year, setYear] = useState(dayjs().year());
|
||||||
|
|
||||||
const [selectedDates, setSelectedDates] = useState([]);
|
const [selectedDates, setSelectedDates] = useState([]);
|
||||||
const [selectingDates, _setSelectingDates] = useState([]);
|
const [selectingDates, _setSelectingDates] = useState([]);
|
||||||
const staticSelectingDates = useRef([]);
|
const staticSelectingDates = useRef([]);
|
||||||
const setSelectingDates = newDates => {
|
const setSelectingDates = newDates => {
|
||||||
staticSelectingDates.current = newDates;
|
staticSelectingDates.current = newDates;
|
||||||
_setSelectingDates(newDates);
|
_setSelectingDates(newDates);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [selectedDays, setSelectedDays] = useState([]);
|
const [selectedDays, setSelectedDays] = useState([]);
|
||||||
const [selectingDays, _setSelectingDays] = useState([]);
|
const [selectingDays, _setSelectingDays] = useState([]);
|
||||||
const staticSelectingDays = useRef([]);
|
const staticSelectingDays = useRef([]);
|
||||||
const setSelectingDays = newDays => {
|
const setSelectingDays = newDays => {
|
||||||
staticSelectingDays.current = newDays;
|
staticSelectingDays.current = newDays;
|
||||||
_setSelectingDays(newDays);
|
_setSelectingDays(newDays);
|
||||||
};
|
};
|
||||||
|
|
||||||
const startPos = useRef({});
|
const startPos = useRef({});
|
||||||
const staticMode = useRef(null);
|
const staticMode = useRef(null);
|
||||||
const [mode, _setMode] = useState(staticMode.current);
|
const [mode, _setMode] = useState(staticMode.current);
|
||||||
const setMode = newMode => {
|
const setMode = newMode => {
|
||||||
staticMode.current = newMode;
|
staticMode.current = newMode;
|
||||||
_setMode(newMode);
|
_setMode(newMode);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => setValue(props.name, type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)), [type, selectedDays, selectedDates, setValue, props.name]);
|
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) {
|
if (dayjs.Ls.hasOwnProperty(locale) && weekStart !== dayjs.Ls[locale].weekStart) {
|
||||||
dayjs.updateLocale(locale, { weekStart });
|
dayjs.updateLocale(locale, { weekStart });
|
||||||
}
|
}
|
||||||
setDates(calculateMonth(month, year, weekStart));
|
setDates(calculateMonth(month, year, weekStart));
|
||||||
}, [weekStart, month, year, locale]);
|
}, [weekStart, month, year, locale]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper locale={locale}>
|
<Wrapper locale={locale}>
|
||||||
{label && <StyledLabel htmlFor={id}>{label}</StyledLabel>}
|
{label && <StyledLabel htmlFor={id}>{label}</StyledLabel>}
|
||||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
type="hidden"
|
type="hidden"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
value={type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)}
|
value={type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToggleField
|
<ToggleField
|
||||||
id="calendarMode"
|
id="calendarMode"
|
||||||
|
|
@ -122,50 +122,50 @@ const CalendarField = forwardRef(({
|
||||||
|
|
||||||
{type === 0 ? (
|
{type === 0 ? (
|
||||||
<>
|
<>
|
||||||
<CalendarHeader>
|
<CalendarHeader>
|
||||||
<Button
|
<Button
|
||||||
size="30px"
|
size="30px"
|
||||||
title={t('form.dates.tooltips.previous')}
|
title={t('form.dates.tooltips.previous')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (month-1 < 0) {
|
if (month-1 < 0) {
|
||||||
setYear(year-1);
|
setYear(year-1);
|
||||||
setMonth(11);
|
setMonth(11);
|
||||||
} else {
|
} else {
|
||||||
setMonth(month-1);
|
setMonth(month-1);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
><</Button>
|
><</Button>
|
||||||
<span>{dayjs.months()[month]} {year}</span>
|
<span>{dayjs.months()[month]} {year}</span>
|
||||||
<Button
|
<Button
|
||||||
size="30px"
|
size="30px"
|
||||||
title={t('form.dates.tooltips.next')}
|
title={t('form.dates.tooltips.next')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (month+1 > 11) {
|
if (month+1 > 11) {
|
||||||
setYear(year+1);
|
setYear(year+1);
|
||||||
setMonth(0);
|
setMonth(0);
|
||||||
} else {
|
} else {
|
||||||
setMonth(month+1);
|
setMonth(month+1);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>></Button>
|
>></Button>
|
||||||
</CalendarHeader>
|
</CalendarHeader>
|
||||||
|
|
||||||
<CalendarDays>
|
<CalendarDays>
|
||||||
{(weekStart ? [...dayjs.weekdaysShort().filter((_,i) => i !== 0), dayjs.weekdaysShort()[0]] : dayjs.weekdaysShort()).map(name =>
|
{(weekStart ? [...dayjs.weekdaysShort().filter((_,i) => i !== 0), dayjs.weekdaysShort()[0]] : dayjs.weekdaysShort()).map(name =>
|
||||||
<Day key={name}>{name}</Day>
|
<Day key={name}>{name}</Day>
|
||||||
)}
|
)}
|
||||||
</CalendarDays>
|
</CalendarDays>
|
||||||
<CalendarBody>
|
<CalendarBody>
|
||||||
{dates.length > 0 && dates.map((dateRow, y) =>
|
{dates.length > 0 && dates.map((dateRow, y) =>
|
||||||
dateRow.map((date, x) =>
|
dateRow.map((date, x) =>
|
||||||
<Date
|
<Date
|
||||||
key={y+x}
|
key={y+x}
|
||||||
otherMonth={date.month() !== month}
|
otherMonth={date.month() !== month}
|
||||||
isToday={date.isToday()}
|
isToday={date.isToday()}
|
||||||
title={`${date.date()} ${dayjs.months()[date.month()]}${date.isToday() ? ` (${t('form.dates.tooltips.today')})` : ''}`}
|
title={`${date.date()} ${dayjs.months()[date.month()]}${date.isToday() ? ` (${t('form.dates.tooltips.today')})` : ''}`}
|
||||||
selected={selectedDates.includes(date.format('DDMMYYYY'))}
|
selected={selectedDates.includes(date.format('DDMMYYYY'))}
|
||||||
selecting={selectingDates.includes(date)}
|
selecting={selectingDates.includes(date)}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
type="button"
|
type="button"
|
||||||
onKeyPress={e => {
|
onKeyPress={e => {
|
||||||
if (e.key === ' ' || e.key === 'Enter') {
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
|
@ -176,37 +176,37 @@ const CalendarField = forwardRef(({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPointerDown={e => {
|
onPointerDown={e => {
|
||||||
startPos.current = {x, y};
|
startPos.current = {x, y};
|
||||||
setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
|
setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
|
||||||
setSelectingDates([date]);
|
setSelectingDates([date]);
|
||||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||||
|
|
||||||
document.addEventListener('pointerup', () => {
|
document.addEventListener('pointerup', () => {
|
||||||
if (staticMode.current === 'add') {
|
if (staticMode.current === 'add') {
|
||||||
setSelectedDates([...selectedDates, ...staticSelectingDates.current.map(d => d.format('DDMMYYYY'))]);
|
setSelectedDates([...selectedDates, ...staticSelectingDates.current.map(d => d.format('DDMMYYYY'))]);
|
||||||
} else if (staticMode.current === 'remove') {
|
} else if (staticMode.current === 'remove') {
|
||||||
const toRemove = staticSelectingDates.current.map(d => d.format('DDMMYYYY'));
|
const toRemove = staticSelectingDates.current.map(d => d.format('DDMMYYYY'));
|
||||||
setSelectedDates(selectedDates.filter(d => !toRemove.includes(d)));
|
setSelectedDates(selectedDates.filter(d => !toRemove.includes(d)));
|
||||||
}
|
}
|
||||||
setMode(null);
|
setMode(null);
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
}}
|
}}
|
||||||
onPointerEnter={() => {
|
onPointerEnter={() => {
|
||||||
if (staticMode.current) {
|
if (staticMode.current) {
|
||||||
let found = [];
|
let found = [];
|
||||||
for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
|
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++) {
|
for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
|
||||||
found.push({y: cy, x: cx});
|
found.push({y: cy, x: cx});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setSelectingDates(found.map(d => dates[d.y][d.x]));
|
setSelectingDates(found.map(d => dates[d.y][d.x]));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>{date.date()}</Date>
|
>{date.date()}</Date>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</CalendarBody>
|
</CalendarBody>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<CalendarBody>
|
<CalendarBody>
|
||||||
|
|
@ -257,8 +257,8 @@ const CalendarField = forwardRef(({
|
||||||
)}
|
)}
|
||||||
</CalendarBody>
|
</CalendarBody>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default CalendarField;
|
export default CalendarField;
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,68 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled.div`
|
||||||
margin: 30px 0;
|
margin: 30px 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledLabel = styled.label`
|
export const StyledLabel = styled.label`
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledSubLabel = styled.label`
|
export const StyledSubLabel = styled.label`
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
opacity: .6;
|
opacity: .6;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CalendarHeader = styled.div`
|
export const CalendarHeader = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CalendarDays = styled.div`
|
export const CalendarDays = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: repeat(7, 1fr);
|
||||||
grid-gap: 2px;
|
grid-gap: 2px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Day = styled.div`
|
export const Day = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 3px 0;
|
padding: 3px 0;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
opacity: .7;
|
opacity: .7;
|
||||||
|
|
||||||
@media (max-width: 350px) {
|
@media (max-width: 350px) {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CalendarBody = styled.div`
|
export const CalendarBody = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: repeat(7, 1fr);
|
||||||
grid-gap: 2px;
|
grid-gap: 2px;
|
||||||
|
|
||||||
& button:first-of-type {
|
& button:first-of-type {
|
||||||
border-top-left-radius: 3px;
|
border-top-left-radius: 3px;
|
||||||
}
|
}
|
||||||
& button:nth-of-type(7) {
|
& button:nth-of-type(7) {
|
||||||
border-top-right-radius: 3px;
|
border-top-right-radius: 3px;
|
||||||
}
|
}
|
||||||
& button:nth-last-of-type(7) {
|
& button:nth-last-of-type(7) {
|
||||||
border-bottom-left-radius: 3px;
|
border-bottom-left-radius: 3px;
|
||||||
}
|
}
|
||||||
& button:last-of-type {
|
& button:last-of-type {
|
||||||
border-bottom-right-radius: 3px;
|
border-bottom-right-radius: 3px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Date = styled.button`
|
export const Date = styled.button`
|
||||||
|
|
@ -77,28 +77,28 @@ export const Date = styled.button`
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
background-color: ${props => props.theme.primaryBackground};
|
background-color: ${props => props.theme.primaryBackground};
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid ${props => props.theme.primary};
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
|
|
||||||
${props => props.otherMonth && `
|
${props => props.otherMonth && `
|
||||||
color: ${props.theme.mode === 'light' ? props.theme.primaryLight : props.theme.primaryDark};
|
color: ${props.theme.mode === 'light' ? props.theme.primaryLight : props.theme.primaryDark};
|
||||||
`}
|
`}
|
||||||
${props => props.isToday && `
|
${props => props.isToday && `
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||||
`}
|
`}
|
||||||
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
||||||
color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};
|
color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};
|
||||||
background-color: ${props.theme.primary};
|
background-color: ${props.theme.primary};
|
||||||
`}
|
`}
|
||||||
${props => props.mode === 'remove' && props.selecting && `
|
${props => props.mode === 'remove' && props.selecting && `
|
||||||
background-color: ${props.theme.primaryBackground};
|
background-color: ${props.theme.primaryBackground};
|
||||||
color: ${props.isToday ? props.theme.primaryDark : (props.otherMonth ? props.theme.primaryLight : 'inherit')};
|
color: ${props.isToday ? props.theme.primaryDark : (props.otherMonth ? props.theme.primaryLight : 'inherit')};
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
const Center = styled.div`
|
const Center = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Center;
|
export default Center;
|
||||||
|
|
|
||||||
|
|
@ -96,9 +96,9 @@ const Donate = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Button
|
<Button
|
||||||
small
|
small
|
||||||
title={t('donate.title')}
|
title={t('donate.title')}
|
||||||
onClick={event => {
|
onClick={event => {
|
||||||
if (closed) {
|
if (closed) {
|
||||||
|
|
@ -125,7 +125,7 @@ const Donate = () => {
|
||||||
role="button"
|
role="button"
|
||||||
aria-expanded={isOpen ? 'true' : 'false'}
|
aria-expanded={isOpen ? 'true' : 'false'}
|
||||||
style={{ whiteSpace: 'nowrap' }}
|
style={{ whiteSpace: 'nowrap' }}
|
||||||
>{t('donate.button')}</Button>
|
>{t('donate.button')}</Button>
|
||||||
|
|
||||||
<Options
|
<Options
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
|
@ -144,7 +144,7 @@ const Donate = () => {
|
||||||
<a onClick={linkPressed} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=10" target="_blank" rel="noreferrer noopener payment">{t('donate.options.$10')}</a>
|
<a onClick={linkPressed} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=10" target="_blank" rel="noreferrer noopener payment">{t('donate.options.$10')}</a>
|
||||||
<a onClick={linkPressed} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD" target="_blank" rel="noreferrer noopener payment">{t('donate.options.choose')}</a>
|
<a onClick={linkPressed} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD" target="_blank" rel="noreferrer noopener payment">{t('donate.options.choose')}</a>
|
||||||
</Options>
|
</Options>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export const Wrapper = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Options = styled.div`
|
export const Options = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: calc(100% + 20px);
|
bottom: calc(100% + 20px);
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: ${props => props.theme.background};
|
background-color: ${props => props.theme.background};
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,14 @@ const Egg = ({ eggKey, onClose }) => {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper title="Click anywhere to close" onClick={() => onClose()}>
|
<Wrapper title="Click anywhere to close" onClick={() => onClose()}>
|
||||||
<Image
|
<Image
|
||||||
src={`https://us-central1-flour-app-services.cloudfunctions.net/charliAPI?v=${eggKey}`}
|
src={`https://us-central1-flour-app-services.cloudfunctions.net/charliAPI?v=${eggKey}`}
|
||||||
onLoadStart={() => setIsLoading(true)}
|
onLoadStart={() => setIsLoading(true)}
|
||||||
onLoad={() => setIsLoading(false)}
|
onLoad={() => setIsLoading(false)}
|
||||||
/>
|
/>
|
||||||
{isLoading && <Loading />}
|
{isLoading && <Loading />}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled.div`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background: rgba(0,0,0,.6);
|
background: rgba(0,0,0,.6);
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
@ -17,7 +17,7 @@ export const Wrapper = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Image = styled.img`
|
export const Image = styled.img`
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
max-height: 80%;
|
max-height: 80%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import { Wrapper, CloseButton } from './errorStyle';
|
import { Wrapper, CloseButton } from './errorStyle';
|
||||||
|
|
||||||
const Error = ({
|
const Error = ({
|
||||||
children,
|
children,
|
||||||
onClose,
|
onClose,
|
||||||
open = true,
|
open = true,
|
||||||
...props
|
...props
|
||||||
}) => (
|
}) => (
|
||||||
<Wrapper role="alert" open={open} {...props}>
|
<Wrapper role="alert" open={open} {...props}>
|
||||||
{children}
|
{children}
|
||||||
<CloseButton type="button" onClick={onClose} title="Close error">
|
<CloseButton type="button" onClick={onClose} title="Close error">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||||
</CloseButton>
|
</CloseButton>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Error;
|
export default Error;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled.div`
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background-color: ${props => props.theme.error};
|
background-color: ${props => props.theme.error};
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -30,14 +30,14 @@ export const Wrapper = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CloseButton = styled.button`
|
export const CloseButton = styled.button`
|
||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const Wrapper = styled.footer`
|
export const Wrapper = styled.footer`
|
||||||
width: 600px;
|
width: 600px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
max-width: calc(100% - 60px);
|
max-width: calc(100% - 60px);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
${props => props.small && `
|
${props => props.small && `
|
||||||
margin: 60px auto 0;
|
margin: 60px auto 0;
|
||||||
|
|
@ -19,4 +19,8 @@ export const Wrapper = styled.footer`
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -93,11 +93,11 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
}
|
}
|
||||||
}, [signedIn]);
|
}, [signedIn]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!signedIn ? (
|
{!signedIn ? (
|
||||||
<Center>
|
<Center>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => signIn()}
|
onClick={() => signIn()}
|
||||||
isLoading={signedIn === undefined}
|
isLoading={signedIn === undefined}
|
||||||
primaryColor="#4286F5"
|
primaryColor="#4286F5"
|
||||||
|
|
@ -161,7 +161,7 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
</CalendarList>
|
</CalendarList>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GoogleCalendar;
|
export default GoogleCalendar;
|
||||||
|
|
|
||||||
|
|
@ -3,46 +3,46 @@ import { useSettingsStore } from 'stores';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
Label,
|
Label,
|
||||||
Bar,
|
Bar,
|
||||||
Grade,
|
Grade,
|
||||||
} from './legendStyle';
|
} from './legendStyle';
|
||||||
|
|
||||||
const Legend = ({
|
const Legend = ({
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
total,
|
total,
|
||||||
onSegmentFocus,
|
onSegmentFocus,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { t } = useTranslation('event');
|
const { t } = useTranslation('event');
|
||||||
const highlight = useSettingsStore(state => state.highlight);
|
const highlight = useSettingsStore(state => state.highlight);
|
||||||
const setHighlight = useSettingsStore(state => state.setHighlight);
|
const setHighlight = useSettingsStore(state => state.setHighlight);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Label>{min}/{total} {t('event:available')}</Label>
|
<Label>{min}/{total} {t('event:available')}</Label>
|
||||||
|
|
||||||
<Bar
|
<Bar
|
||||||
onMouseOut={() => onSegmentFocus(null)}
|
onMouseOut={() => onSegmentFocus(null)}
|
||||||
onClick={() => setHighlight(!highlight)}
|
onClick={() => setHighlight(!highlight)}
|
||||||
title={t('event:group.legend_tooltip')}
|
title={t('event:group.legend_tooltip')}
|
||||||
>
|
>
|
||||||
{[...Array(max+1-min).keys()].map(i => i+min).map(i =>
|
{[...Array(max+1-min).keys()].map(i => i+min).map(i =>
|
||||||
<Grade
|
<Grade
|
||||||
key={i}
|
key={i}
|
||||||
color={`${theme.primary}${Math.round((i/(max))*255).toString(16)}`}
|
color={`${theme.primary}${Math.round((i/(max))*255).toString(16)}`}
|
||||||
highlight={highlight && i === max && max > 0}
|
highlight={highlight && i === max && max > 0}
|
||||||
onMouseOver={() => onSegmentFocus(i)}
|
onMouseOver={() => onSegmentFocus(i)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Bar>
|
</Bar>
|
||||||
|
|
||||||
<Label>{max}/{total} {t('event:available')}</Label>
|
<Label>{max}/{total} {t('event:available')}</Label>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Legend;
|
export default Legend;
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,52 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled.div`
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
& label:last-of-type {
|
& label:last-of-type {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 400px) {
|
@media (max-width: 400px) {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Label = styled.label`
|
export const Label = styled.label`
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Bar = styled.div`
|
export const Bar = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 40%;
|
width: 40%;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
border: 1px solid ${props => props.theme.text};
|
border: 1px solid ${props => props.theme.text};
|
||||||
|
|
||||||
@media (max-width: 400px) {
|
@media (max-width: 400px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Grade = styled.div`
|
export const Grade = styled.div`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: ${props => props.color};
|
background-color: ${props => props.color};
|
||||||
|
|
||||||
${props => props.highlight && `
|
${props => props.highlight && `
|
||||||
background-image: repeating-linear-gradient(
|
background-image: repeating-linear-gradient(
|
||||||
45deg,
|
45deg,
|
||||||
${props.theme.primary},
|
${props.theme.primary},
|
||||||
${props.theme.primary} 4.5px,
|
${props.theme.primary} 4.5px,
|
||||||
${props.theme.primaryDark} 4.5px,
|
${props.theme.primaryDark} 4.5px,
|
||||||
${props.theme.primaryDark} 9px
|
${props.theme.primaryDark} 9px
|
||||||
);
|
);
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export const Wrapper = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const A = styled.a`
|
export const A = styled.a`
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
@keyframes jelly {
|
@keyframes jelly {
|
||||||
from,to {
|
from,to {
|
||||||
|
|
@ -35,31 +35,35 @@ export const A = styled.a`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Top = styled.div`
|
export const Top = styled.div`
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Image = styled.img`
|
export const Image = styled.img`
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Title = styled.span`
|
export const Title = styled.span`
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
color: ${props => props.theme.primary};
|
color: ${props => props.theme.primary};
|
||||||
font-family: 'Molot', sans-serif;
|
font-family: 'Molot', sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-shadow: 0 2px 0 ${props => props.theme.primaryDark};
|
text-shadow: 0 2px 0 ${props => props.theme.primaryDark};
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Tagline = styled.span`
|
export const Tagline = styled.span`
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -160,11 +160,11 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [client]);
|
}, [client]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!client ? (
|
{!client ? (
|
||||||
<Center>
|
<Center>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => signIn()}
|
onClick={() => signIn()}
|
||||||
isLoading={client === undefined}
|
isLoading={client === undefined}
|
||||||
primaryColor="#0364B9"
|
primaryColor="#0364B9"
|
||||||
|
|
@ -228,7 +228,7 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
</CalendarList>
|
</CalendarList>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OutlookCalendar;
|
export default OutlookCalendar;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
|
||||||
import { AboutSection, StyledMain } from '../../pages/Home/homeStyle';
|
import { AboutSection, StyledMain } from '../../pages/Home/homeStyle';
|
||||||
import { Recent } from './recentsStyle';
|
import { Wrapper, Recent } from './recentsStyle';
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
|
@ -14,17 +14,19 @@ const Recents = ({ target }) => {
|
||||||
const { t } = useTranslation(['home', 'common']);
|
const { t } = useTranslation(['home', 'common']);
|
||||||
|
|
||||||
return !!recents.length && (
|
return !!recents.length && (
|
||||||
<AboutSection id="recents">
|
<Wrapper>
|
||||||
<StyledMain>
|
<AboutSection id="recents">
|
||||||
<h2>{t('home:recently_visited')}</h2>
|
<StyledMain>
|
||||||
{recents.map(event => (
|
<h2>{t('home:recently_visited')}</h2>
|
||||||
<Recent href={`/${event.id}`} target={target} key={event.id}>
|
{recents.map(event => (
|
||||||
<span className="name">{event.name}</span>
|
<Recent href={`/${event.id}`} target={target} key={event.id}>
|
||||||
<span locale={locale} className="date" title={dayjs.unix(event.created).format('D MMMM, YYYY')}>{t('common:created', { date: dayjs.unix(event.created).fromNow() })}</span>
|
<span className="name">{event.name}</span>
|
||||||
</Recent>
|
<span locale={locale} className="date" title={dayjs.unix(event.created).format('D MMMM, YYYY')}>{t('common:created', { date: dayjs.unix(event.created).fromNow() })}</span>
|
||||||
))}
|
</Recent>
|
||||||
</StyledMain>
|
))}
|
||||||
</AboutSection>
|
</StyledMain>
|
||||||
|
</AboutSection>
|
||||||
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
export const Wrapper = styled.div`
|
||||||
|
@media print {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const Recent = styled.a`
|
export const Recent = styled.a`
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,43 @@
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
StyledLabel,
|
StyledLabel,
|
||||||
StyledSubLabel,
|
StyledSubLabel,
|
||||||
StyledSelect,
|
StyledSelect,
|
||||||
} from './selectFieldStyle';
|
} from './selectFieldStyle';
|
||||||
|
|
||||||
const SelectField = forwardRef(({
|
const SelectField = forwardRef(({
|
||||||
label,
|
label,
|
||||||
subLabel,
|
subLabel,
|
||||||
id,
|
id,
|
||||||
options = [],
|
options = [],
|
||||||
inline = false,
|
inline = false,
|
||||||
small = false,
|
small = false,
|
||||||
defaultOption,
|
defaultOption,
|
||||||
...props
|
...props
|
||||||
}, ref) => (
|
}, ref) => (
|
||||||
<Wrapper inline={inline} small={small}>
|
<Wrapper inline={inline} small={small}>
|
||||||
{label && <StyledLabel htmlFor={id} inline={inline} small={small}>{label}</StyledLabel>}
|
{label && <StyledLabel htmlFor={id} inline={inline} small={small}>{label}</StyledLabel>}
|
||||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||||
|
|
||||||
<StyledSelect
|
<StyledSelect
|
||||||
id={id}
|
id={id}
|
||||||
small={small}
|
small={small}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{defaultOption && <option value="">{defaultOption}</option>}
|
{defaultOption && <option value="">{defaultOption}</option>}
|
||||||
{Array.isArray(options) ? (
|
{Array.isArray(options) ? (
|
||||||
options.map(value =>
|
options.map(value =>
|
||||||
<option key={value} value={value}>{value}</option>
|
<option key={value} value={value}>{value}</option>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
Object.entries(options).map(([key, value]) =>
|
Object.entries(options).map(([key, value]) =>
|
||||||
<option key={key} value={key}>{value}</option>
|
<option key={key} value={key}>{value}</option>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</StyledSelect>
|
</StyledSelect>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
));
|
));
|
||||||
|
|
||||||
export default SelectField;
|
export default SelectField;
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,60 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled.div`
|
||||||
margin: 30px 0;
|
margin: 30px 0;
|
||||||
|
|
||||||
${props => props.inline && `
|
${props => props.inline && `
|
||||||
margin: 0;
|
margin: 0;
|
||||||
`}
|
`}
|
||||||
${props => props.small && `
|
${props => props.small && `
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledLabel = styled.label`
|
export const StyledLabel = styled.label`
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
|
||||||
${props => props.inline && `
|
${props => props.inline && `
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
`}
|
`}
|
||||||
${props => props.small && `
|
${props => props.small && `
|
||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledSubLabel = styled.label`
|
export const StyledSubLabel = styled.label`
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
opacity: .6;
|
opacity: .6;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledSelect = styled.select`
|
export const StyledSelect = styled.select`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
background: ${props => props.theme.primaryBackground};
|
background: ${props => props.theme.primaryBackground};
|
||||||
color: inherit;
|
color: inherit;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid ${props => props.theme.primary};
|
||||||
box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
|
box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color .15s, box-shadow .15s;
|
transition: border-color .15s, box-shadow .15s;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-image: url("data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><foreignObject width=%22100px%22 height=%22100px%22><div xmlns=%22http://www.w3.org/1999/xhtml%22 style=%22color:${props => encodeURIComponent(props.theme.primary)};font-size:60px;display:flex;align-items:center;justify-content:center;height:100%25;width:100%25%22>▼</div></foreignObject></svg>");
|
background-image: url("data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><foreignObject width=%22100px%22 height=%22100px%22><div xmlns=%22http://www.w3.org/1999/xhtml%22 style=%22color:${props => encodeURIComponent(props.theme.primary)};font-size:60px;display:flex;align-items:center;justify-content:center;height:100%25;width:100%25%22>▼</div></foreignObject></svg>");
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: right 10px center;
|
background-position: right 10px center;
|
||||||
background-size: 1em;
|
background-size: 1em;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
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};
|
box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||||
}
|
}
|
||||||
|
|
||||||
${props => props.small && `
|
${props => props.small && `
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const OpenButton = styled.button`
|
export const OpenButton = styled.button`
|
||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
|
|
@ -33,10 +33,13 @@ export const OpenButton = styled.button`
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
@media print {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Cover = styled.div`
|
export const Cover = styled.div`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
@ -50,7 +53,7 @@ export const Cover = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Modal = styled.div`
|
export const Modal = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 70px;
|
top: 70px;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
background-color: ${props => props.theme.background};
|
background-color: ${props => props.theme.background};
|
||||||
|
|
@ -81,10 +84,13 @@ export const Modal = styled.div`
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
@media print {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Heading = styled.span`
|
export const Heading = styled.span`
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
StyledLabel,
|
StyledLabel,
|
||||||
StyledSubLabel,
|
StyledSubLabel,
|
||||||
StyledInput,
|
StyledInput,
|
||||||
} from './textFieldStyle';
|
} from './textFieldStyle';
|
||||||
|
|
||||||
const TextField = forwardRef(({
|
const TextField = forwardRef(({
|
||||||
label,
|
label,
|
||||||
subLabel,
|
subLabel,
|
||||||
id,
|
id,
|
||||||
inline = false,
|
inline = false,
|
||||||
...props
|
...props
|
||||||
}, ref) => (
|
}, ref) => (
|
||||||
<Wrapper inline={inline}>
|
<Wrapper inline={inline}>
|
||||||
{label && <StyledLabel htmlFor={id} inline={inline}>{label}</StyledLabel>}
|
{label && <StyledLabel htmlFor={id} inline={inline}>{label}</StyledLabel>}
|
||||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||||
<StyledInput id={id} ref={ref} {...props} />
|
<StyledInput id={id} ref={ref} {...props} />
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
));
|
));
|
||||||
|
|
||||||
export default TextField;
|
export default TextField;
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,46 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled.div`
|
||||||
margin: 30px 0;
|
margin: 30px 0;
|
||||||
|
|
||||||
${props => props.inline && `
|
${props => props.inline && `
|
||||||
margin: 0;
|
margin: 0;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledLabel = styled.label`
|
export const StyledLabel = styled.label`
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
|
||||||
${props => props.inline && `
|
${props => props.inline && `
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledSubLabel = styled.label`
|
export const StyledSubLabel = styled.label`
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
opacity: .6;
|
opacity: .6;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledInput = styled.input`
|
export const StyledInput = styled.input`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
background: ${props => props.theme.primaryBackground};
|
background: ${props => props.theme.primaryBackground};
|
||||||
color: inherit;
|
color: inherit;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid ${props => props.theme.primary};
|
||||||
box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
|
box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color .15s, box-shadow .15s;
|
transition: border-color .15s, box-shadow .15s;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
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};
|
box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -4,94 +4,94 @@ import dayjs from 'dayjs';
|
||||||
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
StyledLabel,
|
StyledLabel,
|
||||||
StyledSubLabel,
|
StyledSubLabel,
|
||||||
Range,
|
Range,
|
||||||
Handle,
|
Handle,
|
||||||
Selected,
|
Selected,
|
||||||
} from './timeRangeFieldStyle';
|
} 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 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(({
|
const TimeRangeField = forwardRef(({
|
||||||
label,
|
label,
|
||||||
subLabel,
|
subLabel,
|
||||||
id,
|
id,
|
||||||
setValue,
|
setValue,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const timeFormat = useSettingsStore(state => state.timeFormat);
|
const timeFormat = useSettingsStore(state => state.timeFormat);
|
||||||
const locale = useLocaleUpdateStore(state => state.locale);
|
const locale = useLocaleUpdateStore(state => state.locale);
|
||||||
|
|
||||||
const [start, setStart] = useState(9);
|
const [start, setStart] = useState(9);
|
||||||
const [end, setEnd] = useState(17);
|
const [end, setEnd] = useState(17);
|
||||||
|
|
||||||
const isStartMoving = useRef(false);
|
const isStartMoving = useRef(false);
|
||||||
const isEndMoving = useRef(false);
|
const isEndMoving = useRef(false);
|
||||||
const rangeRef = useRef();
|
const rangeRef = useRef();
|
||||||
const rangeRect = useRef();
|
const rangeRect = useRef();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rangeRef.current) {
|
if (rangeRef.current) {
|
||||||
rangeRect.current = rangeRef.current.getBoundingClientRect();
|
rangeRect.current = rangeRef.current.getBoundingClientRect();
|
||||||
}
|
}
|
||||||
}, [rangeRef]);
|
}, [rangeRef]);
|
||||||
|
|
||||||
useEffect(() => setValue(props.name, JSON.stringify({start, end})), [start, end, setValue, props.name]);
|
useEffect(() => setValue(props.name, JSON.stringify({start, end})), [start, end, setValue, props.name]);
|
||||||
|
|
||||||
const handleMouseMove = e => {
|
const handleMouseMove = e => {
|
||||||
if (isStartMoving.current || isEndMoving.current) {
|
if (isStartMoving.current || isEndMoving.current) {
|
||||||
let step = Math.round(((e.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
let step = Math.round(((e.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
||||||
if (step < 0) step = 0;
|
if (step < 0) step = 0;
|
||||||
if (step > 24) step = 24;
|
if (step > 24) step = 24;
|
||||||
step = Math.abs(step);
|
step = Math.abs(step);
|
||||||
|
|
||||||
if (isStartMoving.current) {
|
if (isStartMoving.current) {
|
||||||
setStart(step);
|
setStart(step);
|
||||||
} else if (isEndMoving.current) {
|
} else if (isEndMoving.current) {
|
||||||
setEnd(step);
|
setEnd(step);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper locale={locale}>
|
<Wrapper locale={locale}>
|
||||||
{label && <StyledLabel htmlFor={id}>{label}</StyledLabel>}
|
{label && <StyledLabel htmlFor={id}>{label}</StyledLabel>}
|
||||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
type="hidden"
|
type="hidden"
|
||||||
value={JSON.stringify({start, end})}
|
value={JSON.stringify({start, end})}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Range ref={rangeRef}>
|
<Range ref={rangeRef}>
|
||||||
<Selected start={start} end={start > end ? 24 : end} />
|
<Selected start={start} end={start > end ? 24 : end} />
|
||||||
{start > end && <Selected start={start > end ? 0 : start} end={end} />}
|
{start > end && <Selected start={start > end ? 0 : start} end={end} />}
|
||||||
<Handle
|
<Handle
|
||||||
value={start}
|
value={start}
|
||||||
label={timeFormat === '24h' ? times[start] : dayjs().hour(times[start]).format('ha')}
|
label={timeFormat === '24h' ? times[start] : dayjs().hour(times[start]).format('ha')}
|
||||||
extraPadding={end - start === 1 ? 'padding-right: 20px;' : (start - end === 1 ? 'padding-left: 20px;' : '')}
|
extraPadding={end - start === 1 ? 'padding-right: 20px;' : (start - end === 1 ? 'padding-left: 20px;' : '')}
|
||||||
onMouseDown={() => {
|
onMouseDown={() => {
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
isStartMoving.current = true;
|
isStartMoving.current = true;
|
||||||
|
|
||||||
document.addEventListener('mouseup', () => {
|
document.addEventListener('mouseup', () => {
|
||||||
isStartMoving.current = false;
|
isStartMoving.current = false;
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
}}
|
}}
|
||||||
onTouchMove={(e) => {
|
onTouchMove={(e) => {
|
||||||
const touch = e.targetTouches[0];
|
const touch = e.targetTouches[0];
|
||||||
|
|
||||||
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
||||||
if (step < 0) step = 0;
|
if (step < 0) step = 0;
|
||||||
if (step > 24) step = 24;
|
if (step > 24) step = 24;
|
||||||
step = Math.abs(step);
|
step = Math.abs(step);
|
||||||
setStart(step);
|
setStart(step);
|
||||||
}}
|
}}
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||||
|
|
@ -103,29 +103,29 @@ const TimeRangeField = forwardRef(({
|
||||||
setStart(Math.min(start+1, 24));
|
setStart(Math.min(start+1, 24));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Handle
|
<Handle
|
||||||
value={end}
|
value={end}
|
||||||
label={timeFormat === '24h' ? times[end] : dayjs().hour(times[end]).format('ha')}
|
label={timeFormat === '24h' ? times[end] : dayjs().hour(times[end]).format('ha')}
|
||||||
extraPadding={end - start === 1 ? 'padding-left: 20px;' : (start - end === 1 ? 'padding-right: 20px;' : '')}
|
extraPadding={end - start === 1 ? 'padding-left: 20px;' : (start - end === 1 ? 'padding-right: 20px;' : '')}
|
||||||
onMouseDown={() => {
|
onMouseDown={() => {
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
isEndMoving.current = true;
|
isEndMoving.current = true;
|
||||||
|
|
||||||
document.addEventListener('mouseup', () => {
|
document.addEventListener('mouseup', () => {
|
||||||
isEndMoving.current = false;
|
isEndMoving.current = false;
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
}}
|
}}
|
||||||
onTouchMove={(e) => {
|
onTouchMove={(e) => {
|
||||||
const touch = e.targetTouches[0];
|
const touch = e.targetTouches[0];
|
||||||
|
|
||||||
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
||||||
if (step < 0) step = 0;
|
if (step < 0) step = 0;
|
||||||
if (step > 24) step = 24;
|
if (step > 24) step = 24;
|
||||||
step = Math.abs(step);
|
step = Math.abs(step);
|
||||||
setEnd(step);
|
setEnd(step);
|
||||||
}}
|
}}
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||||
|
|
@ -137,10 +137,10 @@ const TimeRangeField = forwardRef(({
|
||||||
setEnd(Math.min(end+1, 24));
|
setEnd(Math.min(end+1, 24));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Range>
|
</Range>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default TimeRangeField;
|
export default TimeRangeField;
|
||||||
|
|
|
||||||
|
|
@ -1,82 +1,82 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled.div`
|
||||||
margin: 30px 0;
|
margin: 30px 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledLabel = styled.label`
|
export const StyledLabel = styled.label`
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledSubLabel = styled.label`
|
export const StyledSubLabel = styled.label`
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
opacity: .6;
|
opacity: .6;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Range = styled.div`
|
export const Range = styled.div`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
background-color: ${props => props.theme.primaryBackground};
|
background-color: ${props => props.theme.primaryBackground};
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid ${props => props.theme.primary};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 38px 6px 18px;
|
margin: 38px 6px 18px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Handle = styled.div`
|
export const Handle = styled.div`
|
||||||
height: calc(100% + 20px);
|
height: calc(100% + 20px);
|
||||||
width: 20px;
|
width: 20px;
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid ${props => props.theme.primary};
|
||||||
background-color: ${props => props.theme.primaryLight};
|
background-color: ${props => props.theme.primaryLight};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -10px;
|
top: -10px;
|
||||||
left: calc(${props => props.value * 4.1666666666666666}% - 11px);
|
left: calc(${props => props.value * 4.1666666666666666}% - 11px);
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
transition: left .1s;
|
transition: left .1s;
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: '|||';
|
content: '|||';
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: ${props => props.theme.primaryDark};
|
color: ${props => props.theme.primaryDark};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: '${props => props.label}';
|
content: '${props => props.label}';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: calc(100% + 8px);
|
bottom: calc(100% + 8px);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
${props => props.extraPadding}
|
${props => props.extraPadding}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Selected = styled.div`
|
export const Selected = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: ${props => props.start * 4.1666666666666666}%;
|
left: ${props => props.start * 4.1666666666666666}%;
|
||||||
right: calc(100% - ${props => props.end * 4.1666666666666666}%);
|
right: calc(100% - ${props => props.end * 4.1666666666666666}%);
|
||||||
top: 0;
|
top: 0;
|
||||||
background-color: ${props => props.theme.primary};
|
background-color: ${props => props.theme.primary};
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: left .1s, right .1s;
|
transition: left .1s, right .1s;
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
transition: none;
|
transition: none;
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
ToggleContainer,
|
ToggleContainer,
|
||||||
StyledLabel,
|
StyledLabel,
|
||||||
Option,
|
Option,
|
||||||
HiddenInput,
|
HiddenInput,
|
||||||
LabelButton,
|
LabelButton,
|
||||||
} from './toggleFieldStyle';
|
} from './toggleFieldStyle';
|
||||||
|
|
||||||
const ToggleField = ({
|
const ToggleField = ({
|
||||||
label,
|
label,
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
title = '',
|
title = '',
|
||||||
options = [],
|
options = [],
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
inputRef,
|
inputRef,
|
||||||
...props
|
...props
|
||||||
}) => (
|
}) => (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
{label && <StyledLabel title={title}>{label} {title !== '' && <svg viewBox="0 0 24 24"><path fill="currentColor" d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z" /></svg>}</StyledLabel>}
|
{label && <StyledLabel title={title}>{label} {title !== '' && <svg viewBox="0 0 24 24"><path fill="currentColor" d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z" /></svg>}</StyledLabel>}
|
||||||
|
|
||||||
<ToggleContainer>
|
<ToggleContainer>
|
||||||
{Object.entries(options).map(([key, label]) =>
|
{Object.entries(options).map(([key, label]) =>
|
||||||
|
|
@ -37,7 +37,7 @@ const ToggleField = ({
|
||||||
</Option>
|
</Option>
|
||||||
)}
|
)}
|
||||||
</ToggleContainer>
|
</ToggleContainer>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default ToggleField;
|
export default ToggleField;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled.div`
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ToggleContainer = styled.div`
|
export const ToggleContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid ${props => props.theme.primary};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -29,9 +29,9 @@ export const ToggleContainer = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledLabel = styled.label`
|
export const StyledLabel = styled.label`
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
|
|
||||||
& svg {
|
& svg {
|
||||||
height: 1em;
|
height: 1em;
|
||||||
|
|
@ -41,7 +41,7 @@ export const StyledLabel = styled.label`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Option = styled.div`
|
export const Option = styled.div`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,14 @@ const UpdateDialog = ({ onClose }) => {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<h2>{t('common:update.heading')}</h2>
|
<h2>{t('common:update.heading')}</h2>
|
||||||
<p>{t('common:update.body')}</p>
|
<p>{t('common:update.body')}</p>
|
||||||
<ButtonWrapper>
|
<ButtonWrapper>
|
||||||
<Button secondary onClick={onClose}>{t('common:update.buttons.close')}</Button>
|
<Button secondary onClick={onClose}>{t('common:update.buttons.close')}</Button>
|
||||||
<Button onClick={() => window.location.reload()}>{t('common:update.buttons.reload')}</Button>
|
<Button onClick={() => window.location.reload()}>{t('common:update.buttons.reload')}</Button>
|
||||||
</ButtonWrapper>
|
</ButtonWrapper>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export const Wrapper = styled.div`
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
box-sizing: border-box;
|
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);
|
box-shadow: 0 3px 6px 0 rgba(0,0,0,.3);
|
||||||
|
|
||||||
& h2 {
|
& h2 {
|
||||||
|
|
@ -31,4 +31,5 @@ export const ButtonWrapper = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,21 @@ import timezone from 'dayjs/plugin/timezone';
|
||||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TextField,
|
TextField,
|
||||||
CalendarField,
|
CalendarField,
|
||||||
TimeRangeField,
|
TimeRangeField,
|
||||||
SelectField,
|
SelectField,
|
||||||
Button,
|
Button,
|
||||||
Error,
|
Error,
|
||||||
Recents,
|
Recents,
|
||||||
Footer,
|
Footer,
|
||||||
} from 'components';
|
} from 'components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
StyledMain,
|
StyledMain,
|
||||||
CreateForm,
|
CreateForm,
|
||||||
TitleSmall,
|
TitleSmall,
|
||||||
TitleLarge,
|
TitleLarge,
|
||||||
P,
|
P,
|
||||||
OfflineMessage,
|
OfflineMessage,
|
||||||
ShareInfo,
|
ShareInfo,
|
||||||
|
|
@ -39,14 +39,14 @@ dayjs.extend(timezone);
|
||||||
dayjs.extend(customParseFormat);
|
dayjs.extend(customParseFormat);
|
||||||
|
|
||||||
const Create = ({ offline }) => {
|
const Create = ({ offline }) => {
|
||||||
const { register, handleSubmit, setValue } = useForm({
|
const { register, handleSubmit, setValue } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [createdEvent, setCreatedEvent] = useState(null);
|
const [createdEvent, setCreatedEvent] = useState(null);
|
||||||
const [copied, setCopied] = useState(null);
|
const [copied, setCopied] = useState(null);
|
||||||
const [showFooter, setShowFooter] = useState(true);
|
const [showFooter, setShowFooter] = useState(true);
|
||||||
|
|
||||||
|
|
@ -55,11 +55,11 @@ const Create = ({ offline }) => {
|
||||||
|
|
||||||
const addRecent = useRecentsStore(state => state.addRecent);
|
const addRecent = useRecentsStore(state => state.addRecent);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window.self === window.top) {
|
if (window.self === window.top) {
|
||||||
push('/');
|
push('/');
|
||||||
}
|
}
|
||||||
document.title = 'Create a Crab Fit';
|
document.title = 'Create a Crab Fit';
|
||||||
|
|
||||||
if (window.parent) {
|
if (window.parent) {
|
||||||
window.parent.postMessage('crabfit-create', '*');
|
window.parent.postMessage('crabfit-create', '*');
|
||||||
|
|
@ -71,67 +71,67 @@ const Create = ({ offline }) => {
|
||||||
once: true
|
once: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [push]);
|
}, [push]);
|
||||||
|
|
||||||
const onSubmit = async data => {
|
const onSubmit = async data => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const { start, end } = JSON.parse(data.times);
|
const { start, end } = JSON.parse(data.times);
|
||||||
const dates = JSON.parse(data.dates);
|
const dates = JSON.parse(data.dates);
|
||||||
|
|
||||||
if (dates.length === 0) {
|
if (dates.length === 0) {
|
||||||
return setError(t('home:form.errors.no_dates'));
|
return setError(t('home:form.errors.no_dates'));
|
||||||
}
|
}
|
||||||
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
|
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
|
||||||
if (start === end) {
|
if (start === end) {
|
||||||
return setError(t('home:form.errors.same_times'));
|
return setError(t('home:form.errors.same_times'));
|
||||||
}
|
}
|
||||||
|
|
||||||
let times = dates.reduce((times, date) => {
|
let times = dates.reduce((times, date) => {
|
||||||
let day = [];
|
let day = [];
|
||||||
for (let i = start; i < (start > end ? 24 : end); i++) {
|
for (let i = start; i < (start > end ? 24 : end); i++) {
|
||||||
if (isSpecificDates) {
|
if (isSpecificDates) {
|
||||||
day.push(
|
day.push(
|
||||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
day.push(
|
day.push(
|
||||||
dayjs().tz(data.timezone)
|
dayjs().tz(data.timezone)
|
||||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (start > end) {
|
if (start > end) {
|
||||||
for (let i = 0; i < end; i++) {
|
for (let i = 0; i < end; i++) {
|
||||||
if (isSpecificDates) {
|
if (isSpecificDates) {
|
||||||
day.push(
|
day.push(
|
||||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
day.push(
|
day.push(
|
||||||
dayjs().tz(data.timezone)
|
dayjs().tz(data.timezone)
|
||||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [...times, ...day];
|
return [...times, ...day];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (times.length === 0) {
|
if (times.length === 0) {
|
||||||
return setError(t('home:form.errors.no_time'));
|
return setError(t('home:form.errors.no_time'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await api.post('/event', {
|
const response = await api.post('/event', {
|
||||||
event: {
|
event: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
times: times,
|
times: times,
|
||||||
timezone: data.timezone,
|
timezone: data.timezone,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setCreatedEvent(response.data);
|
setCreatedEvent(response.data);
|
||||||
addRecent({
|
addRecent({
|
||||||
id: response.data.id,
|
id: response.data.id,
|
||||||
|
|
@ -141,19 +141,19 @@ const Create = ({ offline }) => {
|
||||||
gtag('event', 'create_event', {
|
gtag('event', 'create_event', {
|
||||||
'event_category': 'create',
|
'event_category': 'create',
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(t('home:form.errors.unknown'));
|
setError(t('home:form.errors.unknown'));
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledMain>
|
<StyledMain>
|
||||||
<TitleSmall>{t('home:create')}</TitleSmall>
|
<TitleSmall>{t('home:create')}</TitleSmall>
|
||||||
<TitleLarge>CRAB FIT</TitleLarge>
|
<TitleLarge>CRAB FIT</TitleLarge>
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
|
|
||||||
{createdEvent ? (
|
{createdEvent ? (
|
||||||
|
|
@ -173,10 +173,10 @@ const Create = ({ offline }) => {
|
||||||
}
|
}
|
||||||
title={!!navigator.clipboard ? t('event:nav.title') : ''}
|
title={!!navigator.clipboard ? t('event:nav.title') : ''}
|
||||||
>{copied ?? `https://crab.fit/${createdEvent?.id}`}</ShareInfo>
|
>{copied ?? `https://crab.fit/${createdEvent?.id}`}</ShareInfo>
|
||||||
<ShareInfo>
|
<ShareInfo>
|
||||||
{/* eslint-disable-next-line */}
|
{/* eslint-disable-next-line */}
|
||||||
<Trans i18nKey="event:nav.shareinfo_alt">Click the link above to copy it to your clipboard, or share via <a onClick={() => 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</a>.</Trans>
|
<Trans i18nKey="event:nav.shareinfo_alt">Click the link above to copy it to your clipboard, or share via <a onClick={() => 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</a>.</Trans>
|
||||||
</ShareInfo>
|
</ShareInfo>
|
||||||
{showFooter && <Footer small />}
|
{showFooter && <Footer small />}
|
||||||
</OfflineMessage>
|
</OfflineMessage>
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
|
|
@ -191,52 +191,52 @@ const Create = ({ offline }) => {
|
||||||
<P>{t('home:offline')}</P>
|
<P>{t('home:offline')}</P>
|
||||||
</OfflineMessage>
|
</OfflineMessage>
|
||||||
) : (
|
) : (
|
||||||
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
|
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
|
||||||
<TextField
|
<TextField
|
||||||
label={t('home:form.name.label')}
|
label={t('home:form.name.label')}
|
||||||
subLabel={t('home:form.name.sublabel')}
|
subLabel={t('home:form.name.sublabel')}
|
||||||
type="text"
|
type="text"
|
||||||
id="name"
|
id="name"
|
||||||
{...register('name')}
|
{...register('name')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CalendarField
|
<CalendarField
|
||||||
label={t('home:form.dates.label')}
|
label={t('home:form.dates.label')}
|
||||||
subLabel={t('home:form.dates.sublabel')}
|
subLabel={t('home:form.dates.sublabel')}
|
||||||
id="dates"
|
id="dates"
|
||||||
required
|
required
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
{...register('dates')}
|
{...register('dates')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TimeRangeField
|
<TimeRangeField
|
||||||
label={t('home:form.times.label')}
|
label={t('home:form.times.label')}
|
||||||
subLabel={t('home:form.times.sublabel')}
|
subLabel={t('home:form.times.sublabel')}
|
||||||
id="times"
|
id="times"
|
||||||
required
|
required
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
{...register('times')}
|
{...register('times')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
label={t('home:form.timezone.label')}
|
label={t('home:form.timezone.label')}
|
||||||
id="timezone"
|
id="timezone"
|
||||||
options={timezones}
|
options={timezones}
|
||||||
required
|
required
|
||||||
{...register('timezone')}
|
{...register('timezone')}
|
||||||
defaultOption={t('home:form.timezone.defaultOption')}
|
defaultOption={t('home:form.timezone.defaultOption')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Error open={!!error} onClose={() => setError(null)}>{error}</Error>
|
<Error open={!!error} onClose={() => setError(null)}>{error}</Error>
|
||||||
|
|
||||||
<Button type="submit" isLoading={isLoading} disabled={isLoading} style={{ width: '100%' }}>{t('home:form.button')}</Button>
|
<Button type="submit" isLoading={isLoading} disabled={isLoading} style={{ width: '100%' }}>{t('home:form.button')}</Button>
|
||||||
</CreateForm>
|
</CreateForm>
|
||||||
)}
|
)}
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Create;
|
export default Create;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const StyledMain = styled.div`
|
export const StyledMain = styled.div`
|
||||||
width: 600px;
|
width: 600px;
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
max-width: calc(100% - 30px);
|
max-width: calc(100% - 30px);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CreateForm = styled.form`
|
export const CreateForm = styled.form`
|
||||||
|
|
@ -11,43 +11,43 @@ export const CreateForm = styled.form`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TitleSmall = styled.span`
|
export const TitleSmall = styled.span`
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-family: 'Samurai Bob', sans-serif;
|
font-family: 'Samurai Bob', sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: ${props => props.theme.primaryDark};
|
color: ${props => props.theme.primaryDark};
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TitleLarge = styled.h1`
|
export const TitleLarge = styled.h1`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: ${props => props.theme.primary};
|
color: ${props => props.theme.primary};
|
||||||
font-family: 'Molot', sans-serif;
|
font-family: 'Molot', sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-shadow: 0 3px 0 ${props => props.theme.primaryDark};
|
text-shadow: 0 3px 0 ${props => props.theme.primaryDark};
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const P = styled.p`
|
export const P = styled.p`
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.6em;
|
line-height: 1.6em;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const OfflineMessage = styled.div`
|
export const OfflineMessage = styled.div`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 50px 0 20px;
|
margin: 50px 0 20px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ShareInfo = styled.p`
|
export const ShareInfo = styled.p`
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
|
|
||||||
${props => props.onClick && `
|
${props => props.onClick && `
|
||||||
|
|
|
||||||
|
|
@ -9,27 +9,27 @@ import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Footer,
|
Footer,
|
||||||
TextField,
|
TextField,
|
||||||
SelectField,
|
SelectField,
|
||||||
Button,
|
Button,
|
||||||
AvailabilityViewer,
|
AvailabilityViewer,
|
||||||
AvailabilityEditor,
|
AvailabilityEditor,
|
||||||
Error,
|
Error,
|
||||||
Logo,
|
Logo,
|
||||||
} from 'components';
|
} from 'components';
|
||||||
|
|
||||||
import { StyledMain } from '../Home/homeStyle';
|
import { StyledMain } from '../Home/homeStyle';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EventName,
|
EventName,
|
||||||
EventDate,
|
EventDate,
|
||||||
LoginForm,
|
LoginForm,
|
||||||
LoginSection,
|
LoginSection,
|
||||||
Info,
|
Info,
|
||||||
ShareInfo,
|
ShareInfo,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
} from './eventStyle';
|
} from './eventStyle';
|
||||||
|
|
||||||
import api from 'services';
|
import api from 'services';
|
||||||
|
|
@ -47,94 +47,98 @@ const Event = (props) => {
|
||||||
const weekStart = useSettingsStore(state => state.weekStart);
|
const weekStart = useSettingsStore(state => state.weekStart);
|
||||||
|
|
||||||
const addRecent = useRecentsStore(state => state.addRecent);
|
const addRecent = useRecentsStore(state => state.addRecent);
|
||||||
|
const removeRecent = useRecentsStore(state => state.removeRecent);
|
||||||
const locale = useLocaleUpdateStore(state => state.locale);
|
const locale = useLocaleUpdateStore(state => state.locale);
|
||||||
|
|
||||||
const { t } = useTranslation(['common', 'event']);
|
const { t } = useTranslation(['common', 'event']);
|
||||||
|
|
||||||
const { register, handleSubmit, setFocus, reset } = useForm();
|
const { register, handleSubmit, setFocus, reset } = useForm();
|
||||||
const { id } = props.match.params;
|
const { id } = props.match.params;
|
||||||
const { offline } = props;
|
const { offline } = props;
|
||||||
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [password, setPassword] = useState(null);
|
const [password, setPassword] = useState(null);
|
||||||
const [tab, setTab] = useState(user ? 'you' : 'group');
|
const [tab, setTab] = useState(user ? 'you' : 'group');
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [event, setEvent] = useState(null);
|
const [event, setEvent] = useState(null);
|
||||||
const [people, setPeople] = useState([]);
|
const [people, setPeople] = useState([]);
|
||||||
|
|
||||||
const [times, setTimes] = useState([]);
|
const [times, setTimes] = useState([]);
|
||||||
const [timeLabels, setTimeLabels] = useState([]);
|
const [timeLabels, setTimeLabels] = useState([]);
|
||||||
const [dates, setDates] = useState([]);
|
const [dates, setDates] = useState([]);
|
||||||
const [min, setMin] = useState(0);
|
const [min, setMin] = useState(0);
|
||||||
const [max, setMax] = useState(0);
|
const [max, setMax] = useState(0);
|
||||||
|
|
||||||
const [copied, setCopied] = useState(null);
|
const [copied, setCopied] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchEvent = async () => {
|
const fetchEvent = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/event/${id}`);
|
const response = await api.get(`/event/${id}`);
|
||||||
|
|
||||||
setEvent(response.data);
|
setEvent(response.data);
|
||||||
addRecent({
|
addRecent({
|
||||||
id: response.data.id,
|
id: response.data.id,
|
||||||
created: response.data.created,
|
created: response.data.created,
|
||||||
name: response.data.name,
|
name: response.data.name,
|
||||||
});
|
});
|
||||||
document.title = `${response.data.name} | Crab Fit`;
|
document.title = `${response.data.name} | Crab Fit`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
if (e.status === 404) {
|
||||||
setIsLoading(false);
|
removeRecent(id);
|
||||||
}
|
}
|
||||||
};
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
fetchEvent();
|
fetchEvent();
|
||||||
}, [id, addRecent]);
|
}, [id, addRecent, removeRecent]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPeople = async () => {
|
const fetchPeople = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/event/${id}/people`);
|
const response = await api.get(`/event/${id}/people`);
|
||||||
const adjustedPeople = response.data.people.map(person => ({
|
const adjustedPeople = response.data.people.map(person => ({
|
||||||
...person,
|
...person,
|
||||||
availability: (!!person.availability.length && person.availability[0].length === 13)
|
availability: (!!person.availability.length && person.availability[0].length === 13)
|
||||||
? person.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
|
? person.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
|
||||||
: person.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
|
: person.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
|
||||||
}));
|
}));
|
||||||
setPeople(adjustedPeople);
|
setPeople(adjustedPeople);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tab === 'group') {
|
if (tab === 'group') {
|
||||||
fetchPeople();
|
fetchPeople();
|
||||||
}
|
}
|
||||||
}, [tab, id, timezone]);
|
}, [tab, id, timezone]);
|
||||||
|
|
||||||
// Convert to timezone and expand minute segments
|
// Convert to timezone and expand minute segments
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (event) {
|
if (event) {
|
||||||
const isSpecificDates = event.times[0].length === 13;
|
const isSpecificDates = event.times[0].length === 13;
|
||||||
setTimes(event.times.reduce(
|
setTimes(event.times.reduce(
|
||||||
(allTimes, time) => {
|
(allTimes, time) => {
|
||||||
const date = isSpecificDates ?
|
const date = isSpecificDates ?
|
||||||
dayjs(time, 'HHmm-DDMMYYYY').utc(true).tz(timezone)
|
dayjs(time, 'HHmm-DDMMYYYY').utc(true).tz(timezone)
|
||||||
: dayjs(time, 'HHmm').day(time.substring(5)).utc(true).tz(timezone);
|
: dayjs(time, 'HHmm').day(time.substring(5)).utc(true).tz(timezone);
|
||||||
const format = isSpecificDates ? 'HHmm-DDMMYYYY' : 'HHmm-d';
|
const format = isSpecificDates ? 'HHmm-DDMMYYYY' : 'HHmm-d';
|
||||||
return [
|
return [
|
||||||
...allTimes,
|
...allTimes,
|
||||||
date.minute(0).format(format),
|
date.minute(0).format(format),
|
||||||
date.minute(15).format(format),
|
date.minute(15).format(format),
|
||||||
date.minute(30).format(format),
|
date.minute(30).format(format),
|
||||||
date.minute(45).format(format),
|
date.minute(45).format(format),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
).sort((a, b) => {
|
).sort((a, b) => {
|
||||||
if (isSpecificDates) {
|
if (isSpecificDates) {
|
||||||
return dayjs(a, 'HHmm-DDMMYYYY').diff(dayjs(b, 'HHmm-DDMMYYYY'));
|
return dayjs(a, 'HHmm-DDMMYYYY').diff(dayjs(b, 'HHmm-DDMMYYYY'));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -142,154 +146,154 @@ const Event = (props) => {
|
||||||
.diff(dayjs(b, 'HHmm').day((parseInt(b.substring(5))-weekStart % 7 + 7) % 7));
|
.diff(dayjs(b, 'HHmm').day((parseInt(b.substring(5))-weekStart % 7 + 7) % 7));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [event, timezone, weekStart]);
|
}, [event, timezone, weekStart]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!!times.length && !!people.length) {
|
if (!!times.length && !!people.length) {
|
||||||
setMin(times.reduce((min, time) => {
|
setMin(times.reduce((min, time) => {
|
||||||
let total = people.reduce(
|
let total = people.reduce(
|
||||||
(total, person) => person.availability.includes(time) ? total+1 : total,
|
(total, person) => person.availability.includes(time) ? total+1 : total,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
return total < min ? total : min;
|
return total < min ? total : min;
|
||||||
},
|
},
|
||||||
Infinity
|
Infinity
|
||||||
));
|
));
|
||||||
setMax(times.reduce((max, time) => {
|
setMax(times.reduce((max, time) => {
|
||||||
let total = people.reduce(
|
let total = people.reduce(
|
||||||
(total, person) => person.availability.includes(time) ? total+1 : total,
|
(total, person) => person.availability.includes(time) ? total+1 : total,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
return total > max ? total : max;
|
return total > max ? total : max;
|
||||||
},
|
},
|
||||||
-Infinity
|
-Infinity
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}, [times, people]);
|
}, [times, people]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!!times.length) {
|
if (!!times.length) {
|
||||||
setTimeLabels(times.reduce((labels, datetime) => {
|
setTimeLabels(times.reduce((labels, datetime) => {
|
||||||
const time = datetime.substring(0, 4);
|
const time = datetime.substring(0, 4);
|
||||||
if (labels.includes(time)) return labels;
|
if (labels.includes(time)) return labels;
|
||||||
return [...labels, time];
|
return [...labels, time];
|
||||||
}, [])
|
}, [])
|
||||||
.sort((a, b) => parseInt(a) - parseInt(b))
|
.sort((a, b) => parseInt(a) - parseInt(b))
|
||||||
.reduce((labels, time, i, allTimes) => {
|
.reduce((labels, time, i, allTimes) => {
|
||||||
if (time.substring(2) === '30') return [...labels, { label: '', time }];
|
if (time.substring(2) === '30') return [...labels, { label: '', time }];
|
||||||
if (allTimes.length - 1 === i) return [
|
if (allTimes.length - 1 === i) return [
|
||||||
...labels,
|
...labels,
|
||||||
{ label: '', time },
|
{ label: '', time },
|
||||||
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: null }
|
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: null }
|
||||||
];
|
];
|
||||||
if (allTimes.length - 1 > i && parseInt(allTimes[i+1].substring(0, 2))-1 > parseInt(time.substring(0, 2))) return [
|
if (allTimes.length - 1 > i && parseInt(allTimes[i+1].substring(0, 2))-1 > parseInt(time.substring(0, 2))) return [
|
||||||
...labels,
|
...labels,
|
||||||
{ label: '', time },
|
{ label: '', time },
|
||||||
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: 'space' },
|
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: 'space' },
|
||||||
{ label: '', time: 'space' },
|
{ label: '', time: 'space' },
|
||||||
{ label: '', time: 'space' },
|
{ label: '', time: 'space' },
|
||||||
];
|
];
|
||||||
if (time.substring(2) !== '00') return [...labels, { label: '', time }];
|
if (time.substring(2) !== '00') return [...labels, { label: '', time }];
|
||||||
return [...labels, { label: dayjs(time, 'HHmm').format(timeFormat === '12h' ? 'h A' : 'HH'), time }];
|
return [...labels, { label: dayjs(time, 'HHmm').format(timeFormat === '12h' ? 'h A' : 'HH'), time }];
|
||||||
}, []));
|
}, []));
|
||||||
|
|
||||||
setDates(times.reduce((allDates, time) => {
|
setDates(times.reduce((allDates, time) => {
|
||||||
if (time.substring(2, 4) !== '00') return allDates;
|
if (time.substring(2, 4) !== '00') return allDates;
|
||||||
const date = time.substring(5);
|
const date = time.substring(5);
|
||||||
if (allDates.includes(date)) return allDates;
|
if (allDates.includes(date)) return allDates;
|
||||||
return [...allDates, date];
|
return [...allDates, date];
|
||||||
}, []));
|
}, []));
|
||||||
}
|
}
|
||||||
}, [times, timeFormat, locale]);
|
}, [times, timeFormat, locale]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUser = async () => {
|
const fetchUser = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.post(`/event/${id}/people/${user.name}`, { person: { password } });
|
const response = await api.post(`/event/${id}/people/${user.name}`, { person: { password } });
|
||||||
const adjustedUser = {
|
const adjustedUser = {
|
||||||
...response.data,
|
...response.data,
|
||||||
availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
|
availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
|
||||||
? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
|
? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
|
||||||
: response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
|
: response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
|
||||||
};
|
};
|
||||||
setUser(adjustedUser);
|
setUser(adjustedUser);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
fetchUser();
|
fetchUser();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [timezone]);
|
}, [timezone]);
|
||||||
|
|
||||||
const onSubmit = async data => {
|
const onSubmit = async data => {
|
||||||
if (!data.name || data.name.length === 0) {
|
if (!data.name || data.name.length === 0) {
|
||||||
setFocus('name');
|
setFocus('name');
|
||||||
return setError(t('event:form.errors.name_required'));
|
return setError(t('event:form.errors.name_required'));
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoginLoading(true);
|
setIsLoginLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.post(`/event/${id}/people/${data.name}`, {
|
const response = await api.post(`/event/${id}/people/${data.name}`, {
|
||||||
person: {
|
person: {
|
||||||
password: data.password,
|
password: data.password,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setPassword(data.password);
|
setPassword(data.password);
|
||||||
const adjustedUser = {
|
const adjustedUser = {
|
||||||
...response.data,
|
...response.data,
|
||||||
availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
|
availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
|
||||||
? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
|
? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
|
||||||
: response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
|
: response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
|
||||||
};
|
};
|
||||||
setUser(adjustedUser);
|
setUser(adjustedUser);
|
||||||
setTab('you');
|
setTab('you');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401) {
|
if (e.status === 401) {
|
||||||
setError(t('event:form.errors.password_incorrect'));
|
setError(t('event:form.errors.password_incorrect'));
|
||||||
} else if (e.status === 404) {
|
} else if (e.status === 404) {
|
||||||
// Create user
|
// Create user
|
||||||
try {
|
try {
|
||||||
await api.post(`/event/${id}/people`, {
|
await api.post(`/event/${id}/people`, {
|
||||||
person: {
|
person: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
password: data.password,
|
password: data.password,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setPassword(data.password);
|
setPassword(data.password);
|
||||||
setUser({
|
setUser({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
availability: [],
|
availability: [],
|
||||||
});
|
});
|
||||||
setTab('you');
|
setTab('you');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(t('event:form.errors.unknown'));
|
setError(t('event:form.errors.unknown'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoginLoading(false);
|
setIsLoginLoading(false);
|
||||||
gtag('event', 'login', {
|
gtag('event', 'login', {
|
||||||
'event_category': 'event',
|
'event_category': 'event',
|
||||||
});
|
});
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledMain>
|
<StyledMain>
|
||||||
<Logo />
|
<Logo />
|
||||||
|
|
||||||
{(!!event || isLoading) ? (
|
{(!!event || isLoading) ? (
|
||||||
<>
|
<>
|
||||||
<EventName isLoading={isLoading}>{event?.name}</EventName>
|
<EventName isLoading={isLoading}>{event?.name}</EventName>
|
||||||
<EventDate isLoading={isLoading} locale={locale} title={event?.created && dayjs.unix(event?.created).format('D MMMM, YYYY')}>{event?.created && t('common:created', { date: dayjs.unix(event?.created).fromNow() })}</EventDate>
|
<EventDate isLoading={isLoading} locale={locale} title={event?.created && dayjs.unix(event?.created).format('D MMMM, YYYY')}>{event?.created && t('common:created', { date: dayjs.unix(event?.created).fromNow() })}</EventDate>
|
||||||
<ShareInfo
|
<ShareInfo
|
||||||
onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${id}`)
|
onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setCopied(t('event:nav.copied'));
|
setCopied(t('event:nav.copied'));
|
||||||
|
|
@ -302,81 +306,81 @@ const Event = (props) => {
|
||||||
}
|
}
|
||||||
title={!!navigator.clipboard ? t('event:nav.title') : ''}
|
title={!!navigator.clipboard ? t('event:nav.title') : ''}
|
||||||
>{copied ?? `https://crab.fit/${id}`}</ShareInfo>
|
>{copied ?? `https://crab.fit/${id}`}</ShareInfo>
|
||||||
<ShareInfo isLoading={isLoading}>
|
<ShareInfo isLoading={isLoading} className="instructions">
|
||||||
{!!event?.name &&
|
{!!event?.name &&
|
||||||
<Trans i18nKey="event:nav.shareinfo">Copy the link to this page, or share via <a onClick={() => gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: event?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${id}`)}`}>email</a>.</Trans>
|
<Trans i18nKey="event:nav.shareinfo">Copy the link to this page, or share via <a onClick={() => gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: event?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${id}`)}`}>email</a>.</Trans>
|
||||||
}
|
}
|
||||||
</ShareInfo>
|
</ShareInfo>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
offline ? (
|
offline ? (
|
||||||
<div style={{ margin: '100px 0' }}>
|
<div style={{ margin: '100px 0' }}>
|
||||||
<EventName>{t('event:offline.title')}</EventName>
|
<EventName>{t('event:offline.title')}</EventName>
|
||||||
<ShareInfo><Trans i18nKey="event:offline.body" /></ShareInfo>
|
<ShareInfo><Trans i18nKey="event:offline.body" /></ShareInfo>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ margin: '100px 0' }}>
|
<div style={{ margin: '100px 0' }}>
|
||||||
<EventName>{t('event:error.title')}</EventName>
|
<EventName>{t('event:error.title')}</EventName>
|
||||||
<ShareInfo>{t('event:error.body')}</ShareInfo>
|
<ShareInfo>{t('event:error.body')}</ShareInfo>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
|
|
||||||
{(!!event || isLoading) && (
|
{(!!event || isLoading) && (
|
||||||
<>
|
<>
|
||||||
<LoginSection id="login">
|
<LoginSection id="login">
|
||||||
<StyledMain>
|
<StyledMain>
|
||||||
{user ? (
|
{user ? (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '20px 0', flexWrap: 'wrap', gap: '10px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '20px 0', flexWrap: 'wrap', gap: '10px' }}>
|
||||||
<h2 style={{ margin: 0 }}>{t('event:form.signed_in', { name: user.name })}</h2>
|
<h2 style={{ margin: 0 }}>{t('event:form.signed_in', { name: user.name })}</h2>
|
||||||
<Button small onClick={() => {
|
<Button small onClick={() => {
|
||||||
setTab('group');
|
setTab('group');
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setPassword(null);
|
setPassword(null);
|
||||||
}}>{t('event:form.logout_button')}</Button>
|
}}>{t('event:form.logout_button')}</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<h2>{t('event:form.signed_out')}</h2>
|
<h2>{t('event:form.signed_out')}</h2>
|
||||||
<LoginForm onSubmit={handleSubmit(onSubmit)}>
|
<LoginForm onSubmit={handleSubmit(onSubmit)}>
|
||||||
<TextField
|
<TextField
|
||||||
label={t('event:form.name')}
|
label={t('event:form.name')}
|
||||||
type="text"
|
type="text"
|
||||||
id="name"
|
id="name"
|
||||||
inline
|
inline
|
||||||
required
|
required
|
||||||
{...register('name')}
|
{...register('name')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label={t('event:form.password')}
|
label={t('event:form.password')}
|
||||||
type="password"
|
type="password"
|
||||||
id="password"
|
id="password"
|
||||||
inline
|
inline
|
||||||
{...register('password')}
|
{...register('password')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={isLoginLoading}
|
isLoading={isLoginLoading}
|
||||||
disabled={isLoginLoading || isLoading}
|
disabled={isLoginLoading || isLoading}
|
||||||
>{t('event:form.button')}</Button>
|
>{t('event:form.button')}</Button>
|
||||||
</LoginForm>
|
</LoginForm>
|
||||||
<Error open={!!error} onClose={() => setError(null)}>{error}</Error>
|
<Error open={!!error} onClose={() => setError(null)}>{error}</Error>
|
||||||
<Info>{t('event:form.info')}</Info>
|
<Info>{t('event:form.info')}</Info>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
label={t('event:form.timezone')}
|
label={t('event:form.timezone')}
|
||||||
name="timezone"
|
name="timezone"
|
||||||
id="timezone"
|
id="timezone"
|
||||||
inline
|
inline
|
||||||
value={timezone}
|
value={timezone}
|
||||||
onChange={event => setTimezone(event.currentTarget.value)}
|
onChange={event => setTimezone(event.currentTarget.value)}
|
||||||
options={timezones}
|
options={timezones}
|
||||||
/>
|
/>
|
||||||
{/* eslint-disable-next-line */}
|
{/* eslint-disable-next-line */}
|
||||||
{event?.timezone && event.timezone !== timezone && <p><Trans i18nKey="event:form.created_in_timezone">This event was created in the timezone <strong>{{timezone: event.timezone}}</strong>. <a href="#" onClick={e => {
|
{event?.timezone && event.timezone !== timezone && <p><Trans i18nKey="event:form.created_in_timezone">This event was created in the timezone <strong>{{timezone: event.timezone}}</strong>. <a href="#" onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -395,84 +399,84 @@ const Event = (props) => {
|
||||||
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||||
}}>Click here</a> to use it.</Trans></p>
|
}}>Click here</a> to use it.</Trans></p>
|
||||||
)}
|
)}
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
</LoginSection>
|
</LoginSection>
|
||||||
|
|
||||||
<StyledMain>
|
<StyledMain>
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab
|
<Tab
|
||||||
href="#you"
|
href="#you"
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (user) {
|
if (user) {
|
||||||
setTab('you');
|
setTab('you');
|
||||||
} else {
|
} else {
|
||||||
setFocus('name');
|
setFocus('name');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
selected={tab === 'you'}
|
selected={tab === 'you'}
|
||||||
disabled={!user}
|
disabled={!user}
|
||||||
title={user ? '' : t('event:tabs.you_tooltip')}
|
title={user ? '' : t('event:tabs.you_tooltip')}
|
||||||
>{t('event:tabs.you')}</Tab>
|
>{t('event:tabs.you')}</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
href="#group"
|
href="#group"
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setTab('group');
|
setTab('group');
|
||||||
}}
|
}}
|
||||||
selected={tab === 'group'}
|
selected={tab === 'group'}
|
||||||
>{t('event:tabs.group')}</Tab>
|
>{t('event:tabs.group')}</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
|
|
||||||
{tab === 'group' ? (
|
{tab === 'group' ? (
|
||||||
<section id="group">
|
<section id="group">
|
||||||
<AvailabilityViewer
|
<AvailabilityViewer
|
||||||
times={times}
|
times={times}
|
||||||
timeLabels={timeLabels}
|
timeLabels={timeLabels}
|
||||||
dates={dates}
|
dates={dates}
|
||||||
isSpecificDates={!!dates.length && dates[0].length === 8}
|
isSpecificDates={!!dates.length && dates[0].length === 8}
|
||||||
people={people.filter(p => p.availability.length > 0)}
|
people={people.filter(p => p.availability.length > 0)}
|
||||||
min={min}
|
min={min}
|
||||||
max={max}
|
max={max}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<section id="you">
|
<section id="you">
|
||||||
<AvailabilityEditor
|
<AvailabilityEditor
|
||||||
times={times}
|
times={times}
|
||||||
timeLabels={timeLabels}
|
timeLabels={timeLabels}
|
||||||
dates={dates}
|
dates={dates}
|
||||||
timezone={timezone}
|
timezone={timezone}
|
||||||
isSpecificDates={!!dates.length && dates[0].length === 8}
|
isSpecificDates={!!dates.length && dates[0].length === 8}
|
||||||
value={user.availability}
|
value={user.availability}
|
||||||
onChange={async availability => {
|
onChange={async availability => {
|
||||||
const oldAvailability = [...user.availability];
|
const oldAvailability = [...user.availability];
|
||||||
const utcAvailability = (!!availability.length && availability[0].length === 13)
|
const utcAvailability = (!!availability.length && availability[0].length === 13)
|
||||||
? availability.map(date => dayjs.tz(date, 'HHmm-DDMMYYYY', timezone).utc().format('HHmm-DDMMYYYY'))
|
? availability.map(date => dayjs.tz(date, 'HHmm-DDMMYYYY', timezone).utc().format('HHmm-DDMMYYYY'))
|
||||||
: availability.map(date => dayjs.tz(date, 'HHmm', timezone).day(date.substring(5)).utc().format('HHmm-d'));
|
: availability.map(date => dayjs.tz(date, 'HHmm', timezone).day(date.substring(5)).utc().format('HHmm-d'));
|
||||||
setUser({ ...user, availability });
|
setUser({ ...user, availability });
|
||||||
try {
|
try {
|
||||||
await api.patch(`/event/${id}/people/${user.name}`, {
|
await api.patch(`/event/${id}/people/${user.name}`, {
|
||||||
person: {
|
person: {
|
||||||
password,
|
password,
|
||||||
availability: utcAvailability,
|
availability: utcAvailability,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
setUser({ ...user, oldAvailability });
|
setUser({ ...user, oldAvailability });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Event;
|
export default Event;
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const EventName = styled.h1`
|
export const EventName = styled.h1`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
margin: 20px 0 5px;
|
margin: 20px 0 5px;
|
||||||
|
|
||||||
${props => props.isLoading && `
|
${props => props.isLoading && `
|
||||||
&:after {
|
&:after {
|
||||||
content: '';
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
background-color: ${props.theme.loading};
|
background-color: ${props.theme.loading};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const EventDate = styled.span`
|
export const EventDate = styled.span`
|
||||||
|
|
@ -28,63 +28,73 @@ export const EventDate = styled.span`
|
||||||
letter-spacing: .01em;
|
letter-spacing: .01em;
|
||||||
|
|
||||||
${props => props.isLoading && `
|
${props => props.isLoading && `
|
||||||
&:after {
|
&:after {
|
||||||
content: '';
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
background-color: ${props.theme.loading};
|
background-color: ${props.theme.loading};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
&::after {
|
||||||
|
content: ' - ' attr(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LoginForm = styled.form`
|
export const LoginForm = styled.form`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr auto;
|
grid-template-columns: 1fr 1fr auto;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
grid-gap: 18px;
|
grid-gap: 18px;
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
@media (max-width: 400px) {
|
@media (max-width: 400px) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
||||||
& div:last-child {
|
& div:last-child {
|
||||||
--btn-width: 100%;
|
--btn-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LoginSection = styled.section`
|
export const LoginSection = styled.section`
|
||||||
background-color: ${props => props.theme.primaryBackground};
|
background-color: ${props => props.theme.primaryBackground};
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Info = styled.p`
|
export const Info = styled.p`
|
||||||
margin: 18px 0;
|
margin: 18px 0;
|
||||||
opacity: .6;
|
opacity: .6;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ShareInfo = styled.p`
|
export const ShareInfo = styled.p`
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
||||||
${props => props.isLoading && `
|
${props => props.isLoading && `
|
||||||
&:after {
|
&:after {
|
||||||
content: '';
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
background-color: ${props.theme.loading};
|
background-color: ${props.theme.loading};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
|
||||||
${props => props.onClick && `
|
${props => props.onClick && `
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -93,36 +103,46 @@ export const ShareInfo = styled.p`
|
||||||
color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
&.instructions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Tabs = styled.div`
|
export const Tabs = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 30px 0 20px;
|
margin: 30px 0 20px;
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Tab = styled.a`
|
export const Tab = styled.a`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: block;
|
display: block;
|
||||||
color: ${props => props.theme.text};
|
color: ${props => props.theme.text};
|
||||||
padding: 8px 18px;
|
padding: 8px 18px;
|
||||||
background-color: ${props => props.theme.primaryBackground};
|
background-color: ${props => props.theme.primaryBackground};
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid ${props => props.theme.primary};
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
border-top-left-radius: 5px;
|
border-top-left-radius: 5px;
|
||||||
border-top-right-radius: 5px;
|
border-top-right-radius: 5px;
|
||||||
|
|
||||||
${props => props.selected && `
|
${props => props.selected && `
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
background-color: ${props.theme.primary};
|
background-color: ${props.theme.primary};
|
||||||
border-color: ${props.theme.primary};
|
border-color: ${props.theme.primary};
|
||||||
`}
|
`}
|
||||||
|
|
||||||
${props => props.disabled && `
|
${props => props.disabled && `
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -3,41 +3,42 @@ import { Link, useHistory } from 'react-router-dom';
|
||||||
import { useTranslation, Trans } from 'react-i18next';
|
import { useTranslation, Trans } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Center,
|
Center,
|
||||||
Footer,
|
Footer,
|
||||||
AvailabilityViewer,
|
AvailabilityViewer,
|
||||||
Logo,
|
Logo,
|
||||||
} from 'components';
|
} from 'components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
StyledMain,
|
StyledMain,
|
||||||
AboutSection,
|
AboutSection,
|
||||||
P,
|
P,
|
||||||
} from '../Home/homeStyle';
|
} from '../Home/homeStyle';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Step,
|
Step,
|
||||||
FakeCalendar,
|
FakeCalendar,
|
||||||
FakeTimeRange,
|
FakeTimeRange,
|
||||||
|
ButtonArea,
|
||||||
} from './helpStyle';
|
} from './helpStyle';
|
||||||
|
|
||||||
const Help = () => {
|
const Help = () => {
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
const { t } = useTranslation(['common', 'help']);
|
const { t } = useTranslation(['common', 'help']);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = t('help:name');
|
document.title = t('help:name');
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledMain>
|
<StyledMain>
|
||||||
<Logo />
|
<Logo />
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
|
|
||||||
<StyledMain>
|
<StyledMain>
|
||||||
<h1>{t('help:name')}</h1>
|
<h1>{t('help:name')}</h1>
|
||||||
<P>{t('help:p1')}</P>
|
<P>{t('help:p1')}</P>
|
||||||
<P>{t('help:p2')}</P>
|
<P>{t('help:p2')}</P>
|
||||||
|
|
||||||
|
|
@ -80,17 +81,19 @@ const Help = () => {
|
||||||
min={0}
|
min={0}
|
||||||
max={5}
|
max={5}
|
||||||
/>
|
/>
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
|
|
||||||
<AboutSection id="about">
|
<ButtonArea>
|
||||||
<StyledMain>
|
<AboutSection id="about">
|
||||||
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
|
<StyledMain>
|
||||||
</StyledMain>
|
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
|
||||||
</AboutSection>
|
</StyledMain>
|
||||||
|
</AboutSection>
|
||||||
|
</ButtonArea>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Help;
|
export default Help;
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,43 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const Step = styled.h2`
|
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-style: solid;
|
||||||
text-decoration-line: underline;
|
text-decoration-line: underline;
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FakeCalendar = styled.div`
|
export const FakeCalendar = styled.div`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
& div {
|
& div {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: repeat(7, 1fr);
|
||||||
grid-gap: 2px;
|
grid-gap: 2px;
|
||||||
}
|
}
|
||||||
& .days span {
|
& .days span {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 3px 0;
|
padding: 3px 0;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
opacity: .7;
|
opacity: .7;
|
||||||
@media (max-width: 350px) {
|
@media (max-width: 350px) {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& .dates span {
|
& .dates span {
|
||||||
background-color: ${props => props.theme.primaryBackground};
|
background-color: ${props => props.theme.primaryBackground};
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid ${props => props.theme.primary};
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
background-color: ${props => props.theme.primary};
|
background-color: ${props => props.theme.primary};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& .dates span:first-of-type {
|
& .dates span:first-of-type {
|
||||||
|
|
@ -51,45 +51,45 @@ export const FakeCalendar = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FakeTimeRange = styled.div`
|
export const FakeTimeRange = styled.div`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
background-color: ${props => props.theme.primaryBackground};
|
background-color: ${props => props.theme.primaryBackground};
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid ${props => props.theme.primary};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 38px 6px 18px;
|
margin: 38px 6px 18px;
|
||||||
|
|
||||||
& div {
|
& div {
|
||||||
height: calc(100% + 20px);
|
height: calc(100% + 20px);
|
||||||
width: 20px;
|
width: 20px;
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid ${props => props.theme.primary};
|
||||||
background-color: ${props => props.theme.primaryLight};
|
background-color: ${props => props.theme.primaryLight};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -10px;
|
top: -10px;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: '|||';
|
content: '|||';
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: ${props => props.theme.primaryDark};
|
color: ${props => props.theme.primaryDark};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: attr(data-label);
|
content: attr(data-label);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: calc(100% + 8px);
|
bottom: calc(100% + 8px);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& .start {
|
& .start {
|
||||||
left: calc(${11 * 4.1666666666666666}% - 11px);
|
left: calc(${11 * 4.1666666666666666}% - 11px);
|
||||||
|
|
@ -100,11 +100,17 @@ export const FakeTimeRange = styled.div`
|
||||||
&:before {
|
&:before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: ${11 * 4.1666666666666666}%;
|
left: ${11 * 4.1666666666666666}%;
|
||||||
right: calc(100% - ${17 * 4.1666666666666666}%);
|
right: calc(100% - ${17 * 4.1666666666666666}%);
|
||||||
top: 0;
|
top: 0;
|
||||||
background-color: ${props => props.theme.primary};
|
background-color: ${props => props.theme.primary};
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ButtonArea = styled.div`
|
||||||
|
@media print {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -9,36 +9,37 @@ import timezone from 'dayjs/plugin/timezone';
|
||||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TextField,
|
TextField,
|
||||||
CalendarField,
|
CalendarField,
|
||||||
TimeRangeField,
|
TimeRangeField,
|
||||||
SelectField,
|
SelectField,
|
||||||
Button,
|
Button,
|
||||||
Center,
|
Center,
|
||||||
Error,
|
Error,
|
||||||
Footer,
|
Footer,
|
||||||
Recents,
|
Recents,
|
||||||
} from 'components';
|
} from 'components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
StyledMain,
|
StyledMain,
|
||||||
CreateForm,
|
CreateForm,
|
||||||
TitleSmall,
|
TitleSmall,
|
||||||
TitleLarge,
|
TitleLarge,
|
||||||
Logo,
|
Logo,
|
||||||
Links,
|
Links,
|
||||||
AboutSection,
|
AboutSection,
|
||||||
P,
|
P,
|
||||||
Stats,
|
Stats,
|
||||||
Stat,
|
Stat,
|
||||||
StatNumber,
|
StatNumber,
|
||||||
StatLabel,
|
StatLabel,
|
||||||
OfflineMessage,
|
OfflineMessage,
|
||||||
ButtonArea,
|
ButtonArea,
|
||||||
} from './homeStyle';
|
} from './homeStyle';
|
||||||
|
|
||||||
import api from 'services';
|
import api from 'services';
|
||||||
import { detect_browser } from 'utils';
|
import { detect_browser } from 'utils';
|
||||||
|
import { useTWAStore } from 'stores';
|
||||||
|
|
||||||
import logo from 'res/logo.svg';
|
import logo from 'res/logo.svg';
|
||||||
import timezones from 'res/timezones.json';
|
import timezones from 'res/timezones.json';
|
||||||
|
|
@ -48,119 +49,120 @@ dayjs.extend(timezone);
|
||||||
dayjs.extend(customParseFormat);
|
dayjs.extend(customParseFormat);
|
||||||
|
|
||||||
const Home = ({ offline }) => {
|
const Home = ({ offline }) => {
|
||||||
const { register, handleSubmit, setValue } = useForm({
|
const { register, handleSubmit, setValue } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
eventCount: null,
|
eventCount: null,
|
||||||
personCount: null,
|
personCount: null,
|
||||||
version: 'loading...',
|
version: 'loading...',
|
||||||
});
|
});
|
||||||
const [browser, setBrowser] = useState(undefined);
|
const [browser, setBrowser] = useState(undefined);
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
const { t } = useTranslation(['common', 'home']);
|
const { t } = useTranslation(['common', 'home']);
|
||||||
|
const isTWA = useTWAStore(state => state.TWA);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetch = async () => {
|
const fetch = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/stats');
|
const response = await api.get('/stats');
|
||||||
setStats(response.data);
|
setStats(response.data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetch();
|
fetch();
|
||||||
document.title = 'Crab Fit';
|
document.title = 'Crab Fit';
|
||||||
setBrowser(detect_browser());
|
setBrowser(detect_browser());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSubmit = async data => {
|
const onSubmit = async data => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const { start, end } = JSON.parse(data.times);
|
const { start, end } = JSON.parse(data.times);
|
||||||
const dates = JSON.parse(data.dates);
|
const dates = JSON.parse(data.dates);
|
||||||
|
|
||||||
if (dates.length === 0) {
|
if (dates.length === 0) {
|
||||||
return setError(t('home:form.errors.no_dates'));
|
return setError(t('home:form.errors.no_dates'));
|
||||||
}
|
}
|
||||||
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
|
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
|
||||||
if (start === end) {
|
if (start === end) {
|
||||||
return setError(t('home:form.errors.same_times'));
|
return setError(t('home:form.errors.same_times'));
|
||||||
}
|
}
|
||||||
|
|
||||||
let times = dates.reduce((times, date) => {
|
let times = dates.reduce((times, date) => {
|
||||||
let day = [];
|
let day = [];
|
||||||
for (let i = start; i < (start > end ? 24 : end); i++) {
|
for (let i = start; i < (start > end ? 24 : end); i++) {
|
||||||
if (isSpecificDates) {
|
if (isSpecificDates) {
|
||||||
day.push(
|
day.push(
|
||||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
day.push(
|
day.push(
|
||||||
dayjs().tz(data.timezone)
|
dayjs().tz(data.timezone)
|
||||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (start > end) {
|
if (start > end) {
|
||||||
for (let i = 0; i < end; i++) {
|
for (let i = 0; i < end; i++) {
|
||||||
if (isSpecificDates) {
|
if (isSpecificDates) {
|
||||||
day.push(
|
day.push(
|
||||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
day.push(
|
day.push(
|
||||||
dayjs().tz(data.timezone)
|
dayjs().tz(data.timezone)
|
||||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [...times, ...day];
|
return [...times, ...day];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (times.length === 0) {
|
if (times.length === 0) {
|
||||||
return setError(t('home:form.errors.no_time'));
|
return setError(t('home:form.errors.no_time'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await api.post('/event', {
|
const response = await api.post('/event', {
|
||||||
event: {
|
event: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
times: times,
|
times: times,
|
||||||
timezone: data.timezone,
|
timezone: data.timezone,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
push(`/${response.data.id}`);
|
push(`/${response.data.id}`);
|
||||||
gtag('event', 'create_event', {
|
gtag('event', 'create_event', {
|
||||||
'event_category': 'home',
|
'event_category': 'home',
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(t('home:form.errors.unknown'));
|
setError(t('home:form.errors.unknown'));
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledMain>
|
<StyledMain>
|
||||||
<Center>
|
<Center>
|
||||||
<Logo src={logo} alt="" />
|
<Logo src={logo} alt="" />
|
||||||
</Center>
|
</Center>
|
||||||
<TitleSmall altChars={/[A-Z]/g.test(t('home:create'))}>{t('home:create')}</TitleSmall>
|
<TitleSmall altChars={/[A-Z]/g.test(t('home:create'))}>{t('home:create')}</TitleSmall>
|
||||||
<TitleLarge>CRAB FIT</TitleLarge>
|
<TitleLarge>CRAB FIT</TitleLarge>
|
||||||
<Links>
|
<Links>
|
||||||
<a href="#about">{t('home:nav.about')}</a> / <a href="#donate">{t('home:nav.donate')}</a>
|
<a href="#about">{t('home:nav.about')}</a> / <a href="#donate">{t('home:nav.donate')}</a>
|
||||||
</Links>
|
</Links>
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
|
|
||||||
<Recents />
|
<Recents />
|
||||||
|
|
@ -172,107 +174,109 @@ const Home = ({ offline }) => {
|
||||||
<P>{t('home:offline')}</P>
|
<P>{t('home:offline')}</P>
|
||||||
</OfflineMessage>
|
</OfflineMessage>
|
||||||
) : (
|
) : (
|
||||||
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
|
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
|
||||||
<TextField
|
<TextField
|
||||||
label={t('home:form.name.label')}
|
label={t('home:form.name.label')}
|
||||||
subLabel={t('home:form.name.sublabel')}
|
subLabel={t('home:form.name.sublabel')}
|
||||||
type="text"
|
type="text"
|
||||||
id="name"
|
id="name"
|
||||||
{...register('name')}
|
{...register('name')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CalendarField
|
<CalendarField
|
||||||
label={t('home:form.dates.label')}
|
label={t('home:form.dates.label')}
|
||||||
subLabel={t('home:form.dates.sublabel')}
|
subLabel={t('home:form.dates.sublabel')}
|
||||||
id="dates"
|
id="dates"
|
||||||
required
|
required
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
{...register('dates')}
|
{...register('dates')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TimeRangeField
|
<TimeRangeField
|
||||||
label={t('home:form.times.label')}
|
label={t('home:form.times.label')}
|
||||||
subLabel={t('home:form.times.sublabel')}
|
subLabel={t('home:form.times.sublabel')}
|
||||||
id="times"
|
id="times"
|
||||||
required
|
required
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
{...register('times')}
|
{...register('times')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
label={t('home:form.timezone.label')}
|
label={t('home:form.timezone.label')}
|
||||||
id="timezone"
|
id="timezone"
|
||||||
options={timezones}
|
options={timezones}
|
||||||
required
|
required
|
||||||
{...register('timezone')}
|
{...register('timezone')}
|
||||||
defaultOption={t('home:form.timezone.defaultOption')}
|
defaultOption={t('home:form.timezone.defaultOption')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Error open={!!error} onClose={() => setError(null)}>{error}</Error>
|
<Error open={!!error} onClose={() => setError(null)}>{error}</Error>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Button type="submit" isLoading={isLoading} disabled={isLoading}>{t('home:form.button')}</Button>
|
<Button type="submit" isLoading={isLoading} disabled={isLoading}>{t('home:form.button')}</Button>
|
||||||
</Center>
|
</Center>
|
||||||
</CreateForm>
|
</CreateForm>
|
||||||
)}
|
)}
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
|
|
||||||
<AboutSection id="about">
|
<AboutSection id="about">
|
||||||
<StyledMain>
|
<StyledMain>
|
||||||
<h2>{t('home:about.name')}</h2>
|
<h2>{t('home:about.name')}</h2>
|
||||||
<Stats>
|
<Stats>
|
||||||
<Stat>
|
<Stat>
|
||||||
<StatNumber>{stats.eventCount ?? '350+'}</StatNumber>
|
<StatNumber>{stats.eventCount ?? '350+'}</StatNumber>
|
||||||
<StatLabel>{t('home:about.events')}</StatLabel>
|
<StatLabel>{t('home:about.events')}</StatLabel>
|
||||||
</Stat>
|
</Stat>
|
||||||
<Stat>
|
<Stat>
|
||||||
<StatNumber>{stats.personCount ?? '550+'}</StatNumber>
|
<StatNumber>{stats.personCount ?? '550+'}</StatNumber>
|
||||||
<StatLabel>{t('home:about.availabilities')}</StatLabel>
|
<StatLabel>{t('home:about.availabilities')}</StatLabel>
|
||||||
</Stat>
|
</Stat>
|
||||||
</Stats>
|
</Stats>
|
||||||
<P><Trans i18nKey="home:about.content.p1">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.<br /><Link to="/how-to" rel="help">Learn more about how to Crab Fit</Link>.</Trans></P>
|
<P><Trans i18nKey="home:about.content.p1">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.<br /><Link to="/how-to" rel="help">Learn more about how to Crab Fit</Link>.</Trans></P>
|
||||||
<ButtonArea>
|
{isTWA !== true && (
|
||||||
{['chrome', 'firefox', 'safari'].includes(browser) && (
|
<ButtonArea>
|
||||||
|
{['chrome', 'firefox', 'safari'].includes(browser) && (
|
||||||
|
<Button
|
||||||
|
href={{
|
||||||
|
chrome: 'https://chrome.google.com/webstore/detail/crab-fit/pnafiibmjbiljofcpjlbonpgdofjhhkj',
|
||||||
|
firefox: 'https://addons.mozilla.org/en-US/firefox/addon/crab-fit/',
|
||||||
|
safari: 'https://apps.apple.com/us/app/crab-fit/id1570803259',
|
||||||
|
}[browser]}
|
||||||
|
icon={{
|
||||||
|
chrome: <svg aria-hidden="true" focusable="false" viewBox="0 0 24 24"><path fill="currentColor" d="M12,20L15.46,14H15.45C15.79,13.4 16,12.73 16,12C16,10.8 15.46,9.73 14.62,9H19.41C19.79,9.93 20,10.94 20,12A8,8 0 0,1 12,20M4,12C4,10.54 4.39,9.18 5.07,8L8.54,14H8.55C9.24,15.19 10.5,16 12,16C12.45,16 12.88,15.91 13.29,15.77L10.89,19.91C7,19.37 4,16.04 4,12M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12M12,4C14.96,4 17.54,5.61 18.92,8H12C10.06,8 8.45,9.38 8.08,11.21L5.7,7.08C7.16,5.21 9.44,4 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></svg>,
|
||||||
|
firefox: <svg aria-hidden="true" focusable="false" viewBox="0 0 24 24"><path fill="currentColor" d="M9.27 7.94C9.27 7.94 9.27 7.94 9.27 7.94M6.85 6.74C6.86 6.74 6.86 6.74 6.85 6.74M21.28 8.6C20.85 7.55 19.96 6.42 19.27 6.06C19.83 7.17 20.16 8.28 20.29 9.1L20.29 9.12C19.16 6.3 17.24 5.16 15.67 2.68C15.59 2.56 15.5 2.43 15.43 2.3C15.39 2.23 15.36 2.16 15.32 2.09C15.26 1.96 15.2 1.83 15.17 1.69C15.17 1.68 15.16 1.67 15.15 1.67H15.13L15.12 1.67L15.12 1.67L15.12 1.67C12.9 2.97 11.97 5.26 11.74 6.71C11.05 6.75 10.37 6.92 9.75 7.22C9.63 7.27 9.58 7.41 9.62 7.53C9.67 7.67 9.83 7.74 9.96 7.68C10.5 7.42 11.1 7.27 11.7 7.23L11.75 7.23C11.83 7.22 11.92 7.22 12 7.22C12.5 7.21 12.97 7.28 13.44 7.42L13.5 7.44C13.6 7.46 13.67 7.5 13.75 7.5C13.8 7.54 13.86 7.56 13.91 7.58L14.05 7.64C14.12 7.67 14.19 7.7 14.25 7.73C14.28 7.75 14.31 7.76 14.34 7.78C14.41 7.82 14.5 7.85 14.54 7.89C14.58 7.91 14.62 7.94 14.66 7.96C15.39 8.41 16 9.03 16.41 9.77C15.88 9.4 14.92 9.03 14 9.19C17.6 11 16.63 17.19 11.64 16.95C11.2 16.94 10.76 16.85 10.34 16.7C10.24 16.67 10.14 16.63 10.05 16.58C10 16.56 9.93 16.53 9.88 16.5C8.65 15.87 7.64 14.68 7.5 13.23C7.5 13.23 8 11.5 10.83 11.5C11.14 11.5 12 10.64 12.03 10.4C12.03 10.31 10.29 9.62 9.61 8.95C9.24 8.59 9.07 8.42 8.92 8.29C8.84 8.22 8.75 8.16 8.66 8.1C8.43 7.3 8.42 6.45 8.63 5.65C7.6 6.12 6.8 6.86 6.22 7.5H6.22C5.82 7 5.85 5.35 5.87 5C5.86 5 5.57 5.16 5.54 5.18C5.19 5.43 4.86 5.71 4.56 6C4.21 6.37 3.9 6.74 3.62 7.14C3 8.05 2.5 9.09 2.28 10.18C2.28 10.19 2.18 10.59 2.11 11.1L2.08 11.33C2.06 11.5 2.04 11.65 2 11.91L2 11.94L2 12.27L2 12.32C2 17.85 6.5 22.33 12 22.33C16.97 22.33 21.08 18.74 21.88 14C21.9 13.89 21.91 13.76 21.93 13.63C22.13 11.91 21.91 10.11 21.28 8.6Z" /></svg>,
|
||||||
|
safari: <svg aria-hidden="true" focusable="false" viewBox="0 0 24 24"><path fill="currentColor" d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,14.09 4.8,16 6.11,17.41L9.88,9.88L17.41,6.11C16,4.8 14.09,4 12,4M12,20A8,8 0 0,0 20,12C20,9.91 19.2,8 17.89,6.59L14.12,14.12L6.59,17.89C8,19.2 9.91,20 12,20M12,12L11.23,11.23L9.7,14.3L12.77,12.77L12,12M12,17.5H13V19H12V17.5M15.88,15.89L16.59,15.18L17.65,16.24L16.94,16.95L15.88,15.89M17.5,12V11H19V12H17.5M12,6.5H11V5H12V6.5M8.12,8.11L7.41,8.82L6.35,7.76L7.06,7.05L8.12,8.11M6.5,12V13H5V12H6.5Z" /></svg>,
|
||||||
|
}[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]}</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
href={{
|
href="https://play.google.com/store/apps/details?id=fit.crab"
|
||||||
chrome: 'https://chrome.google.com/webstore/detail/crab-fit/pnafiibmjbiljofcpjlbonpgdofjhhkj',
|
icon={<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24"><path fill="currentColor" d="M3,20.5V3.5C3,2.91 3.34,2.39 3.84,2.15L13.69,12L3.84,21.85C3.34,21.6 3,21.09 3,20.5M16.81,15.12L6.05,21.34L14.54,12.85L16.81,15.12M20.16,10.81C20.5,11.08 20.75,11.5 20.75,12C20.75,12.5 20.53,12.9 20.18,13.18L17.89,14.5L15.39,12L17.89,9.5L20.16,10.81M6.05,2.66L16.81,8.88L14.54,11.15L6.05,2.66Z" /></svg>}
|
||||||
firefox: 'https://addons.mozilla.org/en-US/firefox/addon/crab-fit/',
|
onClick={() => gtag('event', 'download_android_app', { 'event_category': 'home' })}
|
||||||
safari: 'https://apps.apple.com/us/app/crab-fit/id1570803259',
|
|
||||||
}[browser]}
|
|
||||||
icon={{
|
|
||||||
chrome: <svg aria-hidden="true" focusable="false" viewBox="0 0 24 24"><path fill="currentColor" d="M12,20L15.46,14H15.45C15.79,13.4 16,12.73 16,12C16,10.8 15.46,9.73 14.62,9H19.41C19.79,9.93 20,10.94 20,12A8,8 0 0,1 12,20M4,12C4,10.54 4.39,9.18 5.07,8L8.54,14H8.55C9.24,15.19 10.5,16 12,16C12.45,16 12.88,15.91 13.29,15.77L10.89,19.91C7,19.37 4,16.04 4,12M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12M12,4C14.96,4 17.54,5.61 18.92,8H12C10.06,8 8.45,9.38 8.08,11.21L5.7,7.08C7.16,5.21 9.44,4 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></svg>,
|
|
||||||
firefox: <svg aria-hidden="true" focusable="false" viewBox="0 0 24 24"><path fill="currentColor" d="M9.27 7.94C9.27 7.94 9.27 7.94 9.27 7.94M6.85 6.74C6.86 6.74 6.86 6.74 6.85 6.74M21.28 8.6C20.85 7.55 19.96 6.42 19.27 6.06C19.83 7.17 20.16 8.28 20.29 9.1L20.29 9.12C19.16 6.3 17.24 5.16 15.67 2.68C15.59 2.56 15.5 2.43 15.43 2.3C15.39 2.23 15.36 2.16 15.32 2.09C15.26 1.96 15.2 1.83 15.17 1.69C15.17 1.68 15.16 1.67 15.15 1.67H15.13L15.12 1.67L15.12 1.67L15.12 1.67C12.9 2.97 11.97 5.26 11.74 6.71C11.05 6.75 10.37 6.92 9.75 7.22C9.63 7.27 9.58 7.41 9.62 7.53C9.67 7.67 9.83 7.74 9.96 7.68C10.5 7.42 11.1 7.27 11.7 7.23L11.75 7.23C11.83 7.22 11.92 7.22 12 7.22C12.5 7.21 12.97 7.28 13.44 7.42L13.5 7.44C13.6 7.46 13.67 7.5 13.75 7.5C13.8 7.54 13.86 7.56 13.91 7.58L14.05 7.64C14.12 7.67 14.19 7.7 14.25 7.73C14.28 7.75 14.31 7.76 14.34 7.78C14.41 7.82 14.5 7.85 14.54 7.89C14.58 7.91 14.62 7.94 14.66 7.96C15.39 8.41 16 9.03 16.41 9.77C15.88 9.4 14.92 9.03 14 9.19C17.6 11 16.63 17.19 11.64 16.95C11.2 16.94 10.76 16.85 10.34 16.7C10.24 16.67 10.14 16.63 10.05 16.58C10 16.56 9.93 16.53 9.88 16.5C8.65 15.87 7.64 14.68 7.5 13.23C7.5 13.23 8 11.5 10.83 11.5C11.14 11.5 12 10.64 12.03 10.4C12.03 10.31 10.29 9.62 9.61 8.95C9.24 8.59 9.07 8.42 8.92 8.29C8.84 8.22 8.75 8.16 8.66 8.1C8.43 7.3 8.42 6.45 8.63 5.65C7.6 6.12 6.8 6.86 6.22 7.5H6.22C5.82 7 5.85 5.35 5.87 5C5.86 5 5.57 5.16 5.54 5.18C5.19 5.43 4.86 5.71 4.56 6C4.21 6.37 3.9 6.74 3.62 7.14C3 8.05 2.5 9.09 2.28 10.18C2.28 10.19 2.18 10.59 2.11 11.1L2.08 11.33C2.06 11.5 2.04 11.65 2 11.91L2 11.94L2 12.27L2 12.32C2 17.85 6.5 22.33 12 22.33C16.97 22.33 21.08 18.74 21.88 14C21.9 13.89 21.91 13.76 21.93 13.63C22.13 11.91 21.91 10.11 21.28 8.6Z" /></svg>,
|
|
||||||
safari: <svg aria-hidden="true" focusable="false" viewBox="0 0 24 24"><path fill="currentColor" d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,14.09 4.8,16 6.11,17.41L9.88,9.88L17.41,6.11C16,4.8 14.09,4 12,4M12,20A8,8 0 0,0 20,12C20,9.91 19.2,8 17.89,6.59L14.12,14.12L6.59,17.89C8,19.2 9.91,20 12,20M12,12L11.23,11.23L9.7,14.3L12.77,12.77L12,12M12,17.5H13V19H12V17.5M15.88,15.89L16.59,15.18L17.65,16.24L16.94,16.95L15.88,15.89M17.5,12V11H19V12H17.5M12,6.5H11V5H12V6.5M8.12,8.11L7.41,8.82L6.35,7.76L7.06,7.05L8.12,8.11M6.5,12V13H5V12H6.5Z" /></svg>,
|
|
||||||
}[browser]}
|
|
||||||
onClick={() => gtag('event', `download_extension_${browser}`, { 'event_category': 'home'})}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
secondary
|
secondary
|
||||||
>{{
|
>{t('home:about.android_app')}</Button>
|
||||||
chrome: t('home:about.chrome_extension'),
|
</ButtonArea>
|
||||||
firefox: t('home:about.firefox_extension'),
|
)}
|
||||||
safari: t('home:about.safari_extension'),
|
<P><Trans i18nKey="home:about.content.p3">Created by <a href="https://bengrant.dev" target="_blank" rel="noreferrer noopener author">Ben Grant</a>, Crab Fit is the modern-day solution to your group event planning debates.</Trans></P>
|
||||||
}[browser]}</Button>
|
<P><Trans i18nKey="home:about.content.p4">The code for Crab Fit is open source, if you find any issues or want to contribute, you can visit the <a href="https://github.com/GRA0007/crab.fit" target="_blank" rel="noreferrer noopener">repository</a>. By using Crab Fit you agree to the <Link to="/privacy" rel="license">privacy policy</Link>.</Trans></P>
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
href="https://play.google.com/store/apps/details?id=fit.crab"
|
|
||||||
icon={<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24"><path fill="currentColor" d="M3,20.5V3.5C3,2.91 3.34,2.39 3.84,2.15L13.69,12L3.84,21.85C3.34,21.6 3,21.09 3,20.5M16.81,15.12L6.05,21.34L14.54,12.85L16.81,15.12M20.16,10.81C20.5,11.08 20.75,11.5 20.75,12C20.75,12.5 20.53,12.9 20.18,13.18L17.89,14.5L15.39,12L17.89,9.5L20.16,10.81M6.05,2.66L16.81,8.88L14.54,11.15L6.05,2.66Z" /></svg>}
|
|
||||||
onClick={() => gtag('event', 'download_android_app', { 'event_category': 'home' })}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
secondary
|
|
||||||
>{t('home:about.android_app')}</Button>
|
|
||||||
</ButtonArea>
|
|
||||||
<P><Trans i18nKey="home:about.content.p3">Created by <a href="https://bengrant.dev" target="_blank" rel="noreferrer noopener author">Ben Grant</a>, Crab Fit is the modern-day solution to your group event planning debates.</Trans></P>
|
|
||||||
<P><Trans i18nKey="home:about.content.p4">The code for Crab Fit is open source, if you find any issues or want to contribute, you can visit the <a href="https://github.com/GRA0007/crab.fit" target="_blank" rel="noreferrer noopener">repository</a>. By using Crab Fit you agree to the <Link to="/privacy" rel="license">privacy policy</Link>.</Trans></P>
|
|
||||||
<P>{t('home:about.content.p6')}</P>
|
<P>{t('home:about.content.p6')}</P>
|
||||||
<P>{t('home:about.content.p5')}</P>
|
<P>{t('home:about.content.p5')}</P>
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
</AboutSection>
|
</AboutSection>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Home;
|
export default Home;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const StyledMain = styled.div`
|
export const StyledMain = styled.div`
|
||||||
width: 600px;
|
width: 600px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
max-width: calc(100% - 60px);
|
max-width: calc(100% - 60px);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CreateForm = styled.form`
|
export const CreateForm = styled.form`
|
||||||
|
|
@ -11,14 +11,14 @@ export const CreateForm = styled.form`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TitleSmall = styled.span`
|
export const TitleSmall = styled.span`
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-family: 'Samurai Bob', sans-serif;
|
font-family: 'Samurai Bob', sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
${props => !props.altChars && `
|
${props => !props.altChars && `
|
||||||
|
|
@ -30,23 +30,23 @@ export const TitleSmall = styled.span`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TitleLarge = styled.h1`
|
export const TitleLarge = styled.h1`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 4rem;
|
font-size: 4rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: ${props => props.theme.primary};
|
color: ${props => props.theme.primary};
|
||||||
font-family: 'Molot', sans-serif;
|
font-family: 'Molot', sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-shadow: 0 4px 0 ${props => props.theme.primaryDark};
|
text-shadow: 0 4px 0 ${props => props.theme.primaryDark};
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
@media (max-width: 350px) {
|
@media (max-width: 350px) {
|
||||||
font-size: 3.5rem;
|
font-size: 3.5rem;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Logo = styled.img`
|
export const Logo = styled.img`
|
||||||
width: 80px;
|
width: 80px;
|
||||||
transition: transform .15s;
|
transition: transform .15s;
|
||||||
animation: jelly .5s 1 .05s;
|
animation: jelly .5s 1 .05s;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
@ -81,14 +81,14 @@ export const Logo = styled.img`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Links = styled.nav`
|
export const Links = styled.nav`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const AboutSection = styled.section`
|
export const AboutSection = styled.section`
|
||||||
margin: 30px 0 0;
|
margin: 30px 0 0;
|
||||||
background-color: ${props => props.theme.primaryBackground};
|
background-color: ${props => props.theme.primaryBackground};
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
|
|
||||||
& a {
|
& a {
|
||||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
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`
|
export const P = styled.p`
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.6em;
|
line-height: 1.6em;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Stats = styled.div`
|
export const Stats = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Stat = styled.div`
|
export const Stat = styled.div`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StatNumber = styled.span`
|
export const StatNumber = styled.span`
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StatLabel = styled.span`
|
export const StatLabel = styled.span`
|
||||||
display: block;
|
display: block;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const OfflineMessage = styled.div`
|
export const OfflineMessage = styled.div`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 50px 0 20px;
|
margin: 50px 0 20px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ButtonArea = styled.div`
|
export const ButtonArea = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,18 @@ import { useHistory } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Center,
|
Center,
|
||||||
Footer,
|
Footer,
|
||||||
Logo,
|
Logo,
|
||||||
} from 'components';
|
} from 'components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
StyledMain,
|
StyledMain,
|
||||||
AboutSection,
|
AboutSection,
|
||||||
P,
|
P,
|
||||||
} from '../Home/homeStyle';
|
} 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.';
|
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 contentRef = useRef();
|
||||||
const [content, setContent] = useState('');
|
const [content, setContent] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = `${t('privacy:name')} - Crab Fit`;
|
document.title = `${t('privacy:name')} - Crab Fit`;
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
useEffect(() => setContent(contentRef.current?.innerText || ''), [contentRef]);
|
useEffect(() => setContent(contentRef.current?.innerText || ''), [contentRef]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledMain>
|
<StyledMain>
|
||||||
<Logo />
|
<Logo />
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
|
|
||||||
<StyledMain>
|
<StyledMain>
|
||||||
<h1>{t('privacy:name')}</h1>
|
<h1>{t('privacy:name')}</h1>
|
||||||
|
|
||||||
{!i18n.language.startsWith('en') && (
|
{!i18n.language.startsWith('en') && (
|
||||||
<p>
|
<p>
|
||||||
|
|
@ -58,9 +58,9 @@ const Privacy = () => {
|
||||||
<h2>Information Collection and Use</h2>
|
<h2>Information Collection and Use</h2>
|
||||||
<P>The Service uses third party services that may collect information used to identify you.</P>
|
<P>The Service uses third party services that may collect information used to identify you.</P>
|
||||||
<P>Links to privacy policies of the third party service providers used by the Service:</P>
|
<P>Links to privacy policies of the third party service providers used by the Service:</P>
|
||||||
<ul>
|
<P as="ul">
|
||||||
<li><a href="https://www.google.com/policies/privacy/" target="blank">Google Play Services</a></li>
|
<li><a href="https://www.google.com/policies/privacy/" target="blank">Google Play Services</a></li>
|
||||||
</ul>
|
</P>
|
||||||
|
|
||||||
<h2>Log Data</h2>
|
<h2>Log Data</h2>
|
||||||
<P>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.</P>
|
<P>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.</P>
|
||||||
|
|
@ -71,12 +71,12 @@ const Privacy = () => {
|
||||||
|
|
||||||
<h2>Service Providers</h2>
|
<h2>Service Providers</h2>
|
||||||
<P>Third-party companies may be employed for the following reasons:</P>
|
<P>Third-party companies may be employed for the following reasons:</P>
|
||||||
<ul>
|
<P as="ul">
|
||||||
<li>To facilitate the Service</li>
|
<li>To facilitate the Service</li>
|
||||||
<li>To provide the Service on our behalf</li>
|
<li>To provide the Service on our behalf</li>
|
||||||
<li>To perform Service-related services</li>
|
<li>To perform Service-related services</li>
|
||||||
<li>To assist in analyzing how the Service is used</li>
|
<li>To assist in analyzing how the Service is used</li>
|
||||||
</ul>
|
</P>
|
||||||
<P>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.</P>
|
<P>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.</P>
|
||||||
|
|
||||||
<h2>Security</h2>
|
<h2>Security</h2>
|
||||||
|
|
@ -98,15 +98,17 @@ const Privacy = () => {
|
||||||
</div>
|
</div>
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
|
|
||||||
<AboutSection id="about">
|
<ButtonArea>
|
||||||
<StyledMain>
|
<AboutSection>
|
||||||
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
|
<StyledMain>
|
||||||
</StyledMain>
|
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
|
||||||
</AboutSection>
|
</StyledMain>
|
||||||
|
</AboutSection>
|
||||||
|
</ButtonArea>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Privacy;
|
export default Privacy;
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,16 @@ export const Note = styled.p`
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.6em;
|
||||||
|
|
||||||
& a {
|
& a {
|
||||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const ButtonArea = styled.div`
|
||||||
|
@media print {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 2443 2500" style="enable-background:new 0 0 2443 2500;" xml:space="preserve">
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 2443 2500" style="enable-background:new 0 0 2443 2500;" xml:space="preserve">
|
||||||
<path style="fill:#FFFFFF" d="M1245.8,1018.7V1481h686.5c-13.8,114.9-88.6,287.9-254.7,404.2v0c-105.2,73.4-246.4,124.6-431.8,124.6
|
<path style="fill:#FFFFFF" d="M1245.8,1018.7V1481h686.5c-13.8,114.9-88.6,287.9-254.7,404.2v0c-105.2,73.4-246.4,124.6-431.8,124.6
|
||||||
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
|
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
|
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
|
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
|
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"/>
|
c0-102.4-8.3-177.2-26.3-254.7H1245.8z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 854 B After Width: | Height: | Size: 859 B |
|
|
@ -1,43 +1 @@
|
||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><style>.cls-1,.cls-4{fill:#f79e00;}.cls-2,.cls-4{isolation:isolate;}.cls-3{fill:#f48600;}</style></defs><path class="cls-1" d="M181.11,311a123,123,0,0,1-123-123h0a123,123,0,0,1,123-123h64.32L211.66,98.73h32.46c0,79.47-108.36,32.47-136.43,50.75C73.65,171.64,181.11,311,181.11,311Z"/><path class="cls-1" d="M404,149.48C375.94,131.2,267.58,178.2,267.58,98.73H300L266.27,65h64.32a123,123,0,0,1,123,123h0a123,123,0,0,1-123,123S438.05,171.64,404,149.48Z"/><rect class="cls-1" x="266.27" y="200.39" width="20.89" height="57.44" rx="10.44"/><rect class="cls-1" x="224.49" y="200.39" width="20.89" height="57.44" rx="10.44"/><path class="cls-1" d="M190.55,229.11H321.11A108.35,108.35,0,0,1,429.47,337.47h0A108.36,108.36,0,0,1,321.11,445.83H190.55A108.37,108.37,0,0,1,82.19,337.47h0A108.36,108.36,0,0,1,190.55,229.11Z"/><g class="cls-2"><path class="cls-3" d="M293.69,268.29a68.83,68.83,0,0,0-37.86,11.29,69.19,69.19,0,1,0,0,115.8,69.19,69.19,0,1,0,37.86-127.09Z"/></g><ellipse class="cls-4" cx="255.83" cy="337.48" rx="31.32" ry="57.9"/><path class="cls-1" d="M402.77,386.23c36,12.31,66,40.07,59.44,60.75C440,431.17,413.11,416.91,389,409.83Z"/><path class="cls-1" d="M420.05,345.89c37.37,7.15,71,30.45,67.35,51.85-24.24-12.55-52.82-22.92-77.67-26.57Z"/><path class="cls-1" d="M417.26,303.44c38.05.39,75.26,17.34,75.5,39-26.08-8-56.05-13.16-81.15-12.32Z"/><path class="cls-1" d="M109.23,386.23c-36,12.31-66,40.07-59.44,60.75C72,431.17,98.89,416.91,123,409.83Z"/><path class="cls-1" d="M92,345.89C54.58,353,21,376.34,24.6,397.74c24.24-12.55,52.82-22.92,77.67-26.57Z"/><path class="cls-1" d="M94.74,303.44c-38,.39-75.26,17.34-75.5,39,26.08-8,56.05-13.16,81.15-12.32Z"/></svg>
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-1 {
|
|
||||||
fill: #f4bb60;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cls-2 {
|
|
||||||
fill: #f79e00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cls-3 {
|
|
||||||
fill: #f47f00;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<g>
|
|
||||||
<path class="cls-1" d="M183.85,311.62a123,123,0,0,1-123-123h0a123,123,0,0,1,123-123h64.32L214.4,99.33h32.46c0,79.47-108.36,32.47-136.43,50.75C76.39,172.24,183.85,311.62,183.85,311.62Z"/>
|
|
||||||
<g>
|
|
||||||
<rect class="cls-2" x="267.75" y="200.99" width="20.89" height="57.44" rx="10.44"/>
|
|
||||||
<rect class="cls-2" x="225.97" y="200.99" width="20.89" height="57.44" rx="10.44"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<rect class="cls-1" x="21" y="326.33" width="107.06" height="23.5" rx="11.75"/>
|
|
||||||
<rect class="cls-1" x="28.83" y="289.77" width="107.06" height="23.5" rx="11.75" transform="translate(91.34 -10.92) rotate(16.92)"/>
|
|
||||||
<rect class="cls-1" x="26.22" y="362.88" width="107.06" height="23.5" rx="11.75" transform="matrix(0.95, -0.31, 0.31, 0.95, -110.74, 42.33)"/>
|
|
||||||
<rect class="cls-1" x="45.81" y="394.21" width="107.06" height="23.5" rx="11.75" transform="translate(-215.65 131.26) rotate(-35.16)"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<rect class="cls-1" x="383.94" y="326.33" width="107.06" height="23.5" rx="11.75" transform="translate(874.94 676.15) rotate(-180)"/>
|
|
||||||
<rect class="cls-1" x="376.11" y="289.77" width="107.06" height="23.5" rx="11.75" transform="translate(928.45 464.91) rotate(163.08)"/>
|
|
||||||
<rect class="cls-1" x="378.72" y="362.88" width="107.06" height="23.5" rx="11.75" transform="translate(729.24 863.49) rotate(-162.19)"/>
|
|
||||||
<rect class="cls-1" x="359.14" y="394.21" width="107.06" height="23.5" rx="11.75" transform="translate(516.28 975.5) rotate(-144.84)"/>
|
|
||||||
</g>
|
|
||||||
<path class="cls-2" d="M214.4,99.33V65.56H183.85a123,123,0,0,0-123,123h0C74.53,116.13,113.69,99.33,214.4,99.33Z"/>
|
|
||||||
<path class="cls-1" d="M331.42,311.62a123,123,0,0,0,123-123h0a123,123,0,0,0-123-123H267.1l33.77,33.77H268.4c0,79.47,108.36,32.47,136.43,50.75C438.87,172.24,331.42,311.62,331.42,311.62Z"/>
|
|
||||||
<path class="cls-2" d="M300.87,99.33V65.56h30.55a123,123,0,0,1,123,123h0C440.74,116.13,401.57,99.33,300.87,99.33Z"/>
|
|
||||||
<rect class="cls-2" x="83.67" y="229.71" width="347.28" height="216.72" rx="108.36"/>
|
|
||||||
<circle class="cls-3" cx="219.44" cy="338.08" r="69.19"/>
|
|
||||||
<circle class="cls-3" cx="295.17" cy="338.08" r="69.19"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,45 +1,45 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
export const instance = axios.create({
|
export const instance = axios.create({
|
||||||
baseURL: process.env.NODE_ENV === 'production' ? 'https://api-dot-crabfit.uc.r.appspot.com' : 'http://localhost:8080',
|
baseURL: process.env.NODE_ENV === 'production' ? 'https://api-dot-crabfit.uc.r.appspot.com' : 'http://localhost:8080',
|
||||||
timeout: 1000 * 300,
|
timeout: 1000 * 300,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleError = error => {
|
const handleError = error => {
|
||||||
if (error.response && error.response.status) {
|
if (error.response && error.response.status) {
|
||||||
console.log('[Error handler] res:', error.response);
|
console.log('[Error handler] res:', error.response);
|
||||||
}
|
}
|
||||||
return Promise.reject(error.response);
|
return Promise.reject(error.response);
|
||||||
};
|
};
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
get: async (endpoint, data) => {
|
get: async (endpoint, data) => {
|
||||||
try {
|
try {
|
||||||
const response = await instance.get(endpoint, data);
|
const response = await instance.get(endpoint, data);
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleError(error);
|
return handleError(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
post: async (endpoint, data, options = {}) => {
|
post: async (endpoint, data, options = {}) => {
|
||||||
try {
|
try {
|
||||||
const response = await instance.post(endpoint, data, options);
|
const response = await instance.post(endpoint, data, options);
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleError(error);
|
return handleError(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
patch: async (endpoint, data) => {
|
patch: async (endpoint, data) => {
|
||||||
try {
|
try {
|
||||||
const response = await instance.patch(endpoint, data);
|
const response = await instance.patch(endpoint, data);
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleError(error);
|
return handleError(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,26 @@
|
||||||
const theme = {
|
const theme = {
|
||||||
light: {
|
light: {
|
||||||
mode: 'light',
|
mode: 'light',
|
||||||
background: '#FFFFFF',
|
background: '#FFFFFF',
|
||||||
text: '#000000',
|
text: '#000000',
|
||||||
primary: '#F79E00',
|
primary: '#F79E00',
|
||||||
primaryDark: '#F48600',
|
primaryDark: '#F48600',
|
||||||
primaryLight: '#F4BB60',
|
primaryLight: '#F4BB60',
|
||||||
primaryBackground: '#FEF2DD',
|
primaryBackground: '#FEF2DD',
|
||||||
error: '#D32F2F',
|
error: '#D32F2F',
|
||||||
loading: '#DDDDDD',
|
loading: '#DDDDDD',
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
mode: 'dark',
|
mode: 'dark',
|
||||||
background: '#111111',
|
background: '#111111',
|
||||||
text: '#DDDDDD',
|
text: '#DDDDDD',
|
||||||
primary: '#F79E00',
|
primary: '#F79E00',
|
||||||
primaryDark: '#CC7313',
|
primaryDark: '#CC7313',
|
||||||
primaryLight: '#F4BB60',
|
primaryLight: '#F4BB60',
|
||||||
primaryBackground: '#30240F',
|
primaryBackground: '#30240F',
|
||||||
error: '#E53935',
|
error: '#E53935',
|
||||||
loading: '#444444',
|
loading: '#444444',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default theme;
|
export default theme;
|
||||||
|
|
|
||||||