nitro-web 0.0.39 → 0.0.40
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/app.tsx +18 -11
- package/client/store.ts +3 -1
- package/components/auth/auth.api.js +2 -2
- package/components/auth/signin.tsx +1 -1
- package/components/partials/element/avatar.tsx +1 -2
- package/components/partials/element/dropdown.tsx +1 -2
- package/components/partials/form/field-date.tsx +5 -1
- package/components/partials/form/field-time.tsx +163 -0
- package/components/partials/form/field.tsx +16 -2
- package/components/partials/icons/clock.tsx +18 -0
- package/components/partials/styleguide.tsx +27 -5
- package/package.json +6 -2
- package/types/util.d.ts +615 -106
- package/types/util.d.ts.map +1 -1
- package/types.ts +5 -2
- package/util.js +870 -437
package/client/app.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createBrowserRouter, createHashRouter, redirect, useParams, RouterProvider } from 'react-router-dom'
|
|
2
2
|
import { Fragment, ReactNode } from 'react'
|
|
3
3
|
import ReactDOM from 'react-dom/client'
|
|
4
|
-
import { AxiosRequestConfig } from '@hokify/axios'
|
|
5
4
|
import { axios, camelCase, pick, toArray, setTimeoutPromise } from 'nitro-web/util'
|
|
6
5
|
import { injectedConfig, preloadedStoreData, exposedStoreData } from './index'
|
|
7
6
|
import { Config, Store } from 'nitro-web/types'
|
|
@@ -27,11 +26,16 @@ type Settings = {
|
|
|
27
26
|
|
|
28
27
|
type Route = {
|
|
29
28
|
component: React.FC<{ route?: Route; params?: object; location?: object; config?: Config }>
|
|
30
|
-
meta?: { title?: string }
|
|
31
29
|
middleware: string[]
|
|
32
30
|
name: string
|
|
33
31
|
path: string
|
|
34
32
|
redirect?: string
|
|
33
|
+
meta?: { title?: string; layout?: number }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type RouteWithPolicies = Route & {
|
|
37
|
+
// Route policies, e.g. { 'get /signin' : ['isUser'] }
|
|
38
|
+
[key: string]: true | string | (string | true)[]
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
export async function setupApp(config: Config, storeContainer: StoreContainer, layouts: React.FC<LayoutProps>[]) {
|
|
@@ -137,7 +141,7 @@ function getRouter({ settings, config }: { settings: Settings, config: Config })
|
|
|
137
141
|
for (const key in file) {
|
|
138
142
|
const isReactFnComponentOrFnRef = typeof file[key] === 'function' || !!file[key]?.render
|
|
139
143
|
if (!file.hasOwnProperty(key) || key.match(/route/i) || !isReactFnComponentOrFnRef) continue
|
|
140
|
-
const componentRoutes = toArray(file[key].route || file.route || file.Route)
|
|
144
|
+
const componentRoutes = toArray(file[key].route || file.route || file.Route) as RouteWithPolicies[]
|
|
141
145
|
const componentName = key || camelCase(key.replace(/^.*[\\\/]|\.jsx$/g, '')) as string // eslint-disable-line
|
|
142
146
|
// console.log(file)
|
|
143
147
|
// Todo: need to retrieve the original function name for default exports during minification.
|
|
@@ -153,13 +157,13 @@ function getRouter({ settings, config }: { settings: Settings, config: Config })
|
|
|
153
157
|
const routePaths = Object.keys(pick(route, /^(get\s+)?\/|^\*$/))
|
|
154
158
|
|
|
155
159
|
for (const routePath of routePaths) {
|
|
156
|
-
const layoutNum = (route.meta?.layout || 1) - 1
|
|
160
|
+
const layoutNum = (parseInt(String(route.meta?.layout || '1')) || 1) - 1
|
|
157
161
|
|
|
158
162
|
// get the routes middleware
|
|
159
|
-
const middleware = toArray(route[routePath]).filter(
|
|
160
|
-
if (
|
|
161
|
-
else if (settings.middleware
|
|
162
|
-
else console.error(`No middleware named '${
|
|
163
|
+
const middleware = toArray(route[routePath]).filter((policyNameOrTrue) => {
|
|
164
|
+
if (policyNameOrTrue === true) return // ignore true
|
|
165
|
+
else if (policyNameOrTrue in settings.middleware) return true
|
|
166
|
+
else console.error(`No middleware named '${policyNameOrTrue}' defined under config.middleware, skipping..`)
|
|
163
167
|
})
|
|
164
168
|
|
|
165
169
|
// Push route to layout
|
|
@@ -171,7 +175,7 @@ function getRouter({ settings, config }: { settings: Settings, config: Config })
|
|
|
171
175
|
layout: layoutNum,
|
|
172
176
|
title: `${route.meta?.title ? `${route.meta.title}${settings.titleSeparator || ' - '}` : ''}${settings.name}`,
|
|
173
177
|
},
|
|
174
|
-
middleware: middleware,
|
|
178
|
+
middleware: middleware as string[],
|
|
175
179
|
name: componentName,
|
|
176
180
|
path: routePath,
|
|
177
181
|
redirect: route.redirect,
|
|
@@ -203,7 +207,10 @@ function getRouter({ settings, config }: { settings: Settings, config: Config })
|
|
|
203
207
|
path: route.path,
|
|
204
208
|
loader: async () => { // request
|
|
205
209
|
// wait for container/exposedStoreData to be setup
|
|
206
|
-
if (!nonce)
|
|
210
|
+
if (!nonce) {
|
|
211
|
+
nonce = true
|
|
212
|
+
await setTimeoutPromise(() => {}, 0)
|
|
213
|
+
}
|
|
207
214
|
for (const key of route.middleware) {
|
|
208
215
|
const error = settings.middleware[key](route, exposedStoreData || {})
|
|
209
216
|
if (error && error.redirect) {
|
|
@@ -278,7 +285,7 @@ async function beforeApp(config: Config) {
|
|
|
278
285
|
// delete window.prehot
|
|
279
286
|
// }
|
|
280
287
|
if (!config.isStatic) {
|
|
281
|
-
storeData = (await axios().get('/api/store', { 'axios-retry': { retries: 3 }, timeout: 4000 }
|
|
288
|
+
storeData = (await axios().get('/api/store', { 'axios-retry': { retries: 3 }, timeout: 4000 })).data
|
|
282
289
|
apiAvailable = true
|
|
283
290
|
}
|
|
284
291
|
} catch (err) {
|
package/client/store.ts
CHANGED
|
@@ -40,6 +40,8 @@ function beforeUpdate<T extends Store>(newStore: T) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// E.g. Cookie matching handy for rare issues, e.g. signout > signin (to a different user on another tab)
|
|
43
|
-
|
|
43
|
+
if (newStore?.user?._id) {
|
|
44
|
+
axios().defaults.headers.authid = newStore?.user?._id
|
|
45
|
+
}
|
|
44
46
|
return newStore
|
|
45
47
|
}
|
|
@@ -34,9 +34,9 @@ export default {
|
|
|
34
34
|
function setup(middleware, _config) {
|
|
35
35
|
// Setup is called automatically when the server starts
|
|
36
36
|
// Set config values
|
|
37
|
-
const configKeys = ['clientUrl', 'emailFrom', 'env', '
|
|
37
|
+
const configKeys = ['clientUrl', 'emailFrom', 'env', 'name', 'mailgunDomain', 'mailgunKey', 'masterPassword']
|
|
38
38
|
config = pick(_config, configKeys)
|
|
39
|
-
for (const key of
|
|
39
|
+
for (const key of ['clientUrl', 'emailFrom', 'env', 'name']) {
|
|
40
40
|
if (!config[key]) throw new Error(`Missing config value for: config.${key}`)
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -17,7 +17,7 @@ export function Signin() {
|
|
|
17
17
|
useEffect(() => {
|
|
18
18
|
// Autofill the email input from ?email=
|
|
19
19
|
const query = util.queryObject(location.search, true)
|
|
20
|
-
if (query.email) setState({ ...state, email: query.email })
|
|
20
|
+
if (query.email) setState({ ...state, email: query.email as string })
|
|
21
21
|
}, [location.search])
|
|
22
22
|
|
|
23
23
|
useEffect(() => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Initials } from 'nitro-web'
|
|
2
2
|
import { s3Image } from 'nitro-web/util'
|
|
3
|
-
import noImage from 'nitro-web/client/imgs/no-image.svg'
|
|
4
3
|
import avatarImg from 'nitro-web/client/imgs/avatar.jpg'
|
|
5
4
|
import { User } from 'nitro-web/types'
|
|
6
5
|
|
|
@@ -32,7 +31,7 @@ export function Avatar({ awsUrl, isRound, user, showPlaceholderImage, className
|
|
|
32
31
|
|
|
33
32
|
return (
|
|
34
33
|
user.avatar
|
|
35
|
-
? <img class={classes} src={s3Image(awsUrl, user.avatar, 'small')
|
|
34
|
+
? <img class={classes} src={s3Image(awsUrl, user.avatar, 'small')} />
|
|
36
35
|
: showPlaceholderImage ? <img class={classes} src={avatarImg} width="30px" />
|
|
37
36
|
: <Initials className={classes} icon={{ initials: getInitials(user), hex: getHex(user) }} isRound={isRound} isMedium={true} />
|
|
38
37
|
)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { css } from 'twin.macro'
|
|
2
2
|
import { forwardRef, cloneElement } from 'react'
|
|
3
|
-
import { toArray } from 'nitro-web/util'
|
|
4
3
|
import { getSelectStyle } from 'nitro-web'
|
|
5
4
|
import { CheckCircleIcon } from '@heroicons/react/24/solid'
|
|
6
5
|
|
|
@@ -99,7 +98,7 @@ export const Dropdown = forwardRef(function Dropdown({
|
|
|
99
98
|
css={style}
|
|
100
99
|
>
|
|
101
100
|
{
|
|
102
|
-
|
|
101
|
+
(Array.isArray(children) ? children : [children]).map((el, key) => {
|
|
103
102
|
const onKeyDown = onMouseDown
|
|
104
103
|
if (!el.type) throw new Error('Dropdown component requires a valid child element')
|
|
105
104
|
return cloneElement(el, { key, onMouseDown, onKeyDown }) // adds onClick
|
|
@@ -17,6 +17,7 @@ export type FieldDateProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
|
17
17
|
value?: null|number|string|(null|number|string)[]
|
|
18
18
|
numberOfMonths?: number
|
|
19
19
|
Icon?: React.ReactNode
|
|
20
|
+
dir?: 'bottom-left'|'bottom-right'|'top-left'|'top-right'
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
type TimePickerProps = {
|
|
@@ -24,7 +25,9 @@ type TimePickerProps = {
|
|
|
24
25
|
onChange: (mode: Mode, value: number|null) => void
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
export function FieldDate({
|
|
28
|
+
export function FieldDate({
|
|
29
|
+
mode='single', onChange, prefix='', value, numberOfMonths, Icon, showTime, dir = 'bottom-left', ...props
|
|
30
|
+
}: FieldDateProps) {
|
|
28
31
|
const localePattern = `d MMM yyyy${showTime && mode == 'single' ? ' hh:mmaa' : ''}`
|
|
29
32
|
const [prefixWidth, setPrefixWidth] = useState(0)
|
|
30
33
|
const dropdownRef = useRef<DropdownRef>(null)
|
|
@@ -100,6 +103,7 @@ export function FieldDate({ mode='single', onChange, prefix='', value, numberOfM
|
|
|
100
103
|
{!!showTime && mode == 'single' && <TimePicker date={dates?.[0]} onChange={onCalendarChange} />}
|
|
101
104
|
</div>
|
|
102
105
|
}
|
|
106
|
+
dir={dir}
|
|
103
107
|
>
|
|
104
108
|
<div className="grid grid-cols-1">
|
|
105
109
|
{Icon}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import React, { useState, useMemo, useCallback, JSX, useEffect } from 'react'
|
|
2
|
+
import { css, theme } from 'twin.macro'
|
|
3
|
+
import { Dropdown } from 'nitro-web'
|
|
4
|
+
import ClockIcon from '../icons/clock'
|
|
5
|
+
|
|
6
|
+
export interface FieldTimeProps {
|
|
7
|
+
className?: string;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
id?: string;
|
|
10
|
+
onChange: (values: { id: string; value: number; isFalse: boolean }) => void;
|
|
11
|
+
value: string;
|
|
12
|
+
isFull?: boolean;
|
|
13
|
+
Icon?: JSX.Element;
|
|
14
|
+
required?: boolean;
|
|
15
|
+
onFalseCondition?: (id: string, time: number) => boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function FieldTime({
|
|
19
|
+
className, placeholder = '', id = '', onChange, value, isFull = true, Icon = <ClockIcon />, required = false, onFalseCondition, ...rest
|
|
20
|
+
}: FieldTimeProps) {
|
|
21
|
+
|
|
22
|
+
// Parse the incoming time value
|
|
23
|
+
const [time, setTime] = useState(() => Number(value) || 0)
|
|
24
|
+
|
|
25
|
+
const handleTimeChange = useCallback((isHour: boolean, e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
26
|
+
const timeStr = e.target.value
|
|
27
|
+
const number = parseInt(timeStr)
|
|
28
|
+
const hours = Math.floor(time / (60 * 60 * 1000))
|
|
29
|
+
const minutes = Math.floor((time % (60 * 60 * 1000)) / (60 * 1000))
|
|
30
|
+
const newTime = isHour ? (number * 60 * 60 * 1000) + (minutes * 60 * 1000) : (hours * 60 * 60 * 1000) + (number * 60 * 1000)
|
|
31
|
+
|
|
32
|
+
const isFalse = onFalseCondition ? onFalseCondition(id, newTime) : false // if false, not update
|
|
33
|
+
|
|
34
|
+
if (!isFalse) setTime(newTime)
|
|
35
|
+
|
|
36
|
+
if (onChange) onChange({ id: id, value: newTime, isFalse: isFalse })
|
|
37
|
+
|
|
38
|
+
}, [time, id, onChange, onFalseCondition])
|
|
39
|
+
|
|
40
|
+
const hoursTime = useMemo(() => Math.floor(time / (60 * 60 * 1000)), [time])
|
|
41
|
+
|
|
42
|
+
const minutesTime = useMemo(() => Math.floor((time % (60 * 60 * 1000)) / (60 * 1000)), [time])
|
|
43
|
+
|
|
44
|
+
const [displayTime, setDisplayTime] = useState('00:00')
|
|
45
|
+
|
|
46
|
+
const secondOptions = useMemo(() => {
|
|
47
|
+
const [_hours, minutes] = displayTime.split(':').map(Number)
|
|
48
|
+
return [...new Set([minutes, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55])]
|
|
49
|
+
}, [displayTime])
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
setDisplayTime(`${String(hoursTime).padStart(2, '0')}:${String(minutesTime).padStart(2, '0')}`)
|
|
53
|
+
}, [hoursTime, minutesTime])
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Dropdown
|
|
57
|
+
className={isFull ? 'w-full' : 'w-auto'}
|
|
58
|
+
// @ts-ignore
|
|
59
|
+
css={style}
|
|
60
|
+
menuToggles={false}
|
|
61
|
+
animate={false}
|
|
62
|
+
menuChildren={
|
|
63
|
+
<div className="time-picker-container">
|
|
64
|
+
<div className="time-picker-selectors">
|
|
65
|
+
<select
|
|
66
|
+
className="time-select"
|
|
67
|
+
value={hoursTime}
|
|
68
|
+
onChange={(e) => handleTimeChange(true, e)}
|
|
69
|
+
>
|
|
70
|
+
{[...Array(24).keys()].map(i => (
|
|
71
|
+
<option key={i} value={i}>{String(i).padStart(2, '0')}</option>
|
|
72
|
+
))}
|
|
73
|
+
</select>
|
|
74
|
+
<span className="time-separator">:</span>
|
|
75
|
+
<select
|
|
76
|
+
className="time-select"
|
|
77
|
+
value={minutesTime}
|
|
78
|
+
onChange={(e) => handleTimeChange(false, e)}
|
|
79
|
+
>
|
|
80
|
+
{secondOptions.map(option => (
|
|
81
|
+
<option key={option} value={option}>{String(option).padStart(2, '0')}</option>
|
|
82
|
+
))}
|
|
83
|
+
</select>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
<div className="grid grid-cols-1">
|
|
89
|
+
{Icon}
|
|
90
|
+
<input
|
|
91
|
+
{...rest}
|
|
92
|
+
key={id}
|
|
93
|
+
id={id}
|
|
94
|
+
autoComplete="off"
|
|
95
|
+
className={`hide-time-icon font-medium text-sm placeholder-font-medium placeholder-sm ${className}`}
|
|
96
|
+
value={displayTime}
|
|
97
|
+
onChange={(e) => {
|
|
98
|
+
const newStr = e.target.value
|
|
99
|
+
const isValid = /^[0-9]{2}:[0-9]{2}$/.test(newStr)
|
|
100
|
+
if (!isValid) {
|
|
101
|
+
setDisplayTime(newStr)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
const [hours, minutes] = newStr.split(':').map(Number)
|
|
105
|
+
const newNum = (hours * 60 + minutes) * 60 * 1000
|
|
106
|
+
|
|
107
|
+
setTime(newNum)
|
|
108
|
+
setDisplayTime(newStr)
|
|
109
|
+
}}
|
|
110
|
+
placeholder={placeholder}
|
|
111
|
+
required={required}
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
</Dropdown>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default FieldTime
|
|
119
|
+
|
|
120
|
+
const style = css`
|
|
121
|
+
.time-picker-container {
|
|
122
|
+
padding: 15px;
|
|
123
|
+
font-size: 14px;
|
|
124
|
+
background-color: white;
|
|
125
|
+
border-radius: 8px;
|
|
126
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.time-picker-selectors {
|
|
130
|
+
display: flex;
|
|
131
|
+
justify-content: center;
|
|
132
|
+
align-items: center;
|
|
133
|
+
gap: 5px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.time-separator {
|
|
137
|
+
font-size: 18px;
|
|
138
|
+
font-weight: bold;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.time-select {
|
|
142
|
+
width: 60px;
|
|
143
|
+
padding: 8px;
|
|
144
|
+
font-size: 14px;
|
|
145
|
+
border: 1px solid #ccc;
|
|
146
|
+
border-radius: 4px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.time-confirm-btn {
|
|
150
|
+
padding: 8px 16px;
|
|
151
|
+
font-size: 16px;
|
|
152
|
+
background-color: ${theme`colors.primary`};
|
|
153
|
+
color: white;
|
|
154
|
+
border: none;
|
|
155
|
+
border-radius: 4px;
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
margin-top: 10px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.time-confirm-btn:hover {
|
|
161
|
+
background-color: ${theme`colors.primary`};
|
|
162
|
+
}
|
|
163
|
+
`
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
EyeSlashIcon,
|
|
12
12
|
} from '@heroicons/react/20/solid'
|
|
13
13
|
// Maybe use fill-current tw class for lucide icons (https://github.com/lucide-icons/lucide/discussions/458)
|
|
14
|
+
import FieldTime, { FieldTimeProps } from './field-time'
|
|
15
|
+
import ClockIcon from '../icons/clock'
|
|
14
16
|
|
|
15
17
|
type InputProps = React.InputHTMLAttributes<HTMLInputElement>
|
|
16
18
|
type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
|
@@ -20,7 +22,7 @@ type FieldExtraProps = {
|
|
|
20
22
|
id?: string
|
|
21
23
|
// state object to get the value, and check errors against
|
|
22
24
|
state?: { errors?: Errors, [key: string]: unknown }
|
|
23
|
-
type?: 'text' | 'password' | 'email' | 'filter' | 'search' | 'textarea' | 'currency' | 'date' | 'color'
|
|
25
|
+
type?: 'text' | 'password' | 'email' | 'filter' | 'search' | 'textarea' | 'currency' | 'date' | 'time' | 'color'
|
|
24
26
|
icon?: React.ReactNode
|
|
25
27
|
iconPos?: 'left' | 'right'
|
|
26
28
|
}
|
|
@@ -36,6 +38,7 @@ export type FieldProps = (
|
|
|
36
38
|
| ({ type: 'currency' } & FieldCurrencyProps & FieldExtraProps)
|
|
37
39
|
| ({ type: 'color' } & FieldColorProps & FieldExtraProps)
|
|
38
40
|
| ({ type: 'date' } & FieldDateProps & FieldExtraProps)
|
|
41
|
+
| ({ type: 'time' } & FieldTimeProps & FieldExtraProps)
|
|
39
42
|
)
|
|
40
43
|
|
|
41
44
|
export function Field({ state, icon, iconPos: ip, ...props }: FieldProps) {
|
|
@@ -57,7 +60,10 @@ export function Field({ state, icon, iconPos: ip, ...props }: FieldProps) {
|
|
|
57
60
|
|
|
58
61
|
// Value: Input is always controlled if state is passed in
|
|
59
62
|
if (props.value) value = props.value as string
|
|
60
|
-
else if (typeof state == 'object')
|
|
63
|
+
else if (typeof state == 'object') {
|
|
64
|
+
const v = util.deepFind(state, props.name) as string | undefined
|
|
65
|
+
value = v ?? ''
|
|
66
|
+
}
|
|
61
67
|
|
|
62
68
|
// Errors: find any that match this field path
|
|
63
69
|
for (const item of (state?.errors || [])) {
|
|
@@ -83,6 +89,8 @@ export function Field({ state, icon, iconPos: ip, ...props }: FieldProps) {
|
|
|
83
89
|
Icon = <IconWrapper iconPos={iconPos} icon={icon || <ColorSvg hex={value}/>} className="size-[17px]" />
|
|
84
90
|
} else if (type == 'date') {
|
|
85
91
|
Icon = <IconWrapper iconPos={iconPos} icon={icon || <CalendarIcon />} className="size-4" />
|
|
92
|
+
} else if (type == 'time') {
|
|
93
|
+
Icon = <IconWrapper iconPos={iconPos} icon={icon || <ClockIcon />} className="size-4" />
|
|
86
94
|
} else {
|
|
87
95
|
Icon = <IconWrapper iconPos={iconPos} icon={icon} />
|
|
88
96
|
}
|
|
@@ -122,6 +130,12 @@ export function Field({ state, icon, iconPos: ip, ...props }: FieldProps) {
|
|
|
122
130
|
<FieldDate {...props} {...commonProps} Icon={Icon} />
|
|
123
131
|
</FieldContainer>
|
|
124
132
|
)
|
|
133
|
+
} else if (type == 'time') {
|
|
134
|
+
return (
|
|
135
|
+
<FieldContainer error={error} className={props.className}>
|
|
136
|
+
<FieldTime {...props} {...commonProps} Icon={Icon} />
|
|
137
|
+
</FieldContainer>
|
|
138
|
+
)
|
|
125
139
|
}
|
|
126
140
|
}
|
|
127
141
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { theme } from 'twin.macro'
|
|
2
|
+
|
|
3
|
+
const ClockIcon = () => {
|
|
4
|
+
return (
|
|
5
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
6
|
+
<g opacity="0.5" clipPath="url(#clip0_12681_10783)">
|
|
7
|
+
<path d="M8.00016 3.99998V7.99998L10.6668 9.33331M14.6668 7.99998C14.6668 11.6819 11.6821 14.6666 8.00016 14.6666C4.31826 14.6666 1.3335 11.6819 1.3335 7.99998C1.3335 4.31808 4.31826 1.33331 8.00016 1.33331C11.6821 1.33331 14.6668 4.31808 14.6668 7.99998Z" stroke={theme`colors.input`} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
8
|
+
</g>
|
|
9
|
+
<defs>
|
|
10
|
+
<clipPath id="clip0_12681_10783">
|
|
11
|
+
<rect width="16" height="16" fill="white" />
|
|
12
|
+
</clipPath>
|
|
13
|
+
</defs>
|
|
14
|
+
</svg>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default ClockIcon
|
|
@@ -14,6 +14,7 @@ export function Styleguide() {
|
|
|
14
14
|
date: Date.now(),
|
|
15
15
|
'date-range': [Date.now(), Date.now() + 1000 * 60 * 60 * 24 * 33],
|
|
16
16
|
'date-time': Date.now(),
|
|
17
|
+
time: '0',
|
|
17
18
|
calendar: [Date.now(), Date.now() + 1000 * 60 * 60 * 24 * 8],
|
|
18
19
|
firstName: 'Bruce',
|
|
19
20
|
errors: [
|
|
@@ -39,13 +40,30 @@ export function Styleguide() {
|
|
|
39
40
|
]
|
|
40
41
|
|
|
41
42
|
function onInputChange (e: { target: { id: string, value: unknown } }) {
|
|
42
|
-
|
|
43
|
+
const {id} = e.target
|
|
44
|
+
let {value} = e.target
|
|
45
|
+
if ((id == 'customer' || id == 'customer2') && value == '') {
|
|
43
46
|
setCustomerSearch('')
|
|
44
|
-
|
|
47
|
+
value = null // clear the selected value
|
|
45
48
|
}
|
|
46
|
-
|
|
49
|
+
const newValue = value
|
|
50
|
+
console.dir('newValue')
|
|
51
|
+
console.dir(newValue)
|
|
52
|
+
setState(s => ({ ...s, [id]: newValue }))
|
|
47
53
|
}
|
|
48
54
|
|
|
55
|
+
/**
|
|
56
|
+
* handleTimeChange
|
|
57
|
+
*/
|
|
58
|
+
const handleTimeChange = () => ({ id, value }: { id: string, value: number | number[] }) => {
|
|
59
|
+
if (Array.isArray(value)) return
|
|
60
|
+
|
|
61
|
+
setState((state) => ({
|
|
62
|
+
...state,
|
|
63
|
+
[id]: value,
|
|
64
|
+
}))
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
function onCustomerSearch (search: string) {
|
|
50
68
|
setCustomerSearch(search || '')
|
|
51
69
|
}
|
|
@@ -266,8 +284,12 @@ export function Styleguide() {
|
|
|
266
284
|
<Field name="date-range" type="date" mode="range" prefix="Date:" state={state} onChange={onInputChange} />
|
|
267
285
|
</div>
|
|
268
286
|
<div>
|
|
269
|
-
<label for="date">Date</label>
|
|
270
|
-
<Field name="date" type="date" state={state} onChange={onInputChange} />
|
|
287
|
+
<label for="date">Date (right aligned)</label>
|
|
288
|
+
<Field name="date" type="date" state={state} onChange={onInputChange} dir="bottom-right" />
|
|
289
|
+
</div>
|
|
290
|
+
<div>
|
|
291
|
+
<label for="time">Time</label>
|
|
292
|
+
<Field name="time" type="time" state={state} value={state.time} onChange={handleTimeChange} />
|
|
271
293
|
</div>
|
|
272
294
|
</div>
|
|
273
295
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nitro-web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.40",
|
|
4
4
|
"repository": "github:boycce/nitro-web",
|
|
5
5
|
"homepage": "https://boycce.github.io/nitro-web/",
|
|
6
6
|
"description": "Nitro is a battle-tested, modular base project to turbocharge your projects, styled using Tailwind 🚀",
|
|
@@ -29,12 +29,13 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@hokify/axios": "^0.19.1",
|
|
32
|
+
"axios": "^1.9.0",
|
|
32
33
|
"axios-retry": "^3.3.1",
|
|
33
34
|
"bcrypt": "^5.0.0",
|
|
34
35
|
"body-parser": "^1.19.0",
|
|
35
36
|
"compression": "^1.7.4",
|
|
36
37
|
"date-fns": "^3.6.0",
|
|
37
|
-
"dateformat": "^
|
|
38
|
+
"dateformat": "^5.0.3",
|
|
38
39
|
"dotenv": "^14.3.2",
|
|
39
40
|
"express": "^4.17.1",
|
|
40
41
|
"express-fileupload": "^1.1.6",
|
|
@@ -49,6 +50,9 @@
|
|
|
49
50
|
"sort-route-addresses-nodeps": "0.0.4",
|
|
50
51
|
"standard-version": "^9.5.0"
|
|
51
52
|
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/dateformat": "^5.0.3"
|
|
55
|
+
},
|
|
52
56
|
"peerDependencies": {
|
|
53
57
|
"@stripe/stripe-js": "^1.34.0",
|
|
54
58
|
"monastery": "^3.5.2",
|