TimeSplitInput.tsx 7.6 KB

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