TimeSplitInput.tsx288 lines · main
1import { format } from 'date-fns'
2import { Clock } from 'lucide-react'
3import { useEffect, useState } from 'react'
4import { cn } from 'ui'
5
6import type { TimeSplitInputProps, TimeType } from './DatePicker.types'
7import {
8 isUnixMicro,
9 unixMicroToIsoTimestamp,
10} from '@/components/interfaces/Settings/Logs/Logs.utils'
11
12const inputStyle = cn(
13 'w-6 p-0 text-center text-xs text-foreground outline-hidden cursor-text',
14 'ring-0 focus:ring-0 ring-none border-none bg-transparent'
15)
16
17export const TimeSplitInput = ({
18 type,
19 time,
20 setTime,
21 setStartTime,
22 setEndTime,
23 startTime,
24 endTime,
25 startDate,
26 endDate,
27}: TimeSplitInputProps) => {
28 const [focus, setFocus] = useState(false)
29
30 function handleOnBlur() {
31 const _time = time
32
33 if (_time.HH.length === 1) _time.HH = '0' + _time.HH
34 if (_time.mm.length === 1) _time.mm = '0' + _time.mm
35 if (_time.ss.length === 1) _time.ss = '0' + _time.ss
36
37 if (!_time.HH) _time.HH = '00'
38 if (!_time.mm) _time.mm = '00'
39 if (!_time.ss) _time.ss = '00'
40
41 let endTimeChanges = false
42 let endTimePayload = endTime
43
44 let startTimeChanges = false
45 let startTimePayload = startTime
46
47 // Only run time conflicts if
48 // startDate and endDate are the same date
49
50 if (format(new Date(startDate), 'dd/mm/yyyy') == format(new Date(endDate), 'dd/mm/yyyy')) {
51 // checks if start time is ahead of end time
52
53 if (type === 'start') {
54 if (_time.HH && Number(_time.HH) > Number(endTime.HH)) {
55 endTimePayload.HH = _time.HH
56 endTimeChanges = true
57 }
58
59 if (
60 // also check the hour
61 _time.HH &&
62 Number(_time.HH) >= Number(endTime.HH) &&
63 // check the minutes
64 _time.mm &&
65 Number(_time.mm) > Number(endTime.mm)
66 ) {
67 endTimePayload.mm = _time.mm
68 endTimeChanges = true
69 }
70
71 if (
72 // also check the hour
73 _time.HH &&
74 Number(_time.HH) >= Number(endTime.HH) &&
75 // check the minutes
76 _time.mm &&
77 Number(_time.mm) >= Number(endTime.mm) &&
78 // check the seconds
79 _time.ss &&
80 Number(_time.ss) > Number(endTime.ss)
81 ) {
82 endTimePayload.ss = _time.ss
83 endTimeChanges = true
84 }
85 }
86
87 if (type === 'end') {
88 if (_time.HH && Number(_time.HH) < Number(startTime.HH)) {
89 startTimePayload.HH = _time.HH
90 startTimeChanges = true
91 }
92
93 if (
94 // also check the hour
95 _time.HH &&
96 Number(_time.HH) <= Number(startTime.HH) &&
97 // check the minutes
98 _time.mm &&
99 Number(_time.mm) < Number(startTime.mm)
100 ) {
101 startTimePayload.mm = _time.mm
102 startTimeChanges = true
103 }
104
105 if (
106 // also check the hour
107 _time.HH &&
108 Number(_time.HH) <= Number(startTime.HH) &&
109 // check the minutes
110 _time.mm &&
111 Number(_time.mm) <= Number(startTime.mm) &&
112 // check the seconds
113 _time.ss &&
114 Number(_time.ss) < Number(startTime.ss)
115 ) {
116 startTimePayload.ss = _time.ss
117 startTimeChanges = true
118 }
119 }
120 }
121
122 setTime({ ..._time })
123
124 if (endTimeChanges) {
125 setEndTime({ ...endTimePayload })
126 }
127 if (startTimeChanges) {
128 setStartTime({ ...startTimePayload })
129 }
130
131 setFocus(false)
132 }
133
134 function handleOnChange(value: string, valueType: TimeType) {
135 const payload = {
136 HH: time.HH,
137 mm: time.mm,
138 ss: time.ss,
139 }
140 if (value.length > 2) return
141
142 switch (valueType) {
143 case 'HH':
144 if (value && Number(value) > 23) return
145 break
146 case 'mm':
147 if (value && Number(value) > 59) return
148 break
149 case 'ss':
150 if (value && Number(value) > 59) return
151 break
152 default:
153 break
154 }
155
156 payload[valueType] = value
157 setTime({ ...payload })
158 }
159
160 const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
161 event.target.select()
162 setFocus(true)
163 // Prevent parent dialog from stealing focus
164 event.stopPropagation()
165 }
166
167 const handleClick = (event: React.MouseEvent<HTMLInputElement>) => {
168 event.stopPropagation()
169 }
170
171 const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
172 // Allow only numbers and navigation keys
173 const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter']
174 const isNumber = /^[0-9]$/.test(event.key)
175
176 if (!isNumber && !allowedKeys.includes(event.key)) {
177 event.preventDefault()
178 }
179
180 // Prevent parent dialog from stealing focus on keydown
181 event.stopPropagation()
182 }
183
184 const handleInput = (event: React.FormEvent<HTMLInputElement>) => {
185 // Prevent parent dialog from stealing focus on input
186 event.stopPropagation()
187 }
188
189 function handlePaste(event: ClipboardEvent) {
190 event.preventDefault()
191 event.stopPropagation()
192
193 navigator.clipboard.readText().then((text) => {
194 let date: null | Date = null
195 if (isUnixMicro(text)) {
196 date = new Date(unixMicroToIsoTimestamp(text))
197 } else {
198 date = new Date(Number(text))
199 }
200
201 if (isNaN(date.getTime())) {
202 console.warn('Invalid date or timestamp in clipboard')
203 return
204 }
205
206 if (date) {
207 // Offset the date by 1s to make sure it will find the logs in that second
208 if (type === 'start') {
209 date.setSeconds(date.getSeconds() - 1)
210 }
211 if (type === 'end') {
212 date.setSeconds(date.getSeconds() + 1)
213 }
214
215 setTime({
216 HH: date.getHours().toString().padStart(2, '0'),
217 mm: date.getMinutes().toString().padStart(2, '0'),
218 ss: date.getSeconds().toString().padStart(2, '0'),
219 })
220 }
221 })
222 }
223
224 useEffect(() => {
225 document.addEventListener('paste', handlePaste)
226 return () => document.removeEventListener('paste', handlePaste)
227 }, [startDate, endDate])
228
229 return (
230 <div
231 className={`
232 flex h-7 items-center justify-center
233 gap-0 rounded-sm border border-strong bg-surface-100 text-xs text-foreground-light
234 ${focus && ' border-stronger outline outline-2 outline-border'}
235 hover:border-stronger transition-colors
236 `}
237 >
238 <div className="mr-1 text-foreground-lighter">
239 <Clock size={14} strokeWidth={1.5} />
240 </div>
241
242 <input
243 type="text"
244 onBlur={() => handleOnBlur()}
245 onFocus={handleFocus}
246 onClick={handleClick}
247 onKeyDown={handleKeyDown}
248 onInput={handleInput}
249 pattern="[0-23]*"
250 placeholder="00"
251 onChange={(e) => handleOnChange(e.target.value, 'HH')}
252 aria-label="Hours"
253 className={inputStyle}
254 value={time.HH}
255 />
256 <span className="text-foreground-lighter">:</span>
257 <input
258 type="text"
259 onBlur={() => handleOnBlur()}
260 onFocus={handleFocus}
261 onClick={handleClick}
262 onKeyDown={handleKeyDown}
263 onInput={handleInput}
264 pattern="[0-59]*"
265 placeholder="00"
266 onChange={(e) => handleOnChange(e.target.value, 'mm')}
267 aria-label="Minutes"
268 className={inputStyle}
269 value={time.mm}
270 />
271 <span className="text-foreground-lighter">:</span>
272 <input
273 type="text"
274 onBlur={() => handleOnBlur()}
275 onFocus={handleFocus}
276 onClick={handleClick}
277 onKeyDown={handleKeyDown}
278 onInput={handleInput}
279 pattern="[0-59]*"
280 placeholder="00"
281 onChange={(e) => handleOnChange(e.target.value, 'ss')}
282 aria-label="Seconds"
283 className={inputStyle}
284 value={time.ss}
285 />
286 </div>
287 )
288}