From e9945a19d533d7c5c1c5b0964236867d736c8f32 Mon Sep 17 00:00:00 2001 From: Ben Grant Date: Sun, 21 May 2023 22:53:22 +1000 Subject: [PATCH] Refactor TimeRangeField --- frontend/.eslintrc.json | 3 +- .../CalendarField/CalendarField.tsx | 2 +- .../CalendarField/components/Month/Month.tsx | 2 +- .../components/Weekdays/Weekdays.tsx | 2 +- .../src/components/CreateForm/CreateForm.tsx | 25 ++- .../TimeRangeField/TimeRangeField.jsx | 146 ------------------ ...d.styles.js => TimeRangeField.module.scss} | 41 ++--- .../TimeRangeField/TimeRangeField.tsx | 127 +++++++++++++++ 8 files changed, 154 insertions(+), 194 deletions(-) delete mode 100644 frontend/src/components/TimeRangeField/TimeRangeField.jsx rename frontend/src/components/TimeRangeField/{TimeRangeField.styles.js => TimeRangeField.module.scss} (60%) create mode 100644 frontend/src/components/TimeRangeField/TimeRangeField.tsx diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 3a8a688..4f707f1 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -8,7 +8,8 @@ "@next/next/no-img-element": "off", "react/display-name": "off", "react-hooks/exhaustive-deps": "off", - "space-infix-ops": "warn" + "space-infix-ops": "warn", + "comma-spacing": "warn" }, "overrides": [ { diff --git a/frontend/src/components/CalendarField/CalendarField.tsx b/frontend/src/components/CalendarField/CalendarField.tsx index f17bc70..919614a 100644 --- a/frontend/src/components/CalendarField/CalendarField.tsx +++ b/frontend/src/components/CalendarField/CalendarField.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { FieldValues,useController, UseControllerProps } from 'react-hook-form' +import { FieldValues, useController, UseControllerProps } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { Description, Label, Wrapper } from '/src/components/Field/Field' diff --git a/frontend/src/components/CalendarField/components/Month/Month.tsx b/frontend/src/components/CalendarField/components/Month/Month.tsx index c2c4e09..a9c2e90 100644 --- a/frontend/src/components/CalendarField/components/Month/Month.tsx +++ b/frontend/src/components/CalendarField/components/Month/Month.tsx @@ -11,7 +11,7 @@ import { makeClass } from '/src/utils' import styles from './Month.module.scss' // TODO: use from giraugh tools -export const rotateArray = (arr: T[], amount = 1): T[] => +export const rotateArray = (arr: T[], amount = 1): T[] => arr.map((_, i) => arr[((( -amount + i ) % arr.length) + arr.length) % arr.length]) interface MonthProps { diff --git a/frontend/src/components/CalendarField/components/Weekdays/Weekdays.tsx b/frontend/src/components/CalendarField/components/Weekdays/Weekdays.tsx index 3fe591a..e1d13c0 100644 --- a/frontend/src/components/CalendarField/components/Weekdays/Weekdays.tsx +++ b/frontend/src/components/CalendarField/components/Weekdays/Weekdays.tsx @@ -9,7 +9,7 @@ import { makeClass } from '/src/utils' import styles from '../Month/Month.module.scss' // TODO: use from giraugh tools -export const rotateArray = (arr: T[], amount = 1): T[] => +export const rotateArray = (arr: T[], amount = 1): T[] => arr.map((_, i) => arr[((( -amount + i ) % arr.length) + arr.length) % arr.length]) interface WeekdaysProps { diff --git a/frontend/src/components/CreateForm/CreateForm.tsx b/frontend/src/components/CreateForm/CreateForm.tsx index 690fb31..4f82af4 100644 --- a/frontend/src/components/CreateForm/CreateForm.tsx +++ b/frontend/src/components/CreateForm/CreateForm.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { useRouter } from 'next/navigation' @@ -8,7 +8,7 @@ import Button from '/src/components/Button/Button' import CalendarField from '/src/components/CalendarField/CalendarField' import { default as ErrorAlert } from '/src/components/Error/Error' import TextField from '/src/components/TextField/TextField' -import ToggleField from '/src/components/ToggleField/ToggleField' +import TimeRangeField from '/src/components/TimeRangeField/TimeRangeField' import { API_BASE } from '/src/config/api' import dayjs from '/src/config/dayjs' import { useTranslation } from '/src/i18n/client' @@ -47,7 +47,7 @@ const CreateForm = () => { const [error, setError] = useState() const onSubmit: SubmitHandler = async values => { - console.log({values}) + console.log({values}) // TODO: setIsLoading(true) setError(undefined) @@ -57,7 +57,7 @@ const CreateForm = () => { if (dates.length === 0) { return setError(t('form.errors.no_dates')) } - const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8 + const isSpecificDates = dates[0].length === 8 if (time.start === time.end) { return setError(t('form.errors.same_times')) } @@ -73,7 +73,7 @@ const CreateForm = () => { } else { day.push( dayjs().tz(timezone) - .day(date).hour(i).minute(0).utc().format('HHmm-d') + .day(Number(date)).hour(i).minute(0).utc().format('HHmm-d') ) } } @@ -87,13 +87,13 @@ const CreateForm = () => { } else { day.push( dayjs().tz(timezone) - .day(date).hour(i).minute(0).utc().format('HHmm-d') + .day(Number(date)).hour(i).minute(0).utc().format('HHmm-d') ) } } } return [...times, ...day] - }, []) + }, [] as string[]) if (times.length === 0) { return setError(t('form.errors.no_time')) @@ -141,15 +141,14 @@ const CreateForm = () => { name="dates" /> - {/* - { - const timeFormat = useSettingsStore(state => state.timeFormat) - const locale = useLocaleUpdateStore(state => state.locale) - - const [start, setStart] = useState(9) - const [end, setEnd] = useState(17) - - 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(() => 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) - - if (isStartMoving.current) { - setStart(step) - } else if (isEndMoving.current) { - setEnd(step) - } - } - } - - return ( - - {label && {label}} - {subLabel && {subLabel}} - - - - end ? 24 : end} /> - {start > end && end ? 0 : start} $end={end} />} - { - 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] - - 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') { - e.preventDefault() - setStart(Math.max(start-1, 0)) - } - if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { - e.preventDefault() - setStart(Math.min(start+1, 24)) - } - }} - /> - { - 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] - - 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') { - e.preventDefault() - setEnd(Math.max(end-1, 0)) - } - if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { - e.preventDefault() - setEnd(Math.min(end+1, 24)) - } - }} - /> - - - ) -}) - -export default TimeRangeField diff --git a/frontend/src/components/TimeRangeField/TimeRangeField.styles.js b/frontend/src/components/TimeRangeField/TimeRangeField.module.scss similarity index 60% rename from frontend/src/components/TimeRangeField/TimeRangeField.styles.js rename to frontend/src/components/TimeRangeField/TimeRangeField.module.scss index fcaecc8..3e344d5 100644 --- a/frontend/src/components/TimeRangeField/TimeRangeField.styles.js +++ b/frontend/src/components/TimeRangeField/TimeRangeField.module.scss @@ -1,24 +1,4 @@ -import { styled } from 'goober' -import { forwardRef } from 'react' - -export const Wrapper = styled('div')` - margin: 30px 0; -` - -export const StyledLabel = styled('label')` - display: block; - padding-bottom: 4px; - font-size: 18px; -` - -export const StyledSubLabel = styled('label')` - display: block; - padding-bottom: 6px; - font-size: 13px; - opacity: .6; -` - -export const Range = styled('div', forwardRef)` +.range { user-select: none; background-color: var(--surface); border: 1px solid var(--primary); @@ -26,9 +6,9 @@ export const Range = styled('div', forwardRef)` height: 50px; position: relative; margin: 38px 6px 18px; -` +} -export const Handle = styled('div')` +.handle { height: calc(100% + 20px); width: 20px; border: 1px solid var(--primary); @@ -36,10 +16,10 @@ export const Handle = styled('div')` border-radius: 3px; position: absolute; top: -10px; - left: calc(${props => props.$value * 4.166}% - 11px); cursor: ew-resize; touch-action: none; transition: left .1s; + @media (prefers-reduced-motion: reduce) { transition: none; } @@ -59,27 +39,26 @@ export const Handle = styled('div')` } &:before { - content: '${props => props.label}'; + content: attr(data-label); position: absolute; bottom: calc(100% + 8px); text-align: center; left: 50%; transform: translateX(-50%); white-space: nowrap; - ${props => props.$extraPadding} + padding-inline: var(--extra-padding); } -` +} -export const Selected = styled('div')` +.selected { position: absolute; height: 100%; - left: ${props => props.$start * 4.166}%; - right: calc(100% - ${props => props.$end * 4.166}%); top: 0; background-color: var(--primary); border-radius: 2px; transition: left .1s, right .1s; + @media (prefers-reduced-motion: reduce) { transition: none; } -` +} diff --git a/frontend/src/components/TimeRangeField/TimeRangeField.tsx b/frontend/src/components/TimeRangeField/TimeRangeField.tsx new file mode 100644 index 0000000..58496d0 --- /dev/null +++ b/frontend/src/components/TimeRangeField/TimeRangeField.tsx @@ -0,0 +1,127 @@ +import { useRef } from 'react' +import { FieldValues, useController, UseControllerProps } from 'react-hook-form' +import dayjs from 'dayjs' + +import { Description, Label, Wrapper } from '/src/components/Field/Field' +import useSettingsStore from '/src/stores/settingsStore' + +import styles from './TimeRangeField.module.scss' + +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'] as const + +interface TimeRangeFieldProps extends UseControllerProps { + label?: React.ReactNode + description?: React.ReactNode +} + +const TimeRangeField = ({ + label, + description, + ...props +}: TimeRangeFieldProps) => { + const { field: { value, onChange } } = useController(props) + + return + {label && } + {description && {description}} + +
+ value.end ? 24 : value.end} + /> + {value.start > value.end && value.end ? 0 : value.start} + end={value.end} + />} + + onChange({ ...value, start })} + labelPadding={value.end - value.start === 1 ? '0 20px' : (value.start - value.end === 1 ? '20px 0' : '0')} + /> + + onChange({ ...value, end })} + labelPadding={value.end - value.start === 1 ? '20px 0' : (value.start - value.end === 1 ? '0 20px' : '0')} + /> +
+
+} + +export default TimeRangeField + +const Selection = ({ start, end }: { start: number, end: number }) =>
+ +interface HandleProps { + value: number + onChange: (value: number) => void + labelPadding: string +} + +const Handle = ({ value, onChange, labelPadding }: HandleProps) => { + const timeFormat = useSettingsStore(state => state.timeFormat) + + const isMoving = useRef(false) + const rangeRect = useRef({ left: 0, width: 0 }) + + const handleMouseMove = (e: MouseEvent) => { + if (isMoving.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) + + onChange(step) + } + } + + return
{ + const bb = el?.parentElement?.getBoundingClientRect() + rangeRect.current = { left: bb?.left ?? 0, width: bb?.width ?? 0 } + }} + className={styles.handle} + style={{ + left: `calc(${value * 4.166}% - 11px)`, + '--extra-padding': labelPadding, + } as React.CSSProperties} + data-label={timeFormat === '24h' ? times[value] : dayjs().hour(Number(times[value])).format('ha')} + onMouseDown={() => { + document.addEventListener('mousemove', handleMouseMove) + isMoving.current = true + + document.addEventListener('mouseup', () => { + isMoving.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) + onChange(step) + }} + tabIndex={0} + onKeyDown={e => { + if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { + e.preventDefault() + onChange(Math.max(value - 1, 0)) + } + if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { + e.preventDefault() + onChange(Math.min(value + 1, 24)) + } + }} + /> +}