Refactor TimeRangeField
This commit is contained in:
parent
12004b8584
commit
e9945a19d5
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { makeClass } from '/src/utils'
|
|||
import styles from './Month.module.scss'
|
||||
|
||||
// TODO: use from giraugh tools
|
||||
export const rotateArray = <T,>(arr: T[], amount = 1): T[] =>
|
||||
export const rotateArray = <T, >(arr: T[], amount = 1): T[] =>
|
||||
arr.map((_, i) => arr[((( -amount + i ) % arr.length) + arr.length) % arr.length])
|
||||
|
||||
interface MonthProps {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { makeClass } from '/src/utils'
|
|||
import styles from '../Month/Month.module.scss'
|
||||
|
||||
// TODO: use from giraugh tools
|
||||
export const rotateArray = <T,>(arr: T[], amount = 1): T[] =>
|
||||
export const rotateArray = <T, >(arr: T[], amount = 1): T[] =>
|
||||
arr.map((_, i) => arr[((( -amount + i ) % arr.length) + arr.length) % arr.length])
|
||||
|
||||
interface WeekdaysProps {
|
||||
|
|
|
|||
|
|
@ -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<React.ReactNode>()
|
||||
|
||||
const onSubmit: SubmitHandler<Fields> = 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"
|
||||
/>
|
||||
|
||||
{/* <TimeRangeField
|
||||
<TimeRangeField
|
||||
label={t('form.times.label')}
|
||||
subLabel={t('form.times.sublabel')}
|
||||
required
|
||||
setValue={setValue}
|
||||
{...register('time')}
|
||||
description={t('form.times.sublabel')}
|
||||
control={control}
|
||||
name="time"
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
{/* <SelectField
|
||||
label={t('form.timezone.label')}
|
||||
options={timezones}
|
||||
required
|
||||
|
|
|
|||
|
|
@ -1,146 +0,0 @@
|
|||
import { useState, useEffect, useRef, forwardRef } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { useSettingsStore, useLocaleUpdateStore } from '/src/stores'
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
StyledLabel,
|
||||
StyledSubLabel,
|
||||
Range,
|
||||
Handle,
|
||||
Selected,
|
||||
} from './TimeRangeField.styles'
|
||||
|
||||
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,
|
||||
setValue,
|
||||
...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 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 (
|
||||
<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}
|
||||
/>
|
||||
|
||||
<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
|
||||
|
||||
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))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<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
|
||||
|
||||
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))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Range>
|
||||
</Wrapper>
|
||||
)
|
||||
})
|
||||
|
||||
export default TimeRangeField
|
||||
|
|
@ -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;
|
||||
}
|
||||
`
|
||||
}
|
||||
127
frontend/src/components/TimeRangeField/TimeRangeField.tsx
Normal file
127
frontend/src/components/TimeRangeField/TimeRangeField.tsx
Normal file
|
|
@ -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<TValues extends FieldValues> extends UseControllerProps<TValues> {
|
||||
label?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
}
|
||||
|
||||
const TimeRangeField = <TValues extends FieldValues>({
|
||||
label,
|
||||
description,
|
||||
...props
|
||||
}: TimeRangeFieldProps<TValues>) => {
|
||||
const { field: { value, onChange } } = useController(props)
|
||||
|
||||
return <Wrapper>
|
||||
{label && <Label>{label}</Label>}
|
||||
{description && <Description>{description}</Description>}
|
||||
|
||||
<div className={styles.range}>
|
||||
<Selection
|
||||
start={value.start}
|
||||
end={value.start > value.end ? 24 : value.end}
|
||||
/>
|
||||
{value.start > value.end && <Selection
|
||||
start={value.start > value.end ? 0 : value.start}
|
||||
end={value.end}
|
||||
/>}
|
||||
|
||||
<Handle
|
||||
value={value.start}
|
||||
onChange={start => onChange({ ...value, start })}
|
||||
labelPadding={value.end - value.start === 1 ? '0 20px' : (value.start - value.end === 1 ? '20px 0' : '0')}
|
||||
/>
|
||||
|
||||
<Handle
|
||||
value={value.end}
|
||||
onChange={end => onChange({ ...value, end })}
|
||||
labelPadding={value.end - value.start === 1 ? '20px 0' : (value.start - value.end === 1 ? '0 20px' : '0')}
|
||||
/>
|
||||
</div>
|
||||
</Wrapper>
|
||||
}
|
||||
|
||||
export default TimeRangeField
|
||||
|
||||
const Selection = ({ start, end }: { start: number, end: number }) => <div
|
||||
className={styles.selected}
|
||||
style={{
|
||||
left: `${start * 4.166}%`,
|
||||
right: `calc(100% - ${end * 4.166}%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
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 <div
|
||||
ref={el => {
|
||||
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))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
Loading…
Reference in a new issue