Merge pull request #19 from GRA0007/dev

Performance, a11y and caching
This commit is contained in:
Benjamin Grant 2021-06-01 04:54:25 +10:00 committed by GitHub
commit 968a7423e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 300 additions and 132 deletions

View 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;
}

View file

@ -41,9 +41,9 @@
"zustand": "^3.3.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
"eslintConfig": {
@ -63,5 +63,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"react-app-rewired": "^2.1.8"
}
}

View file

@ -36,7 +36,7 @@
</script>
</head>
<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>
</body>
</html>

View file

@ -107,6 +107,11 @@ const App = () => {
},
})}
/>
<Suspense fallback={<Loading />}>
<Settings />
</Suspense>
<Switch>
<Route path="/" exact render={props => (
<Suspense fallback={<Loading />}>
@ -135,10 +140,6 @@ const App = () => {
)} />
</Switch>
<Suspense fallback={<Loading />}>
<Settings />
</Suspense>
{eggVisible && <Egg eggKey={eggKey} onClose={() => setEggVisible(false)} />}
</ThemeProvider>
</BrowserRouter>

View file

@ -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 dayjs from 'dayjs';
import localeData from 'dayjs/plugin/localeData';
@ -65,49 +65,7 @@ const AvailabilityViewer = ({
setTouched(people.length <= 1);
}, [people]);
return (
<>
<StyledMain>
<Legend
min={Math.min(min, filteredPeople.length)}
max={Math.min(max, filteredPeople.length)}
total={people.filter(p => p.availability.length > 0).length}
onSegmentFocus={count => setFocusCount(count)}
/>
<Center style={{textAlign: 'center'}}>{t('event:group.info1')}</Center>
{people.length > 1 && (
<>
<Center style={{textAlign: 'center'}}>{t('event:group.info2')}</Center>
<People>
{people.map((person, i) =>
<Person
key={i}
filtered={filteredPeople.includes(person.name)}
onClick={() => {
setTempFocus(null);
if (filteredPeople.includes(person.name)) {
if (!touched) {
setTouched(true);
setFilteredPeople([person.name]);
} else {
setFilteredPeople(filteredPeople.filter(n => n !== person.name));
}
} else {
setFilteredPeople([...filteredPeople, person.name]);
}
}}
onMouseOver={() => setTempFocus(person.name)}
onMouseOut={() => setTempFocus(null)}
title={person.created && dayjs.unix(person.created).fromNow()}
>{person.name}</Person>
)}
</People>
</>
)}
</StyledMain>
<Wrapper ref={wrapper}>
<ScrollWrapper>
const heatmap = useMemo(() => (
<Container>
<TimeLabels>
{!!timeLabels.length && timeLabels.map((label, i) =>
@ -178,6 +136,68 @@ const AvailabilityViewer = ({
);
})}
</Container>
), [
people,
filteredPeople,
tempFocus,
focusCount,
highlight,
locale,
dates,
isSpecificDates,
max,
min,
t,
timeFormat,
timeLabels,
times,
]);
return (
<>
<StyledMain>
<Legend
min={Math.min(min, filteredPeople.length)}
max={Math.min(max, filteredPeople.length)}
total={people.filter(p => p.availability.length > 0).length}
onSegmentFocus={count => setFocusCount(count)}
/>
<Center style={{textAlign: 'center'}}>{t('event:group.info1')}</Center>
{people.length > 1 && (
<>
<Center style={{textAlign: 'center'}}>{t('event:group.info2')}</Center>
<People>
{people.map((person, i) =>
<Person
key={i}
filtered={filteredPeople.includes(person.name)}
onClick={() => {
setTempFocus(null);
if (filteredPeople.includes(person.name)) {
if (!touched) {
setTouched(true);
setFilteredPeople([person.name]);
} else {
setFilteredPeople(filteredPeople.filter(n => n !== person.name));
}
} else {
setFilteredPeople([...filteredPeople, person.name]);
}
}}
onMouseOver={() => setTempFocus(person.name)}
onMouseOut={() => setTempFocus(null)}
title={person.created && dayjs.unix(person.created).fromNow()}
>{person.name}</Person>
)}
</People>
</>
)}
</StyledMain>
<Wrapper ref={wrapper}>
<ScrollWrapper>
{heatmap}
{tooltip && (
<Tooltip
x={tooltip.x}

View file

@ -80,7 +80,7 @@ export const Time = styled.div`
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(
45deg,
${props.theme.primary},

View file

@ -168,7 +168,17 @@ const CalendarField = ({
selected={selectedDates.includes(date.format('DDMMYYYY'))}
selecting={selectingDates.includes(date)}
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};
setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
setSelectingDates([date]);

View file

@ -51,21 +51,27 @@ export const CalendarBody = styled.div`
grid-template-columns: repeat(7, 1fr);
grid-gap: 2px;
& div:first-of-type {
& button:first-of-type {
border-top-left-radius: 3px;
}
& div:nth-of-type(7) {
& button:nth-of-type(7) {
border-top-right-radius: 3px;
}
& div:nth-last-of-type(7) {
& button:nth-last-of-type(7) {
border-bottom-left-radius: 3px;
}
& div:last-of-type {
& button:last-of-type {
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};
border: 1px solid ${props => props.theme.primaryLight};
display: flex;

View file

@ -1,15 +1,33 @@
import { useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { Button } from 'components';
import { useTWAStore } from 'stores';
import { useTranslation } from 'react-i18next';
import {
Wrapper,
Options,
} from './donateStyle';
const PAYMENT_METHOD = 'https://play.google.com/billing';
const SKU = 'crab_donation';
const Donate = ({ onDonate = null }) => {
const Donate = () => {
const store = useTWAStore();
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(() => {
if (store.TWA === undefined) {
store.setTWA(document.referrer.includes('android-app://fit.crab'));
@ -71,7 +89,7 @@ const Donate = ({ onDonate = null }) => {
};
return (
<div style={{ marginTop: 6, marginLeft: 12 }}>
<Wrapper>
<a
onClick={event => {
gtag('event', 'donate', { 'event_category': 'donate' });
@ -82,14 +100,15 @@ const Donate = ({ onDonate = null }) => {
alert(t('donate.messages.error'));
}
}
} else if (onDonate !== null) {
} else {
event.preventDefault();
onDonate();
setIsOpen(true);
}
}}
href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD&amount=5"
target="_blank"
rel="noreferrer"
ref={buttonRef}
>
<Button
buttonHeight="30px"
@ -99,7 +118,21 @@ const Donate = ({ onDonate = null }) => {
title={t('donate.title')}
>{t('donate.button')}</Button>
</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&currency_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&currency_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&currency_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&currency_code=AUD" target="_blank" rel="noreferrer">{t('donate.options.choose')}</a>
</Options>
</Wrapper>
);
}

View 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;
}
}
`;

View file

@ -1,28 +1,15 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Donate } from 'components';
import { Wrapper, Link } from './footerStyle';
import { Wrapper } from './footerStyle';
const Footer = (props) => {
const [donateMode, setDonateMode] = useState(false);
const { t } = useTranslation('common');
return (
<Wrapper id="donate" donateMode={donateMode} {...props}>
{donateMode ? (
<>
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_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&currency_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&currency_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&currency_code=AUD" target="_blank">{t('donate.options.choose')}</Link>
</>
) : (
<>
<Wrapper id="donate" {...props}>
<span>{t('donate.info')}</span>
<Donate onDonate={() => setDonateMode(true)} />
</>
)}
<Donate />
</Wrapper>
);
};

View file

@ -19,20 +19,4 @@ export const Wrapper = styled.footer`
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;
}
`;

View file

@ -34,7 +34,7 @@ const Legend = ({
<Grade
key={i}
color={`${theme.primary}${Math.round((i/(max))*255).toString(16)}`}
highlight={highlight && i === max}
highlight={highlight && i === max && max > 0}
onMouseOver={() => onSegmentFocus(i)}
/>
)}

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useTheme } from '@emotion/react';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
@ -27,9 +27,27 @@ const setDefaults = (lang, store) => {
const Settings = () => {
const theme = useTheme();
const store = useSettingsStore();
const [isOpen, setIsOpen] = useState(false);
const [isOpen, _setIsOpen] = useState(false);
const { t, i18n } = useTranslation('common');
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(() => {
if (Object.keys(locales).includes(i18n.language)) {
@ -57,7 +75,6 @@ const Settings = () => {
<>
<OpenButton
isOpen={isOpen}
tabIndex="1"
type="button"
onClick={() => setIsOpen(!isOpen)} title={t('options.name')}
>
@ -78,6 +95,7 @@ const Settings = () => {
}}
value={store.weekStart === 0 ? 'Sunday' : 'Monday'}
onChange={value => store.setWeekStart(value === 'Sunday' ? 0 : 1)}
inputRef={firstControlRef}
/>
<ToggleField

View file

@ -64,12 +64,14 @@ export const Modal = styled.div`
pointer-events: none;
opacity: 0;
transform: translateY(-10px);
transition: opacity .15s, transform .15s;
visibility: hidden;
transition: opacity .15s, transform .15s, visibility .15s;
${props => props.isOpen && `
pointer-events: all;
opacity: 1;
transform: translateY(0);
visibility: visible;
`}
`;

View file

@ -90,6 +90,17 @@ const TimeRangeField = ({
step = Math.abs(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
value={end}
@ -113,6 +124,17 @@ const TimeRangeField = ({
step = Math.abs(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>
</Wrapper>

View file

@ -15,6 +15,7 @@ const ToggleField = ({
options = [],
value,
onChange,
inputRef,
...props
}) => (
<Wrapper>
@ -30,6 +31,7 @@ const ToggleField = ({
id={`${name}-${label}`}
checked={value === key}
onChange={() => onChange(key)}
ref={inputRef}
/>
<LabelButton htmlFor={`${name}-${label}`}>{label}</LabelButton>
</Option>

View file

@ -9,9 +9,16 @@ export const ToggleContainer = styled.div`
border: 1px solid ${props => props.theme.primary};
border-radius: 3px;
overflow: hidden;
&:focus-within {
outline: Highlight auto 1px;
outline: -webkit-focus-ring-color auto 1px;
&:focus-within label {
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 {
color: ${props => props.theme.background};
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;
align-items: center;
justify-content: center;
transition: box-shadow .15s;
--focus-color: ${props => props.theme.primary};
`;

View file

@ -9151,6 +9151,13 @@ react-app-polyfill@^2.0.0:
regenerator-runtime "^0.13.7"
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:
version "11.0.3"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.3.tgz#b61ed499c7d74f447d4faddcc547e5e671e97c08"