nitro-web 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -115,7 +115,7 @@ function SidebarContents ({ Logo, menu, links }: SidebarProps) {
115
115
  isActive(item.to)
116
116
  ? 'bg-gray-50 text-indigo-600'
117
117
  : 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600',
118
- 'group flex gap-x-3 items-center rounded-md p-2 text-sm/6 font-semibold'
118
+ 'group flex gap-x-3 items-center rounded-md p-2 text-md/6 font-semibold'
119
119
  )}
120
120
  >
121
121
  { item.Icon &&
@@ -143,7 +143,7 @@ function SidebarContents ({ Logo, menu, links }: SidebarProps) {
143
143
  isActive(team.to)
144
144
  ? 'bg-gray-50 text-indigo-600'
145
145
  : 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600',
146
- 'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold'
146
+ 'group flex gap-x-3 rounded-md p-2 text-md/6 font-semibold'
147
147
  )}
148
148
  >
149
149
  <span
@@ -95,7 +95,7 @@ export function Checkbox({ name, id, size='sm', subtext, text, type='checkbox',
95
95
  }
96
96
  </div>
97
97
  {text &&
98
- <label for={id} className="self-center text-sm-label select-none">
98
+ <label for={id} className="self-center text-sm select-none">
99
99
  <span className="text-gray-900">{text}</span>
100
100
  <span className="ml-2 text-gray-500">{subtext}</span>
101
101
  </label>
@@ -3,21 +3,21 @@ import Saturation from '@uiw/react-color-saturation'
3
3
  import Hue from '@uiw/react-color-hue'
4
4
  import { Dropdown, util } from 'nitro-web'
5
5
 
6
- type InputColorProps = {
7
- className?: string
6
+ export type FieldColorProps = React.InputHTMLAttributes<HTMLInputElement> & {
7
+ name: string
8
+ id?: string
8
9
  defaultColor?: string
9
- iconEl?: React.ReactNode
10
- id?: string
11
- onChange?: (e: { target: { id: string, value: string } }) => void
12
- value?: string
13
- [key: string]: unknown
10
+ Icon?: React.ReactNode
11
+ onChange?: (event: { target: { id: string, value: string|null } }) => void
12
+ value?: string|null
14
13
  }
15
14
 
16
- export function InputColor({ className, defaultColor='#333', iconEl, id, onChange, value, ...props }: InputColorProps) {
15
+ export function FieldColor({ defaultColor='#333', Icon, onChange, value, ...props }: FieldColorProps) {
17
16
  const [lastChanged, setLastChanged] = useState(() => `ic-${Date.now()}`)
18
- const isInvalid = className?.includes('is-invalid') ? 'is-invalid' : ''
17
+ const isInvalid = props.className?.includes('is-invalid') ? 'is-invalid' : ''
18
+ const id = props.id || props.name
19
19
 
20
- function onInputChange(e: { target: { id: string, value: string } }) {
20
+ function onInputChange(e: { target: { id: string, value: string|null } }) {
21
21
  setLastChanged(`ic-${Date.now()}`)
22
22
  if (onChange) onChange(e)
23
23
  }
@@ -27,26 +27,27 @@ export function InputColor({ className, defaultColor='#333', iconEl, id, onChang
27
27
  dir="bottom-left"
28
28
  menuToggles={false}
29
29
  menuChildren={
30
- <ColorPicker key={lastChanged} defaultColor={defaultColor} id={id} value={value} onChange={onChange} />
30
+ <ColorPicker key={lastChanged} defaultColor={defaultColor} id={id} name={props.name} value={value} onChange={onChange} />
31
31
  }
32
32
  >
33
33
  <div className="grid grid-cols-1">
34
- {iconEl}
35
- <input
36
- {...props}
37
- className={className + ' ' + isInvalid}
38
- id={id}
39
- value={value}
34
+ {Icon}
35
+ <input
36
+ {...props}
37
+ className={(props.className || '') + ' ' + isInvalid}
38
+ id={id}
39
+ value={value}
40
40
  onChange={onInputChange}
41
- onBlur={() => !validHex(value||'') && onInputChange({ target: { id: id || '', value: '' }})}
42
- autoComplete="off"
41
+ onBlur={() => !validHex(value||'') && onInputChange({ target: { id: id, value: '' }})}
42
+ autoComplete="off"
43
+ type="text"
43
44
  />
44
45
  </div>
45
46
  </Dropdown>
46
47
  )
47
48
  }
48
49
 
49
- function ColorPicker({ id='', onChange, value='', defaultColor='' }: InputColorProps) {
50
+ function ColorPicker({ id='', onChange, value='', defaultColor='' }: FieldColorProps) {
50
51
  const [hsva, setHsva] = useState(() => hexToHsva(validHex(value) ? value : defaultColor))
51
52
  const [debounce] = useState(() => util.throttle(callOnChange, 50))
52
53
 
@@ -1,36 +1,43 @@
1
- // @ts-nocheck
2
1
  import { NumericFormat } from 'react-number-format'
3
- import { getCurrencyPrefixWidth } from 'nitro-web/util'
2
+ import { getPrefixWidth } from 'nitro-web/util'
4
3
 
5
- type InputCurrencyProps = {
6
- /** field name or path on state */
7
- id: string
8
- /** e.g. { currencies: { nzd: { symbol: '$', digits: 2 } } } */
4
+ // Declaring the type here because typescript fails to infer type when referencing NumericFormatProps from react-number-format
5
+ type NumericFormatProps = React.InputHTMLAttributes<HTMLInputElement> & {
6
+ thousandSeparator?: boolean | string;
7
+ decimalSeparator?: string;
8
+ allowedDecimalSeparators?: Array<string>;
9
+ thousandsGroupStyle?: 'thousand' | 'lakh' | 'wan' | 'none';
10
+ decimalScale?: number;
11
+ fixedDecimalScale?: boolean;
12
+ allowNegative?: boolean;
13
+ allowLeadingZeros?: boolean;
14
+ suffix?: string;
15
+ prefix?: string;
16
+ }
17
+
18
+ export type FieldCurrencyProps = NumericFormatProps & {
19
+ name: string
20
+ id?: string
21
+ /** e.g. { currencies: { nzd: { symbol: '$', digits: 2 } } } (check out the nitro example for more info) */
9
22
  config: {
10
23
  currencies: { [key: string]: { symbol: string, digits: number } },
11
24
  countries: { [key: string]: { numberFormats: { currency: string } } }
12
25
  }
13
- className: string
14
- /** currency iso */
26
+ /** currency iso, e.g. 'nzd' */
15
27
  currency: string
16
- onChange: (event: { target: { id: string, value: string } }) => void
17
- /** e.g. 'Amount' */
18
- placeholder: string
19
- /** e.g. 123 (input is always controlled if state is passed in) */
20
- value: number
28
+ onChange?: (event: { target: { id: string, value: string|number|null } }) => void
29
+ /** value should be in cents */
30
+ value?: string|number|null
31
+ defaultValue?: number | string | null
21
32
  }
22
33
 
23
- export function InputCurrency({ id, config, className, currency='nzd', onChange, placeholder, value }: InputCurrencyProps) {
24
- if (!config?.currencies || !config?.countries) {
25
- throw new Error(
26
- 'InputCurrency: `config.currencies` and `config.countries` is required, check out the nitro example for more info.'
27
- )
28
- }
29
- const [dontFix, setDontFix] = useState()
34
+ export function FieldCurrency({ config, currency='nzd', onChange, value, defaultValue, ...props }: FieldCurrencyProps) {
35
+ const [dontFix, setDontFix] = useState(false)
30
36
  const [settings, setSettings] = useState(() => getCurrencySettings(currency))
31
37
  const [dollars, setDollars] = useState(() => toDollars(value, true, settings))
32
- const [prefixWidth, setPrefixWidth] = useState()
38
+ const [prefixWidth, setPrefixWidth] = useState(0)
33
39
  const ref = useRef({ settings, dontFix }) // was null
40
+ const id = props.id || props.name
34
41
  ref.current = { settings, dontFix }
35
42
 
36
43
  useEffect(() => {
@@ -53,21 +60,27 @@ export function InputCurrency({ id, config, className, currency='nzd', onChange,
53
60
 
54
61
  useEffect(() => {
55
62
  // Get the prefix content width
56
- setPrefixWidth(settings.prefix == '$' ? getCurrencyPrefixWidth(settings.prefix, 1) : 0)
63
+ setPrefixWidth(settings.prefix == '$' ? getPrefixWidth(settings.prefix, 1) : 0)
57
64
  }, [settings.prefix])
58
65
 
59
- function toCents(num: number) {
60
- if (!num && num !== 0) return null
61
- const value = Math.round(num * Math.pow(10, ref.current.settings.maxDecimals)) // e.g. 1.23 => 123
62
- // console.log('toCents', num, value)
63
- return value
66
+ function toCents(value?: string|number|null) {
67
+ const maxDecimals = ref.current.settings.maxDecimals
68
+ const parsed = parseFloat(value + '')
69
+ if (!parsed && parsed !== 0) return null
70
+ if (!maxDecimals) return parsed
71
+ const value2 = Math.round(parsed * Math.pow(10, maxDecimals)) // e.g. 1.23 => 123
72
+ // console.log('toCents', parsed, value2)
73
+ return value2
64
74
  }
65
75
 
66
- function toDollars(num: string|number, toFixed: boolean, settings: { maxDecimals: number }) {
67
- if (!num && num !== 0) return null
68
- const value = num / Math.pow(10, (settings || ref.current.settings).maxDecimals) // e.g. 1.23 => 123
69
- // console.log('toDollars', num, value)
70
- return toFixed ? value.toFixed((settings || ref.current.settings).maxDecimals) : value
76
+ function toDollars(value?: string|number|null, toFixed?: boolean, settings?: { maxDecimals?: number }) {
77
+ const maxDecimals = (settings || ref.current.settings).maxDecimals
78
+ const parsed = parseFloat(value + '')
79
+ if (!parsed && parsed !== 0) return null
80
+ if (!maxDecimals) return parsed
81
+ const value2 = parsed / Math.pow(10, maxDecimals) // e.g. 1.23 => 123
82
+ // console.log('toDollars', value, value2)
83
+ return toFixed ? value2.toFixed(maxDecimals) : value2
71
84
  }
72
85
 
73
86
  function getCurrencySettings(currency: string) {
@@ -116,8 +129,9 @@ export function InputCurrency({ id, config, className, currency='nzd', onChange,
116
129
  return (
117
130
  <div className="relative">
118
131
  <NumericFormat
119
- id={id}
120
- className={className}
132
+ {...props}
133
+ id={id}
134
+ name={props.name}
121
135
  decimalSeparator={settings.decimalSeparator}
122
136
  thousandSeparator={settings.thousandSeparator}
123
137
  decimalScale={settings.maxDecimals}
@@ -127,9 +141,11 @@ export function InputCurrency({ id, config, className, currency='nzd', onChange,
127
141
  onChange({ target: { id: id, value: toCents(floatValue) }})
128
142
  }}
129
143
  onBlur={() => { setDollars(toDollars(value, true))}}
130
- placeholder={placeholder || '0.00'}
144
+ placeholder={props.placeholder || '0.00'}
131
145
  value={dollars}
132
146
  style={{ textIndent: `${prefixWidth}px` }}
147
+ type="text"
148
+ defaultValue={defaultValue}
133
149
  />
134
150
  <span
135
151
  class={`absolute top-[1px] bottom-0 left-3 inline-flex items-center select-none text-gray-500 text-sm sm:text-sm/6 ${dollars !== null && settings.prefix == '$' ? 'text-dark' : ''}`}
@@ -1,56 +1,57 @@
1
- // @ts-nocheck
2
- // todo: finish tailwind conversion
3
- import { css } from 'twin.macro'
4
- import { DayPicker } from 'react-day-picker'
5
1
  import { format, isValid, parse } from 'date-fns'
6
- import { getCurrencyPrefixWidth } from 'nitro-web/util'
7
- import { Dropdown } from 'nitro-web'
8
- import 'react-day-picker/dist/style.css'
9
-
10
- export function InputDate({ className, prefix, id, onChange, mode='single', value, ...props }) {
11
- /**
12
- * @param {string} mode - 'single'|'range'|'multiple' - an array is returned for non-single modes
13
- */
14
- const localePattern = 'd MMM yyyy'
15
- const isInvalid = className?.includes('is-invalid') ? 'is-invalid' : ''
16
- const [prefixWidth, setPrefixWidth] = useState()
17
- const ref = useRef(null)
2
+ import { getPrefixWidth } from 'nitro-web/util'
3
+ import { Calendar, Dropdown } from 'nitro-web'
18
4
 
5
+ type Mode = 'single' | 'multiple' | 'range'
6
+ type DropdownRef = {
7
+ setIsActive: (value: boolean) => void
8
+ }
9
+ export type FieldDateProps = React.InputHTMLAttributes<HTMLInputElement> & {
10
+ name: string
11
+ id?: string
12
+ mode?: Mode
13
+ showTime?: boolean
14
+ // an array is returned for non-single modes
15
+ onChange?: (e: { target: { id: string, value: null|number|(null|number)[] } }) => void
16
+ prefix?: string
17
+ value?: null|number|string|(null|number|string)[]
18
+ numberOfMonths?: number
19
+ Icon?: React.ReactNode
20
+ }
21
+
22
+ export function FieldDate({ mode='single', onChange, prefix='', value, numberOfMonths, Icon, showTime, ...props }: FieldDateProps) {
23
+ const localePattern = `d MMM yyyy${showTime && mode == 'single' ? ' HH:mm aa' : ''}`
24
+ const [prefixWidth, setPrefixWidth] = useState(0)
25
+ const dropdownRef = useRef<DropdownRef>(null)
26
+ const id = props.id || props.name
27
+ const [month, setMonth] = useState<number|undefined>()
28
+
29
+ // Convert the value to an array of valid* dates
19
30
  const dates = useMemo(() => {
20
- // Convert the value to an array of valid* dates
21
31
  const _dates = Array.isArray(value) ? value : [value]
22
- return _dates.map(date => isValid(date) ? new Date(date) : undefined)
32
+ return _dates.map(date => isValid(date) ? new Date(date as number) : null) /// change to null
23
33
  }, [value])
24
34
 
25
- // Hold the month in state to control the calendar when the input changes
26
- const [month, setMonth] = useState(dates[0])
27
-
28
35
  // Hold the input value in state
29
36
  const [inputValue, setInputValue] = useState(() => getInputValue(dates))
30
37
 
38
+ // Get the prefix content width
31
39
  useEffect(() => {
32
- // Get the prefix content width
33
- setPrefixWidth(getCurrencyPrefixWidth(prefix, 4))
40
+ setPrefixWidth(getPrefixWidth(prefix, 4))
34
41
  }, [prefix])
35
42
 
36
- function handleDayPickerSelect(newDate) {
37
- if (mode == 'single') {
38
- ref.current.setIsActive(false) // close the dropdown
39
- callOnChange(newDate?.getTime() || null)
40
- setInputValue(getInputValue([newDate]))
41
-
42
- } else if (mode == 'range') {
43
- const {from, to} = newDate || {} // may not exist
44
- callOnChange(from ? [from?.getTime() || null, to?.getTime() || null] : null)
45
- setInputValue(getInputValue(from ? [from, to] : []))
43
+ function onCalendarChange(mode: Mode, value: null|number|(null|number)[]) {
44
+ if (mode == 'single' && !showTime) dropdownRef.current?.setIsActive(false) // Close the dropdown
45
+ setInputValue(getInputValue(value))
46
+ if (onChange) onChange({ target: { id: id, value: value }})
47
+ }
46
48
 
47
- } else {
48
- callOnChange(newDate.filter(o => o).map(d => d.getTime()))
49
- setInputValue(getInputValue(newDate.filter(o => o)))
50
- }
49
+ function getInputValue(dates: Date|number|null|(Date|number|null)[]) {
50
+ const _dates = Array.isArray(dates) ? dates : [dates]
51
+ return _dates.map(o => o ? format(o, localePattern) : '').join(mode == 'range' ? ' - ' : ', ')
51
52
  }
52
53
 
53
- function handleInputChange(e) {
54
+ function onInputChange(e: React.ChangeEvent<HTMLInputElement>) {
54
55
  setInputValue(e.target.value) // keep the input value in sync
55
56
 
56
57
  let split = e.target.value.split(/-|,/).map(o => {
@@ -63,162 +64,132 @@ export function InputDate({ className, prefix, id, onChange, mode='single', valu
63
64
  else if (mode == 'multiple') split = split.filter(o => o) // remove invalid dates
64
65
 
65
66
  // Swap dates if needed
66
- if (mode == 'range' && split[0] > split[1]) split = [split[0], split[0]]
67
+ if (mode == 'range' && (split[0] || 0) > (split[1] || 0)) split = [split[0], split[0]]
67
68
 
68
69
  // Set month
69
70
  for (let i=split.length; i--;) {
70
- if (split[i]) setMonth(split[i])
71
+ if (split[i]) setMonth((split[i] as Date).getTime())
71
72
  break
72
73
  }
73
74
 
74
- // Set dates
75
- callOnChange(mode == 'single' ? split[0] : split)
76
- }
77
-
78
- function getInputValue(dates) {
79
- return dates.map(o => o ? format(o, localePattern) : '').join(mode == 'range' ? ' - ' : ', ')
80
- }
81
-
82
- function callOnChange(value) {
83
- if (onChange) onChange({ target: { id: id, value: value }}) // timestamp|[timestamp]
75
+ // Update
76
+ const value = mode == 'single' ? split[0]?.getTime() ?? null : split.map(d => d?.getTime() ?? null)
77
+ if (onChange) onChange({ target: { id, value }})
84
78
  }
85
79
 
86
80
  return (
87
81
  <Dropdown
88
- ref={ref}
89
- css={style}
82
+ ref={dropdownRef}
90
83
  menuToggles={false}
91
- animate={false}
84
+ // animate={false}
92
85
  // menuIsOpen={true}
86
+ minWidth={0}
93
87
  menuChildren={
94
- <DayPicker
95
- mode={mode}
96
- month={month}
97
- onMonthChange={setMonth}
98
- numberOfMonths={mode == 'range' ? 2 : 1}
99
- selected={mode === 'single' ? dates[0] : mode == 'range' ? { from: dates[0], to: dates[1] } : dates}
100
- onSelect={handleDayPickerSelect}
101
- />
88
+ <div className="flex">
89
+ <Calendar
90
+ {...{ mode, value, numberOfMonths, month }}
91
+ preserveTime={!!showTime}
92
+ onChange={onCalendarChange}
93
+ className="pt-1 pb-2 px-3"
94
+ />
95
+ {!!showTime && mode == 'single' && <TimePicker date={dates?.[0]} onChange={onCalendarChange} />}
96
+ </div>
102
97
  }
103
98
  >
104
- <div>
105
- {prefix && <span class={`input-prefix ${inputValue ? 'has-value' : ''}`}>{prefix}</span>}
99
+ <div className="grid grid-cols-1">
100
+ {Icon}
101
+ {
102
+ prefix &&
103
+ // Similar classNames to the input.tsx:IconWrapper()
104
+ <span className="relative col-start-1 row-start-1 self-center select-none z-[1] justify-self-start text-sm ml-3">{prefix}</span>
105
+ }
106
106
  <input
107
107
  {...props}
108
- key={'k'+prefixWidth}
108
+ key={'k' + prefixWidth}
109
109
  id={id}
110
110
  autoComplete="off"
111
- className={
112
- className + ' ' + isInvalid
113
- }
111
+ className={(props.className||'')}// + props.className?.includes('is-invalid') ? ' is-invalid' : ''}
114
112
  value={inputValue}
115
- onChange={handleInputChange}
113
+ onChange={onInputChange}
116
114
  onBlur={() => setInputValue(getInputValue(dates))}
117
115
  style={{ textIndent: prefixWidth + 'px' }}
116
+ type="text"
118
117
  />
119
118
  </div>
120
119
  </Dropdown>
121
120
  )
122
121
  }
123
122
 
124
- const style = css`
125
- .rdp {
126
- --rdp-cell-size: 34px;
127
- --rdp-caption-font-size: 12px;
128
- --rdp-accent-color: blue; /* theme('colors.primary') */
129
- font-size: 13px;
130
- margin: 0 12px 11px;
131
- svg {
132
- width: 13px;
133
- height: 13px;
134
- }
135
- .rdp-caption_label {
136
- height: var(--rdp-cell-size);
137
- }
138
- .rdp-head_cell {
139
- text-align: center !important;
140
- }
141
- tr {
142
- display: flex;
143
- justify-content: space-around;
144
- align-items: center;
145
- th,
146
- td {
147
- display: flex;
148
- align-items: center;
149
- margin-left: -1px;
150
- margin-top: -1px;
151
- .rdp-day {
152
- border: 0 !important;
153
- position: relative;
154
- border-radius: 0 !important;
155
- color: inherit;
156
- background-color: transparent !important;
157
- &:before {
158
- content: '';
159
- position: absolute;
160
- display: block;
161
- left: 0px;
162
- top: 0px;
163
- bottom: 0px;
164
- right: 0px;
165
- z-index: -1;
166
- }
167
- }
168
- .rdp-day:focus,
169
- .rdp-day:hover,
170
- .rdp-day:active {
171
- &:not([disabled]):not(.rdp-day_selected) {
172
- &:before {
173
- left: 1px;
174
- top: 1px;
175
- bottom: 1px;
176
- right: 1px;
177
- border-radius: 50%;
178
- background-color: #e7edff;
179
- }
180
- &:active {
181
- color: white;
182
- &:before {
183
- background-color: blue; /* theme('colors.primary') */
184
- }
185
- }
186
- }
187
- }
188
- .rdp-day_selected {
189
- color: white;
190
- :before {
191
- border-radius: 50%;
192
- background-color: blue; /* theme('colors.primary') */
193
- }
194
- }
195
- .rdp-day_range_middle {
196
- color: black; /* theme('colors.dark') */
197
- :before {
198
- border-radius: 0;
199
- border: 1px solid rgb(151 133 185);
200
- background-color: blue; /* theme('colors.primary-light') */
201
- }
202
- }
203
- .rdp-day_range_start,
204
- .rdp-day_range_end {
205
- position: relative;
206
- z-index: 1;
207
- &.rdp-day_range_start:before {
208
- border-top-right-radius: 0px;
209
- border-bottom-right-radius: 0px;
210
- }
211
- &.rdp-day_range_end:before {
212
- border-top-left-radius: 0px;
213
- border-bottom-left-radius: 0px;
214
- }
215
- &.rdp-day_range_start.rdp-day_range_end:before {
216
- border-radius: 50%;
217
- }
218
- }
219
- }
123
+ type TimePickerProps = {
124
+ date: Date|null
125
+ onChange: (mode: Mode, value: number|null) => void
126
+ }
127
+
128
+ function TimePicker({ date, onChange }: TimePickerProps) {
129
+ const lists = [
130
+ [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], // hours
131
+ [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55], // minutes
132
+ ['AM', 'PM'], // AM/PM
133
+ ]
134
+
135
+ // Get current values from date or use defaults
136
+ const hour = date ? parseInt(format(date, 'h')) : undefined
137
+ const minute = date ? parseInt(format(date, 'm')) : undefined
138
+ const period = date ? format(date, 'a') : undefined
139
+
140
+ const handleTimeChange = (type: 'hour' | 'minute' | 'period', value: string | number) => {
141
+ // Create a new date object from the current date or current time
142
+ const newDate = new Date(date || new Date())
143
+
144
+ if (type === 'hour') {
145
+ // Parse the time with the new hour value
146
+ const timeString = `${value}:${format(newDate, 'mm')} ${format(newDate, 'a')}`
147
+ const updatedDate = parse(timeString, 'h:mm a', newDate)
148
+ newDate.setHours(updatedDate.getHours(), updatedDate.getMinutes())
149
+ } else if (type === 'minute') {
150
+ // Parse the time with the new minute value
151
+ const timeString = `${format(newDate, 'h')}:${value} ${format(newDate, 'a')}`
152
+ const updatedDate = parse(timeString, 'h:mm a', newDate)
153
+ newDate.setMinutes(updatedDate.getMinutes())
154
+ } else if (type === 'period') {
155
+ // Parse the time with the new period value
156
+ const timeString = `${format(newDate, 'h')}:${format(newDate, 'mm')} ${value}`
157
+ const updatedDate = parse(timeString, 'h:mm a', newDate)
158
+ newDate.setHours(updatedDate.getHours())
220
159
  }
160
+
161
+ onChange('single', newDate.getTime())
221
162
  }
222
- `
223
-
224
163
 
164
+ return (
165
+ lists.map((list, i) => {
166
+ const type = i === 0 ? 'hour' : i === 1 ? 'minute' : 'period'
167
+ const currentValue = i === 0 ? hour : i === 1 ? minute : period
168
+
169
+ return (
170
+ <div key={i} className="w-[60px] py-1 relative overflow-hidden hover:overflow-y-auto border-l border-gray-100">
171
+ <div className="w-[60px] absolute flex flex-col items-center">
172
+ {list.map(item => (
173
+ <div
174
+ className="py-1 flex group cursor-pointer"
175
+ key={item}
176
+ onClick={() => handleTimeChange(type, item)}
177
+ >
178
+ <button
179
+ key={item}
180
+ className={
181
+ 'size-[33px] rounded-full flex justify-center items-center group-hover:bg-gray-100 '
182
+ + (item === currentValue ? '!bg-primary-dark text-white' : '')
183
+ }
184
+ onClick={() => handleTimeChange(type, item)}
185
+ >
186
+ {item.toString().padStart(2, '0').toLowerCase()}
187
+ </button>
188
+ </div>
189
+ ))}
190
+ </div>
191
+ </div>
192
+ )
193
+ })
194
+ )
195
+ }