Tabs -> spaces

I have become my own worst enemy
This commit is contained in:
Ben Grant 2021-06-19 12:04:52 +10:00
parent e94559c4f6
commit fdb7f0ef67
49 changed files with 2424 additions and 2424 deletions

View file

@ -1,7 +1,7 @@
@font-face { @font-face {
font-family: Karla; font-family: Karla;
src: url('fonts/karla-variable.ttf') format('truetype'); src: url('fonts/karla-variable.ttf') format('truetype');
font-weight: 1 999; font-weight: 1 999;
} }
@font-face { @font-face {

View file

@ -5,10 +5,10 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.ico"> <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="theme-color" content="#F79E00"> <meta name="theme-color" content="#F79E00">
<meta <meta
name="keywords" name="keywords"
content="crab, fit, crabfit, schedule, availability, availabilities, when2meet, doodle, meet, plan, time, timezone" content="crab, fit, crabfit, schedule, availability, availabilities, when2meet, doodle, meet, plan, time, timezone"
> >
<meta <meta
name="description" name="description"
content="Enter your availability to find a time that works for everyone!" content="Enter your availability to find a time that works for everyone!"
@ -17,11 +17,11 @@
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png"> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"> <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<meta property="og:title" content="Crab Fit"> <meta property="og:title" content="Crab Fit">
<meta property="og:description" content="Enter your availability to find a time that works for everyone!"> <meta property="og:description" content="Enter your availability to find a time that works for everyone!">
<meta property="og:url" content="https://crab.fit"> <meta property="og:url" content="https://crab.fit">
<link rel="stylesheet" href="%PUBLIC_URL%/index.css"> <link rel="stylesheet" href="%PUBLIC_URL%/index.css">
<title>Crab Fit</title> <title>Crab Fit</title>

View file

@ -20,8 +20,8 @@ const wb = new Workbox('sw.js');
const App = () => { const App = () => {
const colortheme = useSettingsStore(state => state.theme); const colortheme = useSettingsStore(state => state.theme);
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)'); const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
const [isDark, setIsDark] = useState(darkQuery.matches); const [isDark, setIsDark] = useState(darkQuery.matches);
const [offline, setOffline] = useState(!window.navigator.onLine); const [offline, setOffline] = useState(!window.navigator.onLine);
const [eggCount, setEggCount] = useState(0); const [eggCount, setEggCount] = useState(0);
@ -46,7 +46,7 @@ const App = () => {
[eggCount, eggKey] [eggCount, eggKey]
); );
darkQuery.addListener(e => colortheme === 'System' && setIsDark(e.matches)); darkQuery.addListener(e => colortheme === 'System' && setIsDark(e.matches));
useEffect(() => { useEffect(() => {
const onOffline = () => setOffline(true); const onOffline = () => setOffline(true);
@ -87,56 +87,56 @@ const App = () => {
}, [colortheme, darkQuery.matches]); }, [colortheme, darkQuery.matches]);
return ( return (
<BrowserRouter> <BrowserRouter>
<ThemeProvider theme={theme[isDark ? 'dark' : 'light']}> <ThemeProvider theme={theme[isDark ? 'dark' : 'light']}>
<Global <Global
styles={theme => ({ styles={theme => ({
html: { html: {
scrollBehavior: 'smooth', scrollBehavior: 'smooth',
}, },
body: { body: {
backgroundColor: theme.background, backgroundColor: theme.background,
color: theme.text, color: theme.text,
fontFamily: `'Karla', sans-serif`, fontFamily: `'Karla', sans-serif`,
fontWeight: theme.mode === 'dark' ? 500 : 600, fontWeight: theme.mode === 'dark' ? 500 : 600,
margin: 0, margin: 0,
}, },
a: { a: {
color: theme.primary, color: theme.primary,
}, },
'*::-webkit-scrollbar': { '*::-webkit-scrollbar': {
width: 16, width: 16,
height: 16, height: 16,
}, },
'*::-webkit-scrollbar-track': { '*::-webkit-scrollbar-track': {
background: `${theme.primaryBackground}`, background: `${theme.primaryBackground}`,
}, },
'*::-webkit-scrollbar-thumb': { '*::-webkit-scrollbar-thumb': {
borderRadius: 100, borderRadius: 100,
border: `4px solid ${theme.primaryBackground}`, border: `4px solid ${theme.primaryBackground}`,
width: 12, width: 12,
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}AA`, background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}AA`,
}, },
'*::-webkit-scrollbar-thumb:hover': { '*::-webkit-scrollbar-thumb:hover': {
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}CC`, background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}CC`,
}, },
'*::-webkit-scrollbar-thumb:active': { '*::-webkit-scrollbar-thumb:active': {
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}`, background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}`,
}, },
})} })}
/> />
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
<Settings /> <Settings />
</Suspense> </Suspense>
<Switch> <Switch>
<Route path="/" exact render={props => ( <Route path="/" exact render={props => (
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
<Home offline={offline} {...props} /> <Home offline={offline} {...props} />
</Suspense> </Suspense>
)} /> )} />
<Route path="/how-to" exact render={props => ( <Route path="/how-to" exact render={props => (
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
<Help {...props} /> <Help {...props} />
</Suspense> </Suspense>
@ -146,17 +146,17 @@ const App = () => {
<Privacy {...props} /> <Privacy {...props} />
</Suspense> </Suspense>
)} /> )} />
<Route path="/create" exact render={props => ( <Route path="/create" exact render={props => (
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
<Create offline={offline} {...props} /> <Create offline={offline} {...props} />
</Suspense> </Suspense>
)} /> )} />
<Route path="/:id" exact render={props => ( <Route path="/:id" exact render={props => (
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
<Event offline={offline} {...props} /> <Event offline={offline} {...props} />
</Suspense> </Suspense>
)} /> )} />
</Switch> </Switch>
{updateAvailable && ( {updateAvailable && (
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
@ -165,8 +165,8 @@ const App = () => {
)} )}
{eggVisible && <Egg eggKey={eggKey} onClose={() => setEggVisible(false)} />} {eggVisible && <Egg eggKey={eggKey} onClose={() => setEggVisible(false)} />}
</ThemeProvider> </ThemeProvider>
</BrowserRouter> </BrowserRouter>
); );
} }

View file

@ -9,17 +9,17 @@ import dayjs_timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc'; import utc from 'dayjs/plugin/utc';
import { import {
Wrapper, Wrapper,
ScrollWrapper, ScrollWrapper,
Container, Container,
Date, Date,
Times, Times,
DateLabel, DateLabel,
DayLabel, DayLabel,
Spacer, Spacer,
TimeLabels, TimeLabels,
TimeLabel, TimeLabel,
TimeSpace, TimeSpace,
StyledMain, StyledMain,
} from 'components/AvailabilityViewer/availabilityViewerStyle'; } from 'components/AvailabilityViewer/availabilityViewerStyle';
import { Time } from './availabilityEditorStyle'; import { Time } from './availabilityEditorStyle';
@ -37,34 +37,34 @@ dayjs.extend(utc);
dayjs.extend(dayjs_timezone); dayjs.extend(dayjs_timezone);
const AvailabilityEditor = ({ const AvailabilityEditor = ({
times, times,
timeLabels, timeLabels,
dates, dates,
timezone, timezone,
isSpecificDates, isSpecificDates,
value = [], value = [],
onChange, onChange,
...props ...props
}) => { }) => {
const { t } = useTranslation('event'); const { t } = useTranslation('event');
const locale = useLocaleUpdateStore(state => state.locale); const locale = useLocaleUpdateStore(state => state.locale);
const [selectingTimes, _setSelectingTimes] = useState([]); const [selectingTimes, _setSelectingTimes] = useState([]);
const staticSelectingTimes = useRef([]); const staticSelectingTimes = useRef([]);
const setSelectingTimes = newTimes => { const setSelectingTimes = newTimes => {
staticSelectingTimes.current = newTimes; staticSelectingTimes.current = newTimes;
_setSelectingTimes(newTimes); _setSelectingTimes(newTimes);
}; };
const startPos = useRef({}); const startPos = useRef({});
const staticMode = useRef(null); const staticMode = useRef(null);
const [mode, _setMode] = useState(staticMode.current); const [mode, _setMode] = useState(staticMode.current);
const setMode = newMode => { const setMode = newMode => {
staticMode.current = newMode; staticMode.current = newMode;
_setMode(newMode); _setMode(newMode);
}; };
return ( return (
<> <>
<StyledMain> <StyledMain>
<Center style={{textAlign: 'center'}}>{t('event:you.info')}</Center> <Center style={{textAlign: 'center'}}>{t('event:you.info')}</Center>
@ -98,89 +98,89 @@ const AvailabilityEditor = ({
</StyledMain> </StyledMain>
)} )}
<Wrapper locale={locale}> <Wrapper locale={locale}>
<ScrollWrapper> <ScrollWrapper>
<Container> <Container>
<TimeLabels> <TimeLabels>
{!!timeLabels.length && timeLabels.map((label, i) => {!!timeLabels.length && timeLabels.map((label, i) =>
<TimeSpace key={i}> <TimeSpace key={i}>
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>} {label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
</TimeSpace> </TimeSpace>
)} )}
</TimeLabels> </TimeLabels>
{dates.map((date, x) => { {dates.map((date, x) => {
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date); const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
const last = dates.length === x+1 || (isSpecificDates ? dayjs(dates[x+1], 'DDMMYYYY') : dayjs().day(dates[x+1])).diff(parsedDate, 'day') > 1; const last = dates.length === x+1 || (isSpecificDates ? dayjs(dates[x+1], 'DDMMYYYY') : dayjs().day(dates[x+1])).diff(parsedDate, 'day') > 1;
return ( return (
<Fragment key={x}> <Fragment key={x}>
<Date> <Date>
{isSpecificDates && <DateLabel>{parsedDate.format('MMM D')}</DateLabel>} {isSpecificDates && <DateLabel>{parsedDate.format('MMM D')}</DateLabel>}
<DayLabel>{parsedDate.format('ddd')}</DayLabel> <DayLabel>{parsedDate.format('ddd')}</DayLabel>
<Times <Times
borderRight={last} borderRight={last}
borderLeft={x === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[x-1], 'DDMMYYYY') : dayjs().day(dates[x-1]), 'day') > 1} borderLeft={x === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[x-1], 'DDMMYYYY') : dayjs().day(dates[x-1]), 'day') > 1}
> >
{timeLabels.map((timeLabel, y) => { {timeLabels.map((timeLabel, y) => {
if (!timeLabel.time) return null; if (!timeLabel.time) return null;
if (!times.includes(`${timeLabel.time}-${date}`)) { if (!times.includes(`${timeLabel.time}-${date}`)) {
return ( return (
<TimeSpace key={x+y} className='timespace' title={t('event:greyed_times')} /> <TimeSpace key={x+y} className='timespace' title={t('event:greyed_times')} />
); );
} }
const time = `${timeLabel.time}-${date}`; const time = `${timeLabel.time}-${date}`;
return ( return (
<Time <Time
key={x+y} key={x+y}
time={time} time={time}
className="time" className="time"
selected={value.includes(time)} selected={value.includes(time)}
selecting={selectingTimes.includes(time)} selecting={selectingTimes.includes(time)}
mode={mode} mode={mode}
onPointerDown={(e) => { onPointerDown={(e) => {
e.preventDefault(); e.preventDefault();
startPos.current = {x, y}; startPos.current = {x, y};
setMode(value.includes(time) ? 'remove' : 'add'); setMode(value.includes(time) ? 'remove' : 'add');
setSelectingTimes([time]); setSelectingTimes([time]);
e.currentTarget.releasePointerCapture(e.pointerId); e.currentTarget.releasePointerCapture(e.pointerId);
document.addEventListener('pointerup', () => { document.addEventListener('pointerup', () => {
if (staticMode.current === 'add') { if (staticMode.current === 'add') {
onChange([...value, ...staticSelectingTimes.current]); onChange([...value, ...staticSelectingTimes.current]);
} else if (staticMode.current === 'remove') { } else if (staticMode.current === 'remove') {
onChange(value.filter(t => !staticSelectingTimes.current.includes(t))); onChange(value.filter(t => !staticSelectingTimes.current.includes(t)));
} }
setMode(null); setMode(null);
}, { once: true }); }, { once: true });
}} }}
onPointerEnter={() => { onPointerEnter={() => {
if (staticMode.current) { if (staticMode.current) {
let found = []; let found = [];
for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) { for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) { for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
found.push({y: cy, x: cx}); found.push({y: cy, x: cx});
} }
} }
setSelectingTimes(found.filter(d => timeLabels[d.y].time?.length === 4).map(d => `${timeLabels[d.y].time}-${dates[d.x]}`)); setSelectingTimes(found.filter(d => timeLabels[d.y].time?.length === 4).map(d => `${timeLabels[d.y].time}-${dates[d.x]}`));
} }
}} }}
/> />
); );
})} })}
</Times> </Times>
</Date> </Date>
{last && dates.length !== x+1 && ( {last && dates.length !== x+1 && (
<Spacer /> <Spacer />
)} )}
</Fragment> </Fragment>
); );
})} })}
</Container> </Container>
</ScrollWrapper> </ScrollWrapper>
</Wrapper> </Wrapper>
</> </>
); );
}; };
export default AvailabilityEditor; export default AvailabilityEditor;

View file

