commit
968a7423e9
11
crabfit-frontend/config-overrides.js
Normal file
11
crabfit-frontend/config-overrides.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
module.exports = function override(config, env) {
|
||||||
|
config.output.filename = env === 'production'
|
||||||
|
? 'static/js/[name].[contenthash].js'
|
||||||
|
: env === 'development' && 'static/js/bundle.js';
|
||||||
|
|
||||||
|
config.output.chunkFilename = env === 'production'
|
||||||
|
? 'static/js/[name].[contenthash].chunk.js'
|
||||||
|
: env === 'development' && 'static/js/[name].chunk.js';
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
@ -41,9 +41,9 @@
|
||||||
"zustand": "^3.3.2"
|
"zustand": "^3.3.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-app-rewired start",
|
||||||
"build": "react-scripts build",
|
"build": "react-app-rewired build",
|
||||||
"test": "react-scripts test",
|
"test": "react-app-rewired test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
|
@ -63,5 +63,8 @@
|
||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"react-app-rewired": "^2.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript style="font-family: Karla, sans-serif; text-align: center; margin: 20vh 0; display: block;"><h1>🦀 Crab Fit doesn't work without Javascript 🏋️</h1><p>Enable Javascript or try a different browser.<p></noscript>
|
<noscript style="font-family: Karla, sans-serif; text-align: center; margin: 20vh 0; display: block;"><h1>🦀 Crab Fit doesn't work without Javascript 🏋️</h1><p>Enable Javascript or try a different browser.</p></noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,11 @@ const App = () => {
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
|
<Settings />
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" exact render={props => (
|
<Route path="/" exact render={props => (
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
|
|
@ -135,10 +140,6 @@ const App = () => {
|
||||||
)} />
|
)} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
||||||
<Suspense fallback={<Loading />}>
|
|
||||||
<Settings />
|
|
||||||
</Suspense>
|
|
||||||
|
|
||||||
{eggVisible && <Egg eggKey={eggKey} onClose={() => setEggVisible(false)} />}
|
{eggVisible && <Egg eggKey={eggKey} onClose={() => setEggVisible(false)} />}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect, useRef, Fragment } from 'react';
|
import { useState, useEffect, useRef, useMemo, Fragment } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import localeData from 'dayjs/plugin/localeData';
|
import localeData from 'dayjs/plugin/localeData';
|
||||||
|
|
@ -65,6 +65,94 @@ const AvailabilityViewer = ({
|
||||||
setTouched(people.length <= 1);
|
setTouched(people.length <= 1);
|
||||||
}, [people]);
|
}, [people]);
|
||||||
|
|
||||||
|
const heatmap = useMemo(() => (
|
||||||
|
<Container>
|
||||||
|
<TimeLabels>
|
||||||
|
{!!timeLabels.length && timeLabels.map((label, i) =>
|
||||||
|
<TimeSpace key={i}>
|
||||||
|
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
|
||||||
|
</TimeSpace>
|
||||||
|
)}
|
||||||
|
</TimeLabels>
|
||||||
|
{dates.map((date, i) => {
|
||||||
|
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
|
||||||
|
const last = dates.length === i+1 || (isSpecificDates ? dayjs(dates[i+1], 'DDMMYYYY') : dayjs().day(dates[i+1])).diff(parsedDate, 'day') > 1;
|
||||||
|
return (
|
||||||
|
<Fragment key={i}>
|
||||||
|
<Date>
|
||||||
|
{isSpecificDates && <DateLabel locale={locale}>{parsedDate.format('MMM D')}</DateLabel>}
|
||||||
|
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
|
||||||
|
|
||||||
|
<Times
|
||||||
|
borderRight={last}
|
||||||
|
borderLeft={i === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[i-1], 'DDMMYYYY') : dayjs().day(dates[i-1]), 'day') > 1}
|
||||||
|
>
|
||||||
|
{timeLabels.map((timeLabel, i) => {
|
||||||
|
if (!timeLabel.time) return null;
|
||||||
|
if (!times.includes(`${timeLabel.time}-${date}`)) {
|
||||||
|
return (
|
||||||
|
<TimeSpace key={i} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const time = `${timeLabel.time}-${date}`;
|
||||||
|
const peopleHere = tempFocus !== null
|
||||||
|
? people.filter(person => person.availability.includes(time) && tempFocus === person.name).map(person => person.name)
|
||||||
|
: people.filter(person => person.availability.includes(time) && filteredPeople.includes(person.name)).map(person => person.name);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Time
|
||||||
|
key={i}
|
||||||
|
time={time}
|
||||||
|
className="time"
|
||||||
|
peopleCount={focusCount !== null && focusCount !== peopleHere.length ? 0 : peopleHere.length}
|
||||||
|
aria-label={peopleHere.join(', ')}
|
||||||
|
maxPeople={tempFocus !== null ? 1 : Math.min(max, filteredPeople.length)}
|
||||||
|
minPeople={tempFocus !== null ? 0 : Math.min(min, filteredPeople.length)}
|
||||||
|
highlight={highlight}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
const cellBox = e.currentTarget.getBoundingClientRect();
|
||||||
|
const wrapperBox = wrapper?.current?.getBoundingClientRect() ?? { x: 0, y: 0 };
|
||||||
|
const timeText = timeFormat === '12h' ? `h${locales[locale].separator}mma` : `HH${locales[locale].separator}mm`;
|
||||||
|
setTooltip({
|
||||||
|
x: Math.round(cellBox.x-wrapperBox.x + cellBox.width/2),
|
||||||
|
y: Math.round(cellBox.y-wrapperBox.y + cellBox.height)+6,
|
||||||
|
available: `${peopleHere.length} / ${people.length} ${t('event:available')}`,
|
||||||
|
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(2, 4)).format(isSpecificDates ? `${timeText} ddd, D MMM YYYY` : `${timeText} ddd`),
|
||||||
|
people: peopleHere,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setTooltip(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Times>
|
||||||
|
</Date>
|
||||||
|
{last && dates.length !== i+1 && (
|
||||||
|
<Spacer />
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Container>
|
||||||
|
), [
|
||||||
|
people,
|
||||||
|
filteredPeople,
|
||||||
|
tempFocus,
|
||||||
|
focusCount,
|
||||||
|
highlight,
|
||||||
|
locale,
|
||||||
|
dates,
|
||||||
|
isSpecificDates,
|
||||||
|
max,
|
||||||
|
min,
|
||||||
|
t,
|
||||||
|
timeFormat,
|
||||||
|
timeLabels,
|
||||||
|
times,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledMain>
|
<StyledMain>
|
||||||
|
|
@ -108,76 +196,8 @@ const AvailabilityViewer = ({
|
||||||
|
|
||||||
<Wrapper ref={wrapper}>
|
<Wrapper ref={wrapper}>
|
||||||
<ScrollWrapper>
|
<ScrollWrapper>
|
||||||
<Container>
|
{heatmap}
|
||||||
<TimeLabels>
|
|
||||||
{!!timeLabels.length && timeLabels.map((label, i) =>
|
|
||||||
<TimeSpace key={i}>
|
|
||||||
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
|
|
||||||
</TimeSpace>
|
|
||||||
)}
|
|
||||||
</TimeLabels>
|
|
||||||
{dates.map((date, i) => {
|
|
||||||
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
|
|
||||||
const last = dates.length === i+1 || (isSpecificDates ? dayjs(dates[i+1], 'DDMMYYYY') : dayjs().day(dates[i+1])).diff(parsedDate, 'day') > 1;
|
|
||||||
return (
|
|
||||||
<Fragment key={i}>
|
|
||||||
<Date>
|
|
||||||
{isSpecificDates && <DateLabel locale={locale}>{parsedDate.format('MMM D')}</DateLabel>}
|
|
||||||
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
|
|
||||||
|
|
||||||
<Times
|
|
||||||
borderRight={last}
|
|
||||||
borderLeft={i === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[i-1], 'DDMMYYYY') : dayjs().day(dates[i-1]), 'day') > 1}
|
|
||||||
>
|
|
||||||
{timeLabels.map((timeLabel, i) => {
|
|
||||||
if (!timeLabel.time) return null;
|
|
||||||
if (!times.includes(`${timeLabel.time}-${date}`)) {
|
|
||||||
return (
|
|
||||||
<TimeSpace key={i} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const time = `${timeLabel.time}-${date}`;
|
|
||||||
const peopleHere = tempFocus !== null
|
|
||||||
? people.filter(person => person.availability.includes(time) && tempFocus === person.name).map(person => person.name)
|
|
||||||
: people.filter(person => person.availability.includes(time) && filteredPeople.includes(person.name)).map(person => person.name);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Time
|
|
||||||
key={i}
|
|
||||||
time={time}
|
|
||||||
className="time"
|
|
||||||
peopleCount={focusCount !== null && focusCount !== peopleHere.length ? 0 : peopleHere.length}
|
|
||||||
aria-label={peopleHere.join(', ')}
|
|
||||||
maxPeople={tempFocus !== null ? 1 : Math.min(max, filteredPeople.length)}
|
|
||||||
minPeople={tempFocus !== null ? 0 : Math.min(min, filteredPeople.length)}
|
|
||||||
highlight={highlight}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
const cellBox = e.currentTarget.getBoundingClientRect();
|
|
||||||
const wrapperBox = wrapper?.current?.getBoundingClientRect() ?? { x: 0, y: 0 };
|
|
||||||
const timeText = timeFormat === '12h' ? `h${locales[locale].separator}mma` : `HH${locales[locale].separator}mm`;
|
|
||||||
setTooltip({
|
|
||||||
x: Math.round(cellBox.x-wrapperBox.x + cellBox.width/2),
|
|
||||||
y: Math.round(cellBox.y-wrapperBox.y + cellBox.height)+6,
|
|
||||||
available: `${peopleHere.length} / ${people.length} ${t('event:available')}`,
|
|
||||||
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(2, 4)).format(isSpecificDates ? `${timeText} ddd, D MMM YYYY` : `${timeText} ddd`),
|
|
||||||
people: peopleHere,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
setTooltip(null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Times>
|
|
||||||
</Date>
|
|
||||||
{last && dates.length !== i+1 && (
|
|
||||||
<Spacer />
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Container>
|
|
||||||
{tooltip && (
|
{tooltip && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
x={tooltip.x}
|
x={tooltip.x}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ export const Time = styled.div`
|
||||||
border-top: 2px dotted ${props.theme.text};
|
border-top: 2px dotted ${props.theme.text};
|
||||||
`}
|
`}
|
||||||
|
|
||||||
${props => props.highlight && props.peopleCount === props.maxPeople ? `
|
${props => props.highlight && props.peopleCount === props.maxPeople && props.peopleCount > 0 ? `
|
||||||
background-image: repeating-linear-gradient(
|
background-image: repeating-linear-gradient(
|
||||||
45deg,
|
45deg,
|
||||||
${props.theme.primary},
|
${props.theme.primary},
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,17 @@ const CalendarField = ({
|
||||||
selected={selectedDates.includes(date.format('DDMMYYYY'))}
|
selected={selectedDates.includes(date.format('DDMMYYYY'))}
|
||||||
selecting={selectingDates.includes(date)}
|
selecting={selectingDates.includes(date)}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
onPointerDown={(e) => {
|
type="button"
|
||||||
|
onKeyPress={e => {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
if (selectedDates.includes(date.format('DDMMYYYY'))) {
|
||||||
|
setSelectedDates(selectedDates.filter(d => d !== date.format('DDMMYYYY')));
|
||||||
|
} else {
|
||||||
|
setSelectedDates([...selectedDates, date.format('DDMMYYYY')]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onPointerDown={e => {
|
||||||
startPos.current = {x, y};
|
startPos.current = {x, y};
|
||||||
setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
|
setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
|
||||||
setSelectingDates([date]);
|
setSelectingDates([date]);
|
||||||
|
|
|
||||||
|
|
@ -51,21 +51,27 @@ export const CalendarBody = styled.div`
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: repeat(7, 1fr);
|
||||||
grid-gap: 2px;
|
grid-gap: 2px;
|
||||||
|
|
||||||
& div:first-of-type {
|
& button:first-of-type {
|
||||||
border-top-left-radius: 3px;
|
border-top-left-radius: 3px;
|
||||||
}
|
}
|
||||||
& div:nth-of-type(7) {
|
& button:nth-of-type(7) {
|
||||||
border-top-right-radius: 3px;
|
border-top-right-radius: 3px;
|
||||||
}
|
}
|
||||||
& div:nth-last-of-type(7) {
|
& button:nth-last-of-type(7) {
|
||||||
border-bottom-left-radius: 3px;
|
border-bottom-left-radius: 3px;
|
||||||
}
|
}
|
||||||
& div:last-of-type {
|
& button:last-of-type {
|
||||||
border-bottom-right-radius: 3px;
|
border-bottom-right-radius: 3px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Date = styled.div`
|
export const Date = styled.button`
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
background-color: ${props => props.theme.primaryBackground};
|
background-color: ${props => props.theme.primaryBackground};
|
||||||
border: 1px solid ${props => props.theme.primaryLight};
|
border: 1px solid ${props => props.theme.primaryLight};
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,33 @@
|
||||||
import { useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Button } from 'components';
|
import { Button } from 'components';
|
||||||
import { useTWAStore } from 'stores';
|
import { useTWAStore } from 'stores';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Wrapper,
|
||||||
|
Options,
|
||||||
|
} from './donateStyle';
|
||||||
|
|
||||||
const PAYMENT_METHOD = 'https://play.google.com/billing';
|
const PAYMENT_METHOD = 'https://play.google.com/billing';
|
||||||
const SKU = 'crab_donation';
|
const SKU = 'crab_donation';
|
||||||
|
|
||||||
const Donate = ({ onDonate = null }) => {
|
const Donate = () => {
|
||||||
const store = useTWAStore();
|
const store = useTWAStore();
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
|
const firstLinkRef = useRef();
|
||||||
|
const buttonRef = useRef();
|
||||||
|
const modalRef = useRef();
|
||||||
|
const [isOpen, _setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const setIsOpen = open => {
|
||||||
|
_setIsOpen(open);
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
window.setTimeout(() => firstLinkRef.current.focus(), 150);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (store.TWA === undefined) {
|
if (store.TWA === undefined) {
|
||||||
store.setTWA(document.referrer.includes('android-app://fit.crab'));
|
store.setTWA(document.referrer.includes('android-app://fit.crab'));
|
||||||
|
|
@ -71,7 +89,7 @@ const Donate = ({ onDonate = null }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: 6, marginLeft: 12 }}>
|
<Wrapper>
|
||||||
<a
|
<a
|
||||||
onClick={event => {
|
onClick={event => {
|
||||||
gtag('event', 'donate', { 'event_category': 'donate' });
|
gtag('event', 'donate', { 'event_category': 'donate' });
|
||||||
|
|
@ -82,14 +100,15 @@ const Donate = ({ onDonate = null }) => {
|
||||||
alert(t('donate.messages.error'));
|
alert(t('donate.messages.error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (onDonate !== null) {
|
} else {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onDonate();
|
setIsOpen(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=5"
|
href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=5"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
|
ref={buttonRef}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
buttonHeight="30px"
|
buttonHeight="30px"
|
||||||
|
|
@ -99,7 +118,21 @@ const Donate = ({ onDonate = null }) => {
|
||||||
title={t('donate.title')}
|
title={t('donate.title')}
|
||||||
>{t('donate.button')}</Button>
|
>{t('donate.button')}</Button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
|
||||||
|
<Options
|
||||||
|
isOpen={isOpen}
|
||||||
|
ref={modalRef}
|
||||||
|
onBlur={e => {
|
||||||
|
if (modalRef.current.contains(e.relatedTarget)) return;
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a onClick={() => setIsOpen(false)} ref={firstLinkRef} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=2" target="_blank" rel="noreferrer">{t('donate.options.$2')}</a>
|
||||||
|
<a onClick={() => setIsOpen(false)} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=5" target="_blank" rel="noreferrer"><strong>{t('donate.options.$5')}</strong></a>
|
||||||
|
<a onClick={() => setIsOpen(false)} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=10" target="_blank" rel="noreferrer">{t('donate.options.$10')}</a>
|
||||||
|
<a onClick={() => setIsOpen(false)} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD" target="_blank" rel="noreferrer">{t('donate.options.choose')}</a>
|
||||||
|
</Options>
|
||||||
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
52
crabfit-frontend/src/components/Donate/donateStyle.ts
Normal file
52
crabfit-frontend/src/components/Donate/donateStyle.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
export const Wrapper = styled.div`
|
||||||
|
margin-top: 6px;
|
||||||
|
margin-left: 12px;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Options = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 20px);
|
||||||
|
right: 0;
|
||||||
|
background-color: ${props => props.theme.background};
|
||||||
|
${props => props.theme.mode === 'dark' && `
|
||||||
|
border: 1px solid ${props.theme.primaryBackground};
|
||||||
|
`}
|
||||||
|
z-index: 60;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: calc(100vw - 20px);
|
||||||
|
box-shadow: 0 3px 6px 0 rgba(0,0,0,.3);
|
||||||
|
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(5px);
|
||||||
|
transition: opacity .15s, transform .15s, visibility .15s;
|
||||||
|
|
||||||
|
${props => props.isOpen && `
|
||||||
|
pointer-events: all;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
visibility: visible;
|
||||||
|
`}
|
||||||
|
|
||||||
|
& a {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
padding: 4px 20px;
|
||||||
|
margin: 6px 0;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 100px;
|
||||||
|
background-color: ${props => props.theme.primary};
|
||||||
|
color: ${props => props.theme.background};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
@ -1,28 +1,15 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Donate } from 'components';
|
import { Donate } from 'components';
|
||||||
import { Wrapper, Link } from './footerStyle';
|
import { Wrapper } from './footerStyle';
|
||||||
|
|
||||||
const Footer = (props) => {
|
const Footer = (props) => {
|
||||||
const [donateMode, setDonateMode] = useState(false);
|
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper id="donate" donateMode={donateMode} {...props}>
|
<Wrapper id="donate" {...props}>
|
||||||
{donateMode ? (
|
<span>{t('donate.info')}</span>
|
||||||
<>
|
<Donate />
|
||||||
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=2" target="_blank">{t('donate.options.$2')}</Link>
|
|
||||||
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=5" target="_blank"><strong>{t('donate.options.$5')}</strong></Link>
|
|
||||||
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=10" target="_blank">{t('donate.options.$10')}</Link>
|
|
||||||
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD" target="_blank">{t('donate.options.choose')}</Link>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span>{t('donate.info')}</span>
|
|
||||||
<Donate onDonate={() => setDonateMode(true)} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -19,20 +19,4 @@ export const Wrapper = styled.footer`
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
|
||||||
${props => props.donateMode && `
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Link = styled.a`
|
|
||||||
padding: 11px 10px;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
& strong {
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ const Legend = ({
|
||||||
<Grade
|
<Grade
|
||||||
key={i}
|
key={i}
|
||||||
color={`${theme.primary}${Math.round((i/(max))*255).toString(16)}`}
|
color={`${theme.primary}${Math.round((i/(max))*255).toString(16)}`}
|
||||||
highlight={highlight && i === max}
|
highlight={highlight && i === max && max > 0}
|
||||||
onMouseOver={() => onSegmentFocus(i)}
|
onMouseOver={() => onSegmentFocus(i)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
@ -27,9 +27,27 @@ const setDefaults = (lang, store) => {
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, _setIsOpen] = useState(false);
|
||||||
const { t, i18n } = useTranslation('common');
|
const { t, i18n } = useTranslation('common');
|
||||||
const setLocale = useLocaleUpdateStore(state => state.setLocale);
|
const setLocale = useLocaleUpdateStore(state => state.setLocale);
|
||||||
|
const firstControlRef = useRef();
|
||||||
|
|
||||||
|
const onEsc = e => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setIsOpen = open => {
|
||||||
|
_setIsOpen(open);
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
window.setTimeout(() => firstControlRef.current.focus(), 150);
|
||||||
|
document.addEventListener('keyup', onEsc, true);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('keyup', onEsc);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Object.keys(locales).includes(i18n.language)) {
|
if (Object.keys(locales).includes(i18n.language)) {
|
||||||
|
|
@ -57,7 +75,6 @@ const Settings = () => {
|
||||||
<>
|
<>
|
||||||
<OpenButton
|
<OpenButton
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
tabIndex="1"
|
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsOpen(!isOpen)} title={t('options.name')}
|
onClick={() => setIsOpen(!isOpen)} title={t('options.name')}
|
||||||
>
|
>
|
||||||
|
|
@ -78,6 +95,7 @@ const Settings = () => {
|
||||||
}}
|
}}
|
||||||
value={store.weekStart === 0 ? 'Sunday' : 'Monday'}
|
value={store.weekStart === 0 ? 'Sunday' : 'Monday'}
|
||||||
onChange={value => store.setWeekStart(value === 'Sunday' ? 0 : 1)}
|
onChange={value => store.setWeekStart(value === 'Sunday' ? 0 : 1)}
|
||||||
|
inputRef={firstControlRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToggleField
|
<ToggleField
|
||||||
|
|
|
||||||
|
|
@ -64,12 +64,14 @@ export const Modal = styled.div`
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-10px);
|
transform: translateY(-10px);
|
||||||
transition: opacity .15s, transform .15s;
|
visibility: hidden;
|
||||||
|
transition: opacity .15s, transform .15s, visibility .15s;
|
||||||
|
|
||||||
${props => props.isOpen && `
|
${props => props.isOpen && `
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
|
visibility: visible;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,17 @@ const TimeRangeField = ({
|
||||||
step = Math.abs(step);
|
step = Math.abs(step);
|
||||||
setStart(step);
|
setStart(step);
|
||||||
}}
|
}}
|
||||||
|
tabIndex="0"
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
setStart(Math.max(start-1, 0));
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
setStart(Math.min(start+1, 24));
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Handle
|
<Handle
|
||||||
value={end}
|
value={end}
|
||||||
|
|
@ -113,6 +124,17 @@ const TimeRangeField = ({
|
||||||
step = Math.abs(step);
|
step = Math.abs(step);
|
||||||
setEnd(step);
|
setEnd(step);
|
||||||
}}
|
}}
|
||||||
|
tabIndex="0"
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
setEnd(Math.max(end-1, 0));
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
setEnd(Math.min(end+1, 24));
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Range>
|
</Range>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ const ToggleField = ({
|
||||||
options = [],
|
options = [],
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
inputRef,
|
||||||
...props
|
...props
|
||||||
}) => (
|
}) => (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
|
|
@ -30,6 +31,7 @@ const ToggleField = ({
|
||||||
id={`${name}-${label}`}
|
id={`${name}-${label}`}
|
||||||
checked={value === key}
|
checked={value === key}
|
||||||
onChange={() => onChange(key)}
|
onChange={() => onChange(key)}
|
||||||
|
ref={inputRef}
|
||||||
/>
|
/>
|
||||||
<LabelButton htmlFor={`${name}-${label}`}>{label}</LabelButton>
|
<LabelButton htmlFor={`${name}-${label}`}>{label}</LabelButton>
|
||||||
</Option>
|
</Option>
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,16 @@ export const ToggleContainer = styled.div`
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid ${props => props.theme.primary};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
&:focus-within {
|
|
||||||
outline: Highlight auto 1px;
|
&:focus-within label {
|
||||||
outline: -webkit-focus-ring-color auto 1px;
|
box-shadow: inset 0 -3px 0 0 var(--focus-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div:first-of-type label {
|
||||||
|
border-end-start-radius: 2px;
|
||||||
|
}
|
||||||
|
& > div:last-of-type label {
|
||||||
|
border-end-end-radius: 2px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -35,6 +42,7 @@ export const HiddenInput = styled.input`
|
||||||
&:checked + label {
|
&:checked + label {
|
||||||
color: ${props => props.theme.background};
|
color: ${props => props.theme.background};
|
||||||
background-color: ${props => props.theme.primary};
|
background-color: ${props => props.theme.primary};
|
||||||
|
--focus-color: ${props => props.theme.primaryDark};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -48,4 +56,6 @@ export const LabelButton = styled.label`
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
transition: box-shadow .15s;
|
||||||
|
--focus-color: ${props => props.theme.primary};
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -9151,6 +9151,13 @@ react-app-polyfill@^2.0.0:
|
||||||
regenerator-runtime "^0.13.7"
|
regenerator-runtime "^0.13.7"
|
||||||
whatwg-fetch "^3.4.1"
|
whatwg-fetch "^3.4.1"
|
||||||
|
|
||||||
|
react-app-rewired@^2.1.8:
|
||||||
|
version "2.1.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-app-rewired/-/react-app-rewired-2.1.8.tgz#e192f93b98daf96889418d33d3e86cf863812b56"
|
||||||
|
integrity sha512-wjXPdKPLscA7mn0I1de1NHrbfWdXz4S1ladaGgHVKdn1hTgKK5N6EdGIJM0KrS6bKnJBj7WuqJroDTsPKKr66Q==
|
||||||
|
dependencies:
|
||||||
|
semver "^5.6.0"
|
||||||
|
|
||||||
react-dev-utils@^11.0.3:
|
react-dev-utils@^11.0.3:
|
||||||
version "11.0.3"
|
version "11.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.3.tgz#b61ed499c7d74f447d4faddcc547e5e671e97c08"
|
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.3.tgz#b61ed499c7d74f447d4faddcc547e5e671e97c08"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue