|
|
@ -7,3 +7,7 @@ cron:
|
|||
url: /tasks/legacyCleanup
|
||||
schedule: every tuesday 09:00
|
||||
target: api
|
||||
- description: "remove people with an event id that no longer exists"
|
||||
url: /tasks/removeOrphans
|
||||
schedule: 1st wednesday of month 09:00
|
||||
target: api
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const updatePerson = require('./routes/updatePerson');
|
|||
|
||||
const taskCleanup = require('./routes/taskCleanup');
|
||||
const taskLegacyCleanup = require('./routes/taskLegacyCleanup');
|
||||
const taskRemoveOrphans = require('./routes/taskRemoveOrphans');
|
||||
|
||||
const app = express();
|
||||
const port = 8080;
|
||||
|
|
@ -53,6 +54,7 @@ app.patch('/event/:eventId/people/:personName', updatePerson);
|
|||
// Tasks
|
||||
app.get('/tasks/cleanup', taskCleanup);
|
||||
app.get('/tasks/legacyCleanup', taskLegacyCleanup);
|
||||
app.get('/tasks/removeOrphans', taskRemoveOrphans);
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Crabfit API listening at http://localhost:${port} in ${process.env.NODE_ENV === 'production' ? 'prod' : 'dev'} mode`)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
const dayjs = require('dayjs');
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
if (req.header('X-Appengine-Cron') === undefined) {
|
||||
return res.status(400).send('This task can only be run from a cron job');
|
||||
}
|
||||
|
||||
const threeMonthsAgo = dayjs().subtract(3, 'month').unix();
|
||||
|
||||
console.log(`Running cleanup task at ${dayjs().format('h:mma D MMM YYYY')}`);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
const dayjs = require('dayjs');
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
if (req.header('X-Appengine-Cron') === undefined) {
|
||||
return res.status(400).send('This task can only be run from a cron job');
|
||||
}
|
||||
|
||||
const threeMonthsAgo = dayjs().subtract(3, 'month').unix();
|
||||
|
||||
console.log(`Running LEGACY cleanup task at ${dayjs().format('h:mma D MMM YYYY')}`);
|
||||
|
|
|
|||
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"
|
||||
400:
|
||||
description: "Not called from a cron job"
|
||||
"/tasks/removeOrphans":
|
||||
get:
|
||||
summary: "Deletes people if the event they were created under no longer exists"
|
||||
operationId: "taskRemoveOrphans"
|
||||
tags:
|
||||
- tasks
|
||||
responses:
|
||||
200:
|
||||
description: "OK"
|
||||
404:
|
||||
description: "Not found"
|
||||
400:
|
||||
description: "Not called from a cron job"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "Crab Fit",
|
||||
"description": "Enter your availability to find a time that works for everyone!",
|
||||
"version": "1.1",
|
||||
"version": "1.2",
|
||||
"manifest_version": 2,
|
||||
|
||||
"author": "Ben Grant",
|
||||
|
|
|
|||
|
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-family: Karla;
|
||||
src: url('fonts/karla-variable.ttf') format('truetype');
|
||||
font-weight: 1 999;
|
||||
font-family: Karla;
|
||||
src: url('fonts/karla-variable.ttf') format('truetype');
|
||||
font-weight: 1 999;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@
|
|||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="theme-color" content="#F79E00">
|
||||
<meta
|
||||
name="keywords"
|
||||
content="crab, fit, crabfit, schedule, availability, availabilities, when2meet, doodle, meet, plan, time, timezone"
|
||||
>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="crab, fit, crabfit, schedule, availability, availabilities, when2meet, doodle, meet, plan, time, timezone"
|
||||
>
|
||||
<meta
|
||||
name="description"
|
||||
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="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
|
||||
<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:url" content="https://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: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>
|
||||
|
||||
|
|
|
|||
|
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 colortheme = useSettingsStore(state => state.theme);
|
||||
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const [isDark, setIsDark] = useState(darkQuery.matches);
|
||||
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const [isDark, setIsDark] = useState(darkQuery.matches);
|
||||
const [offline, setOffline] = useState(!window.navigator.onLine);
|
||||
|
||||
const [eggCount, setEggCount] = useState(0);
|
||||
|
|
@ -46,7 +46,7 @@ const App = () => {
|
|||
[eggCount, eggKey]
|
||||
);
|
||||
|
||||
darkQuery.addListener(e => colortheme === 'System' && setIsDark(e.matches));
|
||||
darkQuery.addListener(e => colortheme === 'System' && setIsDark(e.matches));
|
||||
|
||||
useEffect(() => {
|
||||
const onOffline = () => setOffline(true);
|
||||
|
|
@ -87,56 +87,57 @@ const App = () => {
|
|||
}, [colortheme, darkQuery.matches]);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ThemeProvider theme={theme[isDark ? 'dark' : 'light']}>
|
||||
<Global
|
||||
styles={theme => ({
|
||||
html: {
|
||||
scrollBehavior: 'smooth',
|
||||
},
|
||||
body: {
|
||||
backgroundColor: theme.background,
|
||||
color: theme.text,
|
||||
fontFamily: `'Karla', sans-serif`,
|
||||
fontWeight: theme.mode === 'dark' ? 500 : 600,
|
||||
margin: 0,
|
||||
},
|
||||
a: {
|
||||
color: theme.primary,
|
||||
},
|
||||
'*::-webkit-scrollbar': {
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
'*::-webkit-scrollbar-track': {
|
||||
background: `${theme.primaryBackground}`,
|
||||
},
|
||||
'*::-webkit-scrollbar-thumb': {
|
||||
borderRadius: 100,
|
||||
border: `4px solid ${theme.primaryBackground}`,
|
||||
width: 12,
|
||||
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}AA`,
|
||||
},
|
||||
'*::-webkit-scrollbar-thumb:hover': {
|
||||
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}CC`,
|
||||
},
|
||||
'*::-webkit-scrollbar-thumb:active': {
|
||||
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}`,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<BrowserRouter>
|
||||
<ThemeProvider theme={theme[isDark ? 'dark' : 'light']}>
|
||||
<Global
|
||||
styles={theme => ({
|
||||
html: {
|
||||
scrollBehavior: 'smooth',
|
||||
'-webkit-print-color-adjust': 'exact',
|
||||
},
|
||||
body: {
|
||||
backgroundColor: theme.background,
|
||||
color: theme.text,
|
||||
fontFamily: `'Karla', sans-serif`,
|
||||
fontWeight: theme.mode === 'dark' ? 500 : 600,
|
||||
margin: 0,
|
||||
},
|
||||
a: {
|
||||
color: theme.primary,
|
||||
},
|
||||
'*::-webkit-scrollbar': {
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
'*::-webkit-scrollbar-track': {
|
||||
background: `${theme.primaryBackground}`,
|
||||
},
|
||||
'*::-webkit-scrollbar-thumb': {
|
||||
borderRadius: 100,
|
||||
border: `4px solid ${theme.primaryBackground}`,
|
||||
width: 12,
|
||||
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}AA`,
|
||||
},
|
||||
'*::-webkit-scrollbar-thumb:hover': {
|
||||
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}CC`,
|
||||
},
|
||||
'*::-webkit-scrollbar-thumb:active': {
|
||||
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}`,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Settings />
|
||||
</Suspense>
|
||||
|
||||
<Switch>
|
||||
<Route path="/" exact render={props => (
|
||||
<Switch>
|
||||
<Route path="/" exact render={props => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Home offline={offline} {...props} />
|
||||
</Suspense>
|
||||
)} />
|
||||
<Route path="/how-to" exact render={props => (
|
||||
<Route path="/how-to" exact render={props => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Help {...props} />
|
||||
</Suspense>
|
||||
|
|
@ -146,17 +147,17 @@ const App = () => {
|
|||
<Privacy {...props} />
|
||||
</Suspense>
|
||||
)} />
|
||||
<Route path="/create" exact render={props => (
|
||||
<Route path="/create" exact render={props => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Create offline={offline} {...props} />
|
||||
</Suspense>
|
||||
)} />
|
||||
<Route path="/:id" exact render={props => (
|
||||
<Route path="/:id" exact render={props => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Event offline={offline} {...props} />
|
||||
</Suspense>
|
||||
)} />
|
||||
</Switch>
|
||||
</Switch>
|
||||
|
||||
{updateAvailable && (
|
||||
<Suspense fallback={<Loading />}>
|
||||
|
|
@ -165,8 +166,8 @@ const App = () => {
|
|||
)}
|
||||
|
||||
{eggVisible && <Egg eggKey={eggKey} onClose={() => setEggVisible(false)} />}
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,17 +9,17 @@ import dayjs_timezone from 'dayjs/plugin/timezone';
|
|||
import utc from 'dayjs/plugin/utc';
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
Wrapper,
|
||||
ScrollWrapper,
|
||||
Container,
|
||||
Date,
|
||||
Times,
|
||||
DateLabel,
|
||||
DayLabel,
|
||||
Spacer,
|
||||
TimeLabels,
|
||||
TimeLabel,
|
||||
TimeSpace,
|
||||
Container,
|
||||
Date,
|
||||
Times,
|
||||
DateLabel,
|
||||
DayLabel,
|
||||
Spacer,
|
||||
TimeLabels,
|
||||
TimeLabel,
|
||||
TimeSpace,
|
||||
StyledMain,
|
||||
} from 'components/AvailabilityViewer/availabilityViewerStyle';
|
||||
import { Time } from './availabilityEditorStyle';
|
||||
|
|
@ -37,34 +37,34 @@ dayjs.extend(utc);
|
|||
dayjs.extend(dayjs_timezone);
|
||||
|
||||
const AvailabilityEditor = ({
|
||||
times,
|
||||
timeLabels,
|
||||
dates,
|
||||
times,
|
||||
timeLabels,
|
||||
dates,
|
||||
timezone,
|
||||
isSpecificDates,
|
||||
value = [],
|
||||
onChange,
|
||||
...props
|
||||
value = [],
|
||||
onChange,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation('event');
|
||||
const locale = useLocaleUpdateStore(state => state.locale);
|
||||
|
||||
const [selectingTimes, _setSelectingTimes] = useState([]);
|
||||
const staticSelectingTimes = useRef([]);
|
||||
const setSelectingTimes = newTimes => {
|
||||
staticSelectingTimes.current = newTimes;
|
||||
_setSelectingTimes(newTimes);
|
||||
};
|
||||
const [selectingTimes, _setSelectingTimes] = useState([]);
|
||||
const staticSelectingTimes = useRef([]);
|
||||
const setSelectingTimes = newTimes => {
|
||||
staticSelectingTimes.current = newTimes;
|
||||
_setSelectingTimes(newTimes);
|
||||
};
|
||||
|
||||
const startPos = useRef({});
|
||||
const staticMode = useRef(null);
|
||||
const [mode, _setMode] = useState(staticMode.current);
|
||||
const setMode = newMode => {
|
||||
staticMode.current = newMode;
|
||||
_setMode(newMode);
|
||||
};
|
||||
const startPos = useRef({});
|
||||
const staticMode = useRef(null);
|
||||
const [mode, _setMode] = useState(staticMode.current);
|
||||
const setMode = newMode => {
|
||||
staticMode.current = newMode;
|
||||
_setMode(newMode);
|
||||
};
|
||||
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
<StyledMain>
|
||||
<Center style={{textAlign: 'center'}}>{t('event:you.info')}</Center>
|
||||
|
|
@ -98,89 +98,89 @@ const AvailabilityEditor = ({
|
|||
</StyledMain>
|
||||
)}
|
||||
|
||||
<Wrapper locale={locale}>
|
||||
<Wrapper locale={locale}>
|
||||
<ScrollWrapper>
|
||||
<Container>
|
||||
<TimeLabels>
|
||||
{!!timeLabels.length && timeLabels.map((label, i) =>
|
||||
<TimeSpace key={i}>
|
||||
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
|
||||
</TimeSpace>
|
||||
)}
|
||||
</TimeLabels>
|
||||
{dates.map((date, x) => {
|
||||
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
|
||||
const last = dates.length === x+1 || (isSpecificDates ? dayjs(dates[x+1], 'DDMMYYYY') : dayjs().day(dates[x+1])).diff(parsedDate, 'day') > 1;
|
||||
return (
|
||||
<Fragment key={x}>
|
||||
<Date>
|
||||
{isSpecificDates && <DateLabel>{parsedDate.format('MMM D')}</DateLabel>}
|
||||
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
|
||||
<Container>
|
||||
<TimeLabels>
|
||||
{!!timeLabels.length && timeLabels.map((label, i) =>
|
||||
<TimeSpace key={i}>
|
||||
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
|
||||
</TimeSpace>
|
||||
)}
|
||||
</TimeLabels>
|
||||
{dates.map((date, x) => {
|
||||
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
|
||||
const last = dates.length === x+1 || (isSpecificDates ? dayjs(dates[x+1], 'DDMMYYYY') : dayjs().day(dates[x+1])).diff(parsedDate, 'day') > 1;
|
||||
return (
|
||||
<Fragment key={x}>
|
||||
<Date>
|
||||
{isSpecificDates && <DateLabel>{parsedDate.format('MMM D')}</DateLabel>}
|
||||
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
|
||||
|
||||
<Times
|
||||
borderRight={last}
|
||||
borderLeft={x === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[x-1], 'DDMMYYYY') : dayjs().day(dates[x-1]), 'day') > 1}
|
||||
>
|
||||
{timeLabels.map((timeLabel, y) => {
|
||||
if (!timeLabel.time) return null;
|
||||
if (!times.includes(`${timeLabel.time}-${date}`)) {
|
||||
return (
|
||||
<TimeSpace key={x+y} className='timespace' title={t('event:greyed_times')} />
|
||||
);
|
||||
}
|
||||
const time = `${timeLabel.time}-${date}`;
|
||||
{timeLabels.map((timeLabel, y) => {
|
||||
if (!timeLabel.time) return null;
|
||||
if (!times.includes(`${timeLabel.time}-${date}`)) {
|
||||
return (
|
||||
<TimeSpace key={x+y} className='timespace' title={t('event:greyed_times')} />
|
||||
);
|
||||
}
|
||||
const time = `${timeLabel.time}-${date}`;
|
||||
|
||||
return (
|
||||
<Time
|
||||
key={x+y}
|
||||
time={time}
|
||||
className="time"
|
||||
selected={value.includes(time)}
|
||||
selecting={selectingTimes.includes(time)}
|
||||
mode={mode}
|
||||
onPointerDown={(e) => {
|
||||
e.preventDefault();
|
||||
startPos.current = {x, y};
|
||||
setMode(value.includes(time) ? 'remove' : 'add');
|
||||
setSelectingTimes([time]);
|
||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||
return (
|
||||
<Time
|
||||
key={x+y}
|
||||
time={time}
|
||||
className="time"
|
||||
selected={value.includes(time)}
|
||||
selecting={selectingTimes.includes(time)}
|
||||
mode={mode}
|
||||
onPointerDown={(e) => {
|
||||
e.preventDefault();
|
||||
startPos.current = {x, y};
|
||||
setMode(value.includes(time) ? 'remove' : 'add');
|
||||
setSelectingTimes([time]);
|
||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||
|
||||
document.addEventListener('pointerup', () => {
|
||||
if (staticMode.current === 'add') {
|
||||
onChange([...value, ...staticSelectingTimes.current]);
|
||||
} else if (staticMode.current === 'remove') {
|
||||
onChange(value.filter(t => !staticSelectingTimes.current.includes(t)));
|
||||
}
|
||||
setMode(null);
|
||||
}, { once: true });
|
||||
}}
|
||||
onPointerEnter={() => {
|
||||
if (staticMode.current) {
|
||||
let found = [];
|
||||
for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
|
||||
for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
|
||||
found.push({y: cy, x: cx});
|
||||
}
|
||||
}
|
||||
setSelectingTimes(found.filter(d => timeLabels[d.y].time?.length === 4).map(d => `${timeLabels[d.y].time}-${dates[d.x]}`));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Times>
|
||||
</Date>
|
||||
{last && dates.length !== x+1 && (
|
||||
<Spacer />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
document.addEventListener('pointerup', () => {
|
||||
if (staticMode.current === 'add') {
|
||||
onChange([...value, ...staticSelectingTimes.current]);
|
||||
} else if (staticMode.current === 'remove') {
|
||||
onChange(value.filter(t => !staticSelectingTimes.current.includes(t)));
|
||||
}
|
||||
setMode(null);
|
||||
}, { once: true });
|
||||
}}
|
||||
onPointerEnter={() => {
|
||||
if (staticMode.current) {
|
||||
let found = [];
|
||||
for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
|
||||
for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
|
||||
found.push({y: cy, x: cx});
|
||||
}
|
||||
}
|
||||
setSelectingTimes(found.filter(d => timeLabels[d.y].time?.length === 4).map(d => `${timeLabels[d.y].time}-${dates[d.x]}`));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Times>
|
||||
</Date>
|
||||
{last && dates.length !== x+1 && (
|
||||
<Spacer />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
</ScrollWrapper>
|
||||
</Wrapper>
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default AvailabilityEditor;
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Time = styled.div`
|
||||
height: 10px;
|
||||
touch-action: none;
|
||||
height: 10px;
|
||||
touch-action: none;
|
||||
transition: background-color .1s;
|
||||
|
||||
${props => props.time.slice(2, 4) === '00' && `
|
||||
border-top: 2px solid ${props.theme.text};
|
||||
`}
|
||||
${props => props.time.slice(2, 4) !== '00' && `
|
||||
border-top: 2px solid transparent;
|
||||
`}
|
||||
${props => props.time.slice(2, 4) === '30' && `
|
||||
border-top: 2px dotted ${props.theme.text};
|
||||
`}
|
||||
border-top: 2px solid ${props.theme.text};
|
||||
`}
|
||||
${props => props.time.slice(2, 4) !== '00' && `
|
||||
border-top: 2px solid transparent;
|
||||
`}
|
||||
${props => props.time.slice(2, 4) === '30' && `
|
||||
border-top: 2px dotted ${props.theme.text};
|
||||
`}
|
||||
|
||||
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
||||
background-color: ${props.theme.primary};
|
||||
`};
|
||||
${props => props.mode === 'remove' && props.selecting && `
|
||||
background-color: ${props.theme.background};
|
||||
`};
|
||||
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
||||
background-color: ${props.theme.primary};
|
||||
`};
|
||||
${props => props.mode === 'remove' && props.selecting && `
|
||||
background-color: ${props.theme.background};
|
||||
`};
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -7,28 +7,29 @@ import relativeTime from 'dayjs/plugin/relativeTime';
|
|||
|
||||
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
||||
|
||||
import { Legend, Center } from 'components';
|
||||
import { Legend } from 'components';
|
||||
import {
|
||||
Wrapper,
|
||||
ScrollWrapper,
|
||||
Container,
|
||||
Date,
|
||||
Times,
|
||||
DateLabel,
|
||||
DayLabel,
|
||||
Time,
|
||||
Spacer,
|
||||
Tooltip,
|
||||
TooltipTitle,
|
||||
TooltipDate,
|
||||
TooltipContent,
|
||||
Wrapper,
|
||||
ScrollWrapper,
|
||||
Container,
|
||||
Date,
|
||||
Times,
|
||||
DateLabel,
|
||||
DayLabel,
|
||||
Time,
|
||||
Spacer,
|
||||
Tooltip,
|
||||
TooltipTitle,
|
||||
TooltipDate,
|
||||
TooltipContent,
|
||||
TooltipPerson,
|
||||
TimeLabels,
|
||||
TimeLabel,
|
||||
TimeSpace,
|
||||
TimeLabels,
|
||||
TimeLabel,
|
||||
TimeSpace,
|
||||
People,
|
||||
Person,
|
||||
StyledMain,
|
||||
Info,
|
||||
} from './availabilityViewerStyle';
|
||||
|
||||
import locales from 'res/dayjs_locales';
|
||||
|
|
@ -38,16 +39,16 @@ dayjs.extend(customParseFormat);
|
|||
dayjs.extend(relativeTime);
|
||||
|
||||
const AvailabilityViewer = ({
|
||||
times,
|
||||
timeLabels,
|
||||
dates,
|
||||
times,
|
||||
timeLabels,
|
||||
dates,
|
||||
isSpecificDates,
|
||||
people = [],
|
||||
min = 0,
|
||||
max = 0,
|
||||
...props
|
||||
people = [],
|
||||
min = 0,
|
||||
max = 0,
|
||||
...props
|
||||
}) => {
|
||||
const [tooltip, setTooltip] = useState(null);
|
||||
const [tooltip, setTooltip] = useState(null);
|
||||
const timeFormat = useSettingsStore(state => state.timeFormat);
|
||||
const highlight = useSettingsStore(state => state.highlight);
|
||||
const [filteredPeople, setFilteredPeople] = useState([]);
|
||||
|
|
@ -153,7 +154,7 @@ const AvailabilityViewer = ({
|
|||
times,
|
||||
]);
|
||||
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
<StyledMain>
|
||||
<Legend
|
||||
|
|
@ -162,10 +163,10 @@ const AvailabilityViewer = ({
|
|||
total={people.filter(p => p.availability.length > 0).length}
|
||||
onSegmentFocus={count => setFocusCount(count)}
|
||||
/>
|
||||
<Center style={{textAlign: 'center'}}>{t('event:group.info1')}</Center>
|
||||
<Info>{t('event:group.info1')}</Info>
|
||||
{people.length > 1 && (
|
||||
<>
|
||||
<Center style={{textAlign: 'center'}}>{t('event:group.info2')}</Center>
|
||||
<Info>{t('event:group.info2')}</Info>
|
||||
<People>
|
||||
{people.map((person, i) =>
|
||||
<Person
|
||||
|
|
@ -194,19 +195,19 @@ const AvailabilityViewer = ({
|
|||
)}
|
||||
</StyledMain>
|
||||
|
||||
<Wrapper ref={wrapper}>
|
||||
<ScrollWrapper>
|
||||
{heatmap}
|
||||
<Wrapper ref={wrapper}>
|
||||
<ScrollWrapper>
|
||||
{heatmap}
|
||||
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
x={tooltip.x}
|
||||
y={tooltip.y}
|
||||
>
|
||||
<TooltipTitle>{tooltip.available}</TooltipTitle>
|
||||
<TooltipDate>{tooltip.date}</TooltipDate>
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
x={tooltip.x}
|
||||
y={tooltip.y}
|
||||
>
|
||||
<TooltipTitle>{tooltip.available}</TooltipTitle>
|
||||
<TooltipDate>{tooltip.date}</TooltipDate>
|
||||
{!!filteredPeople.length && (
|
||||
<TooltipContent>
|
||||
<TooltipContent>
|
||||
{tooltip.people.map(person =>
|
||||
<TooltipPerson key={person}>{person}</TooltipPerson>
|
||||
)}
|
||||
|
|
@ -215,12 +216,12 @@ const AvailabilityViewer = ({
|
|||
)}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
</ScrollWrapper>
|
||||
</Wrapper>
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default AvailabilityViewer;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
overflow-y: visible;
|
||||
margin: 20px 0;
|
||||
overflow-y: visible;
|
||||
margin: 20px 0;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
|
|
@ -11,30 +11,30 @@ export const ScrollWrapper = styled.div`
|
|||
`;
|
||||
|
||||
export const Container = styled.div`
|
||||
display: inline-flex;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
box-sizing: border-box;
|
||||
min-width: 100%;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding: 0 calc(calc(100% - 600px) / 2);
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding: 0 calc(calc(100% - 600px) / 2);
|
||||
|
||||
@media (max-width: 660px) {
|
||||
padding: 0 30px;
|
||||
}
|
||||
@media (max-width: 660px) {
|
||||
padding: 0 30px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Date = styled.div`
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
margin-bottom: 10px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
export const Times = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
border-bottom: 2px solid ${props => props.theme.text};
|
||||
border-left: 1px solid ${props => props.theme.text};
|
||||
|
|
@ -57,44 +57,44 @@ export const Times = styled.div`
|
|||
`;
|
||||
|
||||
export const DateLabel = styled.label`
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export const DayLabel = styled.label`
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export const Time = styled.div`
|
||||
height: 10px;
|
||||
height: 10px;
|
||||
background-origin: border-box;
|
||||
transition: background-color .1s;
|
||||
|
||||
${props => props.time.slice(2, 4) === '00' && `
|
||||
border-top: 2px solid ${props.theme.text};
|
||||
`}
|
||||
${props => props.time.slice(2, 4) !== '00' && `
|
||||
border-top: 2px solid transparent;
|
||||
`}
|
||||
${props => props.time.slice(2, 4) === '30' && `
|
||||
border-top: 2px dotted ${props.theme.text};
|
||||
`}
|
||||
${props => props.time.slice(2, 4) === '00' && `
|
||||
border-top: 2px solid ${props.theme.text};
|
||||
`}
|
||||
${props => props.time.slice(2, 4) !== '00' && `
|
||||
border-top: 2px solid transparent;
|
||||
`}
|
||||
${props => props.time.slice(2, 4) === '30' && `
|
||||
border-top: 2px dotted ${props.theme.text};
|
||||
`}
|
||||
|
||||
background-color: ${props => `${props.theme.primary}${Math.round((props.peopleCount/props.maxPeople)*255).toString(16)}`};
|
||||
|
||||
${props => props.highlight && props.peopleCount === props.maxPeople && props.peopleCount > 0 && `
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
transparent 4.3px,
|
||||
${props.theme.primaryDark} 4.3px,
|
||||
${props.theme.primaryDark} 8.6px
|
||||
);
|
||||
transparent,
|
||||
transparent 4.3px,
|
||||
${props.theme.primaryDark} 4.3px,
|
||||
${props.theme.primaryDark} 8.6px
|
||||
);
|
||||
`}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
|
@ -103,40 +103,40 @@ export const Time = styled.div`
|
|||
`;
|
||||
|
||||
export const Spacer = styled.div`
|
||||
width: 12px;
|
||||
flex-shrink: 0;
|
||||
width: 12px;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
export const Tooltip = styled.div`
|
||||
position: absolute;
|
||||
top: ${props => props.y}px;
|
||||
left: ${props => props.x}px;
|
||||
transform: translateX(-50%);
|
||||
border: 1px solid ${props => props.theme.text};
|
||||
border-radius: 3px;
|
||||
padding: 4px 8px;
|
||||
background-color: ${props => props.theme.background}${props => props.theme.mode === 'light' ? 'EE' : 'DD'};
|
||||
max-width: 200px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: ${props => props.y}px;
|
||||
left: ${props => props.x}px;
|
||||
transform: translateX(-50%);
|
||||
border: 1px solid ${props => props.theme.text};
|
||||
border-radius: 3px;
|
||||
padding: 4px 8px;
|
||||
background-color: ${props => props.theme.background}${props => props.theme.mode === 'light' ? 'EE' : 'DD'};
|
||||
max-width: 200px;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export const TooltipTitle = styled.span`
|
||||
font-size: 15px;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
export const TooltipDate = styled.span`
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
opacity: .8;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
opacity: .8;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
export const TooltipContent = styled.div`
|
||||
font-size: 13px;
|
||||
font-size: 13px;
|
||||
padding: 4px 0;
|
||||
`;
|
||||
|
||||
|
|
@ -154,38 +154,38 @@ export const TooltipPerson = styled.span`
|
|||
`;
|
||||
|
||||
export const TimeLabels = styled.div`
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 40px;
|
||||
padding-right: 6px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 40px;
|
||||
padding-right: 6px;
|
||||
`;
|
||||
|
||||
export const TimeSpace = styled.div`
|
||||
height: 10px;
|
||||
position: relative;
|
||||
border-top: 2px solid transparent;
|
||||
height: 10px;
|
||||
position: relative;
|
||||
border-top: 2px solid transparent;
|
||||
|
||||
&.timespace {
|
||||
background-origin: border-box;
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
transparent 4.3px,
|
||||
${props => props.theme.loading} 4.3px,
|
||||
${props => props.theme.loading} 8.6px
|
||||
);
|
||||
transparent,
|
||||
transparent 4.3px,
|
||||
${props => props.theme.loading} 4.3px,
|
||||
${props => props.theme.loading} 8.6px
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
export const TimeLabel = styled.label`
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -.7em;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -.7em;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const StyledMain = styled.div`
|
||||
|
|
@ -220,3 +220,12 @@ export const Person = styled.button`
|
|||
border-color: ${props.theme.primary};
|
||||
`}
|
||||
`;
|
||||
|
||||
export const Info = styled.span`
|
||||
display: block;
|
||||
text-align: center;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Pressable } from './buttonStyle';
|
||||
|
||||
const Button = ({ href, type = 'button', icon, children, ...props }) => (
|
||||
<Pressable
|
||||
<Pressable
|
||||
type={type}
|
||||
as={href ? 'a' : 'button'}
|
||||
href={href}
|
||||
|
|
|
|||
|
|
@ -60,34 +60,34 @@ export const Pressable = styled.button`
|
|||
}
|
||||
|
||||
${props => props.isLoading && `
|
||||
color: transparent;
|
||||
cursor: wait;
|
||||
color: transparent;
|
||||
cursor: wait;
|
||||
|
||||
& img {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes load {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes load {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: calc(50% - 12px);
|
||||
left: calc(50% - 12px);
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
border: 3px solid ${props.primaryColor ? '#FFF' : props.theme.background};
|
||||
border-left-color: transparent;
|
||||
border-radius: 100px;
|
||||
animation: load .5s linear infinite;
|
||||
}
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: calc(50% - 12px);
|
||||
left: calc(50% - 12px);
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
border: 3px solid ${props.primaryColor ? '#FFF' : props.theme.background};
|
||||
border-left-color: transparent;
|
||||
border-radius: 100px;
|
||||
animation: load .5s linear infinite;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
&:after {
|
||||
|
|
@ -106,7 +106,7 @@ export const Pressable = styled.button`
|
|||
justify-content: center;
|
||||
}
|
||||
}
|
||||
`}
|
||||
`}
|
||||
|
||||
${props => props.secondary && `
|
||||
background: transparent;
|
||||
|
|
@ -121,4 +121,14 @@ export const Pressable = styled.button`
|
|||
transform: none;
|
||||
}
|
||||
`}
|
||||
|
||||
@media print {
|
||||
${props => !props.secondary && `
|
||||
box-shadow: 0 4px 0 0 ${props.secondaryColor || props.theme.primaryDark};
|
||||
`}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ import { Button, ToggleField } from 'components';
|
|||
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
StyledLabel,
|
||||
StyledSubLabel,
|
||||
CalendarHeader,
|
||||
CalendarDays,
|
||||
CalendarBody,
|
||||
Date,
|
||||
Day,
|
||||
Wrapper,
|
||||
StyledLabel,
|
||||
StyledSubLabel,
|
||||
CalendarHeader,
|
||||
CalendarDays,
|
||||
CalendarBody,
|
||||
Date,
|
||||
Day,
|
||||
} from './calendarFieldStyle';
|
||||
|
||||
dayjs.extend(isToday);
|
||||
|
|
@ -24,90 +24,90 @@ dayjs.extend(localeData);
|
|||
dayjs.extend(updateLocale);
|
||||
|
||||
const calculateMonth = (month, year, weekStart) => {
|
||||
const date = dayjs().month(month).year(year);
|
||||
const daysInMonth = date.daysInMonth();
|
||||
const daysBefore = date.date(1).day() - weekStart;
|
||||
const daysAfter = 6 - date.date(daysInMonth).day() + weekStart;
|
||||
const date = dayjs().month(month).year(year);
|
||||
const daysInMonth = date.daysInMonth();
|
||||
const daysBefore = date.date(1).day() - weekStart;
|
||||
const daysAfter = 6 - date.date(daysInMonth).day() + weekStart;
|
||||
|
||||
let dates = [];
|
||||
let curDate = date.date(1).subtract(daysBefore, 'day');
|
||||
let y = 0;
|
||||
let x = 0;
|
||||
for (let i = 0; i < daysBefore + daysInMonth + daysAfter; i++) {
|
||||
if (x === 0) dates[y] = [];
|
||||
dates[y][x] = curDate.clone();
|
||||
curDate = curDate.add(1, 'day');
|
||||
x++;
|
||||
if (x > 6) {
|
||||
x = 0;
|
||||
y++;
|
||||
}
|
||||
}
|
||||
let dates = [];
|
||||
let curDate = date.date(1).subtract(daysBefore, 'day');
|
||||
let y = 0;
|
||||
let x = 0;
|
||||
for (let i = 0; i < daysBefore + daysInMonth + daysAfter; i++) {
|
||||
if (x === 0) dates[y] = [];
|
||||
dates[y][x] = curDate.clone();
|
||||
curDate = curDate.add(1, 'day');
|
||||
x++;
|
||||
if (x > 6) {
|
||||
x = 0;
|
||||
y++;
|
||||
}
|
||||
}
|
||||
|
||||
return dates;
|
||||
return dates;
|
||||
};
|
||||
|
||||
const CalendarField = forwardRef(({
|
||||
label,
|
||||
subLabel,
|
||||
id,
|
||||
label,
|
||||
subLabel,
|
||||
id,
|
||||
setValue,
|
||||
...props
|
||||
...props
|
||||
}, ref) => {
|
||||
const weekStart = useSettingsStore(state => state.weekStart);
|
||||
const locale = useLocaleUpdateStore(state => state.locale);
|
||||
const { t } = useTranslation('home');
|
||||
|
||||
const [type, setType] = useState(0);
|
||||
const [type, setType] = useState(0);
|
||||
|
||||
const [dates, setDates] = useState(calculateMonth(dayjs().month(), dayjs().year(), weekStart));
|
||||
const [month, setMonth] = useState(dayjs().month());
|
||||
const [year, setYear] = useState(dayjs().year());
|
||||
const [month, setMonth] = useState(dayjs().month());
|
||||
const [year, setYear] = useState(dayjs().year());
|
||||
|
||||
const [selectedDates, setSelectedDates] = useState([]);
|
||||
const [selectingDates, _setSelectingDates] = useState([]);
|
||||
const staticSelectingDates = useRef([]);
|
||||
const setSelectingDates = newDates => {
|
||||
staticSelectingDates.current = newDates;
|
||||
_setSelectingDates(newDates);
|
||||
};
|
||||
const [selectedDates, setSelectedDates] = useState([]);
|
||||
const [selectingDates, _setSelectingDates] = useState([]);
|
||||
const staticSelectingDates = useRef([]);
|
||||
const setSelectingDates = newDates => {
|
||||
staticSelectingDates.current = newDates;
|
||||
_setSelectingDates(newDates);
|
||||
};
|
||||
|
||||
const [selectedDays, setSelectedDays] = useState([]);
|
||||
const [selectingDays, _setSelectingDays] = useState([]);
|
||||
const staticSelectingDays = useRef([]);
|
||||
const setSelectingDays = newDays => {
|
||||
staticSelectingDays.current = newDays;
|
||||
_setSelectingDays(newDays);
|
||||
};
|
||||
const [selectedDays, setSelectedDays] = useState([]);
|
||||
const [selectingDays, _setSelectingDays] = useState([]);
|
||||
const staticSelectingDays = useRef([]);
|
||||
const setSelectingDays = newDays => {
|
||||
staticSelectingDays.current = newDays;
|
||||
_setSelectingDays(newDays);
|
||||
};
|
||||
|
||||
const startPos = useRef({});
|
||||
const staticMode = useRef(null);
|
||||
const [mode, _setMode] = useState(staticMode.current);
|
||||
const setMode = newMode => {
|
||||
staticMode.current = newMode;
|
||||
_setMode(newMode);
|
||||
};
|
||||
const startPos = useRef({});
|
||||
const staticMode = useRef(null);
|
||||
const [mode, _setMode] = useState(staticMode.current);
|
||||
const setMode = newMode => {
|
||||
staticMode.current = newMode;
|
||||
_setMode(newMode);
|
||||
};
|
||||
|
||||
useEffect(() => setValue(props.name, type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)), [type, selectedDays, selectedDates, setValue, props.name]);
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (dayjs.Ls.hasOwnProperty(locale) && weekStart !== dayjs.Ls[locale].weekStart) {
|
||||
dayjs.updateLocale(locale, { weekStart });
|
||||
}
|
||||
setDates(calculateMonth(month, year, weekStart));
|
||||
}, [weekStart, month, year, locale]);
|
||||
setDates(calculateMonth(month, year, weekStart));
|
||||
}, [weekStart, month, year, locale]);
|
||||
|
||||
return (
|
||||
<Wrapper locale={locale}>
|
||||
{label && <StyledLabel htmlFor={id}>{label}</StyledLabel>}
|
||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||
<input
|
||||
id={id}
|
||||
type="hidden"
|
||||
return (
|
||||
<Wrapper locale={locale}>
|
||||
{label && <StyledLabel htmlFor={id}>{label}</StyledLabel>}
|
||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||
<input
|
||||
id={id}
|
||||
type="hidden"
|
||||
ref={ref}
|
||||
value={type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)}
|
||||
{...props}
|
||||
/>
|
||||
value={type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<ToggleField
|
||||
id="calendarMode"
|
||||
|
|
@ -122,50 +122,50 @@ const CalendarField = forwardRef(({
|
|||
|
||||
{type === 0 ? (
|
||||
<>
|
||||
<CalendarHeader>
|
||||
<Button
|
||||
size="30px"
|
||||
title={t('form.dates.tooltips.previous')}
|
||||
onClick={() => {
|
||||
if (month-1 < 0) {
|
||||
setYear(year-1);
|
||||
setMonth(11);
|
||||
} else {
|
||||
setMonth(month-1);
|
||||
}
|
||||
}}
|
||||
><</Button>
|
||||
<span>{dayjs.months()[month]} {year}</span>
|
||||
<Button
|
||||
size="30px"
|
||||
title={t('form.dates.tooltips.next')}
|
||||
onClick={() => {
|
||||
if (month+1 > 11) {
|
||||
setYear(year+1);
|
||||
setMonth(0);
|
||||
} else {
|
||||
setMonth(month+1);
|
||||
}
|
||||
}}
|
||||
>></Button>
|
||||
</CalendarHeader>
|
||||
<CalendarHeader>
|
||||
<Button
|
||||
size="30px"
|
||||
title={t('form.dates.tooltips.previous')}
|
||||
onClick={() => {
|
||||
if (month-1 < 0) {
|
||||
setYear(year-1);
|
||||
setMonth(11);
|
||||
} else {
|
||||
setMonth(month-1);
|
||||
}
|
||||
}}
|
||||
><</Button>
|
||||
<span>{dayjs.months()[month]} {year}</span>
|
||||
<Button
|
||||
size="30px"
|
||||
title={t('form.dates.tooltips.next')}
|
||||
onClick={() => {
|
||||
if (month+1 > 11) {
|
||||
setYear(year+1);
|
||||
setMonth(0);
|
||||
} else {
|
||||
setMonth(month+1);
|
||||
}
|
||||
}}
|
||||
>></Button>
|
||||
</CalendarHeader>
|
||||
|
||||
<CalendarDays>
|
||||
{(weekStart ? [...dayjs.weekdaysShort().filter((_,i) => i !== 0), dayjs.weekdaysShort()[0]] : dayjs.weekdaysShort()).map(name =>
|
||||
<Day key={name}>{name}</Day>
|
||||
)}
|
||||
</CalendarDays>
|
||||
<CalendarBody>
|
||||
{dates.length > 0 && dates.map((dateRow, y) =>
|
||||
dateRow.map((date, x) =>
|
||||
<Date
|
||||
key={y+x}
|
||||
otherMonth={date.month() !== month}
|
||||
isToday={date.isToday()}
|
||||
title={`${date.date()} ${dayjs.months()[date.month()]}${date.isToday() ? ` (${t('form.dates.tooltips.today')})` : ''}`}
|
||||
selected={selectedDates.includes(date.format('DDMMYYYY'))}
|
||||
selecting={selectingDates.includes(date)}
|
||||
mode={mode}
|
||||
<CalendarDays>
|
||||
{(weekStart ? [...dayjs.weekdaysShort().filter((_,i) => i !== 0), dayjs.weekdaysShort()[0]] : dayjs.weekdaysShort()).map(name =>
|
||||
<Day key={name}>{name}</Day>
|
||||
)}
|
||||
</CalendarDays>
|
||||
<CalendarBody>
|
||||
{dates.length > 0 && dates.map((dateRow, y) =>
|
||||
dateRow.map((date, x) =>
|
||||
<Date
|
||||
key={y+x}
|
||||
otherMonth={date.month() !== month}
|
||||
isToday={date.isToday()}
|
||||
title={`${date.date()} ${dayjs.months()[date.month()]}${date.isToday() ? ` (${t('form.dates.tooltips.today')})` : ''}`}
|
||||
selected={selectedDates.includes(date.format('DDMMYYYY'))}
|
||||
selecting={selectingDates.includes(date)}
|
||||
mode={mode}
|
||||
type="button"
|
||||
onKeyPress={e => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
|
|
@ -176,37 +176,37 @@ const CalendarField = forwardRef(({
|
|||
}
|
||||
}
|
||||
}}
|
||||
onPointerDown={e => {
|
||||
startPos.current = {x, y};
|
||||
setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
|
||||
setSelectingDates([date]);
|
||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||
onPointerDown={e => {
|
||||
startPos.current = {x, y};
|
||||
setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
|
||||
setSelectingDates([date]);
|
||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||
|
||||
document.addEventListener('pointerup', () => {
|
||||
if (staticMode.current === 'add') {
|
||||
setSelectedDates([...selectedDates, ...staticSelectingDates.current.map(d => d.format('DDMMYYYY'))]);
|
||||
} else if (staticMode.current === 'remove') {
|
||||
const toRemove = staticSelectingDates.current.map(d => d.format('DDMMYYYY'));
|
||||
setSelectedDates(selectedDates.filter(d => !toRemove.includes(d)));
|
||||
}
|
||||
setMode(null);
|
||||
}, { once: true });
|
||||
}}
|
||||
onPointerEnter={() => {
|
||||
if (staticMode.current) {
|
||||
let found = [];
|
||||
for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
|
||||
for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
|
||||
found.push({y: cy, x: cx});
|
||||
}
|
||||
}
|
||||
setSelectingDates(found.map(d => dates[d.y][d.x]));
|
||||
}
|
||||
}}
|
||||
>{date.date()}</Date>
|
||||
)
|
||||
)}
|
||||
</CalendarBody>
|
||||
document.addEventListener('pointerup', () => {
|
||||
if (staticMode.current === 'add') {
|
||||
setSelectedDates([...selectedDates, ...staticSelectingDates.current.map(d => d.format('DDMMYYYY'))]);
|
||||
} else if (staticMode.current === 'remove') {
|
||||
const toRemove = staticSelectingDates.current.map(d => d.format('DDMMYYYY'));
|
||||
setSelectedDates(selectedDates.filter(d => !toRemove.includes(d)));
|
||||
}
|
||||
setMode(null);
|
||||
}, { once: true });
|
||||
}}
|
||||
onPointerEnter={() => {
|
||||
if (staticMode.current) {
|
||||
let found = [];
|
||||
for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
|
||||
for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
|
||||
found.push({y: cy, x: cx});
|
||||
}
|
||||
}
|
||||
setSelectingDates(found.map(d => dates[d.y][d.x]));
|
||||
}
|
||||
}}
|
||||
>{date.date()}</Date>
|
||||
)
|
||||
)}
|
||||
</CalendarBody>
|
||||
</>
|
||||
) : (
|
||||
<CalendarBody>
|
||||
|
|
@ -257,8 +257,8 @@ const CalendarField = forwardRef(({
|
|||
)}
|
||||
</CalendarBody>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
</Wrapper>
|
||||
);
|
||||
});
|
||||
|
||||
export default CalendarField;
|
||||
|
|
|
|||
|
|
@ -1,68 +1,68 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
margin: 30px 0;
|
||||
margin: 30px 0;
|
||||
`;
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
`;
|
||||
|
||||
export const StyledSubLabel = styled.label`
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
opacity: .6;
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
opacity: .6;
|
||||
`;
|
||||
|
||||
export const CalendarHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
user-select: none;
|
||||
padding: 6px 0;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
user-select: none;
|
||||
padding: 6px 0;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
export const CalendarDays = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-gap: 2px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-gap: 2px;
|
||||
`;
|
||||
|
||||
export const Day = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px 0;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
opacity: .7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px 0;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
opacity: .7;
|
||||
|
||||
@media (max-width: 350px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
@media (max-width: 350px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const CalendarBody = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-gap: 2px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-gap: 2px;
|
||||
|
||||
& button:first-of-type {
|
||||
border-top-left-radius: 3px;
|
||||
}
|
||||
& button:nth-of-type(7) {
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
& button:nth-last-of-type(7) {
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
& button:last-of-type {
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
& button:first-of-type {
|
||||
border-top-left-radius: 3px;
|
||||
}
|
||||
& button:nth-of-type(7) {
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
& button:nth-last-of-type(7) {
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
& button:last-of-type {
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Date = styled.button`
|
||||
|
|
@ -77,28 +77,28 @@ export const Date = styled.button`
|
|||
transition: none;
|
||||
}
|
||||
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 0;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 0;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
|
||||
${props => props.otherMonth && `
|
||||
color: ${props.theme.mode === 'light' ? props.theme.primaryLight : props.theme.primaryDark};
|
||||
`}
|
||||
${props => props.isToday && `
|
||||
font-weight: 900;
|
||||
color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
`}
|
||||
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
||||
color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};
|
||||
background-color: ${props.theme.primary};
|
||||
`}
|
||||
${props => props.mode === 'remove' && props.selecting && `
|
||||
background-color: ${props.theme.primaryBackground};
|
||||
color: ${props.isToday ? props.theme.primaryDark : (props.otherMonth ? props.theme.primaryLight : 'inherit')};
|
||||
`}
|
||||
${props => props.otherMonth && `
|
||||
color: ${props.theme.mode === 'light' ? props.theme.primaryLight : props.theme.primaryDark};
|
||||
`}
|
||||
${props => props.isToday && `
|
||||
font-weight: 900;
|
||||
color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
`}
|
||||
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
||||
color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};
|
||||
background-color: ${props.theme.primary};
|
||||
`}
|
||||
${props => props.mode === 'remove' && props.selecting && `
|
||||
background-color: ${props.theme.primaryBackground};
|
||||
color: ${props.isToday ? props.theme.primaryDark : (props.otherMonth ? props.theme.primaryLight : 'inherit')};
|
||||
`}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
const Center = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export default Center;
|
||||
|
|
|
|||
|
|
@ -96,9 +96,9 @@ const Donate = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Button
|
||||
small
|
||||
<Wrapper>
|
||||
<Button
|
||||
small
|
||||
title={t('donate.title')}
|
||||
onClick={event => {
|
||||
if (closed) {
|
||||
|
|
@ -125,7 +125,7 @@ const Donate = () => {
|
|||
role="button"
|
||||
aria-expanded={isOpen ? 'true' : 'false'}
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>{t('donate.button')}</Button>
|
||||
>{t('donate.button')}</Button>
|
||||
|
||||
<Options
|
||||
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" target="_blank" rel="noreferrer noopener payment">{t('donate.options.choose')}</a>
|
||||
</Options>
|
||||
</Wrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export const Wrapper = styled.div`
|
|||
`;
|
||||
|
||||
export const Options = styled.div`
|
||||
position: absolute;
|
||||
position: absolute;
|
||||
bottom: calc(100% + 20px);
|
||||
right: 0;
|
||||
background-color: ${props => props.theme.background};
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ const Egg = ({ eggKey, onClose }) => {
|
|||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
return (
|
||||
<Wrapper title="Click anywhere to close" onClick={() => onClose()}>
|
||||
<Image
|
||||
<Wrapper title="Click anywhere to close" onClick={() => onClose()}>
|
||||
<Image
|
||||
src={`https://us-central1-flour-app-services.cloudfunctions.net/charliAPI?v=${eggKey}`}
|
||||
onLoadStart={() => setIsLoading(true)}
|
||||
onLoad={() => setIsLoading(false)}
|
||||
/>
|
||||
{isLoading && <Loading />}
|
||||
</Wrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
position: fixed;
|
||||
position: fixed;
|
||||
background: rgba(0,0,0,.6);
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
|
@ -17,7 +17,7 @@ export const Wrapper = styled.div`
|
|||
`;
|
||||
|
||||
export const Image = styled.img`
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
position: absolute;
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import { Wrapper, CloseButton } from './errorStyle';
|
||||
|
||||
const Error = ({
|
||||
children,
|
||||
onClose,
|
||||
children,
|
||||
onClose,
|
||||
open = true,
|
||||
...props
|
||||
...props
|
||||
}) => (
|
||||
<Wrapper role="alert" open={open} {...props}>
|
||||
{children}
|
||||
<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>
|
||||
</CloseButton>
|
||||
</Wrapper>
|
||||
<Wrapper role="alert" open={open} {...props}>
|
||||
{children}
|
||||
<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>
|
||||
</CloseButton>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
export default Error;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
border-radius: 3px;
|
||||
background-color: ${props => props.theme.error};
|
||||
color: #FFFFFF;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 18px;
|
||||
border-radius: 3px;
|
||||
background-color: ${props => props.theme.error};
|
||||
color: #FFFFFF;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 18px;
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
margin: 0;
|
||||
|
|
@ -30,14 +30,14 @@ export const Wrapper = styled.div`
|
|||
`;
|
||||
|
||||
export const CloseButton = styled.button`
|
||||
border: 0;
|
||||
background: none;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 16px;
|
||||
border: 0;
|
||||
background: none;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 16px;
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.footer`
|
||||
width: 600px;
|
||||
margin: 20px auto;
|
||||
max-width: calc(100% - 60px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 600px;
|
||||
margin: 20px auto;
|
||||
max-width: calc(100% - 60px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
${props => props.small && `
|
||||
margin: 60px auto 0;
|
||||
|
|
@ -19,4 +19,8 @@ export const Wrapper = styled.footer`
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
`}
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -93,11 +93,11 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
}
|
||||
}, [signedIn]);
|
||||
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
{!signedIn ? (
|
||||
<Center>
|
||||
<Button
|
||||
<Button
|
||||
onClick={() => signIn()}
|
||||
isLoading={signedIn === undefined}
|
||||
primaryColor="#4286F5"
|
||||
|
|
@ -161,7 +161,7 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
</CalendarList>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleCalendar;
|
||||
|
|
|
|||
|
|
@ -3,46 +3,46 @@ import { useSettingsStore } from 'stores';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
Label,
|
||||
Bar,
|
||||
Grade,
|
||||
Wrapper,
|
||||
Label,
|
||||
Bar,
|
||||
Grade,
|
||||
} from './legendStyle';
|
||||
|
||||
const Legend = ({
|
||||
min,
|
||||
max,
|
||||
total,
|
||||
min,
|
||||
max,
|
||||
total,
|
||||
onSegmentFocus,
|
||||
...props
|
||||
...props
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation('event');
|
||||
const highlight = useSettingsStore(state => state.highlight);
|
||||
const setHighlight = useSettingsStore(state => state.setHighlight);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Label>{min}/{total} {t('event:available')}</Label>
|
||||
return (
|
||||
<Wrapper>
|
||||
<Label>{min}/{total} {t('event:available')}</Label>
|
||||
|
||||
<Bar
|
||||
<Bar
|
||||
onMouseOut={() => onSegmentFocus(null)}
|
||||
onClick={() => setHighlight(!highlight)}
|
||||
title={t('event:group.legend_tooltip')}
|
||||
>
|
||||
{[...Array(max+1-min).keys()].map(i => i+min).map(i =>
|
||||
<Grade
|
||||
{[...Array(max+1-min).keys()].map(i => i+min).map(i =>
|
||||
<Grade
|
||||
key={i}
|
||||
color={`${theme.primary}${Math.round((i/(max))*255).toString(16)}`}
|
||||
highlight={highlight && i === max && max > 0}
|
||||
onMouseOver={() => onSegmentFocus(i)}
|
||||
/>
|
||||
)}
|
||||
</Bar>
|
||||
)}
|
||||
</Bar>
|
||||
|
||||
<Label>{max}/{total} {t('event:available')}</Label>
|
||||
</Wrapper>
|
||||
);
|
||||
<Label>{max}/{total} {t('event:available')}</Label>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Legend;
|
||||
|
|
|
|||
|
|
@ -1,52 +1,52 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& label:last-of-type {
|
||||
text-align: right;
|
||||
}
|
||||
& label:last-of-type {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
display: block;
|
||||
}
|
||||
@media (max-width: 400px) {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Label = styled.label`
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
export const Bar = styled.div`
|
||||
display: flex;
|
||||
width: 40%;
|
||||
height: 20px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin: 0 8px;
|
||||
border: 1px solid ${props => props.theme.text};
|
||||
display: flex;
|
||||
width: 40%;
|
||||
height: 20px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin: 0 8px;
|
||||
border: 1px solid ${props => props.theme.text};
|
||||
|
||||
@media (max-width: 400px) {
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
}
|
||||
@media (max-width: 400px) {
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Grade = styled.div`
|
||||
flex: 1;
|
||||
background-color: ${props => props.color};
|
||||
flex: 1;
|
||||
background-color: ${props => props.color};
|
||||
|
||||
${props => props.highlight && `
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
${props.theme.primary},
|
||||
${props.theme.primary} 4.5px,
|
||||
${props.theme.primaryDark} 4.5px,
|
||||
${props.theme.primaryDark} 9px
|
||||
);
|
||||
${props.theme.primary},
|
||||
${props.theme.primary} 4.5px,
|
||||
${props.theme.primaryDark} 4.5px,
|
||||
${props.theme.primaryDark} 9px
|
||||
);
|
||||
`}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export const Wrapper = styled.div`
|
|||
`;
|
||||
|
||||
export const A = styled.a`
|
||||
text-decoration: none;
|
||||
text-decoration: none;
|
||||
|
||||
@keyframes jelly {
|
||||
from,to {
|
||||
|
|
@ -35,31 +35,35 @@ export const A = styled.a`
|
|||
`;
|
||||
|
||||
export const Top = styled.div`
|
||||
display: inline-flex;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const Image = styled.img`
|
||||
width: 2.5rem;
|
||||
margin-right: 16px;
|
||||
width: 2.5rem;
|
||||
margin-right: 16px;
|
||||
`;
|
||||
|
||||
export const Title = styled.span`
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
color: ${props => props.theme.primary};
|
||||
font-family: 'Molot', sans-serif;
|
||||
font-weight: 400;
|
||||
text-shadow: 0 2px 0 ${props => props.theme.primaryDark};
|
||||
line-height: 1em;
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
color: ${props => props.theme.primary};
|
||||
font-family: 'Molot', sans-serif;
|
||||
font-weight: 400;
|
||||
text-shadow: 0 2px 0 ${props => props.theme.primaryDark};
|
||||
line-height: 1em;
|
||||
`;
|
||||
|
||||
export const Tagline = styled.span`
|
||||
text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
font-size: 14px;
|
||||
padding-top: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -160,11 +160,11 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
// eslint-disable-next-line
|
||||
}, [client]);
|
||||
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
{!client ? (
|
||||
<Center>
|
||||
<Button
|
||||
<Button
|
||||
onClick={() => signIn()}
|
||||
isLoading={client === undefined}
|
||||
primaryColor="#0364B9"
|
||||
|
|
@ -228,7 +228,7 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
</CalendarList>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default OutlookCalendar;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import dayjs from 'dayjs';
|
|||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
import { AboutSection, StyledMain } from '../../pages/Home/homeStyle';
|
||||
import { Recent } from './recentsStyle';
|
||||
import { Wrapper, Recent } from './recentsStyle';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
|
|
@ -14,17 +14,19 @@ const Recents = ({ target }) => {
|
|||
const { t } = useTranslation(['home', 'common']);
|
||||
|
||||
return !!recents.length && (
|
||||
<AboutSection id="recents">
|
||||
<StyledMain>
|
||||
<h2>{t('home:recently_visited')}</h2>
|
||||
{recents.map(event => (
|
||||
<Recent href={`/${event.id}`} target={target} key={event.id}>
|
||||
<span className="name">{event.name}</span>
|
||||
<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>
|
||||
<Wrapper>
|
||||
<AboutSection id="recents">
|
||||
<StyledMain>
|
||||
<h2>{t('home:recently_visited')}</h2>
|
||||
{recents.map(event => (
|
||||
<Recent href={`/${event.id}`} target={target} key={event.id}>
|
||||
<span className="name">{event.name}</span>
|
||||
<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>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Recent = styled.a`
|
||||
text-decoration: none;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
|
|
|||
|
|
@ -1,43 +1,43 @@
|
|||
import { forwardRef } from 'react';
|
||||
import {
|
||||
Wrapper,
|
||||
StyledLabel,
|
||||
StyledSubLabel,
|
||||
StyledSelect,
|
||||
Wrapper,
|
||||
StyledLabel,
|
||||
StyledSubLabel,
|
||||
StyledSelect,
|
||||
} from './selectFieldStyle';
|
||||
|
||||
const SelectField = forwardRef(({
|
||||
label,
|
||||
subLabel,
|
||||
id,
|
||||
options = [],
|
||||
inline = false,
|
||||
small = false,
|
||||
defaultOption,
|
||||
...props
|
||||
label,
|
||||
subLabel,
|
||||
id,
|
||||
options = [],
|
||||
inline = false,
|
||||
small = false,
|
||||
defaultOption,
|
||||
...props
|
||||
}, ref) => (
|
||||
<Wrapper inline={inline} small={small}>
|
||||
{label && <StyledLabel htmlFor={id} inline={inline} small={small}>{label}</StyledLabel>}
|
||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||
<Wrapper inline={inline} small={small}>
|
||||
{label && <StyledLabel htmlFor={id} inline={inline} small={small}>{label}</StyledLabel>}
|
||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||
|
||||
<StyledSelect
|
||||
id={id}
|
||||
<StyledSelect
|
||||
id={id}
|
||||
small={small}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{defaultOption && <option value="">{defaultOption}</option>}
|
||||
{Array.isArray(options) ? (
|
||||
{...props}
|
||||
>
|
||||
{defaultOption && <option value="">{defaultOption}</option>}
|
||||
{Array.isArray(options) ? (
|
||||
options.map(value =>
|
||||
<option key={value} value={value}>{value}</option>
|
||||
)
|
||||
<option key={value} value={value}>{value}</option>
|
||||
)
|
||||
) : (
|
||||
Object.entries(options).map(([key, value]) =>
|
||||
<option key={key} value={key}>{value}</option>
|
||||
)
|
||||
<option key={key} value={key}>{value}</option>
|
||||
)
|
||||
)}
|
||||
</StyledSelect>
|
||||
</Wrapper>
|
||||
</StyledSelect>
|
||||
</Wrapper>
|
||||
));
|
||||
|
||||
export default SelectField;
|
||||
|
|
|
|||
|
|
@ -1,60 +1,60 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
margin: 30px 0;
|
||||
margin: 30px 0;
|
||||
|
||||
${props => props.inline && `
|
||||
margin: 0;
|
||||
`}
|
||||
${props => props.small && `
|
||||
margin: 10px 0;
|
||||
`}
|
||||
${props => props.inline && `
|
||||
margin: 0;
|
||||
`}
|
||||
${props => props.small && `
|
||||
margin: 10px 0;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
|
||||
${props => props.inline && `
|
||||
font-size: 16px;
|
||||
`}
|
||||
${props => props.small && `
|
||||
font-size: .9rem;
|
||||
`}
|
||||
${props => props.inline && `
|
||||
font-size: 16px;
|
||||
`}
|
||||
${props => props.small && `
|
||||
font-size: .9rem;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const StyledSubLabel = styled.label`
|
||||
display: block;
|
||||
padding-bottom: 6px;
|
||||
font-size: 13px;
|
||||
opacity: .6;
|
||||
display: block;
|
||||
padding-bottom: 6px;
|
||||
font-size: 13px;
|
||||
opacity: .6;
|
||||
`;
|
||||
|
||||
export const StyledSelect = styled.select`
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
background: ${props => props.theme.primaryBackground};
|
||||
color: inherit;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
background: ${props => props.theme.primaryBackground};
|
||||
color: inherit;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,<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-position: right 10px center;
|
||||
background-size: 1em;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
}
|
||||
&:focus {
|
||||
border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
}
|
||||
|
||||
${props => props.small && `
|
||||
padding: 6px 8px;
|
||||
`}
|
||||
`}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const OpenButton = styled.button`
|
||||
border: 0;
|
||||
background: none;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
background: none;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
|
|
@ -33,10 +33,13 @@ export const OpenButton = styled.button`
|
|||
@media (prefers-reduced-motion: reduce) {
|
||||
transition: none;
|
||||
}
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Cover = styled.div`
|
||||
position: fixed;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
|
@ -50,7 +53,7 @@ export const Cover = styled.div`
|
|||
`;
|
||||
|
||||
export const Modal = styled.div`
|
||||
position: absolute;
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: 12px;
|
||||
background-color: ${props => props.theme.background};
|
||||
|
|
@ -81,10 +84,13 @@ export const Modal = styled.div`
|
|||
@media (prefers-reduced-motion: reduce) {
|
||||
transition: none;
|
||||
}
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Heading = styled.span`
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.5rem;
|
||||
display: block;
|
||||
margin: 6px 0;
|
||||
line-height: 1em;
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
import { forwardRef } from 'react';
|
||||
import {
|
||||
Wrapper,
|
||||
StyledLabel,
|
||||
StyledSubLabel,
|
||||
StyledInput,
|
||||
Wrapper,
|
||||
StyledLabel,
|
||||
StyledSubLabel,
|
||||
StyledInput,
|
||||
} from './textFieldStyle';
|
||||
|
||||
const TextField = forwardRef(({
|
||||
label,
|
||||
subLabel,
|
||||
id,
|
||||
inline = false,
|
||||
...props
|
||||
label,
|
||||
subLabel,
|
||||
id,
|
||||
inline = false,
|
||||
...props
|
||||
}, ref) => (
|
||||
<Wrapper inline={inline}>
|
||||
{label && <StyledLabel htmlFor={id} inline={inline}>{label}</StyledLabel>}
|
||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||
<StyledInput id={id} ref={ref} {...props} />
|
||||
</Wrapper>
|
||||
<Wrapper inline={inline}>
|
||||
{label && <StyledLabel htmlFor={id} inline={inline}>{label}</StyledLabel>}
|
||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||
<StyledInput id={id} ref={ref} {...props} />
|
||||
</Wrapper>
|
||||
));
|
||||
|
||||
export default TextField;
|
||||
|
|
|
|||
|
|
@ -1,46 +1,46 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
margin: 30px 0;
|
||||
margin: 30px 0;
|
||||
|
||||
${props => props.inline && `
|
||||
margin: 0;
|
||||
`}
|
||||
${props => props.inline && `
|
||||
margin: 0;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
|
||||
${props => props.inline && `
|
||||
font-size: 16px;
|
||||
`}
|
||||
${props => props.inline && `
|
||||
font-size: 16px;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const StyledSubLabel = styled.label`
|
||||
display: block;
|
||||
padding-bottom: 6px;
|
||||
font-size: 13px;
|
||||
opacity: .6;
|
||||
display: block;
|
||||
padding-bottom: 6px;
|
||||
font-size: 13px;
|
||||
opacity: .6;
|
||||
`;
|
||||
|
||||
export const StyledInput = styled.input`
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
background: ${props => props.theme.primaryBackground};
|
||||
color: inherit;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
|
||||
border-radius: 3px;
|
||||
font-size: 18px;
|
||||
outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
background: ${props => props.theme.primaryBackground};
|
||||
color: inherit;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
|
||||
border-radius: 3px;
|
||||
font-size: 18px;
|
||||
outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
}
|
||||
&:focus {
|
||||
border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -4,94 +4,94 @@ import dayjs from 'dayjs';
|
|||
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
StyledLabel,
|
||||
StyledSubLabel,
|
||||
Range,
|
||||
Handle,
|
||||
Selected,
|
||||
Wrapper,
|
||||
StyledLabel,
|
||||
StyledSubLabel,
|
||||
Range,
|
||||
Handle,
|
||||
Selected,
|
||||
} from './timeRangeFieldStyle';
|
||||
|
||||
const times = ['00','01','02','03','04','05','06','07','08','09','10','11','12','13','14','15','16','17','18','19','20','21','22','23','24'];
|
||||
|
||||
const TimeRangeField = forwardRef(({
|
||||
label,
|
||||
subLabel,
|
||||
id,
|
||||
label,
|
||||
subLabel,
|
||||
id,
|
||||
setValue,
|
||||
...props
|
||||
...props
|
||||
}, ref) => {
|
||||
const timeFormat = useSettingsStore(state => state.timeFormat);
|
||||
const locale = useLocaleUpdateStore(state => state.locale);
|
||||
|
||||
const [start, setStart] = useState(9);
|
||||
const [end, setEnd] = useState(17);
|
||||
const [start, setStart] = useState(9);
|
||||
const [end, setEnd] = useState(17);
|
||||
|
||||
const isStartMoving = useRef(false);
|
||||
const isEndMoving = useRef(false);
|
||||
const rangeRef = useRef();
|
||||
const rangeRect = useRef();
|
||||
const isStartMoving = useRef(false);
|
||||
const isEndMoving = useRef(false);
|
||||
const rangeRef = useRef();
|
||||
const rangeRect = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
if (rangeRef.current) {
|
||||
rangeRect.current = rangeRef.current.getBoundingClientRect();
|
||||
}
|
||||
}, [rangeRef]);
|
||||
useEffect(() => {
|
||||
if (rangeRef.current) {
|
||||
rangeRect.current = rangeRef.current.getBoundingClientRect();
|
||||
}
|
||||
}, [rangeRef]);
|
||||
|
||||
useEffect(() => setValue(props.name, JSON.stringify({start, end})), [start, end, setValue, props.name]);
|
||||
|
||||
const handleMouseMove = e => {
|
||||
if (isStartMoving.current || isEndMoving.current) {
|
||||
let step = Math.round(((e.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
||||
if (step < 0) step = 0;
|
||||
if (step > 24) step = 24;
|
||||
step = Math.abs(step);
|
||||
const handleMouseMove = e => {
|
||||
if (isStartMoving.current || isEndMoving.current) {
|
||||
let step = Math.round(((e.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
||||
if (step < 0) step = 0;
|
||||
if (step > 24) step = 24;
|
||||
step = Math.abs(step);
|
||||
|
||||
if (isStartMoving.current) {
|
||||
setStart(step);
|
||||
} else if (isEndMoving.current) {
|
||||
setEnd(step);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (isStartMoving.current) {
|
||||
setStart(step);
|
||||
} else if (isEndMoving.current) {
|
||||
setEnd(step);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper locale={locale}>
|
||||
{label && <StyledLabel htmlFor={id}>{label}</StyledLabel>}
|
||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||
<input
|
||||
id={id}
|
||||
type="hidden"
|
||||
value={JSON.stringify({start, end})}
|
||||
return (
|
||||
<Wrapper locale={locale}>
|
||||
{label && <StyledLabel htmlFor={id}>{label}</StyledLabel>}
|
||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||
<input
|
||||
id={id}
|
||||
type="hidden"
|
||||
value={JSON.stringify({start, end})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<Range ref={rangeRef}>
|
||||
<Selected start={start} end={start > end ? 24 : end} />
|
||||
{start > end && <Selected start={start > end ? 0 : start} end={end} />}
|
||||
<Handle
|
||||
value={start}
|
||||
label={timeFormat === '24h' ? times[start] : dayjs().hour(times[start]).format('ha')}
|
||||
<Range ref={rangeRef}>
|
||||
<Selected start={start} end={start > end ? 24 : end} />
|
||||
{start > end && <Selected start={start > end ? 0 : start} end={end} />}
|
||||
<Handle
|
||||
value={start}
|
||||
label={timeFormat === '24h' ? times[start] : dayjs().hour(times[start]).format('ha')}
|
||||
extraPadding={end - start === 1 ? 'padding-right: 20px;' : (start - end === 1 ? 'padding-left: 20px;' : '')}
|
||||
onMouseDown={() => {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
isStartMoving.current = true;
|
||||
onMouseDown={() => {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
isStartMoving.current = true;
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isStartMoving.current = false;
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
}, { once: true });
|
||||
}}
|
||||
onTouchMove={(e) => {
|
||||
const touch = e.targetTouches[0];
|
||||
document.addEventListener('mouseup', () => {
|
||||
isStartMoving.current = false;
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
}, { once: true });
|
||||
}}
|
||||
onTouchMove={(e) => {
|
||||
const touch = e.targetTouches[0];
|
||||
|
||||
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
||||
if (step < 0) step = 0;
|
||||
if (step > 24) step = 24;
|
||||
step = Math.abs(step);
|
||||
setStart(step);
|
||||
}}
|
||||
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
||||
if (step < 0) step = 0;
|
||||
if (step > 24) step = 24;
|
||||
step = Math.abs(step);
|
||||
setStart(step);
|
||||
}}
|
||||
tabIndex="0"
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||
|
|
@ -103,29 +103,29 @@ const TimeRangeField = forwardRef(({
|
|||
setStart(Math.min(start+1, 24));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Handle
|
||||
value={end}
|
||||
label={timeFormat === '24h' ? times[end] : dayjs().hour(times[end]).format('ha')}
|
||||
/>
|
||||
<Handle
|
||||
value={end}
|
||||
label={timeFormat === '24h' ? times[end] : dayjs().hour(times[end]).format('ha')}
|
||||
extraPadding={end - start === 1 ? 'padding-left: 20px;' : (start - end === 1 ? 'padding-right: 20px;' : '')}
|
||||
onMouseDown={() => {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
isEndMoving.current = true;
|
||||
onMouseDown={() => {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
isEndMoving.current = true;
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isEndMoving.current = false;
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
}, { once: true });
|
||||
}}
|
||||
onTouchMove={(e) => {
|
||||
const touch = e.targetTouches[0];
|
||||
document.addEventListener('mouseup', () => {
|
||||
isEndMoving.current = false;
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
}, { once: true });
|
||||
}}
|
||||
onTouchMove={(e) => {
|
||||
const touch = e.targetTouches[0];
|
||||
|
||||
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
||||
if (step < 0) step = 0;
|
||||
if (step > 24) step = 24;
|
||||
step = Math.abs(step);
|
||||
setEnd(step);
|
||||
}}
|
||||
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
||||
if (step < 0) step = 0;
|
||||
if (step > 24) step = 24;
|
||||
step = Math.abs(step);
|
||||
setEnd(step);
|
||||
}}
|
||||
tabIndex="0"
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||
|
|
@ -137,10 +137,10 @@ const TimeRangeField = forwardRef(({
|
|||
setEnd(Math.min(end+1, 24));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Range>
|
||||
</Wrapper>
|
||||
);
|
||||
/>
|
||||
</Range>
|
||||
</Wrapper>
|
||||
);
|
||||
});
|
||||
|
||||
export default TimeRangeField;
|
||||
|
|
|
|||
|
|
@ -1,82 +1,82 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
margin: 30px 0;
|
||||
margin: 30px 0;
|
||||
`;
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
`;
|
||||
|
||||
export const StyledSubLabel = styled.label`
|
||||
display: block;
|
||||
padding-bottom: 6px;
|
||||
font-size: 13px;
|
||||
opacity: .6;
|
||||
display: block;
|
||||
padding-bottom: 6px;
|
||||
font-size: 13px;
|
||||
opacity: .6;
|
||||
`;
|
||||
|
||||
export const Range = styled.div`
|
||||
user-select: none;
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
border-radius: 3px;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
margin: 38px 6px 18px;
|
||||
user-select: none;
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
border-radius: 3px;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
margin: 38px 6px 18px;
|
||||
`;
|
||||
|
||||
export const Handle = styled.div`
|
||||
height: calc(100% + 20px);
|
||||
width: 20px;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
background-color: ${props => props.theme.primaryLight};
|
||||
border-radius: 3px;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: calc(${props => props.value * 4.1666666666666666}% - 11px);
|
||||
cursor: ew-resize;
|
||||
touch-action: none;
|
||||
height: calc(100% + 20px);
|
||||
width: 20px;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
background-color: ${props => props.theme.primaryLight};
|
||||
border-radius: 3px;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: calc(${props => props.value * 4.1666666666666666}% - 11px);
|
||||
cursor: ew-resize;
|
||||
touch-action: none;
|
||||
transition: left .1s;
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '|||';
|
||||
font-size: 8px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${props => props.theme.primaryDark};
|
||||
}
|
||||
&:after {
|
||||
content: '|||';
|
||||
font-size: 8px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${props => props.theme.primaryDark};
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '${props => props.label}';
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
text-align: center;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
&:before {
|
||||
content: '${props => props.label}';
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
text-align: center;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
${props => props.extraPadding}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const Selected = styled.div`
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: ${props => props.start * 4.1666666666666666}%;
|
||||
right: calc(100% - ${props => props.end * 4.1666666666666666}%);
|
||||
top: 0;
|
||||
background-color: ${props => props.theme.primary};
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: ${props => props.start * 4.1666666666666666}%;
|
||||
right: calc(100% - ${props => props.end * 4.1666666666666666}%);
|
||||
top: 0;
|
||||
background-color: ${props => props.theme.primary};
|
||||
border-radius: 2px;
|
||||
transition: left .1s, right .1s;
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
transition: none;
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
import {
|
||||
Wrapper,
|
||||
Wrapper,
|
||||
ToggleContainer,
|
||||
StyledLabel,
|
||||
Option,
|
||||
HiddenInput,
|
||||
StyledLabel,
|
||||
Option,
|
||||
HiddenInput,
|
||||
LabelButton,
|
||||
} from './toggleFieldStyle';
|
||||
|
||||
const ToggleField = ({
|
||||
label,
|
||||
id,
|
||||
label,
|
||||
id,
|
||||
name,
|
||||
title = '',
|
||||
options = [],
|
||||
options = [],
|
||||
value,
|
||||
onChange,
|
||||
inputRef,
|
||||
...props
|
||||
...props
|
||||
}) => (
|
||||
<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>}
|
||||
<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>}
|
||||
|
||||
<ToggleContainer>
|
||||
{Object.entries(options).map(([key, label]) =>
|
||||
|
|
@ -37,7 +37,7 @@ const ToggleField = ({
|
|||
</Option>
|
||||
)}
|
||||
</ToggleContainer>
|
||||
</Wrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
export default ToggleField;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
margin: 10px 0;
|
||||
margin: 10px 0;
|
||||
`;
|
||||
|
||||
export const ToggleContainer = styled.div`
|
||||
display: flex;
|
||||
display: flex;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
|
|
@ -29,9 +29,9 @@ export const ToggleContainer = styled.div`
|
|||
`;
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: .9rem;
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: .9rem;
|
||||
|
||||
& svg {
|
||||
height: 1em;
|
||||
|
|
@ -41,7 +41,7 @@ export const StyledLabel = styled.label`
|
|||
`;
|
||||
|
||||
export const Option = styled.div`
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ const UpdateDialog = ({ onClose }) => {
|
|||
const { t } = useTranslation('common');
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Wrapper>
|
||||
<h2>{t('common:update.heading')}</h2>
|
||||
<p>{t('common:update.body')}</p>
|
||||
<ButtonWrapper>
|
||||
<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>
|
||||
</Wrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const Wrapper = styled.div`
|
|||
border-radius: 3px;
|
||||
width: 400px;
|
||||
box-sizing: border-box;
|
||||
max-width: calc(100% - 20px);
|
||||
max-width: calc(100% - 40px);
|
||||
box-shadow: 0 3px 6px 0 rgba(0,0,0,.3);
|
||||
|
||||
& h2 {
|
||||
|
|
@ -31,4 +31,5 @@ export const ButtonWrapper = styled.div`
|
|||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -9,21 +9,21 @@ import timezone from 'dayjs/plugin/timezone';
|
|||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
|
||||
import {
|
||||
TextField,
|
||||
CalendarField,
|
||||
TimeRangeField,
|
||||
SelectField,
|
||||
Button,
|
||||
Error,
|
||||
TextField,
|
||||
CalendarField,
|
||||
TimeRangeField,
|
||||
SelectField,
|
||||
Button,
|
||||
Error,
|
||||
Recents,
|
||||
Footer,
|
||||
} from 'components';
|
||||
|
||||
import {
|
||||
StyledMain,
|
||||
CreateForm,
|
||||
TitleSmall,
|
||||
TitleLarge,
|
||||
StyledMain,
|
||||
CreateForm,
|
||||
TitleSmall,
|
||||
TitleLarge,
|
||||
P,
|
||||
OfflineMessage,
|
||||
ShareInfo,
|
||||
|
|
@ -39,14 +39,14 @@ dayjs.extend(timezone);
|
|||
dayjs.extend(customParseFormat);
|
||||
|
||||
const Create = ({ offline }) => {
|
||||
const { register, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [createdEvent, setCreatedEvent] = useState(null);
|
||||
const { register, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [createdEvent, setCreatedEvent] = useState(null);
|
||||
const [copied, setCopied] = useState(null);
|
||||
const [showFooter, setShowFooter] = useState(true);
|
||||
|
||||
|
|
@ -55,11 +55,11 @@ const Create = ({ offline }) => {
|
|||
|
||||
const addRecent = useRecentsStore(state => state.addRecent);
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (window.self === window.top) {
|
||||
push('/');
|
||||
}
|
||||
document.title = 'Create a Crab Fit';
|
||||
document.title = 'Create a Crab Fit';
|
||||
|
||||
if (window.parent) {
|
||||
window.parent.postMessage('crabfit-create', '*');
|
||||
|
|
@ -71,67 +71,67 @@ const Create = ({ offline }) => {
|
|||
once: true
|
||||
});
|
||||
}
|
||||
}, [push]);
|
||||
}, [push]);
|
||||
|
||||
const onSubmit = async data => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { start, end } = JSON.parse(data.times);
|
||||
const dates = JSON.parse(data.dates);
|
||||
const onSubmit = async data => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { start, end } = JSON.parse(data.times);
|
||||
const dates = JSON.parse(data.dates);
|
||||
|
||||
if (dates.length === 0) {
|
||||
return setError(t('home:form.errors.no_dates'));
|
||||
}
|
||||
if (dates.length === 0) {
|
||||
return setError(t('home:form.errors.no_dates'));
|
||||
}
|
||||
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
|
||||
if (start === end) {
|
||||
return setError(t('home:form.errors.same_times'));
|
||||
}
|
||||
if (start === end) {
|
||||
return setError(t('home:form.errors.same_times'));
|
||||
}
|
||||
|
||||
let times = dates.reduce((times, date) => {
|
||||
let day = [];
|
||||
for (let i = start; i < (start > end ? 24 : end); i++) {
|
||||
let times = dates.reduce((times, date) => {
|
||||
let day = [];
|
||||
for (let i = start; i < (start > end ? 24 : end); i++) {
|
||||
if (isSpecificDates) {
|
||||
day.push(
|
||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
);
|
||||
day.push(
|
||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
);
|
||||
} else {
|
||||
day.push(
|
||||
dayjs().tz(data.timezone)
|
||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||
);
|
||||
}
|
||||
}
|
||||
if (start > end) {
|
||||
for (let i = 0; i < end; i++) {
|
||||
}
|
||||
if (start > end) {
|
||||
for (let i = 0; i < end; i++) {
|
||||
if (isSpecificDates) {
|
||||
day.push(
|
||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
);
|
||||
day.push(
|
||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
);
|
||||
} else {
|
||||
day.push(
|
||||
dayjs().tz(data.timezone)
|
||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...times, ...day];
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
return [...times, ...day];
|
||||
}, []);
|
||||
|
||||
if (times.length === 0) {
|
||||
return setError(t('home:form.errors.no_time'));
|
||||
}
|
||||
if (times.length === 0) {
|
||||
return setError(t('home:form.errors.no_time'));
|
||||
}
|
||||
|
||||
const response = await api.post('/event', {
|
||||
event: {
|
||||
name: data.name,
|
||||
times: times,
|
||||
const response = await api.post('/event', {
|
||||
event: {
|
||||
name: data.name,
|
||||
times: times,
|
||||
timezone: data.timezone,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
setCreatedEvent(response.data);
|
||||
addRecent({
|
||||
id: response.data.id,
|
||||
|
|
@ -141,19 +141,19 @@ const Create = ({ offline }) => {
|
|||
gtag('event', 'create_event', {
|
||||
'event_category': 'create',
|
||||
});
|
||||
} catch (e) {
|
||||
setError(t('home:form.errors.unknown'));
|
||||
console.error(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
setError(t('home:form.errors.unknown'));
|
||||
console.error(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledMain>
|
||||
<TitleSmall>{t('home:create')}</TitleSmall>
|
||||
<TitleLarge>CRAB FIT</TitleLarge>
|
||||
return (
|
||||
<>
|
||||
<StyledMain>
|
||||
<TitleSmall>{t('home:create')}</TitleSmall>
|
||||
<TitleLarge>CRAB FIT</TitleLarge>
|
||||
</StyledMain>
|
||||
|
||||
{createdEvent ? (
|
||||
|
|
@ -173,10 +173,10 @@ const Create = ({ offline }) => {
|
|||
}
|
||||
title={!!navigator.clipboard ? t('event:nav.title') : ''}
|
||||
>{copied ?? `https://crab.fit/${createdEvent?.id}`}</ShareInfo>
|
||||
<ShareInfo>
|
||||
<ShareInfo>
|
||||
{/* 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>
|
||||
</ShareInfo>
|
||||
<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>
|
||||
{showFooter && <Footer small />}
|
||||
</OfflineMessage>
|
||||
</StyledMain>
|
||||
|
|
@ -191,52 +191,52 @@ const Create = ({ offline }) => {
|
|||
<P>{t('home:offline')}</P>
|
||||
</OfflineMessage>
|
||||
) : (
|
||||
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
|
||||
<TextField
|
||||
label={t('home:form.name.label')}
|
||||
subLabel={t('home:form.name.sublabel')}
|
||||
type="text"
|
||||
id="name"
|
||||
{...register('name')}
|
||||
/>
|
||||
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
|
||||
<TextField
|
||||
label={t('home:form.name.label')}
|
||||
subLabel={t('home:form.name.sublabel')}
|
||||
type="text"
|
||||
id="name"
|
||||
{...register('name')}
|
||||
/>
|
||||
|
||||
<CalendarField
|
||||
label={t('home:form.dates.label')}
|
||||
subLabel={t('home:form.dates.sublabel')}
|
||||
id="dates"
|
||||
required
|
||||
<CalendarField
|
||||
label={t('home:form.dates.label')}
|
||||
subLabel={t('home:form.dates.sublabel')}
|
||||
id="dates"
|
||||
required
|
||||
setValue={setValue}
|
||||
{...register('dates')}
|
||||
/>
|
||||
/>
|
||||
|
||||
<TimeRangeField
|
||||
label={t('home:form.times.label')}
|
||||
subLabel={t('home:form.times.sublabel')}
|
||||
id="times"
|
||||
required
|
||||
<TimeRangeField
|
||||
label={t('home:form.times.label')}
|
||||
subLabel={t('home:form.times.sublabel')}
|
||||
id="times"
|
||||
required
|
||||
setValue={setValue}
|
||||
{...register('times')}
|
||||
/>
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
label={t('home:form.timezone.label')}
|
||||
id="timezone"
|
||||
options={timezones}
|
||||
required
|
||||
<SelectField
|
||||
label={t('home:form.timezone.label')}
|
||||
id="timezone"
|
||||
options={timezones}
|
||||
required
|
||||
{...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>
|
||||
</CreateForm>
|
||||
<Button type="submit" isLoading={isLoading} disabled={isLoading} style={{ width: '100%' }}>{t('home:form.button')}</Button>
|
||||
</CreateForm>
|
||||
)}
|
||||
</StyledMain>
|
||||
</StyledMain>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Create;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const StyledMain = styled.div`
|
||||
width: 600px;
|
||||
margin: 10px auto;
|
||||
max-width: calc(100% - 30px);
|
||||
width: 600px;
|
||||
margin: 10px auto;
|
||||
max-width: calc(100% - 30px);
|
||||
`;
|
||||
|
||||
export const CreateForm = styled.form`
|
||||
|
|
@ -11,43 +11,43 @@ export const CreateForm = styled.form`
|
|||
`;
|
||||
|
||||
export const TitleSmall = styled.span`
|
||||
display: block;
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
font-family: 'Samurai Bob', sans-serif;
|
||||
font-weight: 400;
|
||||
color: ${props => props.theme.primaryDark};
|
||||
line-height: 1em;
|
||||
display: block;
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
font-family: 'Samurai Bob', sans-serif;
|
||||
font-weight: 400;
|
||||
color: ${props => props.theme.primaryDark};
|
||||
line-height: 1em;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
export const TitleLarge = styled.h1`
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
color: ${props => props.theme.primary};
|
||||
font-family: 'Molot', sans-serif;
|
||||
font-weight: 400;
|
||||
text-shadow: 0 3px 0 ${props => props.theme.primaryDark};
|
||||
line-height: 1em;
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
color: ${props => props.theme.primary};
|
||||
font-family: 'Molot', sans-serif;
|
||||
font-weight: 400;
|
||||
text-shadow: 0 3px 0 ${props => props.theme.primaryDark};
|
||||
line-height: 1em;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
export const P = styled.p`
|
||||
font-weight: 500;
|
||||
line-height: 1.6em;
|
||||
font-weight: 500;
|
||||
line-height: 1.6em;
|
||||
`;
|
||||
|
||||
export const OfflineMessage = styled.div`
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
margin: 50px 0 20px;
|
||||
`;
|
||||
|
||||
export const ShareInfo = styled.p`
|
||||
margin: 6px 0;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
margin: 6px 0;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
padding: 10px 0;
|
||||
|
||||
${props => props.onClick && `
|
||||
|
|
|
|||
|
|
@ -9,27 +9,27 @@ import customParseFormat from 'dayjs/plugin/customParseFormat';
|
|||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
import {
|
||||
Footer,
|
||||
TextField,
|
||||
SelectField,
|
||||
Button,
|
||||
AvailabilityViewer,
|
||||
AvailabilityEditor,
|
||||
Error,
|
||||
Footer,
|
||||
TextField,
|
||||
SelectField,
|
||||
Button,
|
||||
AvailabilityViewer,
|
||||
AvailabilityEditor,
|
||||
Error,
|
||||
Logo,
|
||||
} from 'components';
|
||||
|
||||
import { StyledMain } from '../Home/homeStyle';
|
||||
|
||||
import {
|
||||
EventName,
|
||||
EventName,
|
||||
EventDate,
|
||||
LoginForm,
|
||||
LoginSection,
|
||||
Info,
|
||||
ShareInfo,
|
||||
Tabs,
|
||||
Tab,
|
||||
LoginForm,
|
||||
LoginSection,
|
||||
Info,
|
||||
ShareInfo,
|
||||
Tabs,
|
||||
Tab,
|
||||
} from './eventStyle';
|
||||
|
||||
import api from 'services';
|
||||
|
|
@ -47,94 +47,98 @@ const Event = (props) => {
|
|||
const weekStart = useSettingsStore(state => state.weekStart);
|
||||
|
||||
const addRecent = useRecentsStore(state => state.addRecent);
|
||||
const removeRecent = useRecentsStore(state => state.removeRecent);
|
||||
const locale = useLocaleUpdateStore(state => state.locale);
|
||||
|
||||
const { t } = useTranslation(['common', 'event']);
|
||||
|
||||
const { register, handleSubmit, setFocus, reset } = useForm();
|
||||
const { id } = props.match.params;
|
||||
const { register, handleSubmit, setFocus, reset } = useForm();
|
||||
const { id } = props.match.params;
|
||||
const { offline } = props;
|
||||
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
const [user, setUser] = useState(null);
|
||||
const [password, setPassword] = useState(null);
|
||||
const [tab, setTab] = useState(user ? 'you' : 'group');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [event, setEvent] = useState(null);
|
||||
const [people, setPeople] = useState([]);
|
||||
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
const [user, setUser] = useState(null);
|
||||
const [password, setPassword] = useState(null);
|
||||
const [tab, setTab] = useState(user ? 'you' : 'group');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [event, setEvent] = useState(null);
|
||||
const [people, setPeople] = useState([]);
|
||||
|
||||
const [times, setTimes] = useState([]);
|
||||
const [timeLabels, setTimeLabels] = useState([]);
|
||||
const [dates, setDates] = useState([]);
|
||||
const [min, setMin] = useState(0);
|
||||
const [max, setMax] = useState(0);
|
||||
const [times, setTimes] = useState([]);
|
||||
const [timeLabels, setTimeLabels] = useState([]);
|
||||
const [dates, setDates] = useState([]);
|
||||
const [min, setMin] = useState(0);
|
||||
const [max, setMax] = useState(0);
|
||||
|
||||
const [copied, setCopied] = useState(null);
|
||||
const [copied, setCopied] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEvent = async () => {
|
||||
try {
|
||||
const response = await api.get(`/event/${id}`);
|
||||
useEffect(() => {
|
||||
const fetchEvent = async () => {
|
||||
try {
|
||||
const response = await api.get(`/event/${id}`);
|
||||
|
||||
setEvent(response.data);
|
||||
setEvent(response.data);
|
||||
addRecent({
|
||||
id: response.data.id,
|
||||
created: response.data.created,
|
||||
name: response.data.name,
|
||||
});
|
||||
document.title = `${response.data.name} | Crab Fit`;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
document.title = `${response.data.name} | Crab Fit`;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e.status === 404) {
|
||||
removeRecent(id);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchEvent();
|
||||
}, [id, addRecent]);
|
||||
fetchEvent();
|
||||
}, [id, addRecent, removeRecent]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPeople = async () => {
|
||||
try {
|
||||
const response = await api.get(`/event/${id}/people`);
|
||||
const adjustedPeople = response.data.people.map(person => ({
|
||||
...person,
|
||||
availability: (!!person.availability.length && person.availability[0].length === 13)
|
||||
useEffect(() => {
|
||||
const fetchPeople = async () => {
|
||||
try {
|
||||
const response = await api.get(`/event/${id}/people`);
|
||||
const adjustedPeople = response.data.people.map(person => ({
|
||||
...person,
|
||||
availability: (!!person.availability.length && person.availability[0].length === 13)
|
||||
? person.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
|
||||
: person.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
|
||||
}));
|
||||
setPeople(adjustedPeople);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}));
|
||||
setPeople(adjustedPeople);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (tab === 'group') {
|
||||
fetchPeople();
|
||||
}
|
||||
}, [tab, id, timezone]);
|
||||
if (tab === 'group') {
|
||||
fetchPeople();
|
||||
}
|
||||
}, [tab, id, timezone]);
|
||||
|
||||
// Convert to timezone and expand minute segments
|
||||
useEffect(() => {
|
||||
if (event) {
|
||||
// Convert to timezone and expand minute segments
|
||||
useEffect(() => {
|
||||
if (event) {
|
||||
const isSpecificDates = event.times[0].length === 13;
|
||||
setTimes(event.times.reduce(
|
||||
(allTimes, time) => {
|
||||
setTimes(event.times.reduce(
|
||||
(allTimes, time) => {
|
||||
const date = isSpecificDates ?
|
||||
dayjs(time, 'HHmm-DDMMYYYY').utc(true).tz(timezone)
|
||||
: dayjs(time, 'HHmm').day(time.substring(5)).utc(true).tz(timezone);
|
||||
const format = isSpecificDates ? 'HHmm-DDMMYYYY' : 'HHmm-d';
|
||||
return [
|
||||
...allTimes,
|
||||
date.minute(0).format(format),
|
||||
date.minute(15).format(format),
|
||||
date.minute(30).format(format),
|
||||
date.minute(45).format(format),
|
||||
];
|
||||
},
|
||||
[]
|
||||
).sort((a, b) => {
|
||||
return [
|
||||
...allTimes,
|
||||
date.minute(0).format(format),
|
||||
date.minute(15).format(format),
|
||||
date.minute(30).format(format),
|
||||
date.minute(45).format(format),
|
||||
];
|
||||
},
|
||||
[]
|
||||
).sort((a, b) => {
|
||||
if (isSpecificDates) {
|
||||
return dayjs(a, 'HHmm-DDMMYYYY').diff(dayjs(b, 'HHmm-DDMMYYYY'));
|
||||
} else {
|
||||
|
|
@ -142,154 +146,154 @@ const Event = (props) => {
|
|||
.diff(dayjs(b, 'HHmm').day((parseInt(b.substring(5))-weekStart % 7 + 7) % 7));
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [event, timezone, weekStart]);
|
||||
}
|
||||
}, [event, timezone, weekStart]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!!times.length && !!people.length) {
|
||||
setMin(times.reduce((min, time) => {
|
||||
let total = people.reduce(
|
||||
(total, person) => person.availability.includes(time) ? total+1 : total,
|
||||
0
|
||||
);
|
||||
return total < min ? total : min;
|
||||
},
|
||||
Infinity
|
||||
));
|
||||
setMax(times.reduce((max, time) => {
|
||||
let total = people.reduce(
|
||||
(total, person) => person.availability.includes(time) ? total+1 : total,
|
||||
0
|
||||
);
|
||||
return total > max ? total : max;
|
||||
},
|
||||
-Infinity
|
||||
));
|
||||
}
|
||||
}, [times, people]);
|
||||
useEffect(() => {
|
||||
if (!!times.length && !!people.length) {
|
||||
setMin(times.reduce((min, time) => {
|
||||
let total = people.reduce(
|
||||
(total, person) => person.availability.includes(time) ? total+1 : total,
|
||||
0
|
||||
);
|
||||
return total < min ? total : min;
|
||||
},
|
||||
Infinity
|
||||
));
|
||||
setMax(times.reduce((max, time) => {
|
||||
let total = people.reduce(
|
||||
(total, person) => person.availability.includes(time) ? total+1 : total,
|
||||
0
|
||||
);
|
||||
return total > max ? total : max;
|
||||
},
|
||||
-Infinity
|
||||
));
|
||||
}
|
||||
}, [times, people]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!!times.length) {
|
||||
setTimeLabels(times.reduce((labels, datetime) => {
|
||||
const time = datetime.substring(0, 4);
|
||||
if (labels.includes(time)) return labels;
|
||||
return [...labels, time];
|
||||
}, [])
|
||||
.sort((a, b) => parseInt(a) - parseInt(b))
|
||||
.reduce((labels, time, i, allTimes) => {
|
||||
if (time.substring(2) === '30') return [...labels, { label: '', time }];
|
||||
if (allTimes.length - 1 === i) return [
|
||||
...labels,
|
||||
{ label: '', time },
|
||||
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: null }
|
||||
];
|
||||
if (allTimes.length - 1 > i && parseInt(allTimes[i+1].substring(0, 2))-1 > parseInt(time.substring(0, 2))) return [
|
||||
...labels,
|
||||
{ label: '', time },
|
||||
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: 'space' },
|
||||
{ label: '', time: 'space' },
|
||||
{ label: '', time: 'space' },
|
||||
];
|
||||
if (time.substring(2) !== '00') return [...labels, { label: '', time }];
|
||||
return [...labels, { label: dayjs(time, 'HHmm').format(timeFormat === '12h' ? 'h A' : 'HH'), time }];
|
||||
}, []));
|
||||
useEffect(() => {
|
||||
if (!!times.length) {
|
||||
setTimeLabels(times.reduce((labels, datetime) => {
|
||||
const time = datetime.substring(0, 4);
|
||||
if (labels.includes(time)) return labels;
|
||||
return [...labels, time];
|
||||
}, [])
|
||||
.sort((a, b) => parseInt(a) - parseInt(b))
|
||||
.reduce((labels, time, i, allTimes) => {
|
||||
if (time.substring(2) === '30') return [...labels, { label: '', time }];
|
||||
if (allTimes.length - 1 === i) return [
|
||||
...labels,
|
||||
{ label: '', time },
|
||||
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: null }
|
||||
];
|
||||
if (allTimes.length - 1 > i && parseInt(allTimes[i+1].substring(0, 2))-1 > parseInt(time.substring(0, 2))) return [
|
||||
...labels,
|
||||
{ label: '', time },
|
||||
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: 'space' },
|
||||
{ label: '', time: 'space' },
|
||||
{ label: '', time: 'space' },
|
||||
];
|
||||
if (time.substring(2) !== '00') return [...labels, { label: '', time }];
|
||||
return [...labels, { label: dayjs(time, 'HHmm').format(timeFormat === '12h' ? 'h A' : 'HH'), time }];
|
||||
}, []));
|
||||
|
||||
setDates(times.reduce((allDates, time) => {
|
||||
if (time.substring(2, 4) !== '00') return allDates;
|
||||
const date = time.substring(5);
|
||||
if (allDates.includes(date)) return allDates;
|
||||
return [...allDates, date];
|
||||
}, []));
|
||||
}
|
||||
}, [times, timeFormat, locale]);
|
||||
setDates(times.reduce((allDates, time) => {
|
||||
if (time.substring(2, 4) !== '00') return allDates;
|
||||
const date = time.substring(5);
|
||||
if (allDates.includes(date)) return allDates;
|
||||
return [...allDates, date];
|
||||
}, []));
|
||||
}
|
||||
}, [times, timeFormat, locale]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const response = await api.post(`/event/${id}/people/${user.name}`, { person: { password } });
|
||||
const adjustedUser = {
|
||||
...response.data,
|
||||
availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const response = await api.post(`/event/${id}/people/${user.name}`, { person: { password } });
|
||||
const adjustedUser = {
|
||||
...response.data,
|
||||
availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
|
||||
? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
|
||||
: response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
|
||||
};
|
||||
setUser(adjustedUser);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
};
|
||||
setUser(adjustedUser);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
if (user) {
|
||||
fetchUser();
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [timezone]);
|
||||
if (user) {
|
||||
fetchUser();
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [timezone]);
|
||||
|
||||
const onSubmit = async data => {
|
||||
const onSubmit = async data => {
|
||||
if (!data.name || data.name.length === 0) {
|
||||
setFocus('name');
|
||||
return setError(t('event:form.errors.name_required'));
|
||||
}
|
||||
|
||||
setIsLoginLoading(true);
|
||||
setError(null);
|
||||
setIsLoginLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await api.post(`/event/${id}/people/${data.name}`, {
|
||||
person: {
|
||||
password: data.password,
|
||||
},
|
||||
});
|
||||
setPassword(data.password);
|
||||
const adjustedUser = {
|
||||
...response.data,
|
||||
availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
|
||||
try {
|
||||
const response = await api.post(`/event/${id}/people/${data.name}`, {
|
||||
person: {
|
||||
password: data.password,
|
||||
},
|
||||
});
|
||||
setPassword(data.password);
|
||||
const adjustedUser = {
|
||||
...response.data,
|
||||
availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
|
||||
? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
|
||||
: response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
|
||||
};
|
||||
setUser(adjustedUser);
|
||||
setTab('you');
|
||||
} catch (e) {
|
||||
if (e.status === 401) {
|
||||
setError(t('event:form.errors.password_incorrect'));
|
||||
} else if (e.status === 404) {
|
||||
// Create user
|
||||
try {
|
||||
await api.post(`/event/${id}/people`, {
|
||||
person: {
|
||||
name: data.name,
|
||||
password: data.password,
|
||||
},
|
||||
});
|
||||
setPassword(data.password);
|
||||
setUser({
|
||||
name: data.name,
|
||||
availability: [],
|
||||
});
|
||||
setTab('you');
|
||||
} catch (e) {
|
||||
setError(t('event:form.errors.unknown'));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setIsLoginLoading(false);
|
||||
};
|
||||
setUser(adjustedUser);
|
||||
setTab('you');
|
||||
} catch (e) {
|
||||
if (e.status === 401) {
|
||||
setError(t('event:form.errors.password_incorrect'));
|
||||
} else if (e.status === 404) {
|
||||
// Create user
|
||||
try {
|
||||
await api.post(`/event/${id}/people`, {
|
||||
person: {
|
||||
name: data.name,
|
||||
password: data.password,
|
||||
},
|
||||
});
|
||||
setPassword(data.password);
|
||||
setUser({
|
||||
name: data.name,
|
||||
availability: [],
|
||||
});
|
||||
setTab('you');
|
||||
} catch (e) {
|
||||
setError(t('event:form.errors.unknown'));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setIsLoginLoading(false);
|
||||
gtag('event', 'login', {
|
||||
'event_category': 'event',
|
||||
});
|
||||
reset();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledMain>
|
||||
<Logo />
|
||||
return (
|
||||
<>
|
||||
<StyledMain>
|
||||
<Logo />
|
||||
|
||||
{(!!event || isLoading) ? (
|
||||
<>
|
||||
<EventName isLoading={isLoading}>{event?.name}</EventName>
|
||||
{(!!event || isLoading) ? (
|
||||
<>
|
||||
<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>
|
||||
<ShareInfo
|
||||
<ShareInfo
|
||||
onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${id}`)
|
||||
.then(() => {
|
||||
setCopied(t('event:nav.copied'));
|
||||
|
|
@ -302,81 +306,81 @@ const Event = (props) => {
|
|||
}
|
||||
title={!!navigator.clipboard ? t('event:nav.title') : ''}
|
||||
>{copied ?? `https://crab.fit/${id}`}</ShareInfo>
|
||||
<ShareInfo isLoading={isLoading}>
|
||||
{!!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>
|
||||
}
|
||||
</ShareInfo>
|
||||
</>
|
||||
) : (
|
||||
<ShareInfo isLoading={isLoading} className="instructions">
|
||||
{!!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>
|
||||
}
|
||||
</ShareInfo>
|
||||
</>
|
||||
) : (
|
||||
offline ? (
|
||||
<div style={{ margin: '100px 0' }}>
|
||||
<EventName>{t('event:offline.title')}</EventName>
|
||||
<ShareInfo><Trans i18nKey="event:offline.body" /></ShareInfo>
|
||||
</div>
|
||||
<EventName>{t('event:offline.title')}</EventName>
|
||||
<ShareInfo><Trans i18nKey="event:offline.body" /></ShareInfo>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ margin: '100px 0' }}>
|
||||
<EventName>{t('event:error.title')}</EventName>
|
||||
<ShareInfo>{t('event:error.body')}</ShareInfo>
|
||||
</div>
|
||||
<div style={{ margin: '100px 0' }}>
|
||||
<EventName>{t('event:error.title')}</EventName>
|
||||
<ShareInfo>{t('event:error.body')}</ShareInfo>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</StyledMain>
|
||||
)}
|
||||
</StyledMain>
|
||||
|
||||
{(!!event || isLoading) && (
|
||||
<>
|
||||
<LoginSection id="login">
|
||||
<StyledMain>
|
||||
{user ? (
|
||||
{(!!event || isLoading) && (
|
||||
<>
|
||||
<LoginSection id="login">
|
||||
<StyledMain>
|
||||
{user ? (
|
||||
<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={() => {
|
||||
setTab('group');
|
||||
setUser(null);
|
||||
setPassword(null);
|
||||
}}>{t('event:form.logout_button')}</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<h2>{t('event:form.signed_out')}</h2>
|
||||
<LoginForm onSubmit={handleSubmit(onSubmit)}>
|
||||
<TextField
|
||||
label={t('event:form.name')}
|
||||
type="text"
|
||||
id="name"
|
||||
inline
|
||||
required
|
||||
{...register('name')}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<h2>{t('event:form.signed_out')}</h2>
|
||||
<LoginForm onSubmit={handleSubmit(onSubmit)}>
|
||||
<TextField
|
||||
label={t('event:form.name')}
|
||||
type="text"
|
||||
id="name"
|
||||
inline
|
||||
required
|
||||
{...register('name')}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label={t('event:form.password')}
|
||||
type="password"
|
||||
id="password"
|
||||
inline
|
||||
{...register('password')}
|
||||
/>
|
||||
<TextField
|
||||
label={t('event:form.password')}
|
||||
type="password"
|
||||
id="password"
|
||||
inline
|
||||
{...register('password')}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isLoginLoading}
|
||||
disabled={isLoginLoading || isLoading}
|
||||
>{t('event:form.button')}</Button>
|
||||
</LoginForm>
|
||||
<Error open={!!error} onClose={() => setError(null)}>{error}</Error>
|
||||
<Info>{t('event:form.info')}</Info>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isLoginLoading}
|
||||
disabled={isLoginLoading || isLoading}
|
||||
>{t('event:form.button')}</Button>
|
||||
</LoginForm>
|
||||
<Error open={!!error} onClose={() => setError(null)}>{error}</Error>
|
||||
<Info>{t('event:form.info')}</Info>
|
||||
</>
|
||||
)}
|
||||
|
||||
<SelectField
|
||||
label={t('event:form.timezone')}
|
||||
name="timezone"
|
||||
id="timezone"
|
||||
inline
|
||||
value={timezone}
|
||||
onChange={event => setTimezone(event.currentTarget.value)}
|
||||
options={timezones}
|
||||
/>
|
||||
<SelectField
|
||||
label={t('event:form.timezone')}
|
||||
name="timezone"
|
||||
id="timezone"
|
||||
inline
|
||||
value={timezone}
|
||||
onChange={event => setTimezone(event.currentTarget.value)}
|
||||
options={timezones}
|
||||
/>
|
||||
{/* 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 => {
|
||||
e.preventDefault();
|
||||
|
|
@ -395,84 +399,84 @@ const Event = (props) => {
|
|||
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
}}>Click here</a> to use it.</Trans></p>
|
||||
)}
|
||||
</StyledMain>
|
||||
</LoginSection>
|
||||
</StyledMain>
|
||||
</LoginSection>
|
||||
|
||||
<StyledMain>
|
||||
<Tabs>
|
||||
<Tab
|
||||
href="#you"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
if (user) {
|
||||
setTab('you');
|
||||
} else {
|
||||
<StyledMain>
|
||||
<Tabs>
|
||||
<Tab
|
||||
href="#you"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
if (user) {
|
||||
setTab('you');
|
||||
} else {
|
||||
setFocus('name');
|
||||
}
|
||||
}}
|
||||
selected={tab === 'you'}
|
||||
disabled={!user}
|
||||
title={user ? '' : t('event:tabs.you_tooltip')}
|
||||
>{t('event:tabs.you')}</Tab>
|
||||
<Tab
|
||||
href="#group"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setTab('group');
|
||||
}}
|
||||
selected={tab === 'group'}
|
||||
>{t('event:tabs.group')}</Tab>
|
||||
</Tabs>
|
||||
</StyledMain>
|
||||
}}
|
||||
selected={tab === 'you'}
|
||||
disabled={!user}
|
||||
title={user ? '' : t('event:tabs.you_tooltip')}
|
||||
>{t('event:tabs.you')}</Tab>
|
||||
<Tab
|
||||
href="#group"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setTab('group');
|
||||
}}
|
||||
selected={tab === 'group'}
|
||||
>{t('event:tabs.group')}</Tab>
|
||||
</Tabs>
|
||||
</StyledMain>
|
||||
|
||||
{tab === 'group' ? (
|
||||
<section id="group">
|
||||
<AvailabilityViewer
|
||||
times={times}
|
||||
timeLabels={timeLabels}
|
||||
dates={dates}
|
||||
{tab === 'group' ? (
|
||||
<section id="group">
|
||||
<AvailabilityViewer
|
||||
times={times}
|
||||
timeLabels={timeLabels}
|
||||
dates={dates}
|
||||
isSpecificDates={!!dates.length && dates[0].length === 8}
|
||||
people={people.filter(p => p.availability.length > 0)}
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
</section>
|
||||
) : (
|
||||
<section id="you">
|
||||
<AvailabilityEditor
|
||||
times={times}
|
||||
timeLabels={timeLabels}
|
||||
dates={dates}
|
||||
timezone={timezone}
|
||||
people={people.filter(p => p.availability.length > 0)}
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
</section>
|
||||
) : (
|
||||
<section id="you">
|
||||
<AvailabilityEditor
|
||||
times={times}
|
||||
timeLabels={timeLabels}
|
||||
dates={dates}
|
||||
timezone={timezone}
|
||||
isSpecificDates={!!dates.length && dates[0].length === 8}
|
||||
value={user.availability}
|
||||
onChange={async availability => {
|
||||
const oldAvailability = [...user.availability];
|
||||
const utcAvailability = (!!availability.length && availability[0].length === 13)
|
||||
value={user.availability}
|
||||
onChange={async availability => {
|
||||
const oldAvailability = [...user.availability];
|
||||
const utcAvailability = (!!availability.length && availability[0].length === 13)
|
||||
? availability.map(date => dayjs.tz(date, 'HHmm-DDMMYYYY', timezone).utc().format('HHmm-DDMMYYYY'))
|
||||
: availability.map(date => dayjs.tz(date, 'HHmm', timezone).day(date.substring(5)).utc().format('HHmm-d'));
|
||||
setUser({ ...user, availability });
|
||||
try {
|
||||
await api.patch(`/event/${id}/people/${user.name}`, {
|
||||
person: {
|
||||
password,
|
||||
availability: utcAvailability,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setUser({ ...user, oldAvailability });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
setUser({ ...user, availability });
|
||||
try {
|
||||
await api.patch(`/event/${id}/people/${user.name}`, {
|
||||
person: {
|
||||
password,
|
||||
availability: utcAvailability,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setUser({ ...user, oldAvailability });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Event;
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const EventName = styled.h1`
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
margin: 20px 0 5px;
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
margin: 20px 0 5px;
|
||||
|
||||
${props => props.isLoading && `
|
||||
&:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
width: 400px;
|
||||
max-width: 100%;
|
||||
background-color: ${props.theme.loading};
|
||||
border-radius: 3px;
|
||||
}
|
||||
`}
|
||||
${props => props.isLoading && `
|
||||
&:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
width: 400px;
|
||||
max-width: 100%;
|
||||
background-color: ${props.theme.loading};
|
||||
border-radius: 3px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const EventDate = styled.span`
|
||||
|
|
@ -28,63 +28,73 @@ export const EventDate = styled.span`
|
|||
letter-spacing: .01em;
|
||||
|
||||
${props => props.isLoading && `
|
||||
&:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
width: 200px;
|
||||
max-width: 100%;
|
||||
background-color: ${props.theme.loading};
|
||||
border-radius: 3px;
|
||||
}
|
||||
`}
|
||||
&:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
width: 200px;
|
||||
max-width: 100%;
|
||||
background-color: ${props.theme.loading};
|
||||
border-radius: 3px;
|
||||
}
|
||||
`}
|
||||
|
||||
@media print {
|
||||
&::after {
|
||||
content: ' - ' attr(title);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const LoginForm = styled.form`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
align-items: flex-end;
|
||||
grid-gap: 18px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
align-items: flex-end;
|
||||
grid-gap: 18px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
@media (max-width: 400px) {
|
||||
grid-template-columns: 1fr;
|
||||
@media (max-width: 500px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
@media (max-width: 400px) {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
& div:last-child {
|
||||
--btn-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const LoginSection = styled.section`
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
padding: 10px 0;
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
padding: 10px 0;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Info = styled.p`
|
||||
margin: 18px 0;
|
||||
opacity: .6;
|
||||
font-size: 12px;
|
||||
margin: 18px 0;
|
||||
opacity: .6;
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
export const ShareInfo = styled.p`
|
||||
margin: 6px 0;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
margin: 6px 0;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
|
||||
${props => props.isLoading && `
|
||||
&:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
background-color: ${props.theme.loading};
|
||||
border-radius: 3px;
|
||||
}
|
||||
`}
|
||||
${props => props.isLoading && `
|
||||
&:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
background-color: ${props.theme.loading};
|
||||
border-radius: 3px;
|
||||
}
|
||||
`}
|
||||
|
||||
${props => props.onClick && `
|
||||
cursor: pointer;
|
||||
|
|
@ -93,36 +103,46 @@ export const ShareInfo = styled.p`
|
|||
color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
}
|
||||
`}
|
||||
|
||||
@media print {
|
||||
&.instructions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const Tabs = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 30px 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 30px 0 20px;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Tab = styled.a`
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
color: ${props => props.theme.text};
|
||||
padding: 8px 18px;
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
border-bottom: 0;
|
||||
margin: 0 4px;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
color: ${props => props.theme.text};
|
||||
padding: 8px 18px;
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
border-bottom: 0;
|
||||
margin: 0 4px;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
|
||||
${props => props.selected && `
|
||||
color: #FFF;
|
||||
background-color: ${props.theme.primary};
|
||||
border-color: ${props.theme.primary};
|
||||
`}
|
||||
${props => props.selected && `
|
||||
color: #FFF;
|
||||
background-color: ${props.theme.primary};
|
||||
border-color: ${props.theme.primary};
|
||||
`}
|
||||
|
||||
${props => props.disabled && `
|
||||
opacity: .5;
|
||||
cursor: not-allowed;
|
||||
`}
|
||||
${props => props.disabled && `
|
||||
opacity: .5;
|
||||
cursor: not-allowed;
|
||||
`}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -3,41 +3,42 @@ import { Link, useHistory } from 'react-router-dom';
|
|||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Footer,
|
||||
Button,
|
||||
Center,
|
||||
Footer,
|
||||
AvailabilityViewer,
|
||||
Logo,
|
||||
} from 'components';
|
||||
|
||||
import {
|
||||
StyledMain,
|
||||
AboutSection,
|
||||
P,
|
||||
AboutSection,
|
||||
P,
|
||||
} from '../Home/homeStyle';
|
||||
|
||||
import {
|
||||
Step,
|
||||
FakeCalendar,
|
||||
FakeTimeRange,
|
||||
ButtonArea,
|
||||
} from './helpStyle';
|
||||
|
||||
const Help = () => {
|
||||
const { push } = useHistory();
|
||||
const { t } = useTranslation(['common', 'help']);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t('help:name');
|
||||
}, [t]);
|
||||
useEffect(() => {
|
||||
document.title = t('help:name');
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledMain>
|
||||
return (
|
||||
<>
|
||||
<StyledMain>
|
||||
<Logo />
|
||||
</StyledMain>
|
||||
|
||||
<StyledMain>
|
||||
<h1>{t('help:name')}</h1>
|
||||
<h1>{t('help:name')}</h1>
|
||||
<P>{t('help:p1')}</P>
|
||||
<P>{t('help:p2')}</P>
|
||||
|
||||
|
|
@ -80,17 +81,19 @@ const Help = () => {
|
|||
min={0}
|
||||
max={5}
|
||||
/>
|
||||
</StyledMain>
|
||||
</StyledMain>
|
||||
|
||||
<AboutSection id="about">
|
||||
<StyledMain>
|
||||
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
<ButtonArea>
|
||||
<AboutSection id="about">
|
||||
<StyledMain>
|
||||
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
</ButtonArea>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Help;
|
||||
|
|
|
|||
|
|
@ -1,43 +1,43 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Step = styled.h2`
|
||||
text-decoration-color: ${props => props.theme.primary};
|
||||
text-decoration-color: ${props => props.theme.primary};
|
||||
text-decoration-style: solid;
|
||||
text-decoration-line: underline;
|
||||
margin-top: 30px;
|
||||
`;
|
||||
|
||||
export const FakeCalendar = styled.div`
|
||||
user-select: none;
|
||||
user-select: none;
|
||||
|
||||
& div {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-gap: 2px;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-gap: 2px;
|
||||
}
|
||||
& .days span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px 0;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
opacity: .7;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px 0;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
opacity: .7;
|
||||
@media (max-width: 350px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
& .dates span {
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 0;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 0;
|
||||
|
||||
&.selected {
|
||||
color: #FFF;
|
||||
background-color: ${props => props.theme.primary};
|
||||
background-color: ${props => props.theme.primary};
|
||||
}
|
||||
}
|
||||
& .dates span:first-of-type {
|
||||
|
|
@ -51,45 +51,45 @@ export const FakeCalendar = styled.div`
|
|||
`;
|
||||
|
||||
export const FakeTimeRange = styled.div`
|
||||
user-select: none;
|
||||
user-select: none;
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
border-radius: 3px;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
margin: 38px 6px 18px;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
border-radius: 3px;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
margin: 38px 6px 18px;
|
||||
|
||||
& div {
|
||||
height: calc(100% + 20px);
|
||||
width: 20px;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
background-color: ${props => props.theme.primaryLight};
|
||||
border-radius: 3px;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
width: 20px;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
background-color: ${props => props.theme.primaryLight};
|
||||
border-radius: 3px;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
|
||||
&:after {
|
||||
content: '|||';
|
||||
font-size: 8px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${props => props.theme.primaryDark};
|
||||
}
|
||||
&:after {
|
||||
content: '|||';
|
||||
font-size: 8px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${props => props.theme.primaryDark};
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: attr(data-label);
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
text-align: center;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
&:before {
|
||||
content: attr(data-label);
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
text-align: center;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
& .start {
|
||||
left: calc(${11 * 4.1666666666666666}% - 11px);
|
||||
|
|
@ -100,11 +100,17 @@ export const FakeTimeRange = styled.div`
|
|||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: ${11 * 4.1666666666666666}%;
|
||||
right: calc(100% - ${17 * 4.1666666666666666}%);
|
||||
top: 0;
|
||||
background-color: ${props => props.theme.primary};
|
||||
border-radius: 2px;
|
||||
height: 100%;
|
||||
left: ${11 * 4.1666666666666666}%;
|
||||
right: calc(100% - ${17 * 4.1666666666666666}%);
|
||||
top: 0;
|
||||
background-color: ${props => props.theme.primary};
|
||||
border-radius: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ButtonArea = styled.div`
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -9,36 +9,37 @@ import timezone from 'dayjs/plugin/timezone';
|
|||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
|
||||
import {
|
||||
TextField,
|
||||
CalendarField,
|
||||
TimeRangeField,
|
||||
SelectField,
|
||||
Button,
|
||||
Center,
|
||||
Error,
|
||||
TextField,
|
||||
CalendarField,
|
||||
TimeRangeField,
|
||||
SelectField,
|
||||
Button,
|
||||
Center,
|
||||
Error,
|
||||
Footer,
|
||||
Recents,
|
||||
} from 'components';
|
||||
|
||||
import {
|
||||
StyledMain,
|
||||
CreateForm,
|
||||
TitleSmall,
|
||||
TitleLarge,
|
||||
Logo,
|
||||
Links,
|
||||
AboutSection,
|
||||
P,
|
||||
Stats,
|
||||
Stat,
|
||||
StatNumber,
|
||||
StatLabel,
|
||||
StyledMain,
|
||||
CreateForm,
|
||||
TitleSmall,
|
||||
TitleLarge,
|
||||
Logo,
|
||||
Links,
|
||||
AboutSection,
|
||||
P,
|
||||
Stats,
|
||||
Stat,
|
||||
StatNumber,
|
||||
StatLabel,
|
||||
OfflineMessage,
|
||||
ButtonArea,
|
||||
} from './homeStyle';
|
||||
|
||||
import api from 'services';
|
||||
import { detect_browser } from 'utils';
|
||||
import { useTWAStore } from 'stores';
|
||||
|
||||
import logo from 'res/logo.svg';
|
||||
import timezones from 'res/timezones.json';
|
||||
|
|
@ -48,119 +49,120 @@ dayjs.extend(timezone);
|
|||
dayjs.extend(customParseFormat);
|
||||
|
||||
const Home = ({ offline }) => {
|
||||
const { register, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [stats, setStats] = useState({
|
||||
eventCount: null,
|
||||
personCount: null,
|
||||
version: 'loading...',
|
||||
});
|
||||
const { register, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [stats, setStats] = useState({
|
||||
eventCount: null,
|
||||
personCount: null,
|
||||
version: 'loading...',
|
||||
});
|
||||
const [browser, setBrowser] = useState(undefined);
|
||||
const { push } = useHistory();
|
||||
const { push } = useHistory();
|
||||
const { t } = useTranslation(['common', 'home']);
|
||||
const isTWA = useTWAStore(state => state.TWA);
|
||||
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
try {
|
||||
const response = await api.get('/stats');
|
||||
setStats(response.data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
try {
|
||||
const response = await api.get('/stats');
|
||||
setStats(response.data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
fetch();
|
||||
document.title = 'Crab Fit';
|
||||
fetch();
|
||||
document.title = 'Crab Fit';
|
||||
setBrowser(detect_browser());
|
||||
}, []);
|
||||
}, []);
|
||||
|
||||
const onSubmit = async data => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { start, end } = JSON.parse(data.times);
|
||||
const dates = JSON.parse(data.dates);
|
||||
const onSubmit = async data => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { start, end } = JSON.parse(data.times);
|
||||
const dates = JSON.parse(data.dates);
|
||||
|
||||
if (dates.length === 0) {
|
||||
return setError(t('home:form.errors.no_dates'));
|
||||
}
|
||||
if (dates.length === 0) {
|
||||
return setError(t('home:form.errors.no_dates'));
|
||||
}
|
||||
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
|
||||
if (start === end) {
|
||||
return setError(t('home:form.errors.same_times'));
|
||||
}
|
||||
if (start === end) {
|
||||
return setError(t('home:form.errors.same_times'));
|
||||
}
|
||||
|
||||
let times = dates.reduce((times, date) => {
|
||||
let day = [];
|
||||
for (let i = start; i < (start > end ? 24 : end); i++) {
|
||||
let times = dates.reduce((times, date) => {
|
||||
let day = [];
|
||||
for (let i = start; i < (start > end ? 24 : end); i++) {
|
||||
if (isSpecificDates) {
|
||||
day.push(
|
||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
);
|
||||
day.push(
|
||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
);
|
||||
} else {
|
||||
day.push(
|
||||
dayjs().tz(data.timezone)
|
||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||
);
|
||||
}
|
||||
}
|
||||
if (start > end) {
|
||||
for (let i = 0; i < end; i++) {
|
||||
}
|
||||
if (start > end) {
|
||||
for (let i = 0; i < end; i++) {
|
||||
if (isSpecificDates) {
|
||||
day.push(
|
||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
);
|
||||
day.push(
|
||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
);
|
||||
} else {
|
||||
day.push(
|
||||
dayjs().tz(data.timezone)
|
||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...times, ...day];
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
return [...times, ...day];
|
||||
}, []);
|
||||
|
||||
if (times.length === 0) {
|
||||
return setError(t('home:form.errors.no_time'));
|
||||
}
|
||||
if (times.length === 0) {
|
||||
return setError(t('home:form.errors.no_time'));
|
||||
}
|
||||
|
||||
const response = await api.post('/event', {
|
||||
event: {
|
||||
name: data.name,
|
||||
times: times,
|
||||
const response = await api.post('/event', {
|
||||
event: {
|
||||
name: data.name,
|
||||
times: times,
|
||||
timezone: data.timezone,
|
||||
},
|
||||
});
|
||||
push(`/${response.data.id}`);
|
||||
},
|
||||
});
|
||||
push(`/${response.data.id}`);
|
||||
gtag('event', 'create_event', {
|
||||
'event_category': 'home',
|
||||
});
|
||||
} catch (e) {
|
||||
setError(t('home:form.errors.unknown'));
|
||||
console.error(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
setError(t('home:form.errors.unknown'));
|
||||
console.error(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledMain>
|
||||
<Center>
|
||||
<Logo src={logo} alt="" />
|
||||
</Center>
|
||||
<TitleSmall altChars={/[A-Z]/g.test(t('home:create'))}>{t('home:create')}</TitleSmall>
|
||||
<TitleLarge>CRAB FIT</TitleLarge>
|
||||
<Links>
|
||||
<a href="#about">{t('home:nav.about')}</a> / <a href="#donate">{t('home:nav.donate')}</a>
|
||||
</Links>
|
||||
return (
|
||||
<>
|
||||
<StyledMain>
|
||||
<Center>
|
||||
<Logo src={logo} alt="" />
|
||||
</Center>
|
||||
<TitleSmall altChars={/[A-Z]/g.test(t('home:create'))}>{t('home:create')}</TitleSmall>
|
||||
<TitleLarge>CRAB FIT</TitleLarge>
|
||||
<Links>
|
||||
<a href="#about">{t('home:nav.about')}</a> / <a href="#donate">{t('home:nav.donate')}</a>
|
||||
</Links>
|
||||
</StyledMain>
|
||||
|
||||
<Recents />
|
||||
|
|
@ -172,107 +174,109 @@ const Home = ({ offline }) => {
|
|||
<P>{t('home:offline')}</P>
|
||||
</OfflineMessage>
|
||||
) : (
|
||||
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
|
||||
<TextField
|
||||
label={t('home:form.name.label')}
|
||||
subLabel={t('home:form.name.sublabel')}
|
||||
type="text"
|
||||
id="name"
|
||||
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
|
||||
<TextField
|
||||
label={t('home:form.name.label')}
|
||||
subLabel={t('home:form.name.sublabel')}
|
||||
type="text"
|
||||
id="name"
|
||||
{...register('name')}
|
||||
/>
|
||||
/>
|
||||
|
||||
<CalendarField
|
||||
label={t('home:form.dates.label')}
|
||||
subLabel={t('home:form.dates.sublabel')}
|
||||
id="dates"
|
||||
<CalendarField
|
||||
label={t('home:form.dates.label')}
|
||||
subLabel={t('home:form.dates.sublabel')}
|
||||
id="dates"
|
||||
required
|
||||
setValue={setValue}
|
||||
{...register('dates')}
|
||||
/>
|
||||
/>
|
||||
|
||||
<TimeRangeField
|
||||
label={t('home:form.times.label')}
|
||||
subLabel={t('home:form.times.sublabel')}
|
||||
id="times"
|
||||
<TimeRangeField
|
||||
label={t('home:form.times.label')}
|
||||
subLabel={t('home:form.times.sublabel')}
|
||||
id="times"
|
||||
required
|
||||
setValue={setValue}
|
||||
{...register('times')}
|
||||
/>
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
label={t('home:form.timezone.label')}
|
||||
id="timezone"
|
||||
options={timezones}
|
||||
<SelectField
|
||||
label={t('home:form.timezone.label')}
|
||||
id="timezone"
|
||||
options={timezones}
|
||||
required
|
||||
{...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>
|
||||
<Button type="submit" isLoading={isLoading} disabled={isLoading}>{t('home:form.button')}</Button>
|
||||
</Center>
|
||||
</CreateForm>
|
||||
<Center>
|
||||
<Button type="submit" isLoading={isLoading} disabled={isLoading}>{t('home:form.button')}</Button>
|
||||
</Center>
|
||||
</CreateForm>
|
||||
)}
|
||||
</StyledMain>
|
||||
</StyledMain>
|
||||
|
||||
<AboutSection id="about">
|
||||
<StyledMain>
|
||||
<h2>{t('home:about.name')}</h2>
|
||||
<Stats>
|
||||
<Stat>
|
||||
<StatNumber>{stats.eventCount ?? '350+'}</StatNumber>
|
||||
<StatLabel>{t('home:about.events')}</StatLabel>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatNumber>{stats.personCount ?? '550+'}</StatNumber>
|
||||
<StatLabel>{t('home:about.availabilities')}</StatLabel>
|
||||
</Stat>
|
||||
</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>
|
||||
<ButtonArea>
|
||||
{['chrome', 'firefox', 'safari'].includes(browser) && (
|
||||
<AboutSection id="about">
|
||||
<StyledMain>
|
||||
<h2>{t('home:about.name')}</h2>
|
||||
<Stats>
|
||||
<Stat>
|
||||
<StatNumber>{stats.eventCount ?? '350+'}</StatNumber>
|
||||
<StatLabel>{t('home:about.events')}</StatLabel>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatNumber>{stats.personCount ?? '550+'}</StatNumber>
|
||||
<StatLabel>{t('home:about.availabilities')}</StatLabel>
|
||||
</Stat>
|
||||
</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>
|
||||
{isTWA !== true && (
|
||||
<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
|
||||
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'})}
|
||||
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
|
||||
>{{
|
||||
chrome: t('home:about.chrome_extension'),
|
||||
firefox: t('home:about.firefox_extension'),
|
||||
safari: t('home:about.safari_extension'),
|
||||
}[browser]}</Button>
|
||||
)}
|
||||
<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>
|
||||
>{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.p5')}</P>
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const StyledMain = styled.div`
|
||||
width: 600px;
|
||||
margin: 20px auto;
|
||||
max-width: calc(100% - 60px);
|
||||
width: 600px;
|
||||
margin: 20px auto;
|
||||
max-width: calc(100% - 60px);
|
||||
`;
|
||||
|
||||
export const CreateForm = styled.form`
|
||||
|
|
@ -11,14 +11,14 @@ export const CreateForm = styled.form`
|
|||
`;
|
||||
|
||||
export const TitleSmall = styled.span`
|
||||
display: block;
|
||||
margin: 0;
|
||||
font-size: 3rem;
|
||||
text-align: center;
|
||||
font-family: 'Samurai Bob', sans-serif;
|
||||
font-weight: 400;
|
||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
line-height: 1em;
|
||||
display: block;
|
||||
margin: 0;
|
||||
font-size: 3rem;
|
||||
text-align: center;
|
||||
font-family: 'Samurai Bob', sans-serif;
|
||||
font-weight: 400;
|
||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
line-height: 1em;
|
||||
text-transform: uppercase;
|
||||
|
||||
${props => !props.altChars && `
|
||||
|
|
@ -30,23 +30,23 @@ export const TitleSmall = styled.span`
|
|||
`;
|
||||
|
||||
export const TitleLarge = styled.h1`
|
||||
margin: 0;
|
||||
font-size: 4rem;
|
||||
text-align: center;
|
||||
color: ${props => props.theme.primary};
|
||||
font-family: 'Molot', sans-serif;
|
||||
font-weight: 400;
|
||||
text-shadow: 0 4px 0 ${props => props.theme.primaryDark};
|
||||
line-height: 1em;
|
||||
margin: 0;
|
||||
font-size: 4rem;
|
||||
text-align: center;
|
||||
color: ${props => props.theme.primary};
|
||||
font-family: 'Molot', sans-serif;
|
||||
font-weight: 400;
|
||||
text-shadow: 0 4px 0 ${props => props.theme.primaryDark};
|
||||
line-height: 1em;
|
||||
text-transform: uppercase;
|
||||
|
||||
@media (max-width: 350px) {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
@media (max-width: 350px) {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Logo = styled.img`
|
||||
width: 80px;
|
||||
width: 80px;
|
||||
transition: transform .15s;
|
||||
animation: jelly .5s 1 .05s;
|
||||
user-select: none;
|
||||
|
|
@ -81,14 +81,14 @@ export const Logo = styled.img`
|
|||
`;
|
||||
|
||||
export const Links = styled.nav`
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
`;
|
||||
|
||||
export const AboutSection = styled.section`
|
||||
margin: 30px 0 0;
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
padding: 20px 0;
|
||||
margin: 30px 0 0;
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
padding: 20px 0;
|
||||
|
||||
& a {
|
||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
|
|
@ -96,42 +96,42 @@ export const AboutSection = styled.section`
|
|||
`;
|
||||
|
||||
export const P = styled.p`
|
||||
font-weight: 500;
|
||||
line-height: 1.6em;
|
||||
font-weight: 500;
|
||||
line-height: 1.6em;
|
||||
`;
|
||||
|
||||
export const Stats = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
export const Stat = styled.div`
|
||||
text-align: center;
|
||||
padding: 0 6px;
|
||||
min-width: 160px;
|
||||
margin: 10px 0;
|
||||
text-align: center;
|
||||
padding: 0 6px;
|
||||
min-width: 160px;
|
||||
margin: 10px 0;
|
||||
`;
|
||||
|
||||
export const StatNumber = styled.span`
|
||||
display: block;
|
||||
font-weight: 900;
|
||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
font-size: 2em;
|
||||
display: block;
|
||||
font-weight: 900;
|
||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
font-size: 2em;
|
||||
`;
|
||||
|
||||
export const StatLabel = styled.span`
|
||||
display: block;
|
||||
display: block;
|
||||
`;
|
||||
|
||||
export const OfflineMessage = styled.div`
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
margin: 50px 0 20px;
|
||||
`;
|
||||
|
||||
export const ButtonArea = styled.div`
|
||||
display: flex;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -3,18 +3,18 @@ import { useHistory } from 'react-router-dom';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Footer,
|
||||
Button,
|
||||
Center,
|
||||
Footer,
|
||||
Logo,
|
||||
} from 'components';
|
||||
|
||||
import {
|
||||
StyledMain,
|
||||
AboutSection,
|
||||
P,
|
||||
StyledMain,
|
||||
AboutSection,
|
||||
P,
|
||||
} from '../Home/homeStyle';
|
||||
import { Note } from './privacyStyle';
|
||||
import { Note, ButtonArea } from './privacyStyle';
|
||||
|
||||
const translationDisclaimer = 'While the translated document is provided for your convenience, the English version as displayed at https://crab.fit is legally binding.';
|
||||
|
||||
|
|
@ -24,20 +24,20 @@ const Privacy = () => {
|
|||
const contentRef = useRef();
|
||||
const [content, setContent] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${t('privacy:name')} - Crab Fit`;
|
||||
}, [t]);
|
||||
useEffect(() => {
|
||||
document.title = `${t('privacy:name')} - Crab Fit`;
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => setContent(contentRef.current?.innerText || ''), [contentRef]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledMain>
|
||||
return (
|
||||
<>
|
||||
<StyledMain>
|
||||
<Logo />
|
||||
</StyledMain>
|
||||
|
||||
<StyledMain>
|
||||
<h1>{t('privacy:name')}</h1>
|
||||
<h1>{t('privacy:name')}</h1>
|
||||
|
||||
{!i18n.language.startsWith('en') && (
|
||||
<p>
|
||||
|
|
@ -58,9 +58,9 @@ const Privacy = () => {
|
|||
<h2>Information Collection and Use</h2>
|
||||
<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>
|
||||
<ul>
|
||||
<P as="ul">
|
||||
<li><a href="https://www.google.com/policies/privacy/" target="blank">Google Play Services</a></li>
|
||||
</ul>
|
||||
</P>
|
||||
|
||||
<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>
|
||||
|
|
@ -71,12 +71,12 @@ const Privacy = () => {
|
|||
|
||||
<h2>Service Providers</h2>
|
||||
<P>Third-party companies may be employed for the following reasons:</P>
|
||||
<ul>
|
||||
<P as="ul">
|
||||
<li>To facilitate the Service</li>
|
||||
<li>To provide the Service on our behalf</li>
|
||||
<li>To perform Service-related services</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>
|
||||
|
||||
<h2>Security</h2>
|
||||
|
|
@ -98,15 +98,17 @@ const Privacy = () => {
|
|||
</div>
|
||||
</StyledMain>
|
||||
|
||||
<AboutSection id="about">
|
||||
<StyledMain>
|
||||
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
<ButtonArea>
|
||||
<AboutSection>
|
||||
<StyledMain>
|
||||
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
</ButtonArea>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Privacy;
|
||||
|
|
|
|||
|
|
@ -7,8 +7,16 @@ export const Note = styled.p`
|
|||
padding: 12px 16px;
|
||||
margin: 16px 0;
|
||||
box-sizing: border-box;
|
||||
font-weight: 500;
|
||||
line-height: 1.6em;
|
||||
|
||||
& a {
|
||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ButtonArea = styled.div`
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<?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">
|
||||
<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
|
||||
c101-300.4,380.6-517.7,710.1-517.7v0c233.9,0,391.7,101,481.7,185.5l351.6-343.3C1863.2,123.2,1582.2,0,1245.8,0
|
||||
C758.6,0,337.8,279.6,133,686.5h0h0C48.6,855.4,0.1,1045,0.1,1245.7S48.6,1636,133,1804.9h0l0,0
|
||||
c204.8,406.9,625.6,686.5,1112.8,686.5c336.3,0,618.7-110.7,824.9-301.7l0,0h0c235.3-217.3,370.9-537,370.9-916.3
|
||||
c0-102.4-8.3-177.2-26.3-254.7H1245.8z"/>
|
||||
c-329.4,0-609-217.3-708.7-517.7h0l0,0v0c-26.3-77.5-41.5-160.6-41.5-246.4c0-85.8,15.2-168.9,40.1-246.4v0
|
||||
c101-300.4,380.6-517.7,710.1-517.7v0c233.9,0,391.7,101,481.7,185.5l351.6-343.3C1863.2,123.2,1582.2,0,1245.8,0
|
||||
C758.6,0,337.8,279.6,133,686.5h0h0C48.6,855.4,0.1,1045,0.1,1245.7S48.6,1636,133,1804.9h0l0,0
|
||||
c204.8,406.9,625.6,686.5,1112.8,686.5c336.3,0,618.7-110.7,824.9-301.7l0,0h0c235.3-217.3,370.9-537,370.9-916.3
|
||||
c0-102.4-8.3-177.2-26.3-254.7H1245.8z"/>
|
||||
</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">
|
||||
<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>
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,45 +1,45 @@
|
|||
import axios from 'axios';
|
||||
|
||||
export const instance = axios.create({
|
||||
baseURL: process.env.NODE_ENV === 'production' ? 'https://api-dot-crabfit.uc.r.appspot.com' : 'http://localhost:8080',
|
||||
timeout: 1000 * 300,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
baseURL: process.env.NODE_ENV === 'production' ? 'https://api-dot-crabfit.uc.r.appspot.com' : 'http://localhost:8080',
|
||||
timeout: 1000 * 300,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const handleError = error => {
|
||||
if (error.response && error.response.status) {
|
||||
console.log('[Error handler] res:', error.response);
|
||||
}
|
||||
return Promise.reject(error.response);
|
||||
if (error.response && error.response.status) {
|
||||
console.log('[Error handler] res:', error.response);
|
||||
}
|
||||
return Promise.reject(error.response);
|
||||
};
|
||||
|
||||
const api = {
|
||||
get: async (endpoint, data) => {
|
||||
try {
|
||||
const response = await instance.get(endpoint, data);
|
||||
return Promise.resolve(response);
|
||||
} catch (error) {
|
||||
return handleError(error);
|
||||
}
|
||||
},
|
||||
post: async (endpoint, data, options = {}) => {
|
||||
try {
|
||||
const response = await instance.post(endpoint, data, options);
|
||||
return Promise.resolve(response);
|
||||
} catch (error) {
|
||||
return handleError(error);
|
||||
}
|
||||
},
|
||||
patch: async (endpoint, data) => {
|
||||
try {
|
||||
const response = await instance.patch(endpoint, data);
|
||||
return Promise.resolve(response);
|
||||
} catch (error) {
|
||||
return handleError(error);
|
||||
}
|
||||
},
|
||||
get: async (endpoint, data) => {
|
||||
try {
|
||||
const response = await instance.get(endpoint, data);
|
||||
return Promise.resolve(response);
|
||||
} catch (error) {
|
||||
return handleError(error);
|
||||
}
|
||||
},
|
||||
post: async (endpoint, data, options = {}) => {
|
||||
try {
|
||||
const response = await instance.post(endpoint, data, options);
|
||||
return Promise.resolve(response);
|
||||
} catch (error) {
|
||||
return handleError(error);
|
||||
}
|
||||
},
|
||||
patch: async (endpoint, data) => {
|
||||
try {
|
||||
const response = await instance.patch(endpoint, data);
|
||||
return Promise.resolve(response);
|
||||
} catch (error) {
|
||||
return handleError(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
const theme = {
|
||||
light: {
|
||||
mode: 'light',
|
||||
background: '#FFFFFF',
|
||||
text: '#000000',
|
||||
primary: '#F79E00',
|
||||
primaryDark: '#F48600',
|
||||
primaryLight: '#F4BB60',
|
||||
primaryBackground: '#FEF2DD',
|
||||
error: '#D32F2F',
|
||||
loading: '#DDDDDD',
|
||||
},
|
||||
dark: {
|
||||
mode: 'dark',
|
||||
background: '#111111',
|
||||
text: '#DDDDDD',
|
||||
primary: '#F79E00',
|
||||
primaryDark: '#CC7313',
|
||||
primaryLight: '#F4BB60',
|
||||
primaryBackground: '#30240F',
|
||||
error: '#E53935',
|
||||
loading: '#444444',
|
||||
},
|
||||
light: {
|
||||
mode: 'light',
|
||||
background: '#FFFFFF',
|
||||
text: '#000000',
|
||||
primary: '#F79E00',
|
||||
primaryDark: '#F48600',
|
||||
primaryLight: '#F4BB60',
|
||||
primaryBackground: '#FEF2DD',
|
||||
error: '#D32F2F',
|
||||
loading: '#DDDDDD',
|
||||
},
|
||||
dark: {
|
||||
mode: 'dark',
|
||||
background: '#111111',
|
||||
text: '#DDDDDD',
|
||||
primary: '#F79E00',
|
||||
primaryDark: '#CC7313',
|
||||
primaryLight: '#F4BB60',
|
||||
primaryBackground: '#30240F',
|
||||
error: '#E53935',
|
||||
loading: '#444444',
|
||||
},
|
||||
};
|
||||
|
||||
export default theme;
|
||||
|
|
|
|||