TimeInput.tsx155 lines · main
1import dayjs from 'dayjs'
2import { isNaN, noop } from 'lodash'
3import { Clock } from 'lucide-react'
4import { ChangeEvent, useEffect, useState } from 'react'
5import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
6
7import type { Time } from './PITR.types'
8import { formatNumberToTwoDigits, formatTimeToTimeString } from './PITR.utils'
9
10// [Joshen] This is trying to do the same thing as TimeSplitInput.tsx
11// so can we please look to try to combine these 2 components together if possible
12// The problem with TimeSplitInput is that it's tightly coupled to the date + its
13// tightly coupled to the context of a range. Ideally it should be more modular
14// which is what this component is trying to achieve
15
16// [Joshen] Potential extension, give option to toggle 24 hours or AM/PM
17
18interface TimeInputProps {
19 defaultTime?: Time
20 minimumTime?: Time
21 maximumTime?: Time
22 onChange?: (time: Time) => void
23}
24
25const TimeInput = ({ defaultTime, minimumTime, maximumTime, onChange = noop }: TimeInputProps) => {
26 const [isFocused, setIsFocused] = useState(false)
27 const [error, setError] = useState<string>()
28 const [time, setTime] = useState<Time>(defaultTime || { h: 0, m: 0, s: 0 })
29
30 const formattedMinimumTime = minimumTime
31 ? dayjs(formatTimeToTimeString(minimumTime), 'HH:mm:ss', true)
32 : undefined
33
34 const formattedMaximumTime = maximumTime
35 ? dayjs(formatTimeToTimeString(maximumTime), 'HH:mm:ss', true)
36 : undefined
37
38 useEffect(() => {
39 if (minimumTime || maximumTime) validate(time)
40 }, [JSON.stringify(minimumTime), JSON.stringify(maximumTime)])
41
42 useEffect(() => {
43 if (defaultTime) {
44 setTime(defaultTime)
45 validate(defaultTime)
46 }
47 }, [defaultTime])
48
49 const validate = (time: Time) => {
50 let error = undefined
51 const formattedTime = dayjs(formatTimeToTimeString(time), 'HH:mm:ss', true)
52
53 if (!formattedTime.isValid()) {
54 error = 'Please enter a valid time'
55 } else if (formattedMinimumTime && formattedTime.isBefore(formattedMinimumTime)) {
56 error = 'Selected time is before the minimum time allowed'
57 } else if (formattedMaximumTime && formattedTime.isAfter(formattedMaximumTime)) {
58 error = 'Selected time is after the maximum time allowed'
59 }
60
61 setError(error)
62 return error === undefined
63 }
64
65 const onFocus = () => setIsFocused(true)
66
67 const onInputChange = (event: ChangeEvent<HTMLInputElement>, unit: 'h' | 'm' | 's') => {
68 if (isNaN(Number(event.target.value))) return
69 setTime({ ...time, [unit]: event.target.value })
70 }
71
72 const onInputBlur = (event: ChangeEvent<HTMLInputElement>, unit: 'h' | 'm' | 's') => {
73 const formattedInput = Number(event.target.value)
74 const updatedTime = { ...time, [unit]: formattedInput }
75 setTime(updatedTime)
76
77 validate(updatedTime)
78 onChange(updatedTime)
79 setIsFocused(false)
80 }
81
82 return (
83 <>
84 <div
85 className={[
86 'flex items-center justify-between transition',
87 'rounded-md bg-studio border px-3.5 py-2 w-[200px]',
88 `${
89 isFocused ? 'border-stronger' : error === undefined ? 'border-strong' : 'border-red-800'
90 }`,
91 ].join(' ')}
92 >
93 <Clock className="text-foreground-light" size={18} strokeWidth={1.5} />
94 <Tooltip>
95 <TooltipTrigger className="w-1/4" tabIndex={-1}>
96 <input
97 type="text"
98 maxLength={2}
99 pattern="[0-9]*"
100 placeholder="HH"
101 value={formatNumberToTwoDigits(time.h)}
102 onFocus={onFocus}
103 aria-label="Hours"
104 onBlur={(event) => onInputBlur(event, 'h')}
105 onChange={(event) => onInputChange(event, 'h')}
106 className="w-full text-sm bg-transparent p-0 text-center outline-hidden border-none focus:ring-0"
107 />
108 </TooltipTrigger>
109
110 <TooltipContent side="bottom">Hours (HH)</TooltipContent>
111 </Tooltip>
112 <span>:</span>
113 <Tooltip>
114 <TooltipTrigger className="w-1/4" tabIndex={-1}>
115 <input
116 type="text"
117 maxLength={2}
118 pattern="[0-9]*"
119 placeholder="MM"
120 value={formatNumberToTwoDigits(time.m)}
121 onFocus={onFocus}
122 aria-label="Minutes"
123 onBlur={(event) => onInputBlur(event, 'm')}
124 onChange={(event) => onInputChange(event, 'm')}
125 className="w-full text-sm bg-transparent p-0 text-center outline-hidden border-none focus:ring-0"
126 />
127 </TooltipTrigger>
128
129 <TooltipContent side="bottom">Minutes (MM)</TooltipContent>
130 </Tooltip>
131 <span>:</span>
132 <Tooltip>
133 <TooltipTrigger className="w-1/4" tabIndex={-1}>
134 <input
135 type="text"
136 maxLength={2}
137 pattern="[0-9]*"
138 placeholder="SS"
139 value={formatNumberToTwoDigits(time.s)}
140 onFocus={onFocus}
141 aria-label="Seconds"
142 onBlur={(event) => onInputBlur(event, 's')}
143 onChange={(event) => onInputChange(event, 's')}
144 className="w-full text-sm bg-transparent p-0 text-center outline-hidden border-none focus:ring-0"
145 />
146 </TooltipTrigger>
147 <TooltipContent side="bottom">Seconds (SS)</TooltipContent>
148 </Tooltip>
149 </div>
150 {error && <p className="text-sm text-red-900">{error}</p>}
151 </>
152 )
153}
154
155export default TimeInput