nitro-web 0.0.85 → 0.0.87
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/client/globals.ts +10 -6
- package/package.json +14 -6
- package/types/{required-globals.d.ts → globals.d.ts} +3 -1
- package/.editorconfig +0 -9
- package/components/auth/auth.api.js +0 -410
- package/components/auth/reset.tsx +0 -86
- package/components/auth/signin.tsx +0 -76
- package/components/auth/signup.tsx +0 -62
- package/components/billing/stripe.api.js +0 -268
- package/components/dashboard/dashboard.tsx +0 -32
- package/components/partials/element/accordion.tsx +0 -102
- package/components/partials/element/avatar.tsx +0 -40
- package/components/partials/element/button.tsx +0 -98
- package/components/partials/element/calendar.tsx +0 -125
- package/components/partials/element/dropdown.tsx +0 -248
- package/components/partials/element/filters.tsx +0 -194
- package/components/partials/element/github-link.tsx +0 -16
- package/components/partials/element/initials.tsx +0 -66
- package/components/partials/element/message.tsx +0 -141
- package/components/partials/element/modal.tsx +0 -90
- package/components/partials/element/sidebar.tsx +0 -195
- package/components/partials/element/tooltip.tsx +0 -154
- package/components/partials/element/topbar.tsx +0 -15
- package/components/partials/form/checkbox.tsx +0 -150
- package/components/partials/form/drop-handler.tsx +0 -68
- package/components/partials/form/drop.tsx +0 -141
- package/components/partials/form/field-color.tsx +0 -86
- package/components/partials/form/field-currency.tsx +0 -158
- package/components/partials/form/field-date.tsx +0 -252
- package/components/partials/form/field.tsx +0 -231
- package/components/partials/form/form-error.tsx +0 -27
- package/components/partials/form/location.tsx +0 -225
- package/components/partials/form/select.tsx +0 -360
- package/components/partials/is-first-render.ts +0 -14
- package/components/partials/not-found.tsx +0 -7
- package/components/partials/styleguide.tsx +0 -407
- package/semver-updater.cjs +0 -13
- package/tsconfig.json +0 -38
- package/tsconfig.types.json +0 -15
- package/types/core-only-globals.d.ts +0 -9
- package/types.ts +0 -60
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { DayPicker, getDefaultClassNames } from 'react-day-picker'
|
|
2
|
-
import { isValid } from 'date-fns'
|
|
3
|
-
import 'react-day-picker/style.css'
|
|
4
|
-
import { IsFirstRender } from 'nitro-web'
|
|
5
|
-
|
|
6
|
-
export const dayButtonClassName = 'size-[33px] text-sm'
|
|
7
|
-
|
|
8
|
-
type Mode = 'single'|'multiple'|'range'
|
|
9
|
-
type ModeSelection<T extends Mode> = (
|
|
10
|
-
T extends 'single' ? Date | undefined
|
|
11
|
-
: T extends 'multiple' ? Date[]
|
|
12
|
-
: { from?: Date; to?: Date }
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
export type CalendarProps = {
|
|
16
|
-
mode?: Mode
|
|
17
|
-
onChange?: (mode: Mode, value: null|number|(null|number)[]) => void
|
|
18
|
-
value?: null|number|string|(null|number|string)[]
|
|
19
|
-
numberOfMonths?: number
|
|
20
|
-
month?: number // the value may be updated from an outside source, thus the month may have changed
|
|
21
|
-
className?: string
|
|
22
|
-
preserveTime?: boolean // just for single mode
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function Calendar({ mode='single', onChange, value, numberOfMonths, month: monthProp, className, preserveTime }: CalendarProps) {
|
|
26
|
-
const isFirstRender = IsFirstRender()
|
|
27
|
-
const isRange = mode == 'range'
|
|
28
|
-
|
|
29
|
-
// Convert the value to an array of valid* dates
|
|
30
|
-
const dates = useMemo(() => {
|
|
31
|
-
const _dates = Array.isArray(value) ? value : [value]
|
|
32
|
-
return _dates.map(date => isValid(date) ? new Date(date as number) : undefined) ////change to null
|
|
33
|
-
}, [value])
|
|
34
|
-
|
|
35
|
-
// Hold the month in state to control the calendar when the input changes
|
|
36
|
-
const [month, setMonth] = useState(dates[0])
|
|
37
|
-
|
|
38
|
-
// Update the month if its changed from an outside source
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
if (!isFirstRender && monthProp) setMonth(new Date(monthProp))
|
|
41
|
-
}, [monthProp])
|
|
42
|
-
|
|
43
|
-
function handleDayPickerSelect<T extends Mode>(newDate: ModeSelection<T>) {
|
|
44
|
-
switch (mode as T) {
|
|
45
|
-
case 'single': {
|
|
46
|
-
const date = newDate as ModeSelection<'single'>
|
|
47
|
-
preserveTimeFn(date)
|
|
48
|
-
onChange?.(mode, date?.getTime() ?? null)
|
|
49
|
-
break
|
|
50
|
-
}
|
|
51
|
-
case 'range': {
|
|
52
|
-
const { from, to } = (newDate ?? {}) as ModeSelection<'range'>
|
|
53
|
-
onChange?.(mode, from ? [from.getTime() || null, to?.getTime() || null] : null)
|
|
54
|
-
break
|
|
55
|
-
}
|
|
56
|
-
case 'multiple': {
|
|
57
|
-
const dates = (newDate as ModeSelection<'multiple'>)?.filter(Boolean) ?? []
|
|
58
|
-
onChange?.(mode, dates.map((d) => d.getTime()))
|
|
59
|
-
break
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function preserveTimeFn(date?: Date) {
|
|
65
|
-
// Preserve time from the original date if needed
|
|
66
|
-
if (preserveTime && dates[0] && date) {
|
|
67
|
-
const originalDate = dates[0]
|
|
68
|
-
date.setHours(
|
|
69
|
-
originalDate.getHours(),
|
|
70
|
-
originalDate.getMinutes(),
|
|
71
|
-
originalDate.getSeconds(),
|
|
72
|
-
originalDate.getMilliseconds()
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const d = getDefaultClassNames()
|
|
78
|
-
const common = {
|
|
79
|
-
month: month,
|
|
80
|
-
onMonthChange: setMonth,
|
|
81
|
-
onSelect: handleDayPickerSelect,
|
|
82
|
-
numberOfMonths: numberOfMonths || (isRange ? 2 : 1),
|
|
83
|
-
modifiersClassNames: {
|
|
84
|
-
// Add a class without _, TW seems to replace this with a space in the css definition, e.g. &:not(.range middle)
|
|
85
|
-
range_middle: `${d.range_middle} rangemiddle`,
|
|
86
|
-
},
|
|
87
|
-
classNames: {
|
|
88
|
-
root: `${d.root} flex nitro-calendar`,
|
|
89
|
-
months: `${d.months} flex-nowrap`,
|
|
90
|
-
month_caption: `${d.month_caption} text-2xs pl-2`,
|
|
91
|
-
caption_label: `${d.caption_label} z-auto`,
|
|
92
|
-
button_previous: `${d.button_previous} size-8`,// [&:hover>svg]:fill-input-border-focus`,
|
|
93
|
-
button_next: `${d.button_next} size-8`,// [&:hover>svg]:fill-input-border-focus`,
|
|
94
|
-
chevron: `${d.chevron} fill-black size-[18px]`,
|
|
95
|
-
|
|
96
|
-
// Days
|
|
97
|
-
weekday: `${d.weekday} text-[11px] font-bold uppercase`,
|
|
98
|
-
day: `${d.day} size-[33px]`,
|
|
99
|
-
day_button: `${d.day_button} ${dayButtonClassName}`,
|
|
100
|
-
|
|
101
|
-
// States
|
|
102
|
-
focused: `${d.focused} [&>button]:bg-gray-200 [&>button]:border-gray-200`,
|
|
103
|
-
range_start: `${d.range_start} [&>button]:!bg-input-border-focus [&>button]:!border-input-border-focus`,
|
|
104
|
-
range_end: `${d.range_end} [&>button]:!bg-input-border-focus [&>button]:!border-input-border-focus`,
|
|
105
|
-
selected: `${d.selected} font-normal `
|
|
106
|
-
+ '[&:not(.rangemiddle)>button]:!text-white '
|
|
107
|
-
+ '[&:not(.rangemiddle)>button]:!bg-input-border-focus '
|
|
108
|
-
+ '[&:not(.rangemiddle)>button]:!border-input-border-focus ',
|
|
109
|
-
},
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return (
|
|
113
|
-
<div>
|
|
114
|
-
{
|
|
115
|
-
mode === 'single' ? (
|
|
116
|
-
<DayPicker mode="single" selected={dates[0]} {...common} className={className} />
|
|
117
|
-
) : mode === 'range' ? (
|
|
118
|
-
<DayPicker mode="range" selected={{ from: dates[0], to: dates[1] }} {...common} className={className} />
|
|
119
|
-
) : (
|
|
120
|
-
<DayPicker mode="multiple" selected={dates.filter((d) => !!d)} {...common} className={className} />
|
|
121
|
-
)
|
|
122
|
-
}
|
|
123
|
-
</div>
|
|
124
|
-
)
|
|
125
|
-
}
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import { css } from 'twin.macro'
|
|
2
|
-
import { forwardRef, cloneElement } from 'react'
|
|
3
|
-
import { getSelectStyle, twMerge } from 'nitro-web'
|
|
4
|
-
import { CheckCircleIcon } from '@heroicons/react/24/solid'
|
|
5
|
-
|
|
6
|
-
type DropdownProps = {
|
|
7
|
-
allowOverflow?: boolean
|
|
8
|
-
animate?: boolean
|
|
9
|
-
children?: React.ReactNode
|
|
10
|
-
className?: string
|
|
11
|
-
css?: string
|
|
12
|
-
/** The direction of the menu **/
|
|
13
|
-
dir?: 'bottom-left'|'bottom-right'|'top-left'|'top-right'
|
|
14
|
-
options?: { label: string|React.ReactNode, onClick?: Function, isSelected?: boolean, icon?: React.ReactNode, className?: string }[]
|
|
15
|
-
/** Whether the dropdown is hoverable **/
|
|
16
|
-
isHoverable?: boolean
|
|
17
|
-
/** The content to render inside the top of the dropdown **/
|
|
18
|
-
menuContent?: React.ReactNode
|
|
19
|
-
menuClassName?: string
|
|
20
|
-
menuOptionClassName?: string
|
|
21
|
-
menuIsOpen?: boolean
|
|
22
|
-
menuToggles?: boolean
|
|
23
|
-
/** The minimum width of the menu **/
|
|
24
|
-
minWidth?: number | string
|
|
25
|
-
toggleCallback?: (isActive: boolean) => void
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const Dropdown = forwardRef(function Dropdown({
|
|
29
|
-
allowOverflow=false,
|
|
30
|
-
animate=true,
|
|
31
|
-
children,
|
|
32
|
-
className,
|
|
33
|
-
dir='bottom-left',
|
|
34
|
-
options,
|
|
35
|
-
isHoverable,
|
|
36
|
-
menuClassName,
|
|
37
|
-
menuOptionClassName,
|
|
38
|
-
menuContent,
|
|
39
|
-
menuIsOpen,
|
|
40
|
-
menuToggles=true,
|
|
41
|
-
minWidth,
|
|
42
|
-
toggleCallback,
|
|
43
|
-
}: DropdownProps, ref) {
|
|
44
|
-
// https://letsbuildui.dev/articles/building-a-dropdown-menu-component-with-react-hooks
|
|
45
|
-
isHoverable = isHoverable && !menuIsOpen
|
|
46
|
-
const dropdownRef = useRef<HTMLDivElement|null>(null)
|
|
47
|
-
const [isActive, setIsActive] = useState(!!menuIsOpen)
|
|
48
|
-
const menuStyle = getSelectStyle({ name: 'menu' })
|
|
49
|
-
const [direction, setDirection] = useState<null | 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'>(null)
|
|
50
|
-
const [ready, setReady] = useState(false)
|
|
51
|
-
|
|
52
|
-
// Expose the setIsActive function to the parent component
|
|
53
|
-
useImperativeHandle(ref, () => ({ setIsActive }))
|
|
54
|
-
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
const pageClick = (event: MouseEvent | FocusEvent) => {
|
|
57
|
-
try {
|
|
58
|
-
// If the active element exists and is clicked outside of the dropdown, toggle the dropdown
|
|
59
|
-
if (dropdownRef.current !== null && !dropdownRef.current.contains(event.target as Node)) setIsActive(!isActive)
|
|
60
|
-
} catch (_e) {
|
|
61
|
-
// Errors throw for contains() when the user clicks off the webpage when open
|
|
62
|
-
setIsActive(!isActive)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (isActive && !menuIsOpen) {
|
|
66
|
-
// Wait for the next event loop in the case of mousedown'ing the dropdown, while loosing click focus from a checkbox
|
|
67
|
-
setTimeout(() => {
|
|
68
|
-
window.addEventListener('mousedown', pageClick)
|
|
69
|
-
window.addEventListener('focus', pageClick, true) // true needed to capture focus events
|
|
70
|
-
}, 0)
|
|
71
|
-
}
|
|
72
|
-
return () => {
|
|
73
|
-
window.removeEventListener('mousedown', pageClick)
|
|
74
|
-
window.removeEventListener('focus', pageClick, true) // true needed to capture focus events
|
|
75
|
-
}
|
|
76
|
-
}, [isActive, dropdownRef])
|
|
77
|
-
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
if (toggleCallback) toggleCallback(isActive)
|
|
80
|
-
}, [isActive])
|
|
81
|
-
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
setReady(false)
|
|
84
|
-
if (!isActive || !dropdownRef.current) return
|
|
85
|
-
|
|
86
|
-
const ul = dropdownRef.current.querySelector('ul') as HTMLElement
|
|
87
|
-
if (!ul) return
|
|
88
|
-
|
|
89
|
-
// Temporarily show the ul for measurement
|
|
90
|
-
const originalMaxHeight = ul.style.maxHeight
|
|
91
|
-
const originalVisibility = ul.style.visibility
|
|
92
|
-
const originalOpacity = ul.style.opacity
|
|
93
|
-
const originalPointerEvents = ul.style.pointerEvents
|
|
94
|
-
|
|
95
|
-
ul.style.maxHeight = 'none'
|
|
96
|
-
ul.style.visibility = 'hidden'
|
|
97
|
-
ul.style.opacity = '0'
|
|
98
|
-
ul.style.pointerEvents = 'none'
|
|
99
|
-
|
|
100
|
-
const dropdownHeight = ul.getBoundingClientRect().height
|
|
101
|
-
|
|
102
|
-
// Revert styles
|
|
103
|
-
ul.style.maxHeight = originalMaxHeight
|
|
104
|
-
ul.style.visibility = originalVisibility
|
|
105
|
-
ul.style.opacity = originalOpacity
|
|
106
|
-
ul.style.pointerEvents = originalPointerEvents
|
|
107
|
-
|
|
108
|
-
const rect = dropdownRef.current.getBoundingClientRect()
|
|
109
|
-
const spaceBelow = window.innerHeight - rect.bottom
|
|
110
|
-
const spaceAbove = rect.top
|
|
111
|
-
|
|
112
|
-
const side = dir.endsWith('right') ? 'right' : 'left'
|
|
113
|
-
|
|
114
|
-
const newDirection = dir.startsWith('bottom')
|
|
115
|
-
? `${spaceBelow < dropdownHeight && spaceAbove > dropdownHeight ? 'top' : 'bottom'}-${side}`
|
|
116
|
-
: `${spaceAbove < dropdownHeight && spaceBelow > dropdownHeight ? 'bottom' : 'top'}-${side}`
|
|
117
|
-
|
|
118
|
-
setDirection(newDirection as 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right')
|
|
119
|
-
|
|
120
|
-
requestAnimationFrame(() => {
|
|
121
|
-
setReady(true)
|
|
122
|
-
})
|
|
123
|
-
}, [isActive, dir])
|
|
124
|
-
|
|
125
|
-
function onMouseDown(e: { key: string, preventDefault: Function }) {
|
|
126
|
-
if (e.key && e.key != 'Enter') return
|
|
127
|
-
if (e.key) e.preventDefault() // for button, stops buttons firing twice
|
|
128
|
-
if (!isHoverable && !menuIsOpen && ((menuToggles || e.key) || !isActive)) setIsActive(!isActive)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function onClick(option: { onClick?: Function }, e: React.MouseEvent) {
|
|
132
|
-
if (option.onClick) option.onClick(e)
|
|
133
|
-
if (!menuIsOpen) setIsActive(!isActive)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return (
|
|
137
|
-
<div
|
|
138
|
-
class={
|
|
139
|
-
`relative is-${direction || dir}` + // until hovered, show the original direction to prevent scrollbars
|
|
140
|
-
(isHoverable ? ' is-hoverable' : '') +
|
|
141
|
-
(isActive ? ' is-active' : '') +
|
|
142
|
-
(!animate ? ' no-animation' : '') +
|
|
143
|
-
(allowOverflow ? ' is-allowOverflow' : '') +
|
|
144
|
-
' nitro-dropdown' +
|
|
145
|
-
(className ? ` ${className}` : '')
|
|
146
|
-
}
|
|
147
|
-
onClick={(e) => e.stopPropagation()} // required for dropdowns inside row links
|
|
148
|
-
ref={dropdownRef}
|
|
149
|
-
css={style}
|
|
150
|
-
>
|
|
151
|
-
{
|
|
152
|
-
(Array.isArray(children) ? children : [children]).map((el, key) => {
|
|
153
|
-
const onKeyDown = onMouseDown
|
|
154
|
-
if (!el.type) throw new Error('Dropdown component requires a valid child element')
|
|
155
|
-
return cloneElement(el, { key, onMouseDown, onKeyDown }) // adds onClick
|
|
156
|
-
})
|
|
157
|
-
}
|
|
158
|
-
<ul
|
|
159
|
-
style={{ minWidth }}
|
|
160
|
-
class={
|
|
161
|
-
twMerge(`${menuStyle} ${ready ? 'is-ready' : ''} absolute invisible opacity-0 select-none min-w-full z-[1] ${menuClassName||''}`)}
|
|
162
|
-
>
|
|
163
|
-
{menuContent}
|
|
164
|
-
{
|
|
165
|
-
options && options.map((option, i) => {
|
|
166
|
-
const optionStyle = getSelectStyle({ name: 'option', usePrefixes: true, isSelected: option.isSelected })
|
|
167
|
-
return (
|
|
168
|
-
<li
|
|
169
|
-
key={i}
|
|
170
|
-
className={twMerge(`${optionStyle} ${option.className} ${menuOptionClassName}`)}
|
|
171
|
-
onClick={(e: React.MouseEvent) => onClick(option, e)}
|
|
172
|
-
>
|
|
173
|
-
<span class="flex-auto">{option.label}</span>
|
|
174
|
-
{ !!option.icon && option.icon }
|
|
175
|
-
{ option.isSelected && <CheckCircleIcon className="size-[22px] text-primary -my-1 -mx-0.5" /> }
|
|
176
|
-
</li>
|
|
177
|
-
)
|
|
178
|
-
})
|
|
179
|
-
}
|
|
180
|
-
</ul>
|
|
181
|
-
</div>
|
|
182
|
-
)
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
const style = css`
|
|
186
|
-
&>ul {
|
|
187
|
-
transition: transform 0.15s ease, opacity 0.15s ease, visibility 0s 0.15s ease, max-width 0s 0.15s ease, max-height 0s 0.15s ease;
|
|
188
|
-
max-width: 0; // handy if the dropdown ul exceeds the viewport width
|
|
189
|
-
max-height: 0; // handy if the dropdown ul exceeds the viewport height
|
|
190
|
-
pointer-events: none;
|
|
191
|
-
}
|
|
192
|
-
&.is-bottom-right,
|
|
193
|
-
&.is-top-right {
|
|
194
|
-
ul {
|
|
195
|
-
left: auto;
|
|
196
|
-
right: 0;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
&.is-bottom-left,
|
|
200
|
-
&.is-bottom-right {
|
|
201
|
-
&>ul {
|
|
202
|
-
top: 100%;
|
|
203
|
-
transform: translateY(6px);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
&.is-top-left,
|
|
207
|
-
&.is-top-right {
|
|
208
|
-
&>ul {
|
|
209
|
-
bottom: 100%;
|
|
210
|
-
transform: translateY(-10px);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
// active submenu
|
|
214
|
-
&.is-hoverable:hover,
|
|
215
|
-
&:focus,
|
|
216
|
-
&.is-active,
|
|
217
|
-
&>ul>li:hover,
|
|
218
|
-
&>ul>li:focus,
|
|
219
|
-
&>ul>li.is-active {
|
|
220
|
-
&>ul.is-ready {
|
|
221
|
-
opacity: 1;
|
|
222
|
-
visibility: visible;
|
|
223
|
-
transition: transform 0.15s ease, opacity 0.15s ease;
|
|
224
|
-
max-width: 1000px;
|
|
225
|
-
max-height: 1000px;
|
|
226
|
-
pointer-events: auto;
|
|
227
|
-
}
|
|
228
|
-
&.is-allowOverflow > ul {
|
|
229
|
-
overflow: visible;
|
|
230
|
-
}
|
|
231
|
-
&.is-bottom-left > ul,
|
|
232
|
-
&.is-bottom-right > ul {
|
|
233
|
-
transform: translateY(3px) !important;
|
|
234
|
-
}
|
|
235
|
-
&.is-top-left > ul,
|
|
236
|
-
&.is-top-right > ul {
|
|
237
|
-
transform: translateY(-7px) !important;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
// no animation
|
|
241
|
-
&.no-animation {
|
|
242
|
-
&>ul {
|
|
243
|
-
transition: none;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
`
|
|
247
|
-
|
|
248
|
-
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import { forwardRef, Dispatch, SetStateAction, useRef, useEffect, useImperativeHandle } from 'react'
|
|
2
|
-
import { Button, Dropdown, Field, Select, type FieldProps, type SelectProps } from 'nitro-web'
|
|
3
|
-
import { camelCaseToTitle, debounce, omit, queryString, queryObject, twMerge } from 'nitro-web/util'
|
|
4
|
-
import { ListFilterIcon } from 'lucide-react'
|
|
5
|
-
|
|
6
|
-
type CommonProps = {
|
|
7
|
-
label?: string
|
|
8
|
-
rowClassName?: string
|
|
9
|
-
}
|
|
10
|
-
export type FilterType = (
|
|
11
|
-
| FieldProps & CommonProps
|
|
12
|
-
| ({ type: 'select' } & SelectProps & CommonProps)
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
type FilterState = {
|
|
16
|
-
[key: string]: string | true
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
type FiltersProps = {
|
|
20
|
-
state?: FilterState
|
|
21
|
-
setState?: Dispatch<SetStateAction<FilterState>>
|
|
22
|
-
filters?: FilterType[]
|
|
23
|
-
elements?: {
|
|
24
|
-
Button?: typeof Button
|
|
25
|
-
Dropdown?: typeof Dropdown
|
|
26
|
-
Field?: typeof Field
|
|
27
|
-
Select?: typeof Select
|
|
28
|
-
FilterIcon?: typeof ListFilterIcon
|
|
29
|
-
}
|
|
30
|
-
buttonProps?: Partial<React.ComponentProps<typeof Button>>
|
|
31
|
-
buttonText?: string
|
|
32
|
-
buttonCounterClassName?: string
|
|
33
|
-
dropdownProps?: Partial<React.ComponentProps<typeof Dropdown>>
|
|
34
|
-
dropdownFiltersClassName?: string
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export type FiltersHandleType = {
|
|
38
|
-
submit: (includePagination?: boolean) => void
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const debounceTime = 250
|
|
42
|
-
|
|
43
|
-
export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
|
|
44
|
-
filters,
|
|
45
|
-
setState: setState2,
|
|
46
|
-
state: state2,
|
|
47
|
-
buttonProps,
|
|
48
|
-
buttonCounterClassName,
|
|
49
|
-
buttonText,
|
|
50
|
-
dropdownProps,
|
|
51
|
-
dropdownFiltersClassName,
|
|
52
|
-
elements,
|
|
53
|
-
}, ref) => {
|
|
54
|
-
const location = useLocation()
|
|
55
|
-
const navigate = useNavigate()
|
|
56
|
-
const [lastUpdated, setLastUpdated] = useState(0)
|
|
57
|
-
const [debouncedSubmit] = useState(() => debounce(submit, debounceTime))
|
|
58
|
-
const [state3, setState3] = useState(() => ({ ...queryObject(location.search) }))
|
|
59
|
-
const [state, setState] = [state2 || state3, setState2 || setState3]
|
|
60
|
-
const stateRef = useRef(state)
|
|
61
|
-
const count = useMemo(() => Object.keys(state).filter((k) => state[k] && filters?.some((f) => f.name === k)).length, [state, filters])
|
|
62
|
-
|
|
63
|
-
const Elements = {
|
|
64
|
-
Button: elements?.Button || Button,
|
|
65
|
-
Dropdown: elements?.Dropdown || Dropdown,
|
|
66
|
-
Field: elements?.Field || Field,
|
|
67
|
-
Select: elements?.Select || Select,
|
|
68
|
-
FilterIcon: elements?.FilterIcon || ListFilterIcon,
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
useImperativeHandle(ref, () => ({
|
|
72
|
-
submit: debouncedSubmit,
|
|
73
|
-
}))
|
|
74
|
-
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
return () => debouncedSubmit.cancel()
|
|
77
|
-
}, [])
|
|
78
|
-
|
|
79
|
-
useEffect(() => {
|
|
80
|
-
stateRef.current = state
|
|
81
|
-
}, [state])
|
|
82
|
-
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
// Only update the state if the filters haven't been input changed in the last 500ms
|
|
85
|
-
if (Date.now() - lastUpdated > (debounceTime + 250)) {
|
|
86
|
-
setState(() => ({
|
|
87
|
-
...queryObject(location.search),
|
|
88
|
-
}))
|
|
89
|
-
}
|
|
90
|
-
}, [location.search])
|
|
91
|
-
|
|
92
|
-
function reset(e: React.MouseEvent<HTMLAnchorElement>, filter: FilterType) {
|
|
93
|
-
e.preventDefault()
|
|
94
|
-
setState((s) => omit(s, [filter.name]) as FilterState)
|
|
95
|
-
onAfterChange()
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function resetAll(e: React.MouseEvent<HTMLButtonElement>) {
|
|
99
|
-
e.preventDefault()
|
|
100
|
-
setState((s) => omit(s, filters?.map((f) => f.name) || []) as FilterState)
|
|
101
|
-
onAfterChange()
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function onInputChange(e: {target: {name: string, value: unknown}}) {
|
|
105
|
-
// basic name and value (keeping deep paths intact e.g. 'jobCache.location': '10')
|
|
106
|
-
setState((s) => ({ ...s, [e.target.name]: e.target.value as string }))
|
|
107
|
-
onAfterChange()
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function onAfterChange() {
|
|
111
|
-
setLastUpdated(Date.now())
|
|
112
|
-
debouncedSubmit()
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Update the URL by replacing the current entry in the history stack
|
|
116
|
-
function submit(includePagination?: boolean) {
|
|
117
|
-
const queryStr = queryString(omit(stateRef.current, includePagination ? [] : ['page']))
|
|
118
|
-
navigate(location.pathname + queryStr, { replace: true })
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
<Elements.Dropdown
|
|
123
|
-
// menuIsOpen={true}
|
|
124
|
-
dir="bottom-right"
|
|
125
|
-
allowOverflow={true}
|
|
126
|
-
{...dropdownProps}
|
|
127
|
-
menuClassName={twMerge(`min-w-[330px] ${dropdownProps?.menuClassName || ''}`)}
|
|
128
|
-
menuContent={
|
|
129
|
-
<div>
|
|
130
|
-
<div class="flex justify-between items-center border-b p-4 py-3.5">
|
|
131
|
-
<div class="text-lg font-semibold">Filters</div>
|
|
132
|
-
<Button color="clear" size="sm" onClick={resetAll}>Reset All</Button>
|
|
133
|
-
</div>
|
|
134
|
-
{/* <div class="w-[1330px] bg-red-500 absolute">
|
|
135
|
-
This div shouldnt produce a page scrollbar when the dropdown is closed.
|
|
136
|
-
But should be visibile if allowedOverflow is true.
|
|
137
|
-
</div> */}
|
|
138
|
-
<div class={twMerge(`flex flex-wrap gap-4 px-4 py-4 pb-6 ${dropdownFiltersClassName || ''}`)}>
|
|
139
|
-
{
|
|
140
|
-
filters?.map(({label, rowClassName, ...filter}, i) => (
|
|
141
|
-
<div key={i} class={twMerge(`w-full ${rowClassName||''}`)}>
|
|
142
|
-
<div class="flex justify-between">
|
|
143
|
-
<label for={filter.id || filter.name}>{label || camelCaseToTitle(filter.name)}</label>
|
|
144
|
-
<a href="#" class="label font-normal text-secondary underline" onClick={(e) => reset(e, filter)}>Reset</a>
|
|
145
|
-
</div>
|
|
146
|
-
{
|
|
147
|
-
filter.type === 'select' &&
|
|
148
|
-
<Elements.Select
|
|
149
|
-
{...filter}
|
|
150
|
-
class="!mb-0"
|
|
151
|
-
value={state[filter.name] || ''}
|
|
152
|
-
onChange={onInputChange}
|
|
153
|
-
type={undefined}
|
|
154
|
-
/>
|
|
155
|
-
}
|
|
156
|
-
{
|
|
157
|
-
filter.type !== 'select' &&
|
|
158
|
-
<Elements.Field
|
|
159
|
-
{...filter}
|
|
160
|
-
class="!mb-0"
|
|
161
|
-
value={(state[filter.name] as string) || ''}
|
|
162
|
-
onChange={onInputChange}
|
|
163
|
-
/>
|
|
164
|
-
}
|
|
165
|
-
</div>
|
|
166
|
-
))
|
|
167
|
-
}
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
170
|
-
}
|
|
171
|
-
>
|
|
172
|
-
<Elements.Button
|
|
173
|
-
color="white"
|
|
174
|
-
IconLeft={<Elements.FilterIcon size={16} />}
|
|
175
|
-
{...buttonProps}
|
|
176
|
-
className={twMerge(`flex gap-x-2.5 ${buttonProps?.className || ''}`)}
|
|
177
|
-
>
|
|
178
|
-
<span class="flex items-center gap-x-2.5">
|
|
179
|
-
{ buttonText || 'Filter By' }
|
|
180
|
-
{
|
|
181
|
-
!!count &&
|
|
182
|
-
<span
|
|
183
|
-
class={twMerge(`inline-flex items-center justify-center rounded-full text-white bg-primary box-content w-[1em] h-[1em] p-[2px] ${buttonCounterClassName || ''}`)}
|
|
184
|
-
>
|
|
185
|
-
<span class="text-xs">{count}</span>
|
|
186
|
-
</span>
|
|
187
|
-
}
|
|
188
|
-
</span>
|
|
189
|
-
</Elements.Button>
|
|
190
|
-
</Elements.Dropdown>
|
|
191
|
-
)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
Filters.displayName = 'Filters'
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import GithubIcon from 'nitro-web/client/imgs/github.svg'
|
|
2
|
-
|
|
3
|
-
export function GithubLink({ filename }: { filename: string }) {
|
|
4
|
-
const base = 'https://github.com/boycce/nitro-web/blob/master/packages/'
|
|
5
|
-
// Filenames are relative to the webpack start directory
|
|
6
|
-
// 1. Remove ../ from filename (i.e. for _example build)
|
|
7
|
-
// 2. Remove node_modules/nitro-web/ from filename (i.e. for packages using nitro-web)
|
|
8
|
-
const link = base + filename.replace(/^(\.\.\/|.*node_modules\/nitro-web\/)/, '')
|
|
9
|
-
|
|
10
|
-
return (
|
|
11
|
-
// <a href={link}>Go to Github</a>
|
|
12
|
-
<a href={link} className="fixed top-0 right-0 nitro-github">
|
|
13
|
-
<GithubIcon />
|
|
14
|
-
</a>
|
|
15
|
-
)
|
|
16
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { css } from 'twin.macro'
|
|
2
|
-
|
|
3
|
-
type InitialsProps = {
|
|
4
|
-
icon?: { initials: string, hex: string }
|
|
5
|
-
isBig?: boolean
|
|
6
|
-
isMedium?: boolean
|
|
7
|
-
isSmall?: boolean
|
|
8
|
-
isRound?: boolean
|
|
9
|
-
className?: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function Initials({ icon, isBig, isMedium, isSmall, isRound, className }: InitialsProps) {
|
|
13
|
-
return (
|
|
14
|
-
<span
|
|
15
|
-
css={style}
|
|
16
|
-
class={
|
|
17
|
-
'initials-square' +
|
|
18
|
-
(isBig ? ' is-big' : '') +
|
|
19
|
-
(isMedium ? ' is-medium' : '') +
|
|
20
|
-
(isSmall ? ' is-small' : '') +
|
|
21
|
-
(isRound ? ' is-round' : '') +
|
|
22
|
-
(icon ? '' : ' is-empty') +
|
|
23
|
-
' nitro-initials' +
|
|
24
|
-
(className ? ' ' + className : '')
|
|
25
|
-
}
|
|
26
|
-
style={icon ? {backgroundColor: icon?.hex + '15', color: icon?.hex} : {}}
|
|
27
|
-
>
|
|
28
|
-
{icon?.initials}
|
|
29
|
-
</span>
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const style = css`
|
|
34
|
-
// seen in input.jsx
|
|
35
|
-
display: flex;
|
|
36
|
-
align-items: center;
|
|
37
|
-
justify-content: center;
|
|
38
|
-
border-radius: 5px;
|
|
39
|
-
font-weight: 700;
|
|
40
|
-
font-size: 11px;
|
|
41
|
-
width: 24px;
|
|
42
|
-
height: 24px;
|
|
43
|
-
// new
|
|
44
|
-
&.is-medium {
|
|
45
|
-
width: 30px;
|
|
46
|
-
height: 30px;
|
|
47
|
-
font-size: 12px;
|
|
48
|
-
}
|
|
49
|
-
// seen in select.jsx
|
|
50
|
-
&.is-small {
|
|
51
|
-
width: 22px;
|
|
52
|
-
height: 22px;
|
|
53
|
-
font-size: 11px;
|
|
54
|
-
}
|
|
55
|
-
&.is-big {
|
|
56
|
-
width: 48px;
|
|
57
|
-
height: 48px;
|
|
58
|
-
font-size: 14px;
|
|
59
|
-
}
|
|
60
|
-
&.is-round {
|
|
61
|
-
border-radius: 50%;
|
|
62
|
-
}
|
|
63
|
-
&.is-empty {
|
|
64
|
-
width: 0;
|
|
65
|
-
}
|
|
66
|
-
`
|