@ -1,24 +1,24 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const Time = styled.div` export const Time = styled.div`
height: 10px; height: 10px;
touch-action: none; touch-action: none;
transition: background-color .1s; transition: background-color .1s;
${props => props.time.slice(2, 4) === '00' && ` ${props => props.time.slice(2, 4) === '00' && `
border-top: 2px solid ${props.theme.text}; border-top: 2px solid ${props.theme.text};
`} `}
${props => props.time.slice(2, 4) !== '00' && ` ${props => props.time.slice(2, 4) !== '00' && `
border-top: 2px solid transparent; border-top: 2px solid transparent;
`} `}
${props => props.time.slice(2, 4) === '30' && ` ${props => props.time.slice(2, 4) === '30' && `
border-top: 2px dotted ${props.theme.text}; border-top: 2px dotted ${props.theme.text};
`} `}
${props => (props.selected || (props.mode === 'add' && props.selecting)) && ` ${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
background-color: ${props.theme.primary}; background-color: ${props.theme.primary};
`}; `};
${props => props.mode === 'remove' && props.selecting && ` ${props => props.mode === 'remove' && props.selecting && `
background-color: ${props.theme.background}; background-color: ${props.theme.background};
`}; `};
`; `;

View file

@ -9,23 +9,23 @@ import { useSettingsStore, useLocaleUpdateStore } from 'stores';
import { Legend, Center } from 'components'; import { Legend, Center } from 'components';
import { import {
Wrapper, Wrapper,
ScrollWrapper, ScrollWrapper,
Container, Container,
Date, Date,
Times, Times,
DateLabel, DateLabel,
DayLabel, DayLabel,
Time, Time,
Spacer, Spacer,
Tooltip, Tooltip,
TooltipTitle, TooltipTitle,
TooltipDate, TooltipDate,
TooltipContent, TooltipContent,
TooltipPerson, TooltipPerson,
TimeLabels, TimeLabels,
TimeLabel, TimeLabel,
TimeSpace, TimeSpace,
People, People,
Person, Person,
StyledMain, StyledMain,
@ -38,16 +38,16 @@ dayjs.extend(customParseFormat);
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
const AvailabilityViewer = ({ const AvailabilityViewer = ({
times, times,
timeLabels, timeLabels,
dates, dates,
isSpecificDates, isSpecificDates,
people = [], people = [],
min = 0, min = 0,
max = 0, max = 0,
...props ...props
}) => { }) => {
const [tooltip, setTooltip] = useState(null); const [tooltip, setTooltip] = useState(null);
const timeFormat = useSettingsStore(state => state.timeFormat); const timeFormat = useSettingsStore(state => state.timeFormat);
const highlight = useSettingsStore(state => state.highlight); const highlight = useSettingsStore(state => state.highlight);
const [filteredPeople, setFilteredPeople] = useState([]); const [filteredPeople, setFilteredPeople] = useState([]);
@ -153,7 +153,7 @@ const AvailabilityViewer = ({
times, times,
]); ]);
return ( return (
<> <>
<StyledMain> <StyledMain>
<Legend <Legend
@ -194,19 +194,19 @@ const AvailabilityViewer = ({
)} )}
</StyledMain> </StyledMain>
<Wrapper ref={wrapper}> <Wrapper ref={wrapper}>
<ScrollWrapper> <ScrollWrapper>
{heatmap} {heatmap}
{tooltip && ( {tooltip && (
<Tooltip <Tooltip
x={tooltip.x} x={tooltip.x}
y={tooltip.y} y={tooltip.y}
> >
<TooltipTitle>{tooltip.available}</TooltipTitle> <TooltipTitle>{tooltip.available}</TooltipTitle>
<TooltipDate>{tooltip.date}</TooltipDate> <TooltipDate>{tooltip.date}</TooltipDate>
{!!filteredPeople.length && ( {!!filteredPeople.length && (
<TooltipContent> <TooltipContent>
{tooltip.people.map(person => {tooltip.people.map(person =>
<TooltipPerson key={person}>{person}</TooltipPerson> <TooltipPerson key={person}>{person}</TooltipPerson>
)} )}
@ -215,12 +215,12 @@ const AvailabilityViewer = ({
)} )}
</TooltipContent> </TooltipContent>
)} )}
</Tooltip> </Tooltip>
)} )}
</ScrollWrapper> </ScrollWrapper>
</Wrapper> </Wrapper>
</> </>
); );
}; };
export default AvailabilityViewer; export default AvailabilityViewer;

View file

@ -1,8 +1,8 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const Wrapper = styled.div` export const Wrapper = styled.div`
overflow-y: visible; overflow-y: visible;
margin: 20px 0; margin: 20px 0;
position: relative; position: relative;
`; `;
@ -11,30 +11,30 @@ export const ScrollWrapper = styled.div`
`; `;
export const Container = styled.div` export const Container = styled.div`
display: inline-flex; display: inline-flex;
box-sizing: border-box; box-sizing: border-box;
min-width: 100%; min-width: 100%;
align-items: flex-end; align-items: flex-end;
justify-content: center; justify-content: center;
padding: 0 calc(calc(100% - 600px) / 2); padding: 0 calc(calc(100% - 600px) / 2);
@media (max-width: 660px) { @media (max-width: 660px) {
padding: 0 30px; padding: 0 30px;
} }
`; `;
export const Date = styled.div` export const Date = styled.div`
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 60px; width: 60px;
min-width: 60px; min-width: 60px;
margin-bottom: 10px; margin-bottom: 10px;
`; `;
export const Times = styled.div` export const Times = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-bottom: 2px solid ${props => props.theme.text}; border-bottom: 2px solid ${props => props.theme.text};
border-left: 1px solid ${props => props.theme.text}; border-left: 1px solid ${props => props.theme.text};
@ -57,44 +57,44 @@ export const Times = styled.div`
`; `;
export const DateLabel = styled.label` export const DateLabel = styled.label`
display: block; display: block;
font-size: 12px; font-size: 12px;
text-align: center; text-align: center;
user-select: none; user-select: none;
`; `;
export const DayLabel = styled.label` export const DayLabel = styled.label`
display: block; display: block;
font-size: 15px; font-size: 15px;
text-align: center; text-align: center;
user-select: none; user-select: none;
`; `;
export const Time = styled.div` export const Time = styled.div`
height: 10px; height: 10px;
background-origin: border-box; background-origin: border-box;
transition: background-color .1s; transition: background-color .1s;
${props => props.time.slice(2, 4) === '00' && ` ${props => props.time.slice(2, 4) === '00' && `
border-top: 2px solid ${props.theme.text}; border-top: 2px solid ${props.theme.text};
`} `}
${props => props.time.slice(2, 4) !== '00' && ` ${props => props.time.slice(2, 4) !== '00' && `
border-top: 2px solid transparent; border-top: 2px solid transparent;
`} `}
${props => props.time.slice(2, 4) === '30' && ` ${props => props.time.slice(2, 4) === '30' && `
border-top: 2px dotted ${props.theme.text}; border-top: 2px dotted ${props.theme.text};
`} `}
background-color: ${props => `${props.theme.primary}${Math.round((props.peopleCount/props.maxPeople)*255).toString(16)}`}; background-color: ${props => `${props.theme.primary}${Math.round((props.peopleCount/props.maxPeople)*255).toString(16)}`};
${props => props.highlight && props.peopleCount === props.maxPeople && props.peopleCount > 0 && ` ${props => props.highlight && props.peopleCount === props.maxPeople && props.peopleCount > 0 && `
background-image: repeating-linear-gradient( background-image: repeating-linear-gradient(
45deg, 45deg,
transparent, transparent,
transparent 4.3px, transparent 4.3px,
${props.theme.primaryDark} 4.3px, ${props.theme.primaryDark} 4.3px,
${props.theme.primaryDark} 8.6px ${props.theme.primaryDark} 8.6px
); );
`} `}
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
@ -103,40 +103,40 @@ export const Time = styled.div`
`; `;
export const Spacer = styled.div` export const Spacer = styled.div`
width: 12px; width: 12px;
flex-shrink: 0; flex-shrink: 0;
`; `;
export const Tooltip = styled.div` export const Tooltip = styled.div`
position: absolute; position: absolute;
top: ${props => props.y}px; top: ${props => props.y}px;
left: ${props => props.x}px; left: ${props => props.x}px;
transform: translateX(-50%); transform: translateX(-50%);
border: 1px solid ${props => props.theme.text}; border: 1px solid ${props => props.theme.text};
border-radius: 3px; border-radius: 3px;
padding: 4px 8px; padding: 4px 8px;
background-color: ${props => props.theme.background}${props => props.theme.mode === 'light' ? 'EE' : 'DD'}; background-color: ${props => props.theme.background}${props => props.theme.mode === 'light' ? 'EE' : 'DD'};
max-width: 200px; max-width: 200px;
pointer-events: none; pointer-events: none;
z-index: 100; z-index: 100;
user-select: none; user-select: none;
`; `;
export const TooltipTitle = styled.span` export const TooltipTitle = styled.span`
font-size: 15px; font-size: 15px;
display: block; display: block;
font-weight: 700; font-weight: 700;
`; `;
export const TooltipDate = styled.span` export const TooltipDate = styled.span`
font-size: 13px; font-size: 13px;
display: block; display: block;
opacity: .8; opacity: .8;
font-weight: 600; font-weight: 600;
`; `;
export const TooltipContent = styled.div` export const TooltipContent = styled.div`
font-size: 13px; font-size: 13px;
padding: 4px 0; padding: 4px 0;
`; `;
@ -154,38 +154,38 @@ export const TooltipPerson = styled.span`
`; `;
export const TimeLabels = styled.div` export const TimeLabels = styled.div`
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 40px; width: 40px;
padding-right: 6px; padding-right: 6px;
`; `;
export const TimeSpace = styled.div` export const TimeSpace = styled.div`
height: 10px; height: 10px;
position: relative; position: relative;
border-top: 2px solid transparent; border-top: 2px solid transparent;
&.timespace { &.timespace {
background-origin: border-box; background-origin: border-box;
background-image: repeating-linear-gradient( background-image: repeating-linear-gradient(
45deg, 45deg,
transparent, transparent,
transparent 4.3px, transparent 4.3px,
${props => props.theme.loading} 4.3px, ${props => props.theme.loading} 4.3px,
${props => props.theme.loading} 8.6px ${props => props.theme.loading} 8.6px
); );
} }
`; `;
export const TimeLabel = styled.label` export const TimeLabel = styled.label`
display: block; display: block;
position: absolute; position: absolute;
top: -.7em; top: -.7em;
font-size: 12px; font-size: 12px;
text-align: right; text-align: right;
user-select: none; user-select: none;
width: 100%; width: 100%;
`; `;
export const StyledMain = styled.div` export const StyledMain = styled.div`

View file

@ -1,7 +1,7 @@
import { Pressable } from './buttonStyle'; import { Pressable } from './buttonStyle';
const Button = ({ href, type = 'button', icon, children, ...props }) => ( const Button = ({ href, type = 'button', icon, children, ...props }) => (
<Pressable <Pressable
type={type} type={type}
as={href ? 'a' : 'button'} as={href ? 'a' : 'button'}
href={href} href={href}

View file

@ -60,34 +60,34 @@ export const Pressable = styled.button`
} }
${props => props.isLoading && ` ${props => props.isLoading && `
color: transparent; color: transparent;
cursor: wait; cursor: wait;
& img { & img {
opacity: 0; opacity: 0;
} }
@keyframes load { @keyframes load {
from { from {
transform: rotate(0deg); transform: rotate(0deg);
} }
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
&:after { &:after {
content: ''; content: '';
position: absolute; position: absolute;
top: calc(50% - 12px); top: calc(50% - 12px);
left: calc(50% - 12px); left: calc(50% - 12px);
height: 18px; height: 18px;
width: 18px; width: 18px;
border: 3px solid ${props.primaryColor ? '#FFF' : props.theme.background}; border: 3px solid ${props.primaryColor ? '#FFF' : props.theme.background};
border-left-color: transparent; border-left-color: transparent;
border-radius: 100px; border-radius: 100px;
animation: load .5s linear infinite; animation: load .5s linear infinite;
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
&:after { &:after {
@ -106,7 +106,7 @@ export const Pressable = styled.button`
justify-content: center; justify-content: center;
} }
} }
`} `}
${props => props.secondary && ` ${props => props.secondary && `
background: transparent; background: transparent;

View file

@ -9,14 +9,14 @@ import { Button, ToggleField } from 'components';
import { useSettingsStore, useLocaleUpdateStore } from 'stores'; import { useSettingsStore, useLocaleUpdateStore } from 'stores';
import { import {
Wrapper, Wrapper,
StyledLabel, StyledLabel,
StyledSubLabel, StyledSubLabel,
CalendarHeader, CalendarHeader,
CalendarDays, CalendarDays,
CalendarBody, CalendarBody,
Date, Date,
Day, Day,
} from './calendarFieldStyle'; } from './calendarFieldStyle';
dayjs.extend(isToday); dayjs.extend(isToday);
@ -24,90 +24,90 @@ dayjs.extend(localeData);
dayjs.extend(updateLocale); dayjs.extend(updateLocale);
const calculateMonth = (month, year, weekStart) => { const calculateMonth = (month, year, weekStart) => {
const date = dayjs().month(month).year(year); const date = dayjs().month(month).year(year);
const daysInMonth = date.daysInMonth(); const daysInMonth = date.daysInMonth();
const daysBefore = date.date(1).day() - weekStart; const daysBefore = date.date(1).day() - weekStart;
const daysAfter = 6 - date.date(daysInMonth).day() + weekStart; const daysAfter = 6 - date.date(daysInMonth).day() + weekStart;
let dates = []; let dates = [];
let curDate = date.date(1).subtract(daysBefore, 'day'); let curDate = date.date(1).subtract(daysBefore, 'day');
let y = 0; let y = 0;
let x = 0; let x = 0;
for (let i = 0; i < daysBefore + daysInMonth + daysAfter; i++) { for (let i = 0; i < daysBefore + daysInMonth + daysAfter; i++) {
if (x === 0) dates[y] = []; if (x === 0) dates[y] = [];
dates[y][x] = curDate.clone(); dates[y][x] = curDate.clone();
curDate = curDate.add(1, 'day'); curDate = curDate.add(1, 'day');
x++; x++;
if (x > 6) { if (x > 6) {
x = 0; x = 0;
y++; y++;
} }
} }
return dates; return dates;
}; };
const CalendarField = forwardRef(({ const CalendarField = forwardRef(({
label, label,
subLabel, subLabel,
id, id,
setValue, setValue,
...props ...props
}, ref) => { }, ref) => {
const weekStart = useSettingsStore(state => state.weekStart); const weekStart = useSettingsStore(state => state.weekStart);
const locale = useLocaleUpdateStore(state => state.locale); const locale = useLocaleUpdateStore(state => state.locale);
const { t } = useTranslation('home'); const { t } = useTranslation('home');
const [type, setType] = useState(0); const [type, setType] = useState(0);
const [dates, setDates] = useState(calculateMonth(dayjs().month(), dayjs().year(), weekStart)); const [dates, setDates] = useState(calculateMonth(dayjs().month(), dayjs().year(), weekStart));
const [month, setMonth] = useState(dayjs().month()); const [month, setMonth] = useState(dayjs().month());
const [year, setYear] = useState(dayjs().year()); const [year, setYear] = useState(dayjs().year());
const [selectedDates, setSelectedDates] = useState([]); const [selectedDates, setSelectedDates] = useState([]);
const [selectingDates, _setSelectingDates] = useState([]); const [selectingDates, _setSelectingDates] = useState([]);
const staticSelectingDates = useRef([]); const staticSelectingDates = useRef([]);
const setSelectingDates = newDates => { const setSelectingDates = newDates => {
staticSelectingDates.current = newDates; staticSelectingDates.current = newDates;
_setSelectingDates(newDates); _setSelectingDates(newDates);
}; };
const [selectedDays, setSelectedDays] = useState([]); const [selectedDays, setSelectedDays] = useState([]);
const [selectingDays, _setSelectingDays] = useState([]); const [selectingDays, _setSelectingDays] = useState([]);
const staticSelectingDays = useRef([]); const staticSelectingDays = useRef([]);
const setSelectingDays = newDays => { const setSelectingDays = newDays => {
staticSelectingDays.current = newDays; staticSelectingDays.current = newDays;
_setSelectingDays(newDays); _setSelectingDays(newDays);
}; };
const startPos = useRef({}); const startPos = useRef({});
const staticMode = useRef(null); const staticMode = useRef(null);
const [mode, _setMode] = useState(staticMode.current); const [mode, _setMode] = useState(staticMode.current);
const setMode = newMode => { const setMode = newMode => {
staticMode.current = newMode; staticMode.current = newMode;
_setMode(newMode); _setMode(newMode);
}; };
useEffect(() => setValue(props.name, type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)), [type, selectedDays, selectedDates, setValue, props.name]); useEffect(() => setValue(props.name, type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)), [type, selectedDays, selectedDates, setValue, props.name]);
useEffect(() => { useEffect(() => {
if (dayjs.Ls.hasOwnProperty(locale) && weekStart !== dayjs.Ls[locale].weekStart) { if (dayjs.Ls.hasOwnProperty(locale) && weekStart !== dayjs.Ls[locale].weekStart) {
dayjs.updateLocale(locale, { weekStart }); dayjs.updateLocale(locale, { weekStart });
} }
setDates(calculateMonth(month, year, weekStart)); setDates(calculateMonth(month, year, weekStart));
}, [weekStart, month, year, locale]); }, [weekStart, month, year, locale]);
return ( return (
<Wrapper locale={locale}> <Wrapper locale={locale}>
{label && <StyledLabel htmlFor={id}>{label}</StyledLabel>} {label && <StyledLabel htmlFor={id}>{label}</StyledLabel>}
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>} {subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
<input <input
id={id} id={id}
type="hidden" type="hidden"
ref={ref} ref={ref}
value={type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)} value={type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)}
{...props} {...props}
/> />
<ToggleField <ToggleField
id="calendarMode" id="calendarMode"
@ -122,50 +122,50 @@ const CalendarField = forwardRef(({
{type === 0 ? ( {type === 0 ? (
<> <>
<CalendarHeader> <CalendarHeader>
<Button <Button
size="30px" size="30px"
title={t('form.dates.tooltips.previous')} title={t('form.dates.tooltips.previous')}
onClick={() => { onClick={() => {
if (month-1 < 0) { if (month-1 < 0) {
setYear(year-1); setYear(year-1);
setMonth(11); setMonth(11);
} else { } else {
setMonth(month-1); setMonth(month-1);
} }
}} }}
>&lt;</Button> >&lt;</Button>
<span>{dayjs.months()[month]} {year}</span> <span>{dayjs.months()[month]} {year}</span>
<Button <Button
size="30px" size="30px"
title={t('form.dates.tooltips.next')} title={t('form.dates.tooltips.next')}
onClick={() => { onClick={() => {
if (month+1 > 11) { if (month+1 > 11) {
setYear(year+1); setYear(year+1);
setMonth(0); setMonth(0);
} else { } else {
setMonth(month+1); setMonth(month+1);
} }
}} }}
>&gt;</Button> >&gt;</Button>
</CalendarHeader> </CalendarHeader>
<CalendarDays> <CalendarDays>
{(weekStart ? [...dayjs.weekdaysShort().filter((_,i) => i !== 0), dayjs.weekdaysShort()[0]] : dayjs.weekdaysShort()).map(name => {(weekStart ? [...dayjs.weekdaysShort().filter((_,i) => i !== 0), dayjs.weekdaysShort()[0]] : dayjs.weekdaysShort()).map(name =>
<Day key={name}>{name}</Day> <Day key={name}>{name}</Day>
)} )}
</CalendarDays> </CalendarDays>
<CalendarBody> <CalendarBody>
{dates.length > 0 && dates.map((dateRow, y) => {dates.length > 0 && dates.map((dateRow, y) =>
dateRow.map((date, x) => dateRow.map((date, x) =>
<Date <Date
key={y+x} key={y+x}
otherMonth={date.month() !== month} otherMonth={date.month() !== month}
isToday={date.isToday()} isToday={date.isToday()}
title={`${date.date()} ${dayjs.months()[date.month()]}${date.isToday() ? ` (${t('form.dates.tooltips.today')})` : ''}`} title={`${date.date()} ${dayjs.months()[date.month()]}${date.isToday() ? ` (${t('form.dates.tooltips.today')})` : ''}`}
selected={selectedDates.includes(date.format('DDMMYYYY'))} selected={selectedDates.includes(date.format('DDMMYYYY'))}
selecting={selectingDates.includes(date)} selecting={selectingDates.includes(date)}
mode={mode} mode={mode}
type="button" type="button"
onKeyPress={e => { onKeyPress={e => {
if (e.key === ' ' || e.key === 'Enter') { if (e.key === ' ' || e.key === 'Enter') {
@ -176,37 +176,37 @@ const CalendarField = forwardRef(({
} }
} }
}} }}
onPointerDown={e => { onPointerDown={e => {
startPos.current = {x, y}; startPos.current = {x, y};
setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add'); setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
setSelectingDates([date]); setSelectingDates([date]);
e.currentTarget.releasePointerCapture(e.pointerId); e.currentTarget.releasePointerCapture(e.pointerId);
document.addEventListener('pointerup', () => { document.addEventListener('pointerup', () => {
if (staticMode.current === 'add') { if (staticMode.current === 'add') {
setSelectedDates([...selectedDates, ...staticSelectingDates.current.map(d => d.format('DDMMYYYY'))]); setSelectedDates([...selectedDates, ...staticSelectingDates.current.map(d => d.format('DDMMYYYY'))]);
} else if (staticMode.current === 'remove') { } else if (staticMode.current === 'remove') {
const toRemove = staticSelectingDates.current.map(d => d.format('DDMMYYYY')); const toRemove = staticSelectingDates.current.map(d => d.format('DDMMYYYY'));
setSelectedDates(selectedDates.filter(d => !toRemove.includes(d))); setSelectedDates(selectedDates.filter(d => !toRemove.includes(d)));
} }
setMode(null); setMode(null);
}, { once: true }); }, { once: true });
}} }}
onPointerEnter={() => { onPointerEnter={() => {
if (staticMode.current) { if (staticMode.current) {
let found = []; let found = [];
for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) { for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) { for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
found.push({y: cy, x: cx}); found.push({y: cy, x: cx});
} }
} }
setSelectingDates(found.map(d => dates[d.y][d.x])); setSelectingDates(found.map(d => dates[d.y][d.x]));
} }
}} }}
>{date.date()}</Date> >{date.date()}</Date>
) )
)} )}
</CalendarBody> </CalendarBody>
</> </>
) : ( ) : (
<CalendarBody> <CalendarBody>
@ -257,8 +257,8 @@ const CalendarField = forwardRef(({
)} )}
</CalendarBody> </CalendarBody>
)} )}
</Wrapper> </Wrapper>
); );
}); });
export default CalendarField; export default CalendarField;

