TimeSplitInput.tsx288 lines · main
| 1 | import { format } from 'date-fns' |
| 2 | import { Clock } from 'lucide-react' |
| 3 | import { useEffect, useState } from 'react' |
| 4 | import { cn } from 'ui' |
| 5 | |
| 6 | import type { TimeSplitInputProps, TimeType } from './DatePicker.types' |
| 7 | import { |
| 8 | isUnixMicro, |
| 9 | unixMicroToIsoTimestamp, |
| 10 | } from '@/components/interfaces/Settings/Logs/Logs.utils' |
| 11 | |
| 12 | const 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 | |
| 17 | export 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 | } |