nitro-web 0.0.87 → 0.0.89

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.
Files changed (35) hide show
  1. package/client/index.ts +0 -3
  2. package/components/auth/auth.api.js +411 -0
  3. package/components/auth/reset.tsx +86 -0
  4. package/components/auth/signin.tsx +76 -0
  5. package/components/auth/signup.tsx +62 -0
  6. package/components/billing/stripe.api.js +268 -0
  7. package/components/dashboard/dashboard.tsx +32 -0
  8. package/components/partials/element/accordion.tsx +102 -0
  9. package/components/partials/element/avatar.tsx +40 -0
  10. package/components/partials/element/button.tsx +98 -0
  11. package/components/partials/element/calendar.tsx +125 -0
  12. package/components/partials/element/dropdown.tsx +248 -0
  13. package/components/partials/element/filters.tsx +194 -0
  14. package/components/partials/element/github-link.tsx +16 -0
  15. package/components/partials/element/initials.tsx +66 -0
  16. package/components/partials/element/message.tsx +141 -0
  17. package/components/partials/element/modal.tsx +90 -0
  18. package/components/partials/element/sidebar.tsx +195 -0
  19. package/components/partials/element/tooltip.tsx +154 -0
  20. package/components/partials/element/topbar.tsx +15 -0
  21. package/components/partials/form/checkbox.tsx +150 -0
  22. package/components/partials/form/drop-handler.tsx +68 -0
  23. package/components/partials/form/drop.tsx +141 -0
  24. package/components/partials/form/field-color.tsx +86 -0
  25. package/components/partials/form/field-currency.tsx +158 -0
  26. package/components/partials/form/field-date.tsx +252 -0
  27. package/components/partials/form/field.tsx +231 -0
  28. package/components/partials/form/form-error.tsx +27 -0
  29. package/components/partials/form/location.tsx +225 -0
  30. package/components/partials/form/select.tsx +360 -0
  31. package/components/partials/is-first-render.ts +14 -0
  32. package/components/partials/not-found.tsx +7 -0
  33. package/components/partials/styleguide.tsx +407 -0
  34. package/package.json +2 -1
  35. package/types/globals.d.ts +0 -1