View file

@ -1,68 +1,68 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const Wrapper = styled.div` export const Wrapper = styled.div`
margin: 30px 0; margin: 30px 0;
`; `;
export const StyledLabel = styled.label` export const StyledLabel = styled.label`
display: block; display: block;
padding-bottom: 4px; padding-bottom: 4px;
font-size: 18px; font-size: 18px;
`; `;
export const StyledSubLabel = styled.label` export const StyledSubLabel = styled.label`
display: block; display: block;
font-size: 13px; font-size: 13px;
opacity: .6; opacity: .6;
`; `;
export const CalendarHeader = styled.div` export const CalendarHeader = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
user-select: none; user-select: none;
padding: 6px 0; padding: 6px 0;
font-size: 1.2em; font-size: 1.2em;
font-weight: bold; font-weight: bold;
`; `;
export const CalendarDays = styled.div` export const CalendarDays = styled.div`
display: grid; display: grid;
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(7, 1fr);
grid-gap: 2px; grid-gap: 2px;
`; `;
export const Day = styled.div` export const Day = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 3px 0; padding: 3px 0;
font-weight: bold; font-weight: bold;
user-select: none; user-select: none;
opacity: .7; opacity: .7;
@media (max-width: 350px) { @media (max-width: 350px) {
font-size: 12px; font-size: 12px;
} }
`; `;
export const CalendarBody = styled.div` export const CalendarBody = styled.div`
display: grid; display: grid;
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(7, 1fr);
grid-gap: 2px; grid-gap: 2px;
& button:first-of-type { & button:first-of-type {
border-top-left-radius: 3px; border-top-left-radius: 3px;
} }
& button:nth-of-type(7) { & button:nth-of-type(7) {
border-top-right-radius: 3px; border-top-right-radius: 3px;
} }
& button:nth-last-of-type(7) { & button:nth-last-of-type(7) {
border-bottom-left-radius: 3px; border-bottom-left-radius: 3px;
} }
& button:last-of-type { & button:last-of-type {
border-bottom-right-radius: 3px; border-bottom-right-radius: 3px;
} }
`; `;
export const Date = styled.button` export const Date = styled.button`
@ -77,28 +77,28 @@ export const Date = styled.button`
transition: none; transition: none;
} }
background-color: ${props => props.theme.primaryBackground}; background-color: ${props => props.theme.primaryBackground};
border: 1px solid ${props => props.theme.primary}; border: 1px solid ${props => props.theme.primary};
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 10px 0; padding: 10px 0;
user-select: none; user-select: none;
touch-action: none; touch-action: none;
${props => props.otherMonth && ` ${props => props.otherMonth && `
color: ${props.theme.mode === 'light' ? props.theme.primaryLight : props.theme.primaryDark}; color: ${props.theme.mode === 'light' ? props.theme.primaryLight : props.theme.primaryDark};
`} `}
${props => props.isToday && ` ${props => props.isToday && `
font-weight: 900; font-weight: 900;
color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight}; color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
`} `}
${props => (props.selected || (props.mode === 'add' && props.selecting)) && ` ${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'}; color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};
background-color: ${props.theme.primary}; background-color: ${props.theme.primary};
`} `}
${props => props.mode === 'remove' && props.selecting && ` ${props => props.mode === 'remove' && props.selecting && `
background-color: ${props.theme.primaryBackground}; background-color: ${props.theme.primaryBackground};
color: ${props.isToday ? props.theme.primaryDark : (props.otherMonth ? props.theme.primaryLight : 'inherit')}; color: ${props.isToday ? props.theme.primaryDark : (props.otherMonth ? props.theme.primaryLight : 'inherit')};
`} `}
`; `;

View file

@ -1,9 +1,9 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
const Center = styled.div` const Center = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
`; `;
export default Center; export default Center;

View file

@ -96,9 +96,9 @@ const Donate = () => {
}; };
return ( return (
<Wrapper> <Wrapper>
<Button <Button
small small
title={t('donate.title')} title={t('donate.title')}
onClick={event => { onClick={event => {
if (closed) { if (closed) {
@ -125,7 +125,7 @@ const Donate = () => {
role="button" role="button"
aria-expanded={isOpen ? 'true' : 'false'} aria-expanded={isOpen ? 'true' : 'false'}
style={{ whiteSpace: 'nowrap' }} style={{ whiteSpace: 'nowrap' }}
>{t('donate.button')}</Button> >{t('donate.button')}</Button>
<Options <Options
isOpen={isOpen} isOpen={isOpen}
@ -144,7 +144,7 @@ const Donate = () => {
<a onClick={linkPressed} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_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&currency_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&currency_code=AUD" target="_blank" rel="noreferrer noopener payment">{t('donate.options.choose')}</a> <a onClick={linkPressed} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD" target="_blank" rel="noreferrer noopener payment">{t('donate.options.choose')}</a>
</Options> </Options>
</Wrapper> </Wrapper>
); );
} }

View file

@ -7,7 +7,7 @@ export const Wrapper = styled.div`
`; `;
export const Options = styled.div` export const Options = styled.div`
position: absolute; position: absolute;
bottom: calc(100% + 20px); bottom: calc(100% + 20px);
right: 0; right: 0;
background-color: ${props => props.theme.background}; background-color: ${props => props.theme.background};

View file

@ -7,14 +7,14 @@ const Egg = ({ eggKey, onClose }) => {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
return ( return (
<Wrapper title="Click anywhere to close" onClick={() => onClose()}> <Wrapper title="Click anywhere to close" onClick={() => onClose()}>
<Image <Image
src={`https://us-central1-flour-app-services.cloudfunctions.net/charliAPI?v=${eggKey}`} src={`https://us-central1-flour-app-services.cloudfunctions.net/charliAPI?v=${eggKey}`}
onLoadStart={() => setIsLoading(true)} onLoadStart={() => setIsLoading(true)}
onLoad={() => setIsLoading(false)} onLoad={() => setIsLoading(false)}
/> />
{isLoading && <Loading />} {isLoading && <Loading />}
</Wrapper> </Wrapper>
); );
} }

View file

@ -1,7 +1,7 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const Wrapper = styled.div` export const Wrapper = styled.div`
position: fixed; position: fixed;
background: rgba(0,0,0,.6); background: rgba(0,0,0,.6);
top: 0; top: 0;
left: 0; left: 0;
@ -17,7 +17,7 @@ export const Wrapper = styled.div`
`; `;
export const Image = styled.img` export const Image = styled.img`
max-width: 80%; max-width: 80%;
max-height: 80%; max-height: 80%;
position: absolute; position: absolute;
`; `;

View file

@ -1,17 +1,17 @@
import { Wrapper, CloseButton } from './errorStyle'; import { Wrapper, CloseButton } from './errorStyle';
const Error = ({ const Error = ({
children, children,
onClose, onClose,
open = true, open = true,
...props ...props
}) => ( }) => (
<Wrapper role="alert" open={open} {...props}> <Wrapper role="alert" open={open} {...props}>
{children} {children}
<CloseButton type="button" onClick={onClose} title="Close error"> <CloseButton type="button" onClick={onClose} title="Close error">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</CloseButton> </CloseButton>
</Wrapper> </Wrapper>
); );
export default Error; export default Error;

View file

@ -1,14 +1,14 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const Wrapper = styled.div` export const Wrapper = styled.div`
border-radius: 3px; border-radius: 3px;
background-color: ${props => props.theme.error}; background-color: ${props => props.theme.error};
color: #FFFFFF; color: #FFFFFF;
padding: 0 16px; padding: 0 16px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
font-size: 18px; font-size: 18px;
opacity: 0; opacity: 0;
max-height: 0; max-height: 0;
margin: 0; margin: 0;
@ -30,14 +30,14 @@ export const Wrapper = styled.div`
`; `;
export const CloseButton = styled.button` export const CloseButton = styled.button`
border: 0; border: 0;
background: none; background: none;
height: 30px; height: 30px;
width: 30px; width: 30px;
cursor: pointer; cursor: pointer;
color: inherit; color: inherit;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-left: 16px; margin-left: 16px;
`; `;

View file

@ -1,12 +1,12 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const Wrapper = styled.footer` export const Wrapper = styled.footer`
width: 600px; width: 600px;
margin: 20px auto; margin: 20px auto;
max-width: calc(100% - 60px); max-width: calc(100% - 60px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
${props => props.small && ` ${props => props.small && `
margin: 60px auto 0; margin: 60px auto 0;

View file

@ -93,11 +93,11 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
} }
}, [signedIn]); }, [signedIn]);
return ( return (
<> <>
{!signedIn ? ( {!signedIn ? (
<Center> <Center>
<Button <Button
onClick={() => signIn()} onClick={() => signIn()}
isLoading={signedIn === undefined} isLoading={signedIn === undefined}
primaryColor="#4286F5" primaryColor="#4286F5"
@ -161,7 +161,7 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
</CalendarList> </CalendarList>
)} )}
</> </>
); );
}; };
export default GoogleCalendar; export default GoogleCalendar;

View file

@ -3,46 +3,46 @@ import { useSettingsStore } from 'stores';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Wrapper, Wrapper,
Label, Label,
Bar, Bar,
Grade, Grade,
} from './legendStyle'; } from './legendStyle';
const Legend = ({ const Legend = ({
min, min,
max, max,
total, total,
onSegmentFocus, onSegmentFocus,
...props ...props
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation('event'); const { t } = useTranslation('event');
const highlight = useSettingsStore(state => state.highlight); const highlight = useSettingsStore(state => state.highlight);
const setHighlight = useSettingsStore(state => state.setHighlight); const setHighlight = useSettingsStore(state => state.setHighlight);
return ( return (
<Wrapper> <Wrapper>
<Label>{min}/{total} {t('event:available')}</Label> <Label>{min}/{total} {t('event:available')}</Label>
<Bar <Bar
onMouseOut={() => onSegmentFocus(null)} onMouseOut={() => onSegmentFocus(null)}
onClick={() => setHighlight(!highlight)} onClick={() => setHighlight(!highlight)}
title={t('event:group.legend_tooltip')} title={t('event:group.legend_tooltip')}
> >
{[...Array(max+1-min).keys()].map(i => i+min).map(i => {[...Array(max+1-min).keys()].map(i => i+min).map(i =>
<Grade <Grade
key={i} key={i}
color={`${theme.primary}${Math.round((i/(max))*255).toString(16)}`} color={`${theme.primary}${Math.round((i/(max))*255).toString(16)}`}
highlight={highlight && i === max && max > 0} highlight={highlight && i === max && max > 0}
onMouseOver={() => onSegmentFocus(i)} onMouseOver={() => onSegmentFocus(i)}
/> />
)} )}
</Bar> </Bar>
<Label>{max}/{total} {t('event:available')}</Label> <Label>{max}/{total} {t('event:available')}</Label>
</Wrapper> </Wrapper>
); );
}; };
export default Legend; export default Legend;

View file

@ -1,52 +1,52 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const Wrapper = styled.div` export const Wrapper = styled.div`
margin: 10px 0; margin: 10px 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
& label:last-of-type { & label:last-of-type {
text-align: right; text-align: right;
} }
@media (max-width: 400px) { @media (max-width: 400px) {
display: block; display: block;
} }
`; `;
export const Label = styled.label` export const Label = styled.label`
display: block; display: block;
font-size: 14px; font-size: 14px;
text-align: left; text-align: left;
`; `;
export const Bar = styled.div` export const Bar = styled.div`
display: flex; display: flex;
width: 40%; width: 40%;
height: 20px; height: 20px;
border-radius: 3px; border-radius: 3px;
overflow: hidden; overflow: hidden;
margin: 0 8px; margin: 0 8px;
border: 1px solid ${props => props.theme.text}; border: 1px solid ${props => props.theme.text};
@media (max-width: 400px) { @media (max-width: 400px) {
width: 100%; width: 100%;
margin: 8px 0; margin: 8px 0;
} }
`; `;
export const Grade = styled.div` export const Grade = styled.div`
flex: 1; flex: 1;
background-color: ${props => props.color}; background-color: ${props => props.color};
${props => props.highlight && ` ${props => props.highlight && `
background-image: repeating-linear-gradient( background-image: repeating-linear-gradient(
45deg, 45deg,
${props.theme.primary}, ${props.theme.primary},
${props.theme.primary} 4.5px, ${props.theme.primary} 4.5px,
${props.theme.primaryDark} 4.5px, ${props.theme.primaryDark} 4.5px,
${props.theme.primaryDark} 9px ${props.theme.primaryDark} 9px
); );
`} `}
`; `;

View file

@ -7,7 +7,7 @@ export const Wrapper = styled.div`
`; `;
export const A = styled.a` export const A = styled.a`
text-decoration: none; text-decoration: none;
@keyframes jelly { @keyframes jelly {
from,to { from,to {
@ -35,28 +35,28 @@ export const A = styled.a`
`; `;
export const Top = styled.div` export const Top = styled.div`
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
`; `;
export const Image = styled.img` export const Image = styled.img`
width: 2.5rem; width: 2.5rem;
margin-right: 16px; margin-right: 16px;
`; `;
export const Title = styled.span` export const Title = styled.span`
display: block; display: block;
font-size: 2rem; font-size: 2rem;
color: ${props => props.theme.primary}; color: ${props => props.theme.primary};
font-family: 'Molot', sans-serif; font-family: 'Molot', sans-serif;
font-weight: 400; font-weight: 400;
text-shadow: 0 2px 0 ${props => props.theme.primaryDark}; text-shadow: 0 2px 0 ${props => props.theme.primaryDark};
line-height: 1em; line-height: 1em;
`; `;
export const Tagline = styled.span` export const Tagline = styled.span`
text-decoration: underline; text-decoration: underline;
font-size: 14px; font-size: 14px;
padding-top: 2px; padding-top: 2px;
display: flex; display: flex;

View file

@ -160,11 +160,11 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
// eslint-disable-next-line // eslint-disable-next-line
}, [client]); }, [client]);
return ( return (
<> <>
{!client ? ( {!client ? (
<Center> <Center>
<Button <Button
onClick={() => signIn()} onClick={() => signIn()}
isLoading={client === undefined} isLoading={client === undefined}
primaryColor="#0364B9" primaryColor="#0364B9"
@ -228,7 +228,7 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
</CalendarList> </CalendarList>
)} )}
</> </>
); );
}; };
export default OutlookCalendar; export default OutlookCalendar;

