TimeInput.tsx 5.4 KB

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