@@ -0,0 +1,141 @@
1
+ // @ts-nocheck
2
+ import { deepFind, s3Image, getErrorFromState } from 'nitro-web/util'
3
+ import { DropHandler } from 'nitro-web'
4
+ import noImage from 'nitro-web/client/imgs/no-image.svg'
5
+ import { Errors, MonasteryImage } from 'nitro-web/types'
6
+ import { twMerge } from 'nitro-web/util'
7
+
8
+ type DropProps = {
9
+ awsUrl?: string
10
+ className?: string
11
+ /** Field name or path on state (used to match errors), e.g. 'avatar', 'company.avatar' */
12
+ name: string
13
+ /** Optional ID for the input element. Defaults to name if not provided */
14
+ id?: string
15
+ /** Called when file is selected or dropped */
16
+ onChange?: (event: { target: { name: string, value: File|FileList } }) => void
17
+ /** Whether to allow multiple file selection */
18
+ multiple?: boolean
19
+ /** State object to get the value and check errors against */
20
+ state?: {
21
+ errors?: Errors
22
+ [key: string]: unknown
23
+ }
24
+ /** title used to find related error messages */
25
+ errorTitle?: string|RegExp
26
+ /** Props to pass to the input element */
27
+ [key: string]: unknown
28
+ }
29
+
30
+ type Image = File | FileList | MonasteryImage | null
31
+
32
+ export function Drop({ awsUrl, className, id, name, onChange, multiple, state, errorTitle, ...props }: DropProps) {
33
+ if (!name) throw new Error('Drop component requires a `name` prop')
34
+ let value: Image = null
35
+ const error = getErrorFromState(state, errorTitle || name)
36
+ const inputId = id || name
37
+ const [urls, setUrls] = useState([])
38
+ const stateRef = useRef(state)
39
+ stateRef.current = state
40
+
41
+ // Input is always controlled if state is passed in
42
+ if (typeof props.value !== 'undefined') value = props.value as Image
43
+ else if (typeof state == 'object') value = deepFind(state, name) as Image
44
+ if (typeof value == 'undefined') value = null
45
+
46
+ useEffect(() => {
47
+ (async () => setUrls(await getUrls(value as File | FileList | MonasteryImage | null)))()
48
+ }, [value])
49
+
50
+ function tryAgain (e: { preventDefault: Function }) {
51
+ e.preventDefault()
52
+ // clear file input to allow reupload
53
+ const input = document.getElementById(name) as HTMLInputElement
54
+ if (input) input.value = ''
55
+ if (onChange) {
56
+ const errors = (stateRef?.current?.errors || []).filter((e: Errors[]) => e?.title != name)
57
+ onChange({
58
+ // remove file from state
59
+ target: { name: name, value: null },
60
+ // reset (server) errors
61
+ errors: errors.length ? errors : undefined,
62
+ })
63
+ }
64
+ }
65
+
66
+ async function onFileAttach (files: FileList) {
67
+ // files is a FileList object
68
+ if (onChange) onChange({ target: { name: name, value: multiple ? files : files[0] } })
69
+ }
70
+
71
+ async function getUrls(objectOrFileListItem: File | FileList | MonasteryImage | null) {
72
+ /**
73
+ * @param {object|FileList} objectOrFileListItem - FileList object or monastery image object
74
+ * @returns {Promise} - Resolves to an array of image URLs
75
+ */
76
+ // Make sure FileLists are converted to a real array
77
+ if (!objectOrFileListItem) return []
78
+ const array = 'length' in objectOrFileListItem ? Array.from(objectOrFileListItem) : [objectOrFileListItem]
79
+ return Promise.all(array.map((item) => {
80
+ return new Promise((resolve, reject) => {
81
+ if ('lastModified' in item) {
82
+ const reader = new FileReader()
83
+ reader.onload = () => resolve(reader.result)
84
+ reader.onerror = reject
85
+ reader.readAsDataURL(item)
86
+ } else {
87
+ resolve(s3Image(awsUrl, item))
88
+ }
89
+ })
90
+ }))
91
+ }
92
+
93
+ // function getFilename (objectOrFile) {
94
+ // if (objectOrFile.lastModified) return objectOrFile.name
95
+ // else return 'avatar.jpg'
96
+ // }
97
+
98
+ return (
99
+ <div class={'mt-2.5 mb-6 ' + twMerge(`mt-input-before mb-input-after nitro-field nitro-drop ${className || ''}`)}>
100
+ <input
101
+ {...props}
102
+ id={inputId}
103
+ type="file"
104
+ onChange={(e) => onFileAttach(e.target.files as FileList)}
105
+ hidden
106
+ />
107
+ <DropHandler
108
+ onDrop={onFileAttach}
109
+ className="flex flex-column justify-center items-center text-center gap-2 text-grey-300 text-sm px-8 min-h-[300px]"
110
+ >
111
+ {
112
+ !value &&
113
+ <>
114
+ {/* {todo upload svg here} */}
115
+ <div>
116
+ Drag and drop your file here&nbsp;
117
+ <label class="weight-500 inline-block text-sm text-primary" for={inputId}>or select a file</label>
118
+ </div>
119
+ </>
120
+ }
121
+ {
122
+ !!value &&
123
+ <>
124
+ {
125
+ urls.map((url, i) => (
126
+ <div key={i} class="flex align-items-center gap-1">
127
+ <img src={url || noImage} width="100%" />
128
+ </div>
129
+ ))
130
+ }
131
+ <div>
132
+ Your file has been added successfully.&nbsp;
133
+ <Link to="#" class="text-primary" onClick={tryAgain}>Use another file?</Link>
134
+ </div>
135
+ </>
136
+ }
137
+ </DropHandler>
138
+ {error && <div class="form-error mt-0-5">{error.detail}</div>}
139
+ </div>
140
+ )
141
+ }
@@ -0,0 +1,86 @@
1
+ import { hsvaToHex, hexToHsva, validHex, HsvaColor } from '@uiw/color-convert'
2
+ import Saturation from '@uiw/react-color-saturation'
3
+ import Hue from '@uiw/react-color-hue'
4
+ import { Dropdown, util } from 'nitro-web'
5
+
6
+ export type FieldColorProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'|'value'> & {
7
+ name: string
8
+ /** name is applied if id is not provided */
9
+ id?: string
10
+ defaultColor?: string
11
+ Icon?: React.ReactNode
12
+ onChange?: (event: { target: { name: string, value: string } }) => void
13
+ value?: string
14
+ }
15
+
16
+ export function FieldColor({ defaultColor='#333', Icon, onChange: onChangeProp, value: valueProp, ...props }: FieldColorProps) {
17
+ const [lastChanged, setLastChanged] = useState(() => `ic-${Date.now()}`)
18
+ const isInvalid = props.className?.includes('is-invalid') ? 'is-invalid' : ''
19
+ const id = props.id || props.name
20
+
21
+ // Since value and onChange are optional, we need to hold the value in state if not provided
22
+ const [internalValue, setInternalValue] = useState(valueProp ?? defaultColor)
23
+ const value = valueProp ?? internalValue
24
+ const onChange = onChangeProp ?? ((e: { target: { name: string, value: string } }) => setInternalValue(e.target.value))
25
+
26
+ function onInputChange(e: { target: { name: string, value: string } }) {
27
+ setLastChanged(`ic-${Date.now()}`)
28
+ onChange(e)
29
+ }
30
+
31
+ return (
32
+ <Dropdown
33
+ dir="bottom-left"
34
+ menuToggles={false}
35
+ menuContent={
36
+ <ColorPicker key={lastChanged} defaultColor={defaultColor} name={props.name} value={value} onChange={onChange} />
37
+ }
38
+ >
39
+ <div className="grid grid-cols-1">
40
+ {Icon}
41
+ <input
42
+ {...props}
43
+ className={(props.className || '') + ' ' + isInvalid}
44
+ id={id}
45
+ value={value}
46
+ onChange={onInputChange}
47
+ onBlur={() => !validHex(value||'') && onInputChange({ target: { name: props.name, value: '' }})}
48
+ autoComplete="off"
49
+ type="text"
50
+ />
51
+ </div>
52
+ </Dropdown>
53
+ )
54
+ }
55
+
56
+ function ColorPicker({ name='', onChange, value='', defaultColor='' }: FieldColorProps) {
57
+ const [hsva, setHsva] = useState(() => hexToHsva(validHex(value) ? value : defaultColor))
58
+ const [debounce] = useState(() => util.throttle(callOnChange, 50))
59
+
60
+ function callOnChange(newHsva: HsvaColor) {
61
+ if (onChange) onChange({ target: { name: name, value: hsvaToHex(newHsva) }})
62
+ }
63
+
64
+ return (
65
+ <>
66
+ <Saturation
67
+ className="!w-[100%] !h-[150px]"
68
+ hsva={hsva}
69
+ onChange={(newHsva) => {
70
+ setHsva(newHsva)
71
+ if (onChange) debounce(newHsva)
72
+ }}
73
+ />
74
+ <Hue
75
+ hue={hsva.h}
76
+ onChange={(newHue) => {
77
+ setHsva({ ...hsva, ...newHue })
78
+ if (onChange) debounce({ ...hsva, ...newHue })
79
+ }}
80
+ />
81
+ </>
82
+ )
83
+ }
84
+
85
+
86
+
@@ -0,0 +1,158 @@
1
+ import { NumericFormat } from 'react-number-format'
2
+ import { getPrefixWidth } from 'nitro-web/util'
3
+
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
+ /** name is applied if id is not provided */
21
+ id?: string
22
+ /** e.g. { currencies: { nzd: { symbol: '$', digits: 2 } } } (check out the nitro example for more info) */
23
+ config: {
24
+ currencies: { [key: string]: { symbol: string, digits: number } },
25
+ countries: { [key: string]: { numberFormats: { currency: string } } }
26
+ }
27
+ /** currency iso, e.g. 'nzd' */
28
+ currency: string
29
+ onChange?: (event: { target: { name: string, value: string|number|null } }) => void
30
+ /** value should be in cents */
31
+ value?: string|number|null
32
+ defaultValue?: number | string | null
33
+ }
34
+
35
+ export function FieldCurrency({ config, currency='nzd', onChange, value, defaultValue, ...props }: FieldCurrencyProps) {
36
+ const [dontFix, setDontFix] = useState(false)
37
+ const [settings, setSettings] = useState(() => getCurrencySettings(currency))
38
+ const [dollars, setDollars] = useState(() => toDollars(value, true, settings))
39
+ const [prefixWidth, setPrefixWidth] = useState(0)
40
+ const ref = useRef({ settings, dontFix }) // was null
41
+ const id = props.id || props.name
42
+ ref.current = { settings, dontFix }
43
+
44
+ useEffect(() => {
45
+ if (settings.currency !== currency) {
46
+ const settings = getCurrencySettings(currency)
47
+ setSettings(settings)
48
+ setDollars(toDollars(value, true, settings)) // required latest _settings
49
+ }
50
+ }, [currency])
51
+
52
+ useEffect(() => {
53
+ if (ref.current.dontFix) {
54
+ setDollars(toDollars(value))
55
+ setDontFix(false)
56
+ } else {
57
+ setDollars(toDollars(value, true))
58
+ }
59
+ }, [value])
60
+
61
+
62
+ useEffect(() => {
63
+ // Get the prefix content width
64
+ setPrefixWidth(settings.prefix == '$' ? getPrefixWidth(settings.prefix, 1) : 0)
65
+ }, [settings.prefix])
66
+
67
+ function toCents(value?: string|number|null) {
68
+ const maxDecimals = ref.current.settings.maxDecimals
69
+ const parsed = parseFloat(value + '')
70
+ if (!parsed && parsed !== 0) return null
71
+ if (!maxDecimals) return parsed
72
+ const value2 = Math.round(parsed * Math.pow(10, maxDecimals)) // e.g. 1.23 => 123
73
+ // console.log('toCents', parsed, value2)
74
+ return value2
75
+ }
76
+
77
+ function toDollars(value?: string|number|null, toFixed?: boolean, settings?: { maxDecimals?: number }) {
78
+ const maxDecimals = (settings || ref.current.settings).maxDecimals
79
+ const parsed = parseFloat(value + '')
80
+ if (!parsed && parsed !== 0) return null
81
+ if (!maxDecimals) return parsed
82
+ const value2 = parsed / Math.pow(10, maxDecimals) // e.g. 1.23 => 123
83
+ // console.log('toDollars', value, value2)
84
+ return toFixed ? value2.toFixed(maxDecimals) : value2
85
+ }
86
+
87
+ function getCurrencySettings(currency: string) {
88
+ // parse CLDR currency string format, e.g. '¤#,##0.00'
89
+ const output: {
90
+ currency: string, // e.g. 'nzd'
91
+ decimalSeparator?: string, // e.g. '.'
92
+ thousandSeparator?: string, // e.g. ','
93
+ minDecimals?: number, // e.g. 2
94
+ maxDecimals?: number, // e.g. 2
95
+ prefix?: string, // e.g. '$'
96
+ suffix?: string // e.g. ''
97
+ } = { currency }
98
+ const { symbol, digits } = config.currencies[currency]
99
+ let format = config.countries['nz'].numberFormats.currency
100
+
101
+ // Check for currency symbol (¤) and determine its position
102
+ if (format.indexOf('¤') !== -1) {
103
+ const position = format.indexOf('¤') === 0 ? 'prefix' : 'suffix'
104
+ output[position] = symbol
105
+ format = format.replace('¤', '')
106
+ }
107
+
108
+ // Find and set the thousands separator
109
+ const thousandMatch = format.match(/[^0-9#]/)
110
+ if (thousandMatch) output.thousandSeparator = thousandMatch[0]
111
+
112
+ // Find and set the decimal separator and fraction digits
113
+ const decimalMatch = format.match(/0[^0-9]/)
114
+ if (decimalMatch) {
115
+ output.decimalSeparator = decimalMatch[0].slice(1)
116
+ if (typeof digits !== 'undefined') {
117
+ output.minDecimals = digits
118
+ output.maxDecimals = digits
119
+ } else {
120
+ const fractionDigits = format.split(output.decimalSeparator)[1]
121
+ if (fractionDigits) {
122
+ output.minDecimals = fractionDigits.length
123
+ output.maxDecimals = fractionDigits.length
124
+ }
125
+ }
126
+ }
127
+ return output
128
+ }
129
+
130
+ return (
131
+ <div className="relative">
132
+ <NumericFormat
133
+ {...props}
134
+ id={id}
135
+ name={props.name}
136
+ decimalSeparator={settings.decimalSeparator}
137
+ thousandSeparator={settings.thousandSeparator}
138
+ decimalScale={settings.maxDecimals}
139
+ onValueChange={!onChange ? undefined : ({ floatValue }, e) => {
140
+ // console.log('onValueChange', floatValue, e)
141
+ if (e.source === 'event') setDontFix(true)
142
+ onChange({ target: { name: props.name, value: toCents(floatValue) }})
143
+ }}
144
+ onBlur={() => { setDollars(toDollars(value, true))}}
145
+ placeholder={props.placeholder || '0.00'}
146
+ value={dollars}
147
+ style={{ textIndent: `${prefixWidth}px` }}
148
+ type="text"
149
+ defaultValue={defaultValue}
150
+ />
151
+ <span
152
+ class={`absolute top-0 bottom-0 left-[12px] left-input-x inline-flex items-center select-none text-gray-500 text-input-base ${dollars !== null && settings.prefix == '$' ? 'text-foreground' : ''}`}
153
+ >
154
+ {settings.prefix || settings.suffix}
155
+ </span>
156
+ </div>
157
+ )
158
+ }
@@ -0,0 +1,252 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { format, isValid, parse } from 'date-fns'
3
+ import { getPrefixWidth } from 'nitro-web/util'
4
+ import { Calendar, Dropdown } from 'nitro-web'
5
+ import { dayButtonClassName } from '../element/calendar'
6
+
7
+ type Mode = 'single' | 'multiple' | 'range'
8
+ type DropdownRef = {
9
+ setIsActive: (value: boolean) => void
10
+ }
11
+
12
+ type PreFieldDateProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
13
+ /** field name or path on state (used to match errors), e.g. 'date', 'company.email' **/
14
+ name: string
15
+ /** mode of the date picker */
16
+ mode: Mode
17
+ /** name is used as the id if not provided */
18
+ id?: string
19
+ /** show the time picker */
20
+ showTime?: boolean
21
+ /** prefix to add to the input */
22
+ prefix?: string
23
+ /** number of months to show in the dropdown */
24
+ numberOfMonths?: number
25
+ /** icon to show in the input */
26
+ Icon?: React.ReactNode
27
+ /** direction of the dropdown */
28
+ dir?: 'bottom-left'|'bottom-right'|'top-left'|'top-right'
29
+ }
30
+
31
+ // An array is returned for mode = 'multiple' or 'range'
32
+ export type FieldDateProps = (
33
+ | ({ mode: 'single' } & PreFieldDateProps & {
34
+ onChange?: (e: { target: { name: string, value: null|number } }) => void
35
+ value?: null|number|string
36
+ })
37
+ | ({ mode: 'multiple' | 'range' } & PreFieldDateProps & {
38
+ onChange?: (e: { target: { name: string, value: (null|number)[] } }) => void
39
+ value?: null|number|string|(null|number|string)[]
40
+ })
41
+ )
42
+
43
+ type TimePickerProps = {
44
+ date: Date|null
45
+ onChange: (mode: Mode, value: number|null) => void
46
+ }
47
+
48
+ export function FieldDate({
49
+ dir = 'bottom-left',
50
+ Icon,
51
+ mode,
52
+ numberOfMonths,
53
+ onChange: onChangeProp,
54
+ prefix = '',
55
+ showTime,
56
+ value: valueProp,
57
+ ...props
58
+ }: FieldDateProps) {
59
+ const localePattern = `d MMM yyyy${showTime && mode == 'single' ? ' hh:mmaa' : ''}`
60
+ const [prefixWidth, setPrefixWidth] = useState(0)
61
+ const dropdownRef = useRef<DropdownRef>(null)
62
+ const [month, setMonth] = useState<number|undefined>()
63
+ const [lastUpdated, setLastUpdated] = useState(0)
64
+ const id = props.id || props.name
65
+
66
+ // Since value and onChange are optional, we need to hold the value in state if not provided
67
+ const [internalValue, setInternalValue] = useState<typeof valueProp>(valueProp)
68
+ const value = valueProp ?? internalValue
69
+ const onChange = onChangeProp ?? ((e: { target: { name: string, value: any } }) => setInternalValue(e.target.value))
70
+
71
+ // Convert the value to an array of valid* dates
72
+ const dates = useMemo(() => {
73
+ const arrOfNumbers = typeof value === 'string'
74
+ ? value.split(/\s*,\s*/g).map(o => parseFloat(o))
75
+ : Array.isArray(value) ? value : [value]
76
+ const out = arrOfNumbers.map(date => isValid(date) ? new Date(date as number) : null) /// changed to null
77
+ return out
78
+ }, [value])
79
+
80
+ // Hold the input value in state
81
+ const [inputValue, setInputValue] = useState(() => getInputValue(dates))
82
+
83
+ // Update the date's inputValue (text) when the value changes outside of the component
84
+ useEffect(() => {
85
+ if (new Date().getTime() > lastUpdated + 100) setInputValue(getInputValue(dates))
86
+ }, [dates])
87
+
88
+ // Get the prefix content width
89
+ useEffect(() => {
90
+ setPrefixWidth(getPrefixWidth(prefix, 4))
91
+ }, [prefix])
92
+
93
+ function onCalendarChange(mode: Mode, value: null|number|(null|number)[]) {
94
+ if (mode == 'single' && !showTime) dropdownRef.current?.setIsActive(false) // Close the dropdown
95
+ setInputValue(getInputValue(value))
96
+ // Update the value
97
+ onChange({ target: { name: props.name, value: getOutputValue(value) } })
98
+ setLastUpdated(new Date().getTime())
99
+ }
100
+
101
+ function onInputChange(e: React.ChangeEvent<HTMLInputElement>) {
102
+ setInputValue(e.target.value) // keep the input value in sync
103
+
104
+ let split = e.target.value.split(/-|,/).map(o => {
105
+ const date = parse(o.trim(), localePattern, new Date())
106
+ return isValid(date) ? date : null
107
+ })
108
+
109
+ // For single/range we need limit the array
110
+ if (mode == 'range' && split.length > 1) split.length = 2
111
+ else if (mode == 'multiple') split = split.filter(o => o) // remove invalid dates
112
+
113
+ // Swap dates if needed
114
+ if (mode == 'range' && (split[0] || 0) > (split[1] || 0)) split = [split[0], split[0]]
115
+
116
+ // Set month
117
+ for (let i=split.length; i--;) {
118
+ if (split[i]) setMonth((split[i] as Date).getTime())
119
+ break
120
+ }
121
+
122
+ // Update the value
123
+ const value = mode == 'single' ? split[0]?.getTime() ?? null : split.map(d => d?.getTime() ?? null)
124
+ onChange({ target: { name: props.name, value: getOutputValue(value) }})
125
+ setLastUpdated(new Date().getTime())
126
+ }
127
+
128
+ function getInputValue(value: Date|number|null|(Date|number|null)[]) {
129
+ const _dates = Array.isArray(value) ? value : [value]
130
+ return _dates.map(o => o ? format(o, localePattern) : '').join(mode == 'range' ? ' - ' : ', ')
131
+ }
132
+
133
+ function getOutputValue(value: Date|number|null|(Date|number|null)[]): any {
134
+ // console.log(value)
135
+ return value
136
+ }
137
+
138
+ return (
139
+ <Dropdown
140
+ ref={dropdownRef}
141
+ menuToggles={false}
142
+ // animate={false}
143
+ // menuIsOpen={true}
144
+ minWidth={0}
145
+ menuContent={
146
+ <div className="flex">
147
+ <Calendar
148
+ // Calendar actually accepts an array of dates, but the type is not typed correctly
149
+ {...{ mode: mode, value: dates as any, numberOfMonths: numberOfMonths, month: month }}
150
+ preserveTime={!!showTime}
151
+ onChange={onCalendarChange}
152
+ className="pt-1 pb-2 px-3"
153
+ />
154
+ {!!showTime && mode == 'single' && <TimePicker date={dates?.[0]} onChange={onCalendarChange} />}
155
+ </div>
156
+ }
157
+ dir={dir}
158
+ >
159
+ <div className="grid grid-cols-1">
160
+ {Icon}
161
+ {
162
+ prefix &&
163
+ // Similar classNames to the input.tsx:IconWrapper()
164
+ <span className="z-[0] col-start-1 row-start-1 self-center select-none justify-self-start text-input-base ml-[12px] ml-input-x">
165
+ {prefix}
166
+ </span>
167
+ }
168
+ <input
169
+ {...props}
170
+ key={'k' + prefixWidth}
171
+ id={id}
172
+ autoComplete="off"
173
+ className={(props.className||'')}// + props.className?.includes('is-invalid') ? ' is-invalid' : ''}
174
+ onBlur={() => setInputValue(getInputValue(dates))}
175
+ onChange={onInputChange}
176
+ style={{ textIndent: prefixWidth + 'px' }}
177
+ type="text"
178
+ value={inputValue}
179
+ />
180
+ </div>
181
+ </Dropdown>
182
+ )
183
+ }
184
+
185
+ function TimePicker({ date, onChange }: TimePickerProps) {
186
+ const lists = [
187
+ [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], // hours
188
+ [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55], // minutes
189
+ ['AM', 'PM'], // AM/PM
190
+ ]
191
+
192
+ // Get current values from date or use defaults
193
+ const hour = date ? parseInt(format(date, 'h')) : undefined
194
+ const minute = date ? parseInt(format(date, 'm')) : undefined
195
+ const period = date ? format(date, 'a') : undefined
196
+
197
+ const handleTimeChange = (type: 'hour' | 'minute' | 'period', value: string | number) => {
198
+ // Create a new date object from the current date or current time
199
+ const newDate = new Date(date || new Date())
200
+
201
+ if (type === 'hour') {
202
+ // Parse the time with the new hour value
203
+ const timeString = `${value}:${format(newDate, 'mm')} ${format(newDate, 'a')}`
204
+ const updatedDate = parse(timeString, 'h:mm a', newDate)
205
+ newDate.setHours(updatedDate.getHours(), updatedDate.getMinutes())
206
+ } else if (type === 'minute') {
207
+ // Parse the time with the new minute value
208
+ const timeString = `${format(newDate, 'h')}:${value} ${format(newDate, 'a')}`
209
+ const updatedDate = parse(timeString, 'h:mm a', newDate)
210
+ newDate.setMinutes(updatedDate.getMinutes())
211
+ } else if (type === 'period') {
212
+ // Parse the time with the new period value
213
+ const timeString = `${format(newDate, 'h')}:${format(newDate, 'mm')} ${value}`
214
+ const updatedDate = parse(timeString, 'h:mm a', newDate)
215
+ newDate.setHours(updatedDate.getHours())
216
+ }
217
+
218
+ onChange('single', newDate.getTime())
219
+ }
220
+
221
+ return (
222
+ lists.map((list, i) => {
223
+ const type = i === 0 ? 'hour' : i === 1 ? 'minute' : 'period'
224
+ const currentValue = i === 0 ? hour : i === 1 ? minute : period
225
+
226
+ return (
227
+ <div key={i} className="w-[60px] py-1 relative overflow-hidden hover:overflow-y-auto border-l border-gray-100">
228
+ <div className="w-[60px] absolute flex flex-col items-center">
229
+ {list.map(item => (
230
+ <div
231
+ className="py-1 flex group cursor-pointer"
232
+ key={item}
233
+ onClick={() => handleTimeChange(type, item)}
234
+ >
235
+ <button
236
+ key={item}
237
+ className={
238
+ `${dayButtonClassName} rounded-full flex justify-center items-center group-hover:bg-gray-100 `
239
+ + (item === currentValue ? '!bg-input-border-focus text-white' : '')
240
+ }
241
+ onClick={() => handleTimeChange(type, item)}
242
+ >
243
+ {item.toString().padStart(2, '0').toLowerCase()}
244
+ </button>
245
+ </div>
246
+ ))}
247
+ </div>
248
+ </div>
249
+ )
250
+ })
251
+ )
252
+ }