View file

@ -1,7 +1,7 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const Recent = styled.a` export const Recent = styled.a`
text-decoration: none; text-decoration: none;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;

View file

@ -1,43 +1,43 @@
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { import {
Wrapper, Wrapper,
StyledLabel, StyledLabel,
StyledSubLabel, StyledSubLabel,
StyledSelect, StyledSelect,
} from './selectFieldStyle'; } from './selectFieldStyle';
const SelectField = forwardRef(({ const SelectField = forwardRef(({
label, label,
subLabel, subLabel,
id, id,
options = [], options = [],
inline = false, inline = false,
small = false, small = false,
defaultOption, defaultOption,
...props ...props
}, ref) => ( }, ref) => (
<Wrapper inline={inline} small={small}> <Wrapper inline={inline} small={small}>
{label && <StyledLabel htmlFor={id} inline={inline} small={small}>{label}</StyledLabel>} {label && <StyledLabel htmlFor={id} inline={inline} small={small}>{label}</StyledLabel>}
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>} {subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
<StyledSelect <StyledSelect
id={id} id={id}
small={small} small={small}
ref={ref} ref={ref}
{...props} {...props}
> >
{defaultOption && <option value="">{defaultOption}</option>} {defaultOption && <option value="">{defaultOption}</option>}
{Array.isArray(options) ? ( {Array.isArray(options) ? (
options.map(value => options.map(value =>
<option key={value} value={value}>{value}</option> <option key={value} value={value}>{value}</option>
) )
) : ( ) : (
Object.entries(options).map(([key, value]) => Object.entries(options).map(([key, value]) =>
<option key={key} value={key}>{value}</option> <option key={key} value={key}>{value}</option>
) )
)} )}
</StyledSelect> </StyledSelect>
</Wrapper> </Wrapper>
)); ));
export default SelectField; export default SelectField;

View file

@ -1,60 +1,60 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const Wrapper = styled.div` export const Wrapper = styled.div`
margin: 30px 0; margin: 30px 0;
${props => props.inline && ` ${props => props.inline && `
margin: 0; margin: 0;
`} `}
${props => props.small && ` ${props => props.small && `
margin: 10px 0; margin: 10px 0;
`} `}
`; `;
export const StyledLabel = styled.label` export const StyledLabel = styled.label`
display: block; display: block;
padding-bottom: 4px; padding-bottom: 4px;
font-size: 18px; font-size: 18px;
${props => props.inline && ` ${props => props.inline && `
font-size: 16px; font-size: 16px;
`} `}
${props => props.small && ` ${props => props.small && `
font-size: .9rem; font-size: .9rem;
`} `}
`; `;
export const StyledSubLabel = styled.label` export const StyledSubLabel = styled.label`
display: block; display: block;
padding-bottom: 6px; padding-bottom: 6px;
font-size: 13px; font-size: 13px;
opacity: .6; opacity: .6;
`; `;
export const StyledSelect = styled.select` export const StyledSelect = styled.select`
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
font: inherit; font: inherit;
background: ${props => props.theme.primaryBackground}; background: ${props => props.theme.primaryBackground};
color: inherit; color: inherit;
padding: 10px 14px; padding: 10px 14px;
border: 1px solid ${props => props.theme.primary}; border: 1px solid ${props => props.theme.primary};
box-shadow: inset 0 0 0 0 ${props => props.theme.primary}; box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
border-radius: 3px; border-radius: 3px;
outline: none; outline: none;
transition: border-color .15s, box-shadow .15s; transition: border-color .15s, box-shadow .15s;
appearance: none; appearance: none;
background-image: url("data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><foreignObject width=%22100px%22 height=%22100px%22><div xmlns=%22http://www.w3.org/1999/xhtml%22 style=%22color:${props => encodeURIComponent(props.theme.primary)};font-size:60px;display:flex;align-items:center;justify-content:center;height:100%25;width:100%25%22>▼</div></foreignObject></svg>"); background-image: url("data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><foreignObject width=%22100px%22 height=%22100px%22><div xmlns=%22http://www.w3.org/1999/xhtml%22 style=%22color:${props => encodeURIComponent(props.theme.primary)};font-size:60px;display:flex;align-items:center;justify-content:center;height:100%25;width:100%25%22>▼</div></foreignObject></svg>");
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right 10px center; background-position: right 10px center;
background-size: 1em; background-size: 1em;
&:focus { &:focus {
border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight}; border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight}; box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
} }
${props => props.small && ` ${props => props.small && `
padding: 6px 8px; padding: 6px 8px;
`} `}
`; `;

View file

