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-family: Karla;
src: url('fonts/karla-variable.ttf') format('truetype');
font-weight: 1 999;
font-family: Karla;
src: url('fonts/karla-variable.ttf') format('truetype');
font-weight: 1 999;
}
@font-face {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Before

Width:  |  Height:  |  Size: 854 B

After

Width:  |  Height:  |  Size: 859 B

File diff suppressed because it is too large Load diff

View file

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

View file

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