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.
- package/_example/client/css/index.css +1 -1
- package/_example/client/index.ts +1 -2
- package/_example/tailwind.config.js +14 -14
- package/client/index.ts +6 -5
- package/components/auth/reset.tsx +4 -4
- package/components/auth/signin.tsx +3 -3
- package/components/auth/signup.tsx +5 -5
- package/components/partials/element/button.tsx +4 -3
- package/components/partials/element/calendar.tsx +123 -0
- package/components/partials/element/dropdown.tsx +6 -2
- package/components/partials/element/modal.tsx +54 -196
- package/components/partials/element/sidebar.tsx +2 -2
- package/components/partials/form/checkbox.tsx +1 -1
- package/components/partials/form/input-color.tsx +21 -20
- package/components/partials/form/input-currency.tsx +51 -35
- package/components/partials/form/input-date.tsx +137 -166
- package/components/partials/form/input.tsx +123 -92
- package/components/partials/form/select.tsx +4 -4
- package/components/partials/styleguide.tsx +89 -42
- package/components/settings/settings-account.tsx +6 -6
- package/components/settings/settings-business.tsx +12 -12
- package/components/settings/settings-team--member.tsx +8 -8
- package/package.json +8 -4
- package/readme.md +11 -7
- package/types/util.d.ts +19 -10
- package/types/util.d.ts.map +1 -1
- package/types.ts +5 -3
- package/util.js +22 -14
|
@@ -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-
|
|
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-
|
|
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
|
|
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
|
|
7
|
-
|
|
6
|
+
export type FieldColorProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
7
|
+
name: string
|
|
8
|
+
id?: string
|
|
8
9
|
defaultColor?: string
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
-
{
|
|
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
|
|
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='' }:
|
|
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 {
|
|
2
|
+
import { getPrefixWidth } from 'nitro-web/util'
|
|
4
3
|
|
|
5
|
-
type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
14
|
-
/** currency iso */
|
|
26
|
+
/** currency iso, e.g. 'nzd' */
|
|
15
27
|
currency: string
|
|
16
|
-
onChange
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
24
|
-
|
|
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 == '$' ?
|
|
63
|
+
setPrefixWidth(settings.prefix == '$' ? getPrefixWidth(settings.prefix, 1) : 0)
|
|
57
64
|
}, [settings.prefix])
|
|
58
65
|
|
|
59
|
-
function toCents(
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
return
|
|
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(
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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 {
|
|
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) :
|
|
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
|
-
|
|
33
|
-
setPrefixWidth(getCurrencyPrefixWidth(prefix, 4))
|
|
40
|
+
setPrefixWidth(getPrefixWidth(prefix, 4))
|
|
34
41
|
}, [prefix])
|
|
35
42
|
|
|
36
|
-
function
|
|
37
|
-
if (mode == 'single')
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
//
|
|
75
|
-
|
|
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={
|
|
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
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
{
|
|
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={
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
}
|