@ -1,15 +1,15 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const OpenButton = styled.button` export const OpenButton = styled.button`
border: 0; border: 0;
background: none; background: none;
height: 50px; height: 50px;
width: 50px; width: 50px;
cursor: pointer; cursor: pointer;
color: inherit; color: inherit;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: absolute; position: absolute;
top: 12px; top: 12px;
right: 12px; right: 12px;
@ -36,7 +36,7 @@ export const OpenButton = styled.button`
`; `;
export const Cover = styled.div` export const Cover = styled.div`
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
@ -50,7 +50,7 @@ export const Cover = styled.div`
`; `;
export const Modal = styled.div` export const Modal = styled.div`
position: absolute; position: absolute;
top: 70px; top: 70px;
right: 12px; right: 12px;
background-color: ${props => props.theme.background}; background-color: ${props => props.theme.background};
@ -84,7 +84,7 @@ export const Modal = styled.div`
`; `;
export const Heading = styled.span` export const Heading = styled.span`
font-size: 1.5rem; font-size: 1.5rem;
display: block; display: block;
margin: 6px 0; margin: 6px 0;
line-height: 1em; line-height: 1em;

View file

@ -1,23 +1,23 @@
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { import {
Wrapper, Wrapper,
StyledLabel, StyledLabel,
StyledSubLabel, StyledSubLabel,
StyledInput, StyledInput,
} from './textFieldStyle'; } from './textFieldStyle';
const TextField = forwardRef(({ const TextField = forwardRef(({
label, label,
subLabel, subLabel,
id, id,
inline = false, inline = false,
...props ...props
}, ref) => ( }, ref) => (
<Wrapper inline={inline}> <Wrapper inline={inline}>
{label && <StyledLabel htmlFor={id} inline={inline}>{label}</StyledLabel>} {label && <StyledLabel htmlFor={id} inline={inline}>{label}</StyledLabel>}
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>} {subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
<StyledInput id={id} ref={ref} {...props} /> <StyledInput id={id} ref={ref} {...props} />
</Wrapper> </Wrapper>
)); ));
export default TextField; export default TextField;

View file

@ -1,46 +1,46 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const Wrapper = styled.div` export const Wrapper = styled.div`
margin: 30px 0; margin: 30px 0;
${props => props.inline && ` ${props => props.inline && `
margin: 0; margin: 0;
`} `}
`; `;
export const StyledLabel = styled.label` export const StyledLabel = styled.label`
display: block; display: block;
padding-bottom: 4px; padding-bottom: 4px;
font-size: 18px; font-size: 18px;
${props => props.inline && ` ${props => props.inline && `
font-size: 16px; font-size: 16px;
`} `}
`; `;
export const StyledSubLabel = styled.label` export const StyledSubLabel = styled.label`
display: block; display: block;
padding-bottom: 6px; padding-bottom: 6px;
font-size: 13px; font-size: 13px;
opacity: .6; opacity: .6;
`; `;
export const StyledInput = styled.input` export const StyledInput = styled.input`
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
font: inherit; font: inherit;
background: ${props => props.theme.primaryBackground}; background: ${props => props.theme.primaryBackground};
color: inherit; color: inherit;
padding: 10px 14px; padding: 10px 14px;
border: 1px solid ${props => props.theme.primary}; border: 1px solid ${props => props.theme.primary};
box-shadow: inset 0 0 0 0 ${props => props.theme.primary}; box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
border-radius: 3px; border-radius: 3px;
font-size: 18px; font-size: 18px;
outline: none; outline: none;
transition: border-color .15s, box-shadow .15s; transition: border-color .15s, box-shadow .15s;
&:focus { &:focus {
border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight}; border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight}; box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
} }
`; `;

View file

@ -4,94 +4,94 @@ import dayjs from 'dayjs';
import { useSettingsStore, useLocaleUpdateStore } from 'stores'; import { useSettingsStore, useLocaleUpdateStore } from 'stores';
import { import {
Wrapper, Wrapper,
StyledLabel, StyledLabel,
StyledSubLabel, StyledSubLabel,
Range, Range,
Handle, Handle,
Selected, Selected,
} from './timeRangeFieldStyle'; } from './timeRangeFieldStyle';
const times = ['00','01','02','03','04','05','06','07','08','09','10','11','12','13','14','15','16','17','18','19','20','21','22','23','24']; const times = ['00','01','02','03','04','05','06','07','08','09','10','11','12','13','14','15','16','17','18','19','20','21','22','23','24'];
const TimeRangeField = forwardRef(({ const TimeRangeField = forwardRef(({
label, label,
subLabel, subLabel,
id, id,
setValue, setValue,
...props ...props
}, ref) => { }, ref) => {
const timeFormat = useSettingsStore(state => state.timeFormat); const timeFormat = useSettingsStore(state => state.timeFormat);
const locale = useLocaleUpdateStore(state => state.locale); const locale = useLocaleUpdateStore(state => state.locale);
const [start, setStart] = useState(9); const [start, setStart] = useState(9);
const [end, setEnd] = useState(17); const [end, setEnd] = useState(17);
const isStartMoving = useRef(false); const isStartMoving = useRef(false);
const isEndMoving = useRef(false); const isEndMoving = useRef(false);
const rangeRef = useRef(); const rangeRef = useRef();
const rangeRect = useRef(); const rangeRect = useRef();
useEffect(() => { useEffect(() => {
if (rangeRef.current) { if (rangeRef.current) {
rangeRect.current = rangeRef.current.getBoundingClientRect(); rangeRect.current = rangeRef.current.getBoundingClientRect();
} }
}, [rangeRef]); }, [rangeRef]);
useEffect(() => setValue(props.name, JSON.stringify({start, end})), [start, end, setValue, props.name]); useEffect(() => setValue(props.name, JSON.stringify({start, end})), [start, end, setValue, props.name]);
const handleMouseMove = e => { const handleMouseMove = e => {
if (isStartMoving.current || isEndMoving.current) { if (isStartMoving.current || isEndMoving.current) {
let step = Math.round(((e.pageX - rangeRect.current.left) / rangeRect.current.width) * 24); let step = Math.round(((e.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
if (step < 0) step = 0; if (step < 0) step = 0;
if (step > 24) step = 24; if (step > 24) step = 24;
step = Math.abs(step); step = Math.abs(step);
if (isStartMoving.current) { if (isStartMoving.current) {
setStart(step); setStart(step);
} else if (isEndMoving.current) { } else if (isEndMoving.current) {
setEnd(step); setEnd(step);
} }
} }
}; };
return ( return (
<Wrapper locale={locale}> <Wrapper locale={locale}>
{label && <StyledLabel htmlFor={id}>{label}</StyledLabel>} {label && <StyledLabel htmlFor={id}>{label}</StyledLabel>}
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>} {subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
<input <input
id={id} id={id}
type="hidden" type="hidden"
value={JSON.stringify({start, end})} value={JSON.stringify({start, end})}
ref={ref} ref={ref}
{...props} {...props}
/> />
<Range ref={rangeRef}> <Range ref={rangeRef}>
<Selected start={start} end={start > end ? 24 : end} /> <Selected start={start} end={start > end ? 24 : end} />
{start > end && <Selected start={start > end ? 0 : start} end={end} />} {start > end && <Selected start={start > end ? 0 : start} end={end} />}
<Handle <Handle
value={start} value={start}
label={timeFormat === '24h' ? times[start] : dayjs().hour(times[start]).format('ha')} label={timeFormat === '24h' ? times[start] : dayjs().hour(times[start]).format('ha')}
extraPadding={end - start === 1 ? 'padding-right: 20px;' : (start - end === 1 ? 'padding-left: 20px;' : '')} extraPadding={end - start === 1 ? 'padding-right: 20px;' : (start - end === 1 ? 'padding-left: 20px;' : '')}
onMouseDown={() => { onMouseDown={() => {
document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mousemove', handleMouseMove);
isStartMoving.current = true; isStartMoving.current = true;
document.addEventListener('mouseup', () => { document.addEventListener('mouseup', () => {
isStartMoving.current = false; isStartMoving.current = false;
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousemove', handleMouseMove);
}, { once: true }); }, { once: true });
}} }}
onTouchMove={(e) => { onTouchMove={(e) => {
const touch = e.targetTouches[0]; const touch = e.targetTouches[0];
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24); let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
if (step < 0) step = 0; if (step < 0) step = 0;
if (step > 24) step = 24; if (step > 24) step = 24;
step = Math.abs(step); step = Math.abs(step);
setStart(step); setStart(step);
}} }}
tabIndex="0" tabIndex="0"
onKeyDown={e => { onKeyDown={e => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
@ -103,29 +103,29 @@ const TimeRangeField = forwardRef(({
setStart(Math.min(start+1, 24)); setStart(Math.min(start+1, 24));
} }
}} }}
/> />
<Handle <Handle
value={end} value={end}
label={timeFormat === '24h' ? times[end] : dayjs().hour(times[end]).format('ha')} label={timeFormat === '24h' ? times[end] : dayjs().hour(times[end]).format('ha')}
extraPadding={end - start === 1 ? 'padding-left: 20px;' : (start - end === 1 ? 'padding-right: 20px;' : '')} extraPadding={end - start === 1 ? 'padding-left: 20px;' : (start - end === 1 ? 'padding-right: 20px;' : '')}
onMouseDown={() => { onMouseDown={() => {
document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mousemove', handleMouseMove);
isEndMoving.current = true; isEndMoving.current = true;
document.addEventListener('mouseup', () => { document.addEventListener('mouseup', () => {
isEndMoving.current = false; isEndMoving.current = false;
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousemove', handleMouseMove);
}, { once: true }); }, { once: true });
}} }}
onTouchMove={(e) => { onTouchMove={(e) => {
const touch = e.targetTouches[0]; const touch = e.targetTouches[0];
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24); let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
if (step < 0) step = 0; if (step < 0) step = 0;
if (step > 24) step = 24; if (step > 24) step = 24;
step = Math.abs(step); step = Math.abs(step);
setEnd(step); setEnd(step);
}} }}
tabIndex="0" tabIndex="0"
onKeyDown={e => { onKeyDown={e => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
@ -137,10 +137,10 @@ const TimeRangeField = forwardRef(({
setEnd(Math.min(end+1, 24)); setEnd(Math.min(end+1, 24));
} }
}} }}
/> />
</Range> </Range>
</Wrapper> </Wrapper>
); );
}); });
export default TimeRangeField; export default TimeRangeField;

View file

@ -1,82 +1,82 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const Wrapper = styled.div` export const Wrapper = styled.div`
margin: 30px 0; margin: 30px 0;
`; `;
export const StyledLabel = styled.label` export const StyledLabel = styled.label`
display: block; display: block;
padding-bottom: 4px; padding-bottom: 4px;
font-size: 18px; font-size: 18px;
`; `;
export const StyledSubLabel = styled.label` export const StyledSubLabel = styled.label`
display: block; display: block;
padding-bottom: 6px; padding-bottom: 6px;
font-size: 13px; font-size: 13px;
opacity: .6; opacity: .6;
`; `;
export const Range = styled.div` export const Range = styled.div`
user-select: none; user-select: none;
background-color: ${props => props.theme.primaryBackground}; background-color: ${props => props.theme.primaryBackground};
border: 1px solid ${props => props.theme.primary}; border: 1px solid ${props => props.theme.primary};
border-radius: 3px; border-radius: 3px;
height: 50px; height: 50px;
position: relative; position: relative;
margin: 38px 6px 18px; margin: 38px 6px 18px;
`; `;
export const Handle = styled.div` export const Handle = styled.div`
height: calc(100% + 20px); height: calc(100% + 20px);
width: 20px; width: 20px;
border: 1px solid ${props => props.theme.primary}; border: 1px solid ${props => props.theme.primary};
background-color: ${props => props.theme.primaryLight}; background-color: ${props => props.theme.primaryLight};
border-radius: 3px; border-radius: 3px;
position: absolute; position: absolute;
top: -10px; top: -10px;
left: calc(${props => props.value * 4.1666666666666666}% - 11px); left: calc(${props => props.value * 4.1666666666666666}% - 11px);
cursor: ew-resize; cursor: ew-resize;
touch-action: none; touch-action: none;
transition: left .1s; transition: left .1s;
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
transition: none; transition: none;
} }
&:after { &:after {
content: '|||'; content: '|||';
font-size: 8px; font-size: 8px;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: ${props => props.theme.primaryDark}; color: ${props => props.theme.primaryDark};
} }
&:before { &:before {
content: '${props => props.label}'; content: '${props => props.label}';
position: absolute; position: absolute;
bottom: calc(100% + 8px); bottom: calc(100% + 8px);
text-align: center; text-align: center;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
white-space: nowrap; white-space: nowrap;
${props => props.extraPadding} ${props => props.extraPadding}
} }
`; `;
export const Selected = styled.div` export const Selected = styled.div`
position: absolute; position: absolute;
height: 100%; height: 100%;
left: ${props => props.start * 4.1666666666666666}%; left: ${props => props.start * 4.1666666666666666}%;
right: calc(100% - ${props => props.end * 4.1666666666666666}%); right: calc(100% - ${props => props.end * 4.1666666666666666}%);
top: 0; top: 0;
background-color: ${props => props.theme.primary}; background-color: ${props => props.theme.primary};
border-radius: 2px; border-radius: 2px;
transition: left .1s, right .1s; transition: left .1s, right .1s;
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
transition: none; transition: none;

View file

@ -1,25 +1,25 @@
import { import {
Wrapper, Wrapper,
ToggleContainer, ToggleContainer,
StyledLabel, StyledLabel,
Option, Option,
HiddenInput, HiddenInput,
LabelButton, LabelButton,
} from './toggleFieldStyle'; } from './toggleFieldStyle';
const ToggleField = ({ const ToggleField = ({
label, label,
id, id,
name, name,
title = '', title = '',
options = [], options = [],
value, value,
onChange, onChange,
inputRef, inputRef,
...props ...props
}) => ( }) => (
<Wrapper> <Wrapper>
{label && <StyledLabel title={title}>{label} {title !== '' && <svg viewBox="0 0 24 24"><path fill="currentColor" d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z" /></svg>}</StyledLabel>} {label && <StyledLabel title={title}>{label} {title !== '' && <svg viewBox="0 0 24 24"><path fill="currentColor" d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z" /></svg>}</StyledLabel>}
<ToggleContainer> <ToggleContainer>
{Object.entries(options).map(([key, label]) => {Object.entries(options).map(([key, label]) =>
@ -37,7 +37,7 @@ const ToggleField = ({
</Option> </Option>
)} )}
</ToggleContainer> </ToggleContainer>
</Wrapper> </Wrapper>
); );
export default ToggleField; export default ToggleField;

View file

@ -1,11 +1,11 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const Wrapper = styled.div` export const Wrapper = styled.div`
margin: 10px 0; margin: 10px 0;
`; `;
export const ToggleContainer = styled.div` export const ToggleContainer = styled.div`
display: flex; display: flex;
border: 1px solid ${props => props.theme.primary}; border: 1px solid ${props => props.theme.primary};
border-radius: 3px; border-radius: 3px;
overflow: hidden; overflow: hidden;
@ -29,9 +29,9 @@ export const ToggleContainer = styled.div`
`; `;
export const StyledLabel = styled.label` export const StyledLabel = styled.label`
display: block; display: block;
padding-bottom: 4px; padding-bottom: 4px;
font-size: .9rem; font-size: .9rem;
& svg { & svg {
height: 1em; height: 1em;
@ -41,7 +41,7 @@ export const StyledLabel = styled.label`
`; `;
export const Option = styled.div` export const Option = styled.div`
flex: 1; flex: 1;
position: relative; position: relative;
`; `;

View file

@ -10,14 +10,14 @@ const UpdateDialog = ({ onClose }) => {
const { t } = useTranslation('common'); const { t } = useTranslation('common');
return ( return (
<Wrapper> <Wrapper>
<h2>{t('common:update.heading')}</h2> <h2>{t('common:update.heading')}</h2>
<p>{t('common:update.body')}</p> <p>{t('common:update.body')}</p>
<ButtonWrapper> <ButtonWrapper>
<Button secondary onClick={onClose}>{t('common:update.buttons.close')}</Button> <Button secondary onClick={onClose}>{t('common:update.buttons.close')}</Button>
<Button onClick={() => window.location.reload()}>{t('common:update.buttons.reload')}</Button> <Button onClick={() => window.location.reload()}>{t('common:update.buttons.reload')}</Button>
</ButtonWrapper> </ButtonWrapper>
</Wrapper> </Wrapper>
); );
} }

View file

@ -9,21 +9,21 @@ import timezone from 'dayjs/plugin/timezone';
import customParseFormat from 'dayjs/plugin/customParseFormat'; import customParseFormat from 'dayjs/plugin/customParseFormat';
import { import {
TextField, TextField,
CalendarField, CalendarField,
TimeRangeField, TimeRangeField,
SelectField, SelectField,
Button, Button,
Error, Error,
Recents, Recents,
Footer, Footer,
} from 'components'; } from 'components';
import { import {
StyledMain, StyledMain,
CreateForm, CreateForm,
TitleSmall, TitleSmall,
TitleLarge, TitleLarge,
P, P,
OfflineMessage, OfflineMessage,
ShareInfo, ShareInfo,
@ -39,14 +39,14 @@ dayjs.extend(timezone);
dayjs.extend(customParseFormat); dayjs.extend(customParseFormat);
const Create = ({ offline }) => { const Create = ({ offline }) => {
const { register, handleSubmit, setValue } = useForm({ const { register, handleSubmit, setValue } = useForm({
defaultValues: { defaultValues: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
}, },
}); });
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [createdEvent, setCreatedEvent] = useState(null); const [createdEvent, setCreatedEvent] = useState(null);
const [copied, setCopied] = useState(null); const [copied, setCopied] = useState(null);
const [showFooter, setShowFooter] = useState(true); const [showFooter, setShowFooter] = useState(true);
@ -55,11 +55,11 @@ const Create = ({ offline }) => {
const addRecent = useRecentsStore(state => state.addRecent); const addRecent = useRecentsStore(state => state.addRecent);
useEffect(() => { useEffect(() => {
if (window.self === window.top) { if (window.self === window.top) {
push('/'); push('/');
} }
document.title = 'Create a Crab Fit'; document.title = 'Create a Crab Fit';
if (window.parent) { if (window.parent) {
window.parent.postMessage('crabfit-create', '*'); window.parent.postMessage('crabfit-create', '*');
@ -71,67 +71,67 @@ const Create = ({ offline }) => {
once: true once: true
}); });
} }
}, [push]); }, [push]);
const onSubmit = async data => { const onSubmit = async data => {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
try { try {
const { start, end } = JSON.parse(data.times); const { start, end } = JSON.parse(data.times);
const dates = JSON.parse(data.dates); const dates = JSON.parse(data.dates);
if (dates.length === 0) { if (dates.length === 0) {
return setError(t('home:form.errors.no_dates')); return setError(t('home:form.errors.no_dates'));
} }
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8; const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
if (start === end) { if (start === end) {
return setError(t('home:form.errors.same_times')); return setError(t('home:form.errors.same_times'));
} }
let times = dates.reduce((times, date) => { let times = dates.reduce((times, date) => {
let day = []; let day = [];
for (let i = start; i < (start > end ? 24 : end); i++) { for (let i = start; i < (start > end ? 24 : end); i++) {
if (isSpecificDates) { if (isSpecificDates) {
day.push( day.push(
dayjs.tz(date, 'DDMMYYYY', data.timezone) dayjs.tz(date, 'DDMMYYYY', data.timezone)
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY') .hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
); );
} else { } else {
day.push( day.push(
dayjs().tz(data.timezone) dayjs().tz(data.timezone)
.day(date).hour(i).minute(0).utc().format('HHmm-d') .day(date).hour(i).minute(0).utc().format('HHmm-d')
); );
} }
} }
if (start > end) { if (start > end) {
for (let i = 0; i < end; i++) { for (let i = 0; i < end; i++) {
if (isSpecificDates) { if (isSpecificDates) {
day.push( day.push(
dayjs.tz(date, 'DDMMYYYY', data.timezone) dayjs.tz(date, 'DDMMYYYY', data.timezone)
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY') .hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
); );
} else { } else {
day.push( day.push(
dayjs().tz(data.timezone) dayjs().tz(data.timezone)
.day(date).hour(i).minute(0).utc().format('HHmm-d') .day(date).hour(i).minute(0).utc().format('HHmm-d')
); );
} }
} }
} }
return [...times, ...day]; return [...times, ...day];
}, []); }, []);
if (times.length === 0) { if (times.length === 0) {
return setError(t('home:form.errors.no_time')); return setError(t('home:form.errors.no_time'));
} }
const response = await api.post('/event', { const response = await api.post('/event', {
event: { event: {
name: data.name, name: data.name,
times: times, times: times,
timezone: data.timezone, timezone: data.timezone,
}, },
}); });
setCreatedEvent(response.data); setCreatedEvent(response.data);
addRecent({ addRecent({
id: response.data.id, id: response.data.id,
@ -141,19 +141,19 @@ const Create = ({ offline }) => {
gtag('event', 'create_event', { gtag('event', 'create_event', {
'event_category': 'create', 'event_category': 'create',
}); });
} catch (e) { } catch (e) {
setError(t('home:form.errors.unknown')); setError(t('home:form.errors.unknown'));
console.error(e); console.error(e);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
return ( return (
<> <>
<StyledMain> <StyledMain>
<TitleSmall>{t('home:create')}</TitleSmall> <TitleSmall>{t('home:create')}</TitleSmall>
<TitleLarge>CRAB FIT</TitleLarge> <TitleLarge>CRAB FIT</TitleLarge>
</StyledMain> </StyledMain>
{createdEvent ? ( {createdEvent ? (
@ -173,10 +173,10 @@ const Create = ({ offline }) => {
} }
title={!!navigator.clipboard ? t('event:nav.title') : ''} title={!!navigator.clipboard ? t('event:nav.title') : ''}
>{copied ?? `https://crab.fit/${createdEvent?.id}`}</ShareInfo> >{copied ?? `https://crab.fit/${createdEvent?.id}`}</ShareInfo>
<ShareInfo> <ShareInfo>
{/* eslint-disable-next-line */} {/* eslint-disable-next-line */}
<Trans i18nKey="event:nav.shareinfo_alt">Click the link above to copy it to your clipboard, or share via <a onClick={() => gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: createdEvent?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${createdEvent?.id}`)}`} target="_blank">email</a>.</Trans> <Trans i18nKey="event:nav.shareinfo_alt">Click the link above to copy it to your clipboard, or share via <a onClick={() => gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: createdEvent?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${createdEvent?.id}`)}`} target="_blank">email</a>.</Trans>
</ShareInfo> </ShareInfo>
{showFooter && <Footer small />} {showFooter && <Footer small />}
</OfflineMessage> </OfflineMessage>
</StyledMain> </StyledMain>
@ -191,52 +191,52 @@ const Create = ({ offline }) => {
<P>{t('home:offline')}</P> <P>{t('home:offline')}</P>
</OfflineMessage> </OfflineMessage>
) : ( ) : (
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create"> <CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
<TextField <TextField
label={t('home:form.name.label')} label={t('home:form.name.label')}
subLabel={t('home:form.name.sublabel')} subLabel={t('home:form.name.sublabel')}
type="text" type="text"
id="name" id="name"
{...register('name')} {...register('name')}
/> />
<CalendarField <CalendarField
label={t('home:form.dates.label')} label={t('home:form.dates.label')}
subLabel={t('home:form.dates.sublabel')} subLabel={t('home:form.dates.sublabel')}
id="dates" id="dates"
required required
setValue={setValue} setValue={setValue}
{...register('dates')} {...register('dates')}
/> />
<TimeRangeField <TimeRangeField
label={t('home:form.times.label')} label={t('home:form.times.label')}
subLabel={t('home:form.times.sublabel')} subLabel={t('home:form.times.sublabel')}
id="times" id="times"
required required
setValue={setValue} setValue={setValue}
{...register('times')} {...register('times')}
/> />
<SelectField <SelectField
label={t('home:form.timezone.label')} label={t('home:form.timezone.label')}
id="timezone" id="timezone"
options={timezones} options={timezones}
required required
{...register('timezone')} {...register('timezone')}
defaultOption={t('home:form.timezone.defaultOption')} defaultOption={t('home:form.timezone.defaultOption')}
/> />
<Error open={!!error} onClose={() => setError(null)}>{error}</Error> <Error open={!!error} onClose={() => setError(null)}>{error}</Error>
<Button type="submit" isLoading={isLoading} disabled={isLoading} style={{ width: '100%' }}>{t('home:form.button')}</Button> <Button type="submit" isLoading={isLoading} disabled={isLoading} style={{ width: '100%' }}>{t('home:form.button')}</Button>
</CreateForm> </CreateForm>
)} )}
</StyledMain> </StyledMain>
</> </>
)} )}
</> </>
); );
}; };
export default Create; export default Create;

View file

