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,248 @@
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
+
@@ -0,0 +1,194 @@
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'
@@ -0,0 +1,16 @@
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
+ }
@@ -0,0 +1,66 @@
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
+ `
@@ -0,0 +1,141 @@
1
+ // Todo: show correct message type, e.g. error, warning, info, success `${store.message.type || 'success'}`
2
+ import { isObject, isString, queryObject } from 'nitro-web/util'
3
+ import { X, CircleCheck } from 'lucide-react'
4
+ import { MessageObject } from 'nitro-web/types'
5
+ import { twMerge } from 'nitro-web'
6
+
7
+ type MessageProps = {
8
+ className?: string
9
+ classNameWrapper?: string
10
+ position?: 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
11
+ }
12
+ /**
13
+ * Shows a message
14
+ * Triggered by navigating to a link with a valid query string, or by setting store.message to a string or more explicitly, to an object
15
+ **/
16
+ export function Message({ className, classNameWrapper, position='top-right' }: MessageProps) {
17
+ const devDontHide = false
18
+ const [store, setStore] = useTracked()
19
+ const [visible, setVisible] = useState(false)
20
+ const location = useLocation()
21
+ const messageQueryMap = {
22
+ 'added': { type: 'success', text: 'Added successfully 👍️' },
23
+ 'created': { type: 'success', text: 'Created successfully 👍️' },
24
+ 'error': { type: 'error', text: 'Sorry, there was an error' },
25
+ 'oauth-error': { type: 'error', text: 'There was an error trying to signin, please try again' },
26
+ 'removed': { type: 'success', text: 'Removed' },
27
+ 'signin': { type: 'error', text: 'Please sign in to access this page' },
28
+ 'updated': { type: 'success', text: 'Updated successfully' },
29
+ 'unauth': { type: 'error', text: 'You are unauthorised' },
30
+ }
31
+ const colorMap = {
32
+ 'error': 'text-danger',
33
+ 'warning': 'text-warning',
34
+ 'info': 'text-info',
35
+ 'success': 'text-success',
36
+ }
37
+ const positionMap = {
38
+ 'top-left': ['sm:items-start sm:justify-start', 'sm:translate-y-0 sm:translate-x-[-0.5rem]'],
39
+ 'top-center': ['sm:items-start sm:justify-center', 'sm:translate-y-[-0.5rem]'],
40
+ 'top-right': ['sm:items-start sm:justify-end', 'sm:translate-y-0 sm:translate-x-1'],
41
+ 'bottom-left': ['sm:items-end sm:justify-start', 'sm:translate-y-0 sm:translate-x-[-0.5rem]'],
42
+ 'bottom-center': ['sm:items-end sm:justify-center', 'sm:translate-y-1'],
43
+ 'bottom-right': ['sm:items-end sm:justify-end', 'sm:translate-y-0 sm:translate-x-1'],
44
+ }
45
+ const color = colorMap[(store.message as MessageObject)?.type || 'success']
46
+ const positionArr = positionMap[(position as keyof typeof positionMap)]
47
+
48
+ useEffect(() => {
49
+ return () => {
50
+ setStore(s => ({ ...s, message: '' }))
51
+ }
52
+ }, [])
53
+
54
+ useEffect(() => {
55
+ // Finds a message in a query string and show it
56
+ let message
57
+ const query = queryObject(location.search, true)
58
+ for (const key in query) {
59
+ if (!query.hasOwnProperty(key)) continue
60
+ for (const key2 in messageQueryMap) {
61
+ if (key != key2) continue
62
+ // @ts-expect-error
63
+ message = { ...messageQueryMap[key] }
64
+ if (query[key] !== true) message.text = decodeURIComponent(query[key])
65
+ }
66
+ }
67
+ if (message) setStore(s => ({ ...s, message: message }))
68
+ }, [location.search])
69
+
70
+ useEffect(() => {
71
+ // Message detection and autohiding
72
+ const now = new Date().getTime()
73
+ const messageObject = store.message as MessageObject
74
+
75
+ if (!store.message) {
76
+ return
77
+ // Convert a string into a message object
78
+ } else if (isString(store.message)) {
79
+ setStore(s => ({ ...s, message: { type: 'success', text: store.message as string, date: now }}))
80
+ // Add a date to the message
81
+ } else if (!messageObject.date) {
82
+ setStore(s => ({ ...s, message: { ...messageObject, date: now }}))
83
+ // Show message and hide it again after some time. Send back cleanup if store.message changes
84
+ } else if (messageObject && now - 500 < messageObject.date) {
85
+ const timeout1 = setTimeout(() => setVisible(true), 50)
86
+ if (messageObject.timeout !== 0 && !devDontHide) var timeout2 = setTimeout(hide, messageObject.timeout || 5000)
87
+ return () => {
88
+ clearTimeout(timeout1)
89
+ clearTimeout(timeout2)
90
+ }
91
+ }
92
+ }, [JSON.stringify(store.message)])
93
+
94
+ function hide() {
95
+ setVisible(false)
96
+ setTimeout(() => setStore(s => ({ ...s, message: undefined })), 250)
97
+ }
98
+
99
+ return (
100
+ <>
101
+ {/* Global notification live region, render this permanently at the end of the document */}
102
+ <div
103
+ aria-live="assertive"
104
+ className={`${twMerge(`pointer-events-none items-end justify-center fixed inset-0 flex px-4 py-6 sm:p-6 z-[101] nitro-message ${positionArr[0]} ${classNameWrapper || ''}`)}`}
105
+ >
106
+ <div className="flex flex-col items-center space-y-4">
107
+ {isObject(store.message) && (
108
+ <div className={twMerge(
109
+ 'overflow-hidden translate-y-[0.5rem] opacity-0 pointer-events-auto max-w-[350px] rounded-md bg-white shadow-lg ring-1 ring-black/5 transition text-sm font-medium text-gray-900',
110
+ positionArr[1],
111
+ (visible ? 'translate-x-0 translate-y-0 sm:translate-x-0 sm:translate-y-0 opacity-100' : ''),
112
+ className
113
+ )}>
114
+ <div className="p-3">
115
+ <div className="flex items-start gap-3 leading-[1.4em]">
116
+ <div className="flex items-center shrink-0 min-h-[1.4em]">
117
+ <CircleCheck aria-hidden="true" size={19} className={`${color}`} />
118
+ </div>
119
+ <div className="flex flex-1 items-center min-h-[1.4em]">
120
+ <p>{typeof store.message === 'object' && store.message?.text}</p>
121
+ </div>
122
+ <div className="flex items-center shrink-0 min-h-[1.4em]">
123
+ <button
124
+ type="button"
125
+ onClick={hide}
126
+ className="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2
127
+ focus:ring-indigo-500 focus:ring-offset-2"
128
+ >
129
+ <span className="sr-only">Close</span>
130
+ <X aria-hidden="true" size={19} />
131
+ </button>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ )}
137
+ </div>
138
+ </div>
139
+ </>
140
+ )
141
+ }