diff --git a/crabfit-frontend/config-overrides.js b/crabfit-frontend/config-overrides.js new file mode 100644 index 0000000..44e3ac7 --- /dev/null +++ b/crabfit-frontend/config-overrides.js @@ -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; +} diff --git a/crabfit-frontend/package.json b/crabfit-frontend/package.json index aeefe16..32303b8 100644 --- a/crabfit-frontend/package.json +++ b/crabfit-frontend/package.json @@ -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" } } diff --git a/crabfit-frontend/public/index.html b/crabfit-frontend/public/index.html index 0ff263d..49cc9b7 100644 --- a/crabfit-frontend/public/index.html +++ b/crabfit-frontend/public/index.html @@ -36,7 +36,7 @@ -
diff --git a/crabfit-frontend/src/App.tsx b/crabfit-frontend/src/App.tsx index a31812d..743c72f 100644 --- a/crabfit-frontend/src/App.tsx +++ b/crabfit-frontend/src/App.tsx @@ -107,6 +107,11 @@ const App = () => { }, })} /> + + }> + + + ( }> @@ -135,10 +140,6 @@ const App = () => { )} /> - }> - - - {eggVisible && setEggVisible(false)} />} diff --git a/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx b/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx index 881eaee..4597830 100644 --- a/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx +++ b/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx @@ -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,6 +65,94 @@ const AvailabilityViewer = ({ setTouched(people.length <= 1); }, [people]); + const heatmap = useMemo(() => ( + + + {!!timeLabels.length && timeLabels.map((label, i) => + + {label.label?.length !== '' && {label.label}} + + )} + + {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 ( + + + {isSpecificDates && {parsedDate.format('MMM D')}} + {parsedDate.format('ddd')} + + 1} + > + {timeLabels.map((timeLabel, i) => { + if (!timeLabel.time) return null; + if (!times.includes(`${timeLabel.time}-${date}`)) { + return ( + + ); + } + 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 ( + + + {last && dates.length !== i+1 && ( + + )} + + ); + })} + + ), [ + people, + filteredPeople, + tempFocus, + focusCount, + highlight, + locale, + dates, + isSpecificDates, + max, + min, + t, + timeFormat, + timeLabels, + times, + ]); + return ( <> @@ -108,76 +196,8 @@ const AvailabilityViewer = ({ - - - {!!timeLabels.length && timeLabels.map((label, i) => - - {label.label?.length !== '' && {label.label}} - - )} - - {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 ( - - - {isSpecificDates && {parsedDate.format('MMM D')}} - {parsedDate.format('ddd')} + {heatmap} - 1} - > - {timeLabels.map((timeLabel, i) => { - if (!timeLabel.time) return null; - if (!times.includes(`${timeLabel.time}-${date}`)) { - return ( - - ); - } - 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 ( - - - {last && dates.length !== i+1 && ( - - )} - - ); - })} - {tooltip && ( 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}, diff --git a/crabfit-frontend/src/components/CalendarField/CalendarField.tsx b/crabfit-frontend/src/components/CalendarField/CalendarField.tsx index 96c7eb4..2ce8897 100644 --- a/crabfit-frontend/src/components/CalendarField/CalendarField.tsx +++ b/crabfit-frontend/src/components/CalendarField/CalendarField.tsx @@ -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]); diff --git a/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts b/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts index b664e32..e351b19 100644 --- a/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts +++ b/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts @@ -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; diff --git a/crabfit-frontend/src/components/Donate/Donate.tsx b/crabfit-frontend/src/components/Donate/Donate.tsx index 03dabfe..d245e57 100644 --- a/crabfit-frontend/src/components/Donate/Donate.tsx +++ b/crabfit-frontend/src/components/Donate/Donate.tsx @@ -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 ( - + + { + if (modalRef.current.contains(e.relatedTarget)) return; + setIsOpen(false); + }} + > + 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')} + setIsOpen(false)} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=5" target="_blank" rel="noreferrer">{t('donate.options.$5')} + 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')} + 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')} + + ); } diff --git a/crabfit-frontend/src/components/Donate/donateStyle.ts b/crabfit-frontend/src/components/Donate/donateStyle.ts new file mode 100644 index 0000000..280813f --- /dev/null +++ b/crabfit-frontend/src/components/Donate/donateStyle.ts @@ -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; + } + } +`; diff --git a/crabfit-frontend/src/components/Footer/Footer.tsx b/crabfit-frontend/src/components/Footer/Footer.tsx index cbbfcef..8603aaf 100644 --- a/crabfit-frontend/src/components/Footer/Footer.tsx +++ b/crabfit-frontend/src/components/Footer/Footer.tsx @@ -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 ( - diff --git a/crabfit-frontend/src/components/ToggleField/ToggleField.tsx b/crabfit-frontend/src/components/ToggleField/ToggleField.tsx index d1a73f8..2387faf 100644 --- a/crabfit-frontend/src/components/ToggleField/ToggleField.tsx +++ b/crabfit-frontend/src/components/ToggleField/ToggleField.tsx @@ -15,6 +15,7 @@ const ToggleField = ({ options = [], value, onChange, + inputRef, ...props }) => ( @@ -30,6 +31,7 @@ const ToggleField = ({ id={`${name}-${label}`} checked={value === key} onChange={() => onChange(key)} + ref={inputRef} /> {label} diff --git a/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts b/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts index 461676d..7b2e9d8 100644 --- a/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts +++ b/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts @@ -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}; `; diff --git a/crabfit-frontend/yarn.lock b/crabfit-frontend/yarn.lock index f799fc3..140d018 100644 --- a/crabfit-frontend/yarn.lock +++ b/crabfit-frontend/yarn.lock @@ -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"