@ -1,9 +1,9 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const StyledMain = styled.div` export const StyledMain = styled.div`
width: 600px; width: 600px;
margin: 10px auto; margin: 10px auto;
max-width: calc(100% - 30px); max-width: calc(100% - 30px);
`; `;
export const CreateForm = styled.form` export const CreateForm = styled.form`
@ -11,43 +11,43 @@ export const CreateForm = styled.form`
`; `;
export const TitleSmall = styled.span` export const TitleSmall = styled.span`
display: block; display: block;
margin: 0; margin: 0;
font-size: 2rem; font-size: 2rem;
text-align: center; text-align: center;
font-family: 'Samurai Bob', sans-serif; font-family: 'Samurai Bob', sans-serif;
font-weight: 400; font-weight: 400;
color: ${props => props.theme.primaryDark}; color: ${props => props.theme.primaryDark};
line-height: 1em; line-height: 1em;
text-transform: uppercase; text-transform: uppercase;
`; `;
export const TitleLarge = styled.h1` export const TitleLarge = styled.h1`
margin: 0; margin: 0;
font-size: 2rem; font-size: 2rem;
text-align: center; text-align: center;
color: ${props => props.theme.primary}; color: ${props => props.theme.primary};
font-family: 'Molot', sans-serif; font-family: 'Molot', sans-serif;
font-weight: 400; font-weight: 400;
text-shadow: 0 3px 0 ${props => props.theme.primaryDark}; text-shadow: 0 3px 0 ${props => props.theme.primaryDark};
line-height: 1em; line-height: 1em;
text-transform: uppercase; text-transform: uppercase;
`; `;
export const P = styled.p` export const P = styled.p`
font-weight: 500; font-weight: 500;
line-height: 1.6em; line-height: 1.6em;
`; `;
export const OfflineMessage = styled.div` export const OfflineMessage = styled.div`
text-align: center; text-align: center;
margin: 50px 0 20px; margin: 50px 0 20px;
`; `;
export const ShareInfo = styled.p` export const ShareInfo = styled.p`
margin: 6px 0; margin: 6px 0;
text-align: center; text-align: center;
font-size: 15px; font-size: 15px;
padding: 10px 0; padding: 10px 0;
${props => props.onClick && ` ${props => props.onClick && `

View file

@ -9,27 +9,27 @@ import customParseFormat from 'dayjs/plugin/customParseFormat';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
import { import {
Footer, Footer,
TextField, TextField,
SelectField, SelectField,
Button, Button,
AvailabilityViewer, AvailabilityViewer,
AvailabilityEditor, AvailabilityEditor,
Error, Error,
Logo, Logo,
} from 'components'; } from 'components';
import { StyledMain } from '../Home/homeStyle'; import { StyledMain } from '../Home/homeStyle';
import { import {
EventName, EventName,
EventDate, EventDate,
LoginForm, LoginForm,
LoginSection, LoginSection,
Info, Info,
ShareInfo, ShareInfo,
Tabs, Tabs,
Tab, Tab,
} from './eventStyle'; } from './eventStyle';
import api from 'services'; import api from 'services';
@ -51,90 +51,90 @@ const Event = (props) => {
const { t } = useTranslation(['common', 'event']); const { t } = useTranslation(['common', 'event']);
const { register, handleSubmit, setFocus, reset } = useForm(); const { register, handleSubmit, setFocus, reset } = useForm();
const { id } = props.match.params; const { id } = props.match.params;
const { offline } = props; const { offline } = props;
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone); const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [password, setPassword] = useState(null); const [password, setPassword] = useState(null);
const [tab, setTab] = useState(user ? 'you' : 'group'); const [tab, setTab] = useState(user ? 'you' : 'group');
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isLoginLoading, setIsLoginLoading] = useState(false); const [isLoginLoading, setIsLoginLoading] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [event, setEvent] = useState(null); const [event, setEvent] = useState(null);
const [people, setPeople] = useState([]); const [people, setPeople] = useState([]);
const [times, setTimes] = useState([]); const [times, setTimes] = useState([]);
const [timeLabels, setTimeLabels] = useState([]); const [timeLabels, setTimeLabels] = useState([]);
const [dates, setDates] = useState([]); const [dates, setDates] = useState([]);
const [min, setMin] = useState(0); const [min, setMin] = useState(0);
const [max, setMax] = useState(0); const [max, setMax] = useState(0);
const [copied, setCopied] = useState(null); const [copied, setCopied] = useState(null);
useEffect(() => { useEffect(() => {
const fetchEvent = async () => { const fetchEvent = async () => {
try { try {
const response = await api.get(`/event/${id}`); const response = await api.get(`/event/${id}`);
setEvent(response.data); setEvent(response.data);
addRecent({ addRecent({
id: response.data.id, id: response.data.id,
created: response.data.created, created: response.data.created,
name: response.data.name, name: response.data.name,
}); });
document.title = `${response.data.name} | Crab Fit`; document.title = `${response.data.name} | Crab Fit`;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
fetchEvent(); fetchEvent();
}, [id, addRecent]); }, [id, addRecent]);
useEffect(() => { useEffect(() => {
const fetchPeople = async () => { const fetchPeople = async () => {
try { try {
const response = await api.get(`/event/${id}/people`); const response = await api.get(`/event/${id}/people`);
const adjustedPeople = response.data.people.map(person => ({ const adjustedPeople = response.data.people.map(person => ({
...person, ...person,
availability: (!!person.availability.length && person.availability[0].length === 13) availability: (!!person.availability.length && person.availability[0].length === 13)
? person.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY')) ? person.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
: person.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')), : person.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
})); }));
setPeople(adjustedPeople); setPeople(adjustedPeople);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
} }
if (tab === 'group') { if (tab === 'group') {
fetchPeople(); fetchPeople();
} }
}, [tab, id, timezone]); }, [tab, id, timezone]);
// Convert to timezone and expand minute segments // Convert to timezone and expand minute segments
useEffect(() => { useEffect(() => {
if (event) { if (event) {
const isSpecificDates = event.times[0].length === 13; const isSpecificDates = event.times[0].length === 13;
setTimes(event.times.reduce( setTimes(event.times.reduce(
(allTimes, time) => { (allTimes, time) => {
const date = isSpecificDates ? const date = isSpecificDates ?
dayjs(time, 'HHmm-DDMMYYYY').utc(true).tz(timezone) dayjs(time, 'HHmm-DDMMYYYY').utc(true).tz(timezone)
: dayjs(time, 'HHmm').day(time.substring(5)).utc(true).tz(timezone); : dayjs(time, 'HHmm').day(time.substring(5)).utc(true).tz(timezone);
const format = isSpecificDates ? 'HHmm-DDMMYYYY' : 'HHmm-d'; const format = isSpecificDates ? 'HHmm-DDMMYYYY' : 'HHmm-d';
return [ return [
...allTimes, ...allTimes,
date.minute(0).format(format), date.minute(0).format(format),
date.minute(15).format(format), date.minute(15).format(format),
date.minute(30).format(format), date.minute(30).format(format),
date.minute(45).format(format), date.minute(45).format(format),
]; ];
}, },
[] []
).sort((a, b) => { ).sort((a, b) => {
if (isSpecificDates) { if (isSpecificDates) {
return dayjs(a, 'HHmm-DDMMYYYY').diff(dayjs(b, 'HHmm-DDMMYYYY')); return dayjs(a, 'HHmm-DDMMYYYY').diff(dayjs(b, 'HHmm-DDMMYYYY'));
} else { } else {
@ -142,154 +142,154 @@ const Event = (props) => {
.diff(dayjs(b, 'HHmm').day((parseInt(b.substring(5))-weekStart % 7 + 7) % 7)); .diff(dayjs(b, 'HHmm').day((parseInt(b.substring(5))-weekStart % 7 + 7) % 7));
} }
})); }));
} }
}, [event, timezone, weekStart]); }, [event, timezone, weekStart]);
useEffect(() => { useEffect(() => {
if (!!times.length && !!people.length) { if (!!times.length && !!people.length) {
setMin(times.reduce((min, time) => { setMin(times.reduce((min, time) => {
let total = people.reduce( let total = people.reduce(
(total, person) => person.availability.includes(time) ? total+1 : total, (total, person) => person.availability.includes(time) ? total+1 : total,
0 0
); );
return total < min ? total : min; return total < min ? total : min;
}, },
Infinity Infinity
)); ));
setMax(times.reduce((max, time) => { setMax(times.reduce((max, time) => {
let total = people.reduce( let total = people.reduce(
(total, person) => person.availability.includes(time) ? total+1 : total, (total, person) => person.availability.includes(time) ? total+1 : total,
0 0
); );
return total > max ? total : max; return total > max ? total : max;
}, },
-Infinity -Infinity
)); ));
} }
}, [times, people]); }, [times, people]);
useEffect(() => { useEffect(() => {
if (!!times.length) { if (!!times.length) {
setTimeLabels(times.reduce((labels, datetime) => { setTimeLabels(times.reduce((labels, datetime) => {
const time = datetime.substring(0, 4); const time = datetime.substring(0, 4);
if (labels.includes(time)) return labels; if (labels.includes(time)) return labels;
return [...labels, time]; return [...labels, time];
}, []) }, [])
.sort((a, b) => parseInt(a) - parseInt(b)) .sort((a, b) => parseInt(a) - parseInt(b))
.reduce((labels, time, i, allTimes) => { .reduce((labels, time, i, allTimes) => {
if (time.substring(2) === '30') return [...labels, { label: '', time }]; if (time.substring(2) === '30') return [...labels, { label: '', time }];
if (allTimes.length - 1 === i) return [ if (allTimes.length - 1 === i) return [
...labels, ...labels,
{ label: '', time }, { label: '', time },
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: null } { label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: null }
]; ];
if (allTimes.length - 1 > i && parseInt(allTimes[i+1].substring(0, 2))-1 > parseInt(time.substring(0, 2))) return [ if (allTimes.length - 1 > i && parseInt(allTimes[i+1].substring(0, 2))-1 > parseInt(time.substring(0, 2))) return [
...labels, ...labels,
{ label: '', time }, { label: '', time },
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: 'space' }, { label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: 'space' },
{ label: '', time: 'space' }, { label: '', time: 'space' },
{ label: '', time: 'space' }, { label: '', time: 'space' },
]; ];
if (time.substring(2) !== '00') return [...labels, { label: '', time }]; if (time.substring(2) !== '00') return [...labels, { label: '', time }];
return [...labels, { label: dayjs(time, 'HHmm').format(timeFormat === '12h' ? 'h A' : 'HH'), time }]; return [...labels, { label: dayjs(time, 'HHmm').format(timeFormat === '12h' ? 'h A' : 'HH'), time }];
}, [])); }, []));
setDates(times.reduce((allDates, time) => { setDates(times.reduce((allDates, time) => {
if (time.substring(2, 4) !== '00') return allDates; if (time.substring(2, 4) !== '00') return allDates;
const date = time.substring(5); const date = time.substring(5);
if (allDates.includes(date)) return allDates; if (allDates.includes(date)) return allDates;
return [...allDates, date]; return [...allDates, date];
}, [])); }, []));
} }
}, [times, timeFormat, locale]); }, [times, timeFormat, locale]);
useEffect(() => { useEffect(() => {
const fetchUser = async () => { const fetchUser = async () => {
try { try {
const response = await api.post(`/event/${id}/people/${user.name}`, { person: { password } }); const response = await api.post(`/event/${id}/people/${user.name}`, { person: { password } });
const adjustedUser = { const adjustedUser = {
...response.data, ...response.data,
availability: (!!response.data.availability.length && response.data.availability[0].length === 13) availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY')) ? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
: response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')), : response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
}; };
setUser(adjustedUser); setUser(adjustedUser);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} }
}; };
if (user) { if (user) {
fetchUser(); fetchUser();
} }
// eslint-disable-next-line // eslint-disable-next-line
}, [timezone]); }, [timezone]);
const onSubmit = async data => { const onSubmit = async data => {
if (!data.name || data.name.length === 0) { if (!data.name || data.name.length === 0) {
setFocus('name'); setFocus('name');
return setError(t('event:form.errors.name_required')); return setError(t('event:form.errors.name_required'));
} }
setIsLoginLoading(true); setIsLoginLoading(true);
setError(null); setError(null);
try { try {
const response = await api.post(`/event/${id}/people/${data.name}`, { const response = await api.post(`/event/${id}/people/${data.name}`, {
person: { person: {
password: data.password, password: data.password,
}, },
}); });
setPassword(data.password); setPassword(data.password);
const adjustedUser = { const adjustedUser = {
...response.data, ...response.data,
availability: (!!response.data.availability.length && response.data.availability[0].length === 13) availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY')) ? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
: response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')), : response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
}; };
setUser(adjustedUser); setUser(adjustedUser);
setTab('you'); setTab('you');
} catch (e) { } catch (e) {
if (e.status === 401) { if (e.status === 401) {
setError(t('event:form.errors.password_incorrect')); setError(t('event:form.errors.password_incorrect'));
} else if (e.status === 404) { } else if (e.status === 404) {
// Create user // Create user
try { try {
await api.post(`/event/${id}/people`, { await api.post(`/event/${id}/people`, {
person: { person: {
name: data.name, name: data.name,
password: data.password, password: data.password,
}, },
}); });
setPassword(data.password); setPassword(data.password);
setUser({ setUser({
name: data.name, name: data.name,
availability: [], availability: [],
}); });
setTab('you'); setTab('you');
} catch (e) { } catch (e) {
setError(t('event:form.errors.unknown')); setError(t('event:form.errors.unknown'));
} }
} }
} finally { } finally {
setIsLoginLoading(false); setIsLoginLoading(false);
gtag('event', 'login', { gtag('event', 'login', {
'event_category': 'event', 'event_category': 'event',
}); });
reset(); reset();
} }
}; };
return ( return (
<> <>
<StyledMain> <StyledMain>
<Logo /> <Logo />
{(!!event || isLoading) ? ( {(!!event || isLoading) ? (
<> <>
<EventName isLoading={isLoading}>{event?.name}</EventName> <EventName isLoading={isLoading}>{event?.name}</EventName>
<EventDate isLoading={isLoading} locale={locale} title={event?.created && dayjs.unix(event?.created).format('D MMMM, YYYY')}>{event?.created && t('common:created', { date: dayjs.unix(event?.created).fromNow() })}</EventDate> <EventDate isLoading={isLoading} locale={locale} title={event?.created && dayjs.unix(event?.created).format('D MMMM, YYYY')}>{event?.created && t('common:created', { date: dayjs.unix(event?.created).fromNow() })}</EventDate>
<ShareInfo <ShareInfo
onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${id}`) onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${id}`)
.then(() => { .then(() => {
setCopied(t('event:nav.copied')); setCopied(t('event:nav.copied'));
@ -302,81 +302,81 @@ const Event = (props) => {
} }
title={!!navigator.clipboard ? t('event:nav.title') : ''} title={!!navigator.clipboard ? t('event:nav.title') : ''}
>{copied ?? `https://crab.fit/${id}`}</ShareInfo> >{copied ?? `https://crab.fit/${id}`}</ShareInfo>
<ShareInfo isLoading={isLoading}> <ShareInfo isLoading={isLoading}>
{!!event?.name && {!!event?.name &&
<Trans i18nKey="event:nav.shareinfo">Copy the link to this page, or share via <a onClick={() => gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: event?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${id}`)}`}>email</a>.</Trans> <Trans i18nKey="event:nav.shareinfo">Copy the link to this page, or share via <a onClick={() => gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: event?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${id}`)}`}>email</a>.</Trans>
} }
</ShareInfo> </ShareInfo>
</> </>
) : ( ) : (
offline ? ( offline ? (
<div style={{ margin: '100px 0' }}> <div style={{ margin: '100px 0' }}>
<EventName>{t('event:offline.title')}</EventName> <EventName>{t('event:offline.title')}</EventName>
<ShareInfo><Trans i18nKey="event:offline.body" /></ShareInfo> <ShareInfo><Trans i18nKey="event:offline.body" /></ShareInfo>
</div> </div>
) : ( ) : (
<div style={{ margin: '100px 0' }}> <div style={{ margin: '100px 0' }}>
<EventName>{t('event:error.title')}</EventName> <EventName>{t('event:error.title')}</EventName>
<ShareInfo>{t('event:error.body')}</ShareInfo> <ShareInfo>{t('event:error.body')}</ShareInfo>
</div> </div>
) )
)} )}
</StyledMain> </StyledMain>
{(!!event || isLoading) && ( {(!!event || isLoading) && (
<> <>
<LoginSection id="login"> <LoginSection id="login">
<StyledMain> <StyledMain>
{user ? ( {user ? (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '20px 0', flexWrap: 'wrap', gap: '10px' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '20px 0', flexWrap: 'wrap', gap: '10px' }}>
<h2 style={{ margin: 0 }}>{t('event:form.signed_in', { name: user.name })}</h2> <h2 style={{ margin: 0 }}>{t('event:form.signed_in', { name: user.name })}</h2>
<Button small onClick={() => { <Button small onClick={() => {
setTab('group'); setTab('group');
setUser(null); setUser(null);
setPassword(null); setPassword(null);
}}>{t('event:form.logout_button')}</Button> }}>{t('event:form.logout_button')}</Button>
</div> </div>
) : ( ) : (
<> <>
<h2>{t('event:form.signed_out')}</h2> <h2>{t('event:form.signed_out')}</h2>
<LoginForm onSubmit={handleSubmit(onSubmit)}> <LoginForm onSubmit={handleSubmit(onSubmit)}>
<TextField <TextField
label={t('event:form.name')} label={t('event:form.name')}
type="text" type="text"
id="name" id="name"
inline inline
required required
{...register('name')} {...register('name')}
/> />
<TextField <TextField
label={t('event:form.password')} label={t('event:form.password')}
type="password" type="password"
id="password" id="password"
inline inline
{...register('password')} {...register('password')}
/> />
<Button <Button
type="submit" type="submit"
isLoading={isLoginLoading} isLoading={isLoginLoading}
disabled={isLoginLoading || isLoading} disabled={isLoginLoading || isLoading}
>{t('event:form.button')}</Button> >{t('event:form.button')}</Button>
</LoginForm> </LoginForm>
<Error open={!!error} onClose={() => setError(null)}>{error}</Error> <Error open={!!error} onClose={() => setError(null)}>{error}</Error>
<Info>{t('event:form.info')}</Info> <Info>{t('event:form.info')}</Info>
</> </>
)} )}
<SelectField <SelectField
label={t('event:form.timezone')} label={t('event:form.timezone')}
name="timezone" name="timezone"
id="timezone" id="timezone"
inline inline
value={timezone} value={timezone}
onChange={event => setTimezone(event.currentTarget.value)} onChange={event => setTimezone(event.currentTarget.value)}
options={timezones} options={timezones}
/> />
{/* eslint-disable-next-line */} {/* eslint-disable-next-line */}
{event?.timezone && event.timezone !== timezone && <p><Trans i18nKey="event:form.created_in_timezone">This event was created in the timezone <strong>{{timezone: event.timezone}}</strong>. <a href="#" onClick={e => { {event?.timezone && event.timezone !== timezone && <p><Trans i18nKey="event:form.created_in_timezone">This event was created in the timezone <strong>{{timezone: event.timezone}}</strong>. <a href="#" onClick={e => {
e.preventDefault(); e.preventDefault();
@ -395,84 +395,84 @@ const Event = (props) => {
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone); setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
}}>Click here</a> to use it.</Trans></p> }}>Click here</a> to use it.</Trans></p>
)} )}
</StyledMain> </StyledMain>
</LoginSection> </LoginSection>
<StyledMain> <StyledMain>
<Tabs> <Tabs>
<Tab <Tab
href="#you" href="#you"
onClick={e => { onClick={e => {
e.preventDefault(); e.preventDefault();
if (user) { if (user) {
setTab('you'); setTab('you');
} else { } else {
setFocus('name'); setFocus('name');
} }
}} }}
selected={tab === 'you'} selected={tab === 'you'}
disabled={!user} disabled={!user}
title={user ? '' : t('event:tabs.you_tooltip')} title={user ? '' : t('event:tabs.you_tooltip')}
>{t('event:tabs.you')}</Tab> >{t('event:tabs.you')}</Tab>
<Tab <Tab
href="#group" href="#group"
onClick={e => { onClick={e => {
e.preventDefault(); e.preventDefault();
setTab('group'); setTab('group');
}} }}
selected={tab === 'group'} selected={tab === 'group'}
>{t('event:tabs.group')}</Tab> >{t('event:tabs.group')}</Tab>
</Tabs> </Tabs>
</StyledMain> </StyledMain>
{tab === 'group' ? ( {tab === 'group' ? (
<section id="group"> <section id="group">
<AvailabilityViewer <AvailabilityViewer
times={times} times={times}
timeLabels={timeLabels} timeLabels={timeLabels}
dates={dates} dates={dates}
isSpecificDates={!!dates.length && dates[0].length === 8} isSpecificDates={!!dates.length && dates[0].length === 8}
people={people.filter(p => p.availability.length > 0)} people={people.filter(p => p.availability.length > 0)}
min={min} min={min}
max={max} max={max}
/> />
</section> </section>
) : ( ) : (
<section id="you"> <section id="you">
<AvailabilityEditor <AvailabilityEditor
times={times} times={times}
timeLabels={timeLabels} timeLabels={timeLabels}
dates={dates} dates={dates}
timezone={timezone} timezone={timezone}
isSpecificDates={!!dates.length && dates[0].length === 8} isSpecificDates={!!dates.length && dates[0].length === 8}
value={user.availability} value={user.availability}
onChange={async availability => { onChange={async availability => {
const oldAvailability = [...user.availability]; const oldAvailability = [...user.availability];
const utcAvailability = (!!availability.length && availability[0].length === 13) const utcAvailability = (!!availability.length && availability[0].length === 13)
? availability.map(date => dayjs.tz(date, 'HHmm-DDMMYYYY', timezone).utc().format('HHmm-DDMMYYYY')) ? availability.map(date => dayjs.tz(date, 'HHmm-DDMMYYYY', timezone).utc().format('HHmm-DDMMYYYY'))
: availability.map(date => dayjs.tz(date, 'HHmm', timezone).day(date.substring(5)).utc().format('HHmm-d')); : availability.map(date => dayjs.tz(date, 'HHmm', timezone).day(date.substring(5)).utc().format('HHmm-d'));
setUser({ ...user, availability }); setUser({ ...user, availability });
try { try {
await api.patch(`/event/${id}/people/${user.name}`, { await api.patch(`/event/${id}/people/${user.name}`, {
person: { person: {
password, password,
availability: utcAvailability, availability: utcAvailability,
}, },
}); });
} catch (e) { } catch (e) {
console.log(e); console.log(e);
setUser({ ...user, oldAvailability }); setUser({ ...user, oldAvailability });
} }
}} }}
/> />
</section> </section>
)} )}
</> </>
)} )}
<Footer /> <Footer />
</> </>
); );
}; };
export default Event; export default Event;

View file

@ -1,21 +1,21 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const EventName = styled.h1` export const EventName = styled.h1`
text-align: center; text-align: center;
font-weight: 800; font-weight: 800;
margin: 20px 0 5px; margin: 20px 0 5px;
${props => props.isLoading && ` ${props => props.isLoading && `
&:after { &:after {
content: ''; content: '';
display: inline-block; display: inline-block;
height: 1em; height: 1em;
width: 400px; width: 400px;
max-width: 100%; max-width: 100%;
background-color: ${props.theme.loading}; background-color: ${props.theme.loading};
border-radius: 3px; border-radius: 3px;
} }
`} `}
`; `;
export const EventDate = styled.span` export const EventDate = styled.span`
@ -28,63 +28,63 @@ export const EventDate = styled.span`
letter-spacing: .01em; letter-spacing: .01em;
${props => props.isLoading && ` ${props => props.isLoading && `
&:after { &:after {
content: ''; content: '';
display: inline-block; display: inline-block;
height: 1em; height: 1em;
width: 200px; width: 200px;
max-width: 100%; max-width: 100%;
background-color: ${props.theme.loading}; background-color: ${props.theme.loading};
border-radius: 3px; border-radius: 3px;
} }
`} `}
`; `;
export const LoginForm = styled.form` export const LoginForm = styled.form`
display: grid; display: grid;
grid-template-columns: 1fr 1fr auto; grid-template-columns: 1fr 1fr auto;
align-items: flex-end; align-items: flex-end;
grid-gap: 18px; grid-gap: 18px;
@media (max-width: 500px) { @media (max-width: 500px) {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
@media (max-width: 400px) { @media (max-width: 400px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
& div:last-child { & div:last-child {
--btn-width: 100%; --btn-width: 100%;
} }
} }
`; `;
export const LoginSection = styled.section` export const LoginSection = styled.section`
background-color: ${props => props.theme.primaryBackground}; background-color: ${props => props.theme.primaryBackground};
padding: 10px 0; padding: 10px 0;
`; `;
export const Info = styled.p` export const Info = styled.p`
margin: 18px 0; margin: 18px 0;
opacity: .6; opacity: .6;
font-size: 12px; font-size: 12px;
`; `;
export const ShareInfo = styled.p` export const ShareInfo = styled.p`
margin: 6px 0; margin: 6px 0;
text-align: center; text-align: center;
font-size: 15px; font-size: 15px;
${props => props.isLoading && ` ${props => props.isLoading && `
&:after { &:after {
content: ''; content: '';
display: inline-block; display: inline-block;
height: 1em; height: 1em;
width: 300px; width: 300px;
max-width: 100%; max-width: 100%;
background-color: ${props.theme.loading}; background-color: ${props.theme.loading};
border-radius: 3px; border-radius: 3px;
} }
`} `}
${props => props.onClick && ` ${props => props.onClick && `
cursor: pointer; cursor: pointer;
@ -96,33 +96,33 @@ export const ShareInfo = styled.p`
`; `;
export const Tabs = styled.div` export const Tabs = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 30px 0 20px; margin: 30px 0 20px;
`; `;
export const Tab = styled.a` export const Tab = styled.a`
user-select: none; user-select: none;
text-decoration: none; text-decoration: none;
display: block; display: block;
color: ${props => props.theme.text}; color: ${props => props.theme.text};
padding: 8px 18px; padding: 8px 18px;
background-color: ${props => props.theme.primaryBackground}; background-color: ${props => props.theme.primaryBackground};
border: 1px solid ${props => props.theme.primary}; border: 1px solid ${props => props.theme.primary};
border-bottom: 0; border-bottom: 0;
margin: 0 4px; margin: 0 4px;
border-top-left-radius: 5px; border-top-left-radius: 5px;
border-top-right-radius: 5px; border-top-right-radius: 5px;
${props => props.selected && ` ${props => props.selected && `
color: #FFF; color: #FFF;
background-color: ${props.theme.primary}; background-color: ${props.theme.primary};
border-color: ${props.theme.primary}; border-color: ${props.theme.primary};
`} `}
${props => props.disabled && ` ${props => props.disabled && `
opacity: .5; opacity: .5;
cursor: not-allowed; cursor: not-allowed;
`} `}
`; `;

View file

@ -3,17 +3,17 @@ import { Link, useHistory } from 'react-router-dom';
import { useTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next';
import { import {
Button, Button,
Center, Center,
Footer, Footer,
AvailabilityViewer, AvailabilityViewer,
Logo, Logo,
} from 'components'; } from 'components';
import { import {
StyledMain, StyledMain,
AboutSection, AboutSection,
P, P,
} from '../Home/homeStyle'; } from '../Home/homeStyle';
import { import {
@ -26,18 +26,18 @@ const Help = () => {
const { push } = useHistory(); const { push } = useHistory();
const { t } = useTranslation(['common', 'help']); const { t } = useTranslation(['common', 'help']);
useEffect(() => { useEffect(() => {
document.title = t('help:name'); document.title = t('help:name');
}, [t]); }, [t]);
return ( return (
<> <>
<StyledMain> <StyledMain>
<Logo /> <Logo />
</StyledMain> </StyledMain>
<StyledMain> <StyledMain>
<h1>{t('help:name')}</h1> <h1>{t('help:name')}</h1>
<P>{t('help:p1')}</P> <P>{t('help:p1')}</P>
<P>{t('help:p2')}</P> <P>{t('help:p2')}</P>
@ -80,17 +80,17 @@ const Help = () => {
min={0} min={0}
max={5} max={5}
/> />
</StyledMain> </StyledMain>
<AboutSection id="about"> <AboutSection id="about">
<StyledMain> <StyledMain>
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center> <Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
</StyledMain> </StyledMain>
</AboutSection> </AboutSection>
<Footer /> <Footer />
</> </>
); );
}; };
export default Help; export default Help;

View file

@ -1,43 +1,43 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const Step = styled.h2` export const Step = styled.h2`
text-decoration-color: ${props => props.theme.primary}; text-decoration-color: ${props => props.theme.primary};
text-decoration-style: solid; text-decoration-style: solid;
text-decoration-line: underline; text-decoration-line: underline;
margin-top: 30px; margin-top: 30px;
`; `;
export const FakeCalendar = styled.div` export const FakeCalendar = styled.div`
user-select: none; user-select: none;
& div { & div {
display: grid; display: grid;
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(7, 1fr);
grid-gap: 2px; grid-gap: 2px;
} }
& .days span { & .days span {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 3px 0; padding: 3px 0;
font-weight: bold; font-weight: bold;
user-select: none; user-select: none;
opacity: .7; opacity: .7;
@media (max-width: 350px) { @media (max-width: 350px) {
font-size: 12px; font-size: 12px;
} }
} }
& .dates span { & .dates span {
background-color: ${props => props.theme.primaryBackground}; background-color: ${props => props.theme.primaryBackground};
border: 1px solid ${props => props.theme.primary}; border: 1px solid ${props => props.theme.primary};
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 10px 0; padding: 10px 0;
&.selected { &.selected {
color: #FFF; color: #FFF;
background-color: ${props => props.theme.primary}; background-color: ${props => props.theme.primary};
} }
} }
& .dates span:first-of-type { & .dates span:first-of-type {
@ -51,45 +51,45 @@ export const FakeCalendar = styled.div`
`; `;
export const FakeTimeRange = styled.div` export const FakeTimeRange = styled.div`
user-select: none; user-select: none;
background-color: ${props => props.theme.primaryBackground}; background-color: ${props => props.theme.primaryBackground};
border: 1px solid ${props => props.theme.primary}; border: 1px solid ${props => props.theme.primary};
border-radius: 3px; border-radius: 3px;
height: 50px; height: 50px;
position: relative; position: relative;
margin: 38px 6px 18px; margin: 38px 6px 18px;
& div { & div {
height: calc(100% + 20px); height: calc(100% + 20px);
width: 20px; width: 20px;
border: 1px solid ${props => props.theme.primary}; border: 1px solid ${props => props.theme.primary};
background-color: ${props => props.theme.primaryLight}; background-color: ${props => props.theme.primaryLight};
border-radius: 3px; border-radius: 3px;
position: absolute; position: absolute;
top: -10px; top: -10px;
&:after { &:after {
content: '|||'; content: '|||';
font-size: 8px; font-size: 8px;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: ${props => props.theme.primaryDark}; color: ${props => props.theme.primaryDark};
} }
&:before { &:before {
content: attr(data-label); content: attr(data-label);
position: absolute; position: absolute;
bottom: calc(100% + 8px); bottom: calc(100% + 8px);
text-align: center; text-align: center;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
} }
} }
& .start { & .start {
left: calc(${11 * 4.1666666666666666}% - 11px); left: calc(${11 * 4.1666666666666666}% - 11px);
@ -100,11 +100,11 @@ export const FakeTimeRange = styled.div`
&:before { &:before {
content: ''; content: '';
position: absolute; position: absolute;
height: 100%; height: 100%;
left: ${11 * 4.1666666666666666}%; left: ${11 * 4.1666666666666666}%;
right: calc(100% - ${17 * 4.1666666666666666}%); right: calc(100% - ${17 * 4.1666666666666666}%);
top: 0; top: 0;
background-color: ${props => props.theme.primary}; background-color: ${props => props.theme.primary};
border-radius: 2px; border-radius: 2px;
} }
`; `;

View file

@ -9,30 +9,30 @@ import timezone from 'dayjs/plugin/timezone';
import customParseFormat from 'dayjs/plugin/customParseFormat'; import customParseFormat from 'dayjs/plugin/customParseFormat';
import { import {
TextField, TextField,
CalendarField, CalendarField,
TimeRangeField, TimeRangeField,
SelectField, SelectField,
Button, Button,
Center, Center,
Error, Error,
Footer, Footer,
Recents, Recents,
} from 'components'; } from 'components';
import { import {
StyledMain, StyledMain,
CreateForm, CreateForm,
TitleSmall, TitleSmall,
TitleLarge, TitleLarge,
Logo, Logo,
Links, Links,
AboutSection, AboutSection,
P, P,
Stats, Stats,
Stat, Stat,
StatNumber, StatNumber,
StatLabel, StatLabel,
OfflineMessage, OfflineMessage,
ButtonArea, ButtonArea,
} from './homeStyle'; } from './homeStyle';
@ -49,120 +49,120 @@ dayjs.extend(timezone);
dayjs.extend(customParseFormat); dayjs.extend(customParseFormat);
const Home = ({ offline }) => { const Home = ({ offline }) => {
const { register, handleSubmit, setValue } = useForm({ const { register, handleSubmit, setValue } = useForm({
defaultValues: { defaultValues: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
}, },
}); });
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [stats, setStats] = useState({ const [stats, setStats] = useState({
eventCount: null, eventCount: null,
personCount: null, personCount: null,
version: 'loading...', version: 'loading...',
}); });
const [browser, setBrowser] = useState(undefined); const [browser, setBrowser] = useState(undefined);
const { push } = useHistory(); const { push } = useHistory();
const { t } = useTranslation(['common', 'home']); const { t } = useTranslation(['common', 'home']);
const isTWA = useTWAStore(state => state.TWA); const isTWA = useTWAStore(state => state.TWA);
useEffect(() => { useEffect(() => {
const fetch = async () => { const fetch = async () => {
try { try {
const response = await api.get('/stats'); const response = await api.get('/stats');
setStats(response.data); setStats(response.data);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
}; };
fetch(); fetch();
document.title = 'Crab Fit'; document.title = 'Crab Fit';
setBrowser(detect_browser()); setBrowser(detect_browser());
}, []); }, []);
const onSubmit = async data => { const onSubmit = async data => {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
try { try {
const { start, end } = JSON.parse(data.times); const { start, end } = JSON.parse(data.times);
const dates = JSON.parse(data.dates); const dates = JSON.parse(data.dates);
if (dates.length === 0) { if (dates.length === 0) {
return setError(t('home:form.errors.no_dates')); return setError(t('home:form.errors.no_dates'));
} }
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8; const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
if (start === end) { if (start === end) {
return setError(t('home:form.errors.same_times')); return setError(t('home:form.errors.same_times'));
} }
let times = dates.reduce((times, date) => { let times = dates.reduce((times, date) => {
let day = []; let day = [];
for (let i = start; i < (start > end ? 24 : end); i++) { for (let i = start; i < (start > end ? 24 : end); i++) {
if (isSpecificDates) { if (isSpecificDates) {
day.push( day.push(
dayjs.tz(date, 'DDMMYYYY', data.timezone) dayjs.tz(date, 'DDMMYYYY', data.timezone)
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY') .hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
); );
} else { } else {
day.push( day.push(
dayjs().tz(data.timezone) dayjs().tz(data.timezone)
.day(date).hour(i).minute(0).utc().format('HHmm-d') .day(date).hour(i).minute(0).utc().format('HHmm-d')
); );
} }
} }
if (start > end) { if (start > end) {
for (let i = 0; i < end; i++) { for (let i = 0; i < end; i++) {
if (isSpecificDates) { if (isSpecificDates) {
day.push( day.push(
dayjs.tz(date, 'DDMMYYYY', data.timezone) dayjs.tz(date, 'DDMMYYYY', data.timezone)
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY') .hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
); );
} else { } else {
day.push( day.push(
dayjs().tz(data.timezone) dayjs().tz(data.timezone)
.day(date).hour(i).minute(0).utc().format('HHmm-d') .day(date).hour(i).minute(0).utc().format('HHmm-d')
); );
} }
} }
} }
return [...times, ...day]; return [...times, ...day];
}, []); }, []);
if (times.length === 0) { if (times.length === 0) {
return setError(t('home:form.errors.no_time')); return setError(t('home:form.errors.no_time'));
} }
const response = await api.post('/event', { const response = await api.post('/event', {
event: { event: {
name: data.name, name: data.name,
times: times, times: times,
timezone: data.timezone, timezone: data.timezone,
}, },
}); });
push(`/${response.data.id}`); push(`/${response.data.id}`);
gtag('event', 'create_event', { gtag('event', 'create_event', {
'event_category': 'home', 'event_category': 'home',
}); });
} catch (e) { } catch (e) {
setError(t('home:form.errors.unknown')); setError(t('home:form.errors.unknown'));
console.error(e); console.error(e);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
return ( return (
<> <>
<StyledMain> <StyledMain>
<Center> <Center>
<Logo src={logo} alt="" /> <Logo src={logo} alt="" />
</Center> </Center>
<TitleSmall altChars={/[A-Z]/g.test(t('home:create'))}>{t('home:create')}</TitleSmall> <TitleSmall altChars={/[A-Z]/g.test(t('home:create'))}>{t('home:create')}</TitleSmall>
<TitleLarge>CRAB FIT</TitleLarge> <TitleLarge>CRAB FIT</TitleLarge>
<Links> <Links>
<a href="#about">{t('home:nav.about')}</a> / <a href="#donate">{t('home:nav.donate')}</a> <a href="#about">{t('home:nav.about')}</a> / <a href="#donate">{t('home:nav.donate')}</a>
</Links> </Links>
</StyledMain> </StyledMain>
<Recents /> <Recents />
@ -174,65 +174,65 @@ const Home = ({ offline }) => {
<P>{t('home:offline')}</P> <P>{t('home:offline')}</P>
</OfflineMessage> </OfflineMessage>
) : ( ) : (
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create"> <CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
<TextField <TextField
label={t('home:form.name.label')} label={t('home:form.name.label')}
subLabel={t('home:form.name.sublabel')} subLabel={t('home:form.name.sublabel')}
type="text" type="text"
id="name" id="name"
{...register('name')} {...register('name')}
/> />
<CalendarField <CalendarField
label={t('home:form.dates.label')} label={t('home:form.dates.label')}
subLabel={t('home:form.dates.sublabel')} subLabel={t('home:form.dates.sublabel')}
id="dates" id="dates"
required required
setValue={setValue} setValue={setValue}
{...register('dates')} {...register('dates')}
/> />
<TimeRangeField <TimeRangeField
label={t('home:form.times.label')} label={t('home:form.times.label')}
subLabel={t('home:form.times.sublabel')} subLabel={t('home:form.times.sublabel')}
id="times" id="times"
required required
setValue={setValue} setValue={setValue}
{...register('times')} {...register('times')}
/> />
<SelectField <SelectField
label={t('home:form.timezone.label')} label={t('home:form.timezone.label')}
id="timezone" id="timezone"
options={timezones} options={timezones}
required required
{...register('timezone')} {...register('timezone')}
defaultOption={t('home:form.timezone.defaultOption')} defaultOption={t('home:form.timezone.defaultOption')}
/> />
<Error open={!!error} onClose={() => setError(null)}>{error}</Error> <Error open={!!error} onClose={() => setError(null)}>{error}</Error>
<Center> <Center>
<Button type="submit" isLoading={isLoading} disabled={isLoading}>{t('home:form.button')}</Button> <Button type="submit" isLoading={isLoading} disabled={isLoading}>{t('home:form.button')}</Button>
</Center> </Center>
</CreateForm> </CreateForm>
)} )}
</StyledMain> </StyledMain>
<AboutSection id="about"> <AboutSection id="about">
<StyledMain> <StyledMain>
<h2>{t('home:about.name')}</h2> <h2>{t('home:about.name')}</h2>
<Stats> <Stats>
<Stat> <Stat>
<StatNumber>{stats.eventCount ?? '350+'}</StatNumber> <StatNumber>{stats.eventCount ?? '350+'}</StatNumber>
<StatLabel>{t('home:about.events')}</StatLabel> <StatLabel>{t('home:about.events')}</StatLabel>
</Stat> </Stat>
<Stat> <Stat>
<StatNumber>{stats.personCount ?? '550+'}</StatNumber> <StatNumber>{stats.personCount ?? '550+'}</StatNumber>
<StatLabel>{t('home:about.availabilities')}</StatLabel> <StatLabel>{t('home:about.availabilities')}</StatLabel>
</Stat> </Stat>
</Stats> </Stats>
<P><Trans i18nKey="home:about.content.p1">Crab Fit helps you fit your event around everyone's schedules. Simply create an event above and send the link to everyone that is participating. Results update live and you will be able to see a heat-map of when everyone is free.<br /><Link to="/how-to" rel="help">Learn more about how to Crab Fit</Link>.</Trans></P> <P><Trans i18nKey="home:about.content.p1">Crab Fit helps you fit your event around everyone's schedules. Simply create an event above and send the link to everyone that is participating. Results update live and you will be able to see a heat-map of when everyone is free.<br /><Link to="/how-to" rel="help">Learn more about how to Crab Fit</Link>.</Trans></P>
{isTWA !== true && ( {isTWA !== true && (
<ButtonArea> <ButtonArea>
{['chrome', 'firefox', 'safari'].includes(browser) && ( {['chrome', 'firefox', 'safari'].includes(browser) && (
@ -267,16 +267,16 @@ const Home = ({ offline }) => {
>{t('home:about.android_app')}</Button> >{t('home:about.android_app')}</Button>
</ButtonArea> </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.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><Trans i18nKey="home:about.content.p4">The code for Crab Fit is open source, if you find any issues or want to contribute, you can visit the <a href="https://github.com/GRA0007/crab.fit" target="_blank" rel="noreferrer noopener">repository</a>. By using Crab Fit you agree to the <Link to="/privacy" rel="license">privacy policy</Link>.</Trans></P>
<P>{t('home:about.content.p6')}</P> <P>{t('home:about.content.p6')}</P>
<P>{t('home:about.content.p5')}</P> <P>{t('home:about.content.p5')}</P>
</StyledMain> </StyledMain>
</AboutSection> </AboutSection>
<Footer /> <Footer />
</> </>
); );
}; };
export default Home; export default Home;

View file

@ -1,9 +1,9 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const StyledMain = styled.div` export const StyledMain = styled.div`
width: 600px; width: 600px;
margin: 20px auto; margin: 20px auto;
max-width: calc(100% - 60px); max-width: calc(100% - 60px);
`; `;
export const CreateForm = styled.form` export const CreateForm = styled.form`
@ -11,14 +11,14 @@ export const CreateForm = styled.form`
`; `;
export const TitleSmall = styled.span` export const TitleSmall = styled.span`
display: block; display: block;
margin: 0; margin: 0;
font-size: 3rem; font-size: 3rem;
text-align: center; text-align: center;
font-family: 'Samurai Bob', sans-serif; font-family: 'Samurai Bob', sans-serif;
font-weight: 400; font-weight: 400;
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight}; color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
line-height: 1em; line-height: 1em;
text-transform: uppercase; text-transform: uppercase;
${props => !props.altChars && ` ${props => !props.altChars && `
@ -30,23 +30,23 @@ export const TitleSmall = styled.span`
`; `;
export const TitleLarge = styled.h1` export const TitleLarge = styled.h1`
margin: 0; margin: 0;
font-size: 4rem; font-size: 4rem;
text-align: center; text-align: center;
color: ${props => props.theme.primary}; color: ${props => props.theme.primary};
font-family: 'Molot', sans-serif; font-family: 'Molot', sans-serif;
font-weight: 400; font-weight: 400;
text-shadow: 0 4px 0 ${props => props.theme.primaryDark}; text-shadow: 0 4px 0 ${props => props.theme.primaryDark};
line-height: 1em; line-height: 1em;
text-transform: uppercase; text-transform: uppercase;
@media (max-width: 350px) { @media (max-width: 350px) {
font-size: 3.5rem; font-size: 3.5rem;
} }
`; `;
export const Logo = styled.img` export const Logo = styled.img`
width: 80px; width: 80px;
transition: transform .15s; transition: transform .15s;
animation: jelly .5s 1 .05s; animation: jelly .5s 1 .05s;
user-select: none; user-select: none;
@ -81,14 +81,14 @@ export const Logo = styled.img`
`; `;
export const Links = styled.nav` export const Links = styled.nav`
text-align: center; text-align: center;
margin: 20px 0; margin: 20px 0;
`; `;
export const AboutSection = styled.section` export const AboutSection = styled.section`
margin: 30px 0 0; margin: 30px 0 0;
background-color: ${props => props.theme.primaryBackground}; background-color: ${props => props.theme.primaryBackground};
padding: 20px 0; padding: 20px 0;
& a { & a {
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight}; color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
@ -96,42 +96,42 @@ export const AboutSection = styled.section`
`; `;
export const P = styled.p` export const P = styled.p`
font-weight: 500; font-weight: 500;
line-height: 1.6em; line-height: 1.6em;
`; `;
export const Stats = styled.div` export const Stats = styled.div`
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
align-items: flex-start; align-items: flex-start;
flex-wrap: wrap; flex-wrap: wrap;
`; `;
export const Stat = styled.div` export const Stat = styled.div`
text-align: center; text-align: center;
padding: 0 6px; padding: 0 6px;
min-width: 160px; min-width: 160px;
margin: 10px 0; margin: 10px 0;
`; `;
export const StatNumber = styled.span` export const StatNumber = styled.span`
display: block; display: block;
font-weight: 900; font-weight: 900;
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight}; color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
font-size: 2em; font-size: 2em;
`; `;
export const StatLabel = styled.span` export const StatLabel = styled.span`
display: block; display: block;
`; `;
export const OfflineMessage = styled.div` export const OfflineMessage = styled.div`
text-align: center; text-align: center;
margin: 50px 0 20px; margin: 50px 0 20px;
`; `;
export const ButtonArea = styled.div` export const ButtonArea = styled.div`
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View file

@ -3,16 +3,16 @@ import { useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Button, Button,
Center, Center,
Footer, Footer,
Logo, Logo,
} from 'components'; } from 'components';
import { import {
StyledMain, StyledMain,
AboutSection, AboutSection,
P, P,
} from '../Home/homeStyle'; } from '../Home/homeStyle';
import { Note } from './privacyStyle'; import { Note } from './privacyStyle';
@ -24,20 +24,20 @@ const Privacy = () => {
const contentRef = useRef(); const contentRef = useRef();
const [content, setContent] = useState(''); const [content, setContent] = useState('');
useEffect(() => { useEffect(() => {
document.title = `${t('privacy:name')} - Crab Fit`; document.title = `${t('privacy:name')} - Crab Fit`;
}, [t]); }, [t]);
useEffect(() => setContent(contentRef.current?.innerText || ''), [contentRef]); useEffect(() => setContent(contentRef.current?.innerText || ''), [contentRef]);
return ( return (
<> <>
<StyledMain> <StyledMain>
<Logo /> <Logo />
</StyledMain> </StyledMain>
<StyledMain> <StyledMain>
<h1>{t('privacy:name')}</h1> <h1>{t('privacy:name')}</h1>
{!i18n.language.startsWith('en') && ( {!i18n.language.startsWith('en') && (
<p> <p>
@ -98,15 +98,15 @@ const Privacy = () => {
</div> </div>
</StyledMain> </StyledMain>
<AboutSection id="about"> <AboutSection id="about">
<StyledMain> <StyledMain>
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center> <Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
</StyledMain> </StyledMain>
</AboutSection> </AboutSection>
<Footer /> <Footer />
</> </>
); );
}; };
export default Privacy; export default Privacy;

View file

@ -8,7 +8,7 @@ export const Note = styled.p`
margin: 16px 0; margin: 16px 0;
box-sizing: border-box; box-sizing: border-box;
font-weight: 500; font-weight: 500;
line-height: 1.6em; line-height: 1.6em;
& a { & a {
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight}; color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 2443 2500" style="enable-background:new 0 0 2443 2500;" xml:space="preserve"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 2443 2500" style="enable-background:new 0 0 2443 2500;" xml:space="preserve">
<path style="fill:#FFFFFF" d="M1245.8,1018.7V1481h686.5c-13.8,114.9-88.6,287.9-254.7,404.2v0c-105.2,73.4-246.4,124.6-431.8,124.6 <path style="fill:#FFFFFF" d="M1245.8,1018.7V1481h686.5c-13.8,114.9-88.6,287.9-254.7,404.2v0c-105.2,73.4-246.4,124.6-431.8,124.6
c-329.4,0-609-217.3-708.7-517.7h0l0,0v0c-26.3-77.5-41.5-160.6-41.5-246.4c0-85.8,15.2-168.9,40.1-246.4v0 c-329.4,0-609-217.3-708.7-517.7h0l0,0v0c-26.3-77.5-41.5-160.6-41.5-246.4c0-85.8,15.2-168.9,40.1-246.4v0
c101-300.4,380.6-517.7,710.1-517.7v0c233.9,0,391.7,101,481.7,185.5l351.6-343.3C1863.2,123.2,1582.2,0,1245.8,0 c101-300.4,380.6-517.7,710.1-517.7v0c233.9,0,391.7,101,481.7,185.5l351.6-343.3C1863.2,123.2,1582.2,0,1245.8,0
C758.6,0,337.8,279.6,133,686.5h0h0C48.6,855.4,0.1,1045,0.1,1245.7S48.6,1636,133,1804.9h0l0,0 C758.6,0,337.8,279.6,133,686.5h0h0C48.6,855.4,0.1,1045,0.1,1245.7S48.6,1636,133,1804.9h0l0,0
c204.8,406.9,625.6,686.5,1112.8,686.5c336.3,0,618.7-110.7,824.9-301.7l0,0h0c235.3-217.3,370.9-537,370.9-916.3 c204.8,406.9,625.6,686.5,1112.8,686.5c336.3,0,618.7-110.7,824.9-301.7l0,0h0c235.3-217.3,370.9-537,370.9-916.3
c0-102.4-8.3-177.2-26.3-254.7H1245.8z"/> c0-102.4-8.3-177.2-26.3-254.7H1245.8z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 854 B

After

Width:  |  Height:  |  Size: 859 B

File diff suppressed because it is too large Load diff

View file

@ -1,45 +1,45 @@
import axios from 'axios'; import axios from 'axios';
export const instance = axios.create({ export const instance = axios.create({
baseURL: process.env.NODE_ENV === 'production' ? 'https://api-dot-crabfit.uc.r.appspot.com' : 'http://localhost:8080', baseURL: process.env.NODE_ENV === 'production' ? 'https://api-dot-crabfit.uc.r.appspot.com' : 'http://localhost:8080',
timeout: 1000 * 300, timeout: 1000 * 300,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); });
const handleError = error => { const handleError = error => {
if (error.response && error.response.status) { if (error.response && error.response.status) {
console.log('[Error handler] res:', error.response); console.log('[Error handler] res:', error.response);
} }
return Promise.reject(error.response); return Promise.reject(error.response);
}; };
const api = { const api = {
get: async (endpoint, data) => { get: async (endpoint, data) => {
try { try {
const response = await instance.get(endpoint, data); const response = await instance.get(endpoint, data);
return Promise.resolve(response); return Promise.resolve(response);
} catch (error) { } catch (error) {
return handleError(error); return handleError(error);
} }
}, },
post: async (endpoint, data, options = {}) => { post: async (endpoint, data, options = {}) => {
try { try {
const response = await instance.post(endpoint, data, options); const response = await instance.post(endpoint, data, options);
return Promise.resolve(response); return Promise.resolve(response);
} catch (error) { } catch (error) {
return handleError(error); return handleError(error);
} }
}, },
patch: async (endpoint, data) => { patch: async (endpoint, data) => {
try { try {
const response = await instance.patch(endpoint, data); const response = await instance.patch(endpoint, data);
return Promise.resolve(response); return Promise.resolve(response);
} catch (error) { } catch (error) {
return handleError(error); return handleError(error);
} }
}, },
}; };
export default api; export default api;

View file

@ -1,26 +1,26 @@
const theme = { const theme = {
light: { light: {
mode: 'light', mode: 'light',
background: '#FFFFFF', background: '#FFFFFF',
text: '#000000', text: '#000000',
primary: '#F79E00', primary: '#F79E00',
primaryDark: '#F48600', primaryDark: '#F48600',
primaryLight: '#F4BB60', primaryLight: '#F4BB60',
primaryBackground: '#FEF2DD', primaryBackground: '#FEF2DD',
error: '#D32F2F', error: '#D32F2F',
loading: '#DDDDDD', loading: '#DDDDDD',
}, },
dark: { dark: {
mode: 'dark', mode: 'dark',
background: '#111111', background: '#111111',
text: '#DDDDDD', text: '#DDDDDD',
primary: '#F79E00', primary: '#F79E00',
primaryDark: '#CC7313', primaryDark: '#CC7313',
primaryLight: '#F4BB60', primaryLight: '#F4BB60',
primaryBackground: '#30240F', primaryBackground: '#30240F',
error: '#E53935', error: '#E53935',
loading: '#444444', loading: '#444444',
}, },
}; };
export default theme; export default theme;