nitro-web 0.0.27 → 0.0.28
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 +20 -44
- package/client/globals.ts +0 -5
- package/client/index.ts +6 -1
- package/client/store.ts +36 -21
- package/components/auth/reset.tsx +6 -6
- package/components/auth/signin.tsx +6 -6
- package/components/auth/signup.tsx +4 -4
- package/components/partials/element/button.tsx +37 -32
- package/components/partials/element/sidebar.tsx +5 -6
- package/components/partials/styleguide.tsx +7 -5
- package/components/settings/settings-account.tsx +3 -3
- package/components/settings/settings-business.tsx +2 -2
- package/components/settings/settings-team--member.tsx +1 -1
- package/components/settings/settings-team.tsx +1 -1
- package/package.json +1 -1
- package/types/util.d.ts +1 -1
- package/util.js +4 -4
package/client/app.tsx
CHANGED
|
@@ -2,10 +2,13 @@ import { createBrowserRouter, createHashRouter, redirect, useParams, RouterProvi
|
|
|
2
2
|
import { Fragment, ReactNode } from 'react'
|
|
3
3
|
import ReactDOM from 'react-dom/client'
|
|
4
4
|
import { AxiosRequestConfig } from '@hokify/axios'
|
|
5
|
-
import { beforeCreate, Provider, exposedData } from './store'
|
|
6
5
|
import { axios, camelCase, pick, toArray, setTimeoutPromise } from 'nitro-web/util'
|
|
6
|
+
import { injectedConfig, preloadedStoreData, exposedStoreData } from './index'
|
|
7
7
|
import { Config, Store } from 'nitro-web/types'
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
type StoreContainer = {
|
|
10
|
+
Provider: React.FC<{ children: ReactNode }>
|
|
11
|
+
}
|
|
9
12
|
|
|
10
13
|
type LayoutProps = {
|
|
11
14
|
config: Config;
|
|
@@ -14,7 +17,7 @@ type LayoutProps = {
|
|
|
14
17
|
type Settings = {
|
|
15
18
|
afterApp?: () => void
|
|
16
19
|
beforeApp: (config: Config) => Promise<object>
|
|
17
|
-
beforeStoreUpdate: (prevStore: Store | null, newData: Store) => Store
|
|
20
|
+
// beforeStoreUpdate: (prevStore: Store | null, newData: Store) => Store
|
|
18
21
|
isStatic?: boolean
|
|
19
22
|
layouts: React.FC<LayoutProps>[]
|
|
20
23
|
middleware: Record<string, (route: unknown, store: Store) => undefined | { redirect: string }>
|
|
@@ -31,11 +34,11 @@ type Route = {
|
|
|
31
34
|
redirect?: string
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
export async function setupApp(config: Config, layouts: React.FC<LayoutProps>[]) {
|
|
37
|
+
export async function setupApp(config: Config, storeContainer: StoreContainer, layouts: React.FC<LayoutProps>[]) {
|
|
38
|
+
if (!layouts) throw new Error('layouts are required')
|
|
35
39
|
// Fetch state and init app
|
|
36
40
|
const settings: Settings = {
|
|
37
41
|
beforeApp: config.beforeApp || beforeApp,
|
|
38
|
-
beforeStoreUpdate: config.beforeStoreUpdate || beforeStoreUpdate,
|
|
39
42
|
isStatic: config.isStatic,
|
|
40
43
|
layouts: layouts,
|
|
41
44
|
middleware: Object.assign(defaultMiddleware, config.middleware || {}),
|
|
@@ -46,12 +49,13 @@ export async function setupApp(config: Config, layouts: React.FC<LayoutProps>[])
|
|
|
46
49
|
// Setup the jwt token
|
|
47
50
|
updateJwt(localStorage.getItem(injectedConfig.jwtName))
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
+
// Fetch the store/state
|
|
53
|
+
const data = (await settings.beforeApp(config)) || {}
|
|
54
|
+
// Make the store data available to the store
|
|
55
|
+
Object.assign(preloadedStoreData, data)
|
|
52
56
|
|
|
53
57
|
const root = ReactDOM.createRoot(document.getElementById('app') as HTMLElement)
|
|
54
|
-
root.render(<App settings={settings} config={config} />)
|
|
58
|
+
root.render(<App settings={settings} config={config} storeContainer={storeContainer} />)
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
export function updateJwt(token?: string | null) {
|
|
@@ -62,7 +66,7 @@ export function updateJwt(token?: string | null) {
|
|
|
62
66
|
else delete axios().defaults.headers.Authorization
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
function App({ settings, config }: { settings: Settings, config: Config }): ReactNode {
|
|
69
|
+
function App({ settings, config, storeContainer }: { settings: Settings, config: Config, storeContainer: StoreContainer }): ReactNode {
|
|
66
70
|
// const themeNormalised = theme
|
|
67
71
|
const router = getRouter({ settings, config })
|
|
68
72
|
// const theme = pick(themeNormalised, []) // e.g. 'topPanelHeight'
|
|
@@ -89,12 +93,12 @@ function App({ settings, config }: { settings: Settings, config: Config }): Reac
|
|
|
89
93
|
}, [!!router])
|
|
90
94
|
|
|
91
95
|
return (
|
|
92
|
-
<Provider>
|
|
96
|
+
<storeContainer.Provider>
|
|
93
97
|
{/* <ThemeProvider theme={themeNormalised}> */}
|
|
94
98
|
{ router && <RouterProvider router={router} /> }
|
|
95
99
|
<AfterApp settings={settings} />
|
|
96
100
|
{/* </ThemeProvider> */}
|
|
97
|
-
</Provider>
|
|
101
|
+
</storeContainer.Provider>
|
|
98
102
|
)
|
|
99
103
|
}
|
|
100
104
|
|
|
@@ -199,10 +203,10 @@ function getRouter({ settings, config }: { settings: Settings, config: Config })
|
|
|
199
203
|
),
|
|
200
204
|
path: route.path,
|
|
201
205
|
loader: async () => { // request
|
|
202
|
-
// wait for container/
|
|
206
|
+
// wait for container/exposedStoreData to be setup
|
|
203
207
|
if (!nonce) nonce = true && await setTimeoutPromise(() => {}, 0) // eslint-disable-line
|
|
204
208
|
for (const key of route.middleware) {
|
|
205
|
-
const error = settings.middleware[key](route,
|
|
209
|
+
const error = settings.middleware[key](route, exposedStoreData || {})
|
|
206
210
|
if (error && error.redirect) {
|
|
207
211
|
return redirect(error.redirect)
|
|
208
212
|
}
|
|
@@ -274,7 +278,7 @@ async function beforeApp(config: Config) {
|
|
|
274
278
|
// sharedStoreCache = window.prehot.sharedStoreCache
|
|
275
279
|
// delete window.prehot
|
|
276
280
|
// }
|
|
277
|
-
if (!
|
|
281
|
+
if (!config.isStatic) {
|
|
278
282
|
stateData = (await axios().get('/api/state', { 'axios-retry': { retries: 3 }, timeout: 4000 } as AxiosRequestConfig)).data
|
|
279
283
|
apiAvailable = true
|
|
280
284
|
}
|
|
@@ -282,35 +286,7 @@ async function beforeApp(config: Config) {
|
|
|
282
286
|
console.error('We had trouble connecting to the API, please refresh')
|
|
283
287
|
console.log(err)
|
|
284
288
|
}
|
|
285
|
-
return { ...
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function beforeStoreUpdate(prevStore: Store | null, newData: Store) {
|
|
289
|
-
/**
|
|
290
|
-
* Get store object (called on signup/signin/signout/state)
|
|
291
|
-
* @param {object} store - existing store
|
|
292
|
-
* @param {object} <newStoreData> - pass to override store with /login or /state request data
|
|
293
|
-
* @return {object} store
|
|
294
|
-
*/
|
|
295
|
-
if (!newData) return newData
|
|
296
|
-
|
|
297
|
-
// If newData.jwt is present, update the jwt token
|
|
298
|
-
if (newData.jwt) {
|
|
299
|
-
updateJwt(newData.jwt)
|
|
300
|
-
delete newData.jwt
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const store = {
|
|
304
|
-
...(prevStore || {
|
|
305
|
-
message: undefined,
|
|
306
|
-
user: undefined, // defined if user is signed in
|
|
307
|
-
}),
|
|
308
|
-
...(newData || {}),
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// E.g. Cookie matching handy for rare issues, e.g. signout > signin (to a different user on another tab)
|
|
312
|
-
axios().defaults.headers.authid = store?.user?._id
|
|
313
|
-
return store
|
|
289
|
+
return { ...stateData, apiAvailable }
|
|
314
290
|
}
|
|
315
291
|
|
|
316
292
|
const defaultMiddleware = {
|
package/client/globals.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
|
|
3
3
|
import { onChange } from 'nitro-web'
|
|
4
|
-
import { useTracked } from './store'
|
|
5
4
|
|
|
6
5
|
declare global {
|
|
7
6
|
// Common application globals
|
|
8
7
|
const onChange: typeof import('nitro-web').onChange
|
|
9
|
-
/** Global shared store, a react-tracked container initialized in `setupApp()` */
|
|
10
|
-
let useTracked: typeof import('./store').useTracked
|
|
11
8
|
|
|
12
9
|
// Common aependency globals
|
|
13
10
|
/** The public API for rendering a history-aware `<a>`. */
|
|
@@ -26,8 +23,6 @@ declare global {
|
|
|
26
23
|
Object.assign(window, {
|
|
27
24
|
// application globals
|
|
28
25
|
onChange: onChange,
|
|
29
|
-
useTracked: useTracked,
|
|
30
|
-
|
|
31
26
|
// dependency globals
|
|
32
27
|
Link: Link,
|
|
33
28
|
useCallback: useCallback,
|
package/client/index.ts
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
import '../types/required-globals.d.ts'
|
|
3
3
|
|
|
4
4
|
// export const pi = parseFloat(3.142)
|
|
5
|
-
|
|
5
|
+
// Utility functions
|
|
6
6
|
export * from '../util.js'
|
|
7
7
|
export * as util from '../util.js'
|
|
8
|
+
export * from '../types'
|
|
9
|
+
|
|
10
|
+
// Main app functions
|
|
11
|
+
export { setupApp, updateJwt } from './app'
|
|
12
|
+
export { createStore, exposedStoreData, preloadedStoreData, setStoreWrapper } from './store'
|
|
8
13
|
|
|
9
14
|
// Component Pages
|
|
10
15
|
export { Signin } from '../components/auth/signin'
|
package/client/store.ts
CHANGED
|
@@ -1,30 +1,45 @@
|
|
|
1
1
|
import { createContainer } from 'react-tracked'
|
|
2
|
+
import { Dispatch, SetStateAction } from 'react'
|
|
3
|
+
import { axios, isObject } from 'nitro-web/util'
|
|
4
|
+
import { updateJwt } from 'nitro-web'
|
|
2
5
|
import { Store } from 'nitro-web/types'
|
|
3
6
|
|
|
4
|
-
export
|
|
7
|
+
export let preloadedStoreData: Store
|
|
8
|
+
export let exposedStoreData: Store
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
10
|
+
export function createStore<T extends Store>(store: T) {
|
|
11
|
+
const container = createContainer(() => {
|
|
12
|
+
// const [state, setState] = useState<T>(() => (initData || store || {}) as T)
|
|
13
|
+
const [state, setState] = useState<T>(() => beforeUpdate((preloadedStoreData || store || {}) as T))
|
|
14
|
+
exposedStoreData = state
|
|
15
|
+
return [state, setStoreWrapper(setState)]
|
|
16
|
+
})
|
|
17
|
+
return container
|
|
18
|
+
}
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
setStore((prevStore: Store) => beforeUpdate(prevStore, updater))
|
|
18
|
-
}
|
|
20
|
+
export function setStoreWrapper<T extends Store>(setState: Dispatch<SetStateAction<T>>, _beforeUpdate?: (newStore: T) => T) {
|
|
21
|
+
_beforeUpdate = _beforeUpdate || beforeUpdate
|
|
22
|
+
return (updater: SetStateAction<T>) => {
|
|
23
|
+
if (typeof updater === 'function') setState((prev: T) => beforeUpdate(updater(prev)))
|
|
24
|
+
else setState(() => beforeUpdate(updater))
|
|
19
25
|
}
|
|
26
|
+
}
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
function beforeUpdate<T extends Store>(newStore: T) {
|
|
29
|
+
/**
|
|
30
|
+
* Get store object (called on signup/signin/signout/state)
|
|
31
|
+
* @param {object} <newData> - pass to override store with /login or /state request data
|
|
32
|
+
* @return {object} store
|
|
33
|
+
*/
|
|
34
|
+
if (!newStore || !isObject(newStore)) return newStore
|
|
35
|
+
|
|
36
|
+
// If newData.jwt is present, update the jwt token
|
|
37
|
+
if (newStore?.jwt) {
|
|
38
|
+
updateJwt(newStore.jwt)
|
|
39
|
+
delete newStore.jwt
|
|
40
|
+
}
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
initData = _initData // normally provided from a /login or /state request data
|
|
29
|
-
beforeUpdate = _beforeUpdate
|
|
42
|
+
// E.g. Cookie matching handy for rare issues, e.g. signout > signin (to a different user on another tab)
|
|
43
|
+
axios().defaults.headers.authid = newStore?.user?._id
|
|
44
|
+
return newStore
|
|
30
45
|
}
|
|
@@ -3,13 +3,13 @@ import { Errors } from 'nitro-web/types'
|
|
|
3
3
|
|
|
4
4
|
export function ResetInstructions() {
|
|
5
5
|
const navigate = useNavigate()
|
|
6
|
-
const isLoading = useState(
|
|
6
|
+
const isLoading = useState()
|
|
7
7
|
const [, setStore] = useTracked()
|
|
8
8
|
const [state, setState] = useState({ email: '', errors: [] as Errors })
|
|
9
9
|
|
|
10
10
|
async function onSubmit (event: React.FormEvent<HTMLFormElement>) {
|
|
11
11
|
try {
|
|
12
|
-
await util.request(
|
|
12
|
+
await util.request('post /api/reset-instructions', state, event, isLoading)
|
|
13
13
|
setStore(s => ({ ...s, message: 'Done! Please check your email.' }))
|
|
14
14
|
navigate('/signin')
|
|
15
15
|
} catch (e) {
|
|
@@ -32,7 +32,7 @@ export function ResetInstructions() {
|
|
|
32
32
|
<FormError state={state} className="pt-2" />
|
|
33
33
|
</div>
|
|
34
34
|
|
|
35
|
-
<Button className="w-full" isLoading={
|
|
35
|
+
<Button className="w-full" isLoading={isLoading[0]} type="submit">Email me a reset password link</Button>
|
|
36
36
|
</form>
|
|
37
37
|
</div>
|
|
38
38
|
)
|
|
@@ -41,7 +41,7 @@ export function ResetInstructions() {
|
|
|
41
41
|
export function ResetPassword() {
|
|
42
42
|
const navigate = useNavigate()
|
|
43
43
|
const params = useParams()
|
|
44
|
-
const isLoading = useState(
|
|
44
|
+
const isLoading = useState()
|
|
45
45
|
const [, setStore] = useTracked()
|
|
46
46
|
const [state, setState] = useState(() => ({
|
|
47
47
|
password: '',
|
|
@@ -52,7 +52,7 @@ export function ResetPassword() {
|
|
|
52
52
|
|
|
53
53
|
async function onSubmit (event: React.FormEvent<HTMLFormElement>) {
|
|
54
54
|
try {
|
|
55
|
-
const data = await util.request(
|
|
55
|
+
const data = await util.request('post /api/reset-password', state, event, isLoading)
|
|
56
56
|
setStore(() => data)
|
|
57
57
|
navigate('/')
|
|
58
58
|
} catch (e) {
|
|
@@ -79,7 +79,7 @@ export function ResetPassword() {
|
|
|
79
79
|
<FormError state={state} className="pt-2" />
|
|
80
80
|
</div>
|
|
81
81
|
|
|
82
|
-
<Button class="w-full" isLoading={
|
|
82
|
+
<Button class="w-full" isLoading={isLoading[0]} type="submit">Reset Password</Button>
|
|
83
83
|
</form>
|
|
84
84
|
</div>
|
|
85
85
|
)
|
|
@@ -5,7 +5,7 @@ export function Signin() {
|
|
|
5
5
|
const navigate = useNavigate()
|
|
6
6
|
const location = useLocation()
|
|
7
7
|
const isSignout = location.pathname == '/signout'
|
|
8
|
-
const isLoading = useState(isSignout
|
|
8
|
+
const isLoading = useState(isSignout)
|
|
9
9
|
const [, setStore] = useTracked()
|
|
10
10
|
const [state, setState] = useState({
|
|
11
11
|
email: injectedConfig.env == 'development' ? (injectedConfig.placeholderEmail || '') : '',
|
|
@@ -24,17 +24,17 @@ export function Signin() {
|
|
|
24
24
|
setStore(() => ({ user: null }))
|
|
25
25
|
// util.axios().get('/api/signout')
|
|
26
26
|
Promise.resolve()
|
|
27
|
-
.then(() => isLoading[1](
|
|
27
|
+
.then(() => isLoading[1](false))
|
|
28
28
|
.then(() => updateJwt())
|
|
29
29
|
.then(() => navigate({ pathname: '/signin', search: location.search }, { replace: true }))
|
|
30
|
-
.catch(err => (console.error(err), isLoading[1](
|
|
30
|
+
.catch(err => (console.error(err), isLoading[1](false)))
|
|
31
31
|
}
|
|
32
32
|
}, [isSignout])
|
|
33
33
|
|
|
34
34
|
async function onSubmit (e: React.FormEvent<HTMLFormElement>) {
|
|
35
35
|
try {
|
|
36
|
-
const data = await util.request(
|
|
37
|
-
isLoading[1](
|
|
36
|
+
const data = await util.request('post /api/signin', state, e, isLoading)
|
|
37
|
+
isLoading[1](true)
|
|
38
38
|
setStore(() => data)
|
|
39
39
|
setTimeout(() => { // wait for setStore
|
|
40
40
|
if (location.search.includes('redirect')) navigate(location.search.replace('?redirect=', ''))
|
|
@@ -67,7 +67,7 @@ export function Signin() {
|
|
|
67
67
|
<FormError state={state} className="pt-2" />
|
|
68
68
|
</div>
|
|
69
69
|
|
|
70
|
-
<Button class="w-full" isLoading={
|
|
70
|
+
<Button class="w-full" isLoading={isLoading[0]} type="submit">Sign In</Button>
|
|
71
71
|
</form>
|
|
72
72
|
</div>
|
|
73
73
|
)
|
|
@@ -3,7 +3,7 @@ import { Errors } from 'nitro-web/types'
|
|
|
3
3
|
|
|
4
4
|
export function Signup() {
|
|
5
5
|
const navigate = useNavigate()
|
|
6
|
-
const isLoading = useState(
|
|
6
|
+
const isLoading = useState(false)
|
|
7
7
|
const [, setStore] = useTracked()
|
|
8
8
|
const [state, setState] = useState({
|
|
9
9
|
email: injectedConfig.env === 'development' ? (injectedConfig.placeholderEmail || '') : '',
|
|
@@ -15,8 +15,8 @@ export function Signup() {
|
|
|
15
15
|
|
|
16
16
|
async function onSubmit (e: React.FormEvent<HTMLFormElement>) {
|
|
17
17
|
try {
|
|
18
|
-
const data = await util.request(
|
|
19
|
-
isLoading[1](
|
|
18
|
+
const data = await util.request('post /api/signup', state, e, isLoading)
|
|
19
|
+
isLoading[1](true)
|
|
20
20
|
setStore(() => data)
|
|
21
21
|
setTimeout(() => navigate('/'), 0) // wait for setStore
|
|
22
22
|
} catch (e) {
|
|
@@ -53,7 +53,7 @@ export function Signup() {
|
|
|
53
53
|
<FormError state={state} className="pt-2" />
|
|
54
54
|
</div>
|
|
55
55
|
|
|
56
|
-
<Button class="w-full" isLoading={
|
|
56
|
+
<Button class="w-full" isLoading={isLoading[0]} type="submit">Create Account</Button>
|
|
57
57
|
</form>
|
|
58
58
|
</div>
|
|
59
59
|
)
|
|
@@ -1,27 +1,43 @@
|
|
|
1
1
|
import { twMerge } from 'tailwind-merge'
|
|
2
2
|
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/20/solid'
|
|
3
3
|
|
|
4
|
-
type
|
|
5
|
-
color?: 'primary'|'secondary'|'white'
|
|
4
|
+
type Button = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
5
|
+
color?: 'primary'|'secondary'|'black'|'white'
|
|
6
6
|
size?: 'xs'|'sm'|'md'|'lg'
|
|
7
7
|
className?: string
|
|
8
8
|
isLoading?: boolean
|
|
9
9
|
IconLeft?: React.ReactNode|'v'
|
|
10
|
+
IconLeftEnd?: React.ReactNode|'v'
|
|
10
11
|
IconRight?: React.ReactNode|'v'
|
|
11
|
-
|
|
12
|
+
IconRightEnd?: React.ReactNode|'v'
|
|
12
13
|
children?: React.ReactNode|'v'
|
|
13
|
-
[key: string]: unknown
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function Button({
|
|
16
|
+
export function Button({
|
|
17
|
+
size='md',
|
|
18
|
+
color='primary',
|
|
19
|
+
className,
|
|
20
|
+
isLoading,
|
|
21
|
+
IconLeft,
|
|
22
|
+
IconLeftEnd,
|
|
23
|
+
IconRight,
|
|
24
|
+
IconRightEnd,
|
|
25
|
+
children,
|
|
26
|
+
...props
|
|
27
|
+
}: Button) {
|
|
17
28
|
// const size = (color.match(/xs|sm|md|lg/)?.[0] || 'md') as 'xs'|'sm'|'md'|'lg'
|
|
18
|
-
const iconPosition = IconLeft ? 'left' : IconRight ? 'right' :
|
|
19
|
-
const base =
|
|
29
|
+
const iconPosition = IconLeft ? 'left' : IconLeftEnd ? 'leftEnd' : IconRight ? 'right' : IconRightEnd ? 'rightEnd' : 'none'
|
|
30
|
+
const base =
|
|
31
|
+
'relative inline-block text-center font-medium shadow-sm focus-visible:outline focus-visible:outline-2 ' +
|
|
32
|
+
'focus-visible:outline-offset-2 text-white'
|
|
20
33
|
|
|
21
|
-
// Button
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
// Button colors, you can use custom colors by using className instead
|
|
35
|
+
const colors = {
|
|
36
|
+
primary: 'bg-primary hover:bg-primary-hover',
|
|
37
|
+
secondary: 'bg-secondary hover:bg-secondary-hover',
|
|
38
|
+
black: 'bg-black hover:bg-gray-700',
|
|
39
|
+
white: 'bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 [&>.loader]:border-black',
|
|
40
|
+
}
|
|
25
41
|
|
|
26
42
|
// Button sizes
|
|
27
43
|
const sizes = {
|
|
@@ -31,20 +47,7 @@ export function Button({ color='primary', size='md', className, isLoading, IconL
|
|
|
31
47
|
lg: 'px-3.5 py-2.5 text-sm rounded-md',
|
|
32
48
|
}
|
|
33
49
|
|
|
34
|
-
|
|
35
|
-
const contentLayouts = {
|
|
36
|
-
left: 'w-full inline-flex items-center gap-x-1.5',
|
|
37
|
-
right: 'w-full inline-flex items-center gap-x-1.5',
|
|
38
|
-
right2: 'w-full inline-flex items-center justify-between gap-x-1.5',
|
|
39
|
-
none: 'w-full gap-x-1.5',
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let colorAndSize = ''
|
|
43
|
-
if (color.match(/primary/)) colorAndSize = `${primary} ${sizes[size]}`
|
|
44
|
-
else if (color.match(/secondary/)) colorAndSize = `${secondary} ${sizes[size]}`
|
|
45
|
-
else if (color.match(/white/)) colorAndSize = `${white} ${sizes[size]}`
|
|
46
|
-
|
|
47
|
-
const contentLayout = `${contentLayouts[iconPosition]}`
|
|
50
|
+
const contentLayout = `w-full gap-x-1.5 ${iconPosition == 'none' ? '' : 'inline-flex items-center justify-center'}`
|
|
48
51
|
const loading = isLoading ? '[&>*]:opacity-0 text-opacity-0' : ''
|
|
49
52
|
|
|
50
53
|
function getIcon(Icon: React.ReactNode | 'v') {
|
|
@@ -54,16 +57,18 @@ export function Button({ color='primary', size='md', className, isLoading, IconL
|
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
return (
|
|
57
|
-
<button class={twMerge(`${base} ${
|
|
60
|
+
<button class={twMerge(`${base} ${colors[color]} ${sizes[size]} ${contentLayout} ${loading} ${className||''}`)} {...props}>
|
|
58
61
|
{IconLeft && getIcon(IconLeft)}
|
|
59
|
-
{
|
|
62
|
+
{IconLeftEnd && getIcon(IconLeftEnd)}
|
|
63
|
+
<span class={`${iconPosition == 'leftEnd' || iconPosition == 'rightEnd' ? 'flex-1' : ''}`}>{children}</span>
|
|
60
64
|
{IconRight && getIcon(IconRight)}
|
|
61
|
-
{
|
|
65
|
+
{IconRightEnd && getIcon(IconRightEnd)}
|
|
62
66
|
{
|
|
63
|
-
isLoading &&
|
|
64
|
-
<span
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
isLoading &&
|
|
68
|
+
<span className={
|
|
69
|
+
'loader !opacity-100 absolute top-[50%] left-[50%] w-[1rem] h-[1rem] ml-[-0.5rem] mt-[-0.5rem] ' +
|
|
70
|
+
'rounded-full animate-spin border-2 !border-t-transparent border-white'
|
|
71
|
+
} />
|
|
67
72
|
}
|
|
68
73
|
</button>
|
|
69
74
|
)
|
|
@@ -16,14 +16,13 @@ export type SidebarProps = {
|
|
|
16
16
|
Logo?: React.FC<{ width?: string, height?: string }>;
|
|
17
17
|
menu?: { name: string; to: string; Icon: React.FC<{ className?: string }> }[]
|
|
18
18
|
links?: { name: string; to: string; initial: string }[]
|
|
19
|
-
version?: string
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
function classNames(...classes: string[]) {
|
|
23
22
|
return classes.filter(Boolean).join(' ')
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
export function Sidebar({ Logo, menu, links
|
|
25
|
+
export function Sidebar({ Logo, menu, links }: SidebarProps) {
|
|
27
26
|
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
28
27
|
return (
|
|
29
28
|
<>
|
|
@@ -46,14 +45,14 @@ export function Sidebar({ Logo, menu, links, version }: SidebarProps) {
|
|
|
46
45
|
</button>
|
|
47
46
|
</div>
|
|
48
47
|
</TransitionChild>
|
|
49
|
-
<SidebarContents Logo={Logo} menu={menu} links={links}
|
|
48
|
+
<SidebarContents Logo={Logo} menu={menu} links={links} />
|
|
50
49
|
</DialogPanel>
|
|
51
50
|
</div>
|
|
52
51
|
</Dialog>
|
|
53
52
|
|
|
54
53
|
{/* Static sidebar for desktop */}
|
|
55
54
|
<div className={`hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:flex-col ${sidebarWidth}`}>
|
|
56
|
-
<SidebarContents Logo={Logo} menu={menu} links={links}
|
|
55
|
+
<SidebarContents Logo={Logo} menu={menu} links={links} />
|
|
57
56
|
</div>
|
|
58
57
|
|
|
59
58
|
{/* mobile sidebar closed */}
|
|
@@ -72,7 +71,7 @@ export function Sidebar({ Logo, menu, links, version }: SidebarProps) {
|
|
|
72
71
|
)
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
function SidebarContents ({ Logo, menu, links
|
|
74
|
+
function SidebarContents ({ Logo, menu, links }: SidebarProps) {
|
|
76
75
|
const location = useLocation()
|
|
77
76
|
const [store] = useTracked()
|
|
78
77
|
const user = store.user
|
|
@@ -102,7 +101,7 @@ function SidebarContents ({ Logo, menu, links, version }: SidebarProps) {
|
|
|
102
101
|
<Link to="/">
|
|
103
102
|
<Logo width="70" height={undefined} />
|
|
104
103
|
</Link>
|
|
105
|
-
<span className="text-[9px] text-gray-900 font-semibold mt-4">{version}</span>
|
|
104
|
+
<span className="text-[9px] text-gray-900 font-semibold mt-4">{injectedConfig.version}</span>
|
|
106
105
|
</div>
|
|
107
106
|
)}
|
|
108
107
|
<nav className="flex flex-1 flex-col">
|
|
@@ -88,12 +88,12 @@ export function Styleguide({ config }: { config: Config }) {
|
|
|
88
88
|
minWidth="330px"
|
|
89
89
|
options={[{ label: <><b>New Customer</b> / Add <b>Bruce Lee</b></>, className: 'border-bottom-with-space' }, ...options]}
|
|
90
90
|
>
|
|
91
|
-
<Button
|
|
91
|
+
<Button color="white" IconRight="v" class="gap-x-3">Dropdown bottom-right</Button>
|
|
92
92
|
</Dropdown>
|
|
93
93
|
</div>
|
|
94
94
|
<div>
|
|
95
95
|
<Dropdown options={options} dir="top-left" minWidth="250px">
|
|
96
|
-
<Button
|
|
96
|
+
<Button color="white" IconRight="v" class="gap-x-3">Dropdown top-left</Button>
|
|
97
97
|
</Dropdown>
|
|
98
98
|
</div>
|
|
99
99
|
</div>
|
|
@@ -107,9 +107,11 @@ export function Styleguide({ config }: { config: Config }) {
|
|
|
107
107
|
<div><Button color="primary" size="sm">*-sm button</Button></div>
|
|
108
108
|
<div><Button color="primary">*-md (default)</Button></div>
|
|
109
109
|
<div><Button color="primary" size="lg">*-lg button</Button></div>
|
|
110
|
-
<div><Button IconLeft={<CheckIcon class="size-5 -my-5 -mx-0.5" />}>IconLeft
|
|
111
|
-
<div><Button
|
|
112
|
-
<div><Button
|
|
110
|
+
<div><Button IconLeft={<CheckIcon class="size-5 -my-5 -mx-0.5" />}>IconLeft</Button></div>
|
|
111
|
+
<div><Button IconLeft={<CheckIcon class="size-5 -my-5 -mx-0.5" />} className="w-[160px]">IconLeft 160px</Button></div>
|
|
112
|
+
<div><Button IconLeftEnd={<CheckIcon class="size-5 -my-5 -mx-0.5" />} className="w-[190px]">IconLeftEnd 190px</Button></div>
|
|
113
|
+
<div><Button IconRight="v">IconRight</Button></div>
|
|
114
|
+
<div><Button IconRightEnd="v" className="w-[190px]">IconRightEnd 190px</Button></div>
|
|
113
115
|
<div><Button color="primary" IconRight="v" isLoading>primary isLoading</Button></div>
|
|
114
116
|
</div>
|
|
115
117
|
|
|
@@ -5,7 +5,7 @@ import SvgTick from 'nitro-web/client/imgs/icons/tick.svg'
|
|
|
5
5
|
import { Button, FormError, Field, Modal, Topbar, Tabbar } from 'nitro-web'
|
|
6
6
|
|
|
7
7
|
export function SettingsAccount() {
|
|
8
|
-
const isLoading = useState(
|
|
8
|
+
const isLoading = useState(false)
|
|
9
9
|
const [removeModal, setRemoveModal] = useState()
|
|
10
10
|
const [{user}, setStore] = sharedStore.useTracked()
|
|
11
11
|
const [state, setState] = useState({
|
|
@@ -17,7 +17,7 @@ export function SettingsAccount() {
|
|
|
17
17
|
|
|
18
18
|
async function onSubmit (e) {
|
|
19
19
|
try {
|
|
20
|
-
const res = await util.request(
|
|
20
|
+
const res = await util.request(`put /api/user/${user._id}?files=true`, state, e, isLoading)
|
|
21
21
|
setStore((s) => ({ ...s, user: { ...s.user, ...res }, message: 'Saved successfully 👍️' }))
|
|
22
22
|
} catch (errors) {
|
|
23
23
|
return setState({ ...state, errors })
|
|
@@ -92,7 +92,7 @@ export function RemoveModal ({ show, setShow }) {
|
|
|
92
92
|
|
|
93
93
|
async function onSubmit (e) {
|
|
94
94
|
try {
|
|
95
|
-
await util.request(
|
|
95
|
+
await util.request(`delete /api/account/${state._id}`, null, e, isLoading)
|
|
96
96
|
close()
|
|
97
97
|
setStore(o => ({ ...o, message: 'Data deleted successfully, Goodbye 👋...' }))
|
|
98
98
|
setTimeout(() => navigate('/signout'), 6000) // wait for setStore
|
|
@@ -7,7 +7,7 @@ import SvgTick from 'nitro-web/client/imgs/icons/tick.svg'
|
|
|
7
7
|
import { Button, Field, Select, Topbar, Tabbar } from 'nitro-web'
|
|
8
8
|
|
|
9
9
|
export function SettingsBusiness({ config }) {
|
|
10
|
-
const isLoading = useState(
|
|
10
|
+
const isLoading = useState(false)
|
|
11
11
|
const [{ user }, setStore] = sharedStore.useTracked()
|
|
12
12
|
const [state, setState] = useState(() => {
|
|
13
13
|
const company = user.company
|
|
@@ -26,7 +26,7 @@ export function SettingsBusiness({ config }) {
|
|
|
26
26
|
|
|
27
27
|
async function onSubmit (e) {
|
|
28
28
|
try {
|
|
29
|
-
const company = await util.request(
|
|
29
|
+
const company = await util.request(`put /api/company/${user.company._id}`, state, e, isLoading)
|
|
30
30
|
setStore((s) => ({ ...s, user: { ...s.user, company }, message: 'Saved successfully 👍️' }))
|
|
31
31
|
} catch (errors) {
|
|
32
32
|
console.log(errors)
|
|
@@ -13,7 +13,7 @@ type SettingsTeamMemberProps = {
|
|
|
13
13
|
export function SettingsTeamMember ({ showModal, setShowModal, config }: SettingsTeamMemberProps) {
|
|
14
14
|
// @param {object} showModal - user
|
|
15
15
|
const [{ user }] = sharedStore.useTracked()
|
|
16
|
-
const [isLoading] = useState(
|
|
16
|
+
const [isLoading] = useState(false)
|
|
17
17
|
const [state, setState] = useState({
|
|
18
18
|
business: {
|
|
19
19
|
name: '',
|
|
@@ -5,7 +5,7 @@ import SvgPlus from 'nitro-web/client/imgs/icons/plus.svg'
|
|
|
5
5
|
import { Button, Table, Avatar, Tabbar, Topbar, SettingsTeamMember } from 'nitro-web'
|
|
6
6
|
|
|
7
7
|
export function SettingsTeam({ config }) {
|
|
8
|
-
const isLoading = useState(
|
|
8
|
+
const isLoading = useState(false)
|
|
9
9
|
const [showModal, setShowModal] = useState()
|
|
10
10
|
const [{ user }] = sharedStore.useTracked()
|
|
11
11
|
const [state] = useState({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nitro-web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28",
|
|
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 🚀",
|
package/types/util.d.ts
CHANGED
|
@@ -158,7 +158,7 @@ export function pad(num: any, padLeft: any, fixedRight: any): any;
|
|
|
158
158
|
export function pick(obj: any, keys: any): {};
|
|
159
159
|
export function queryObject(search: any, assignTrue: any): any;
|
|
160
160
|
export function queryString(obj: any): string;
|
|
161
|
-
export function request(
|
|
161
|
+
export function request(route: any, data: any, event: any, isLoading: any): Promise<any>;
|
|
162
162
|
export function removeUndefined(variable: any): any;
|
|
163
163
|
export function s3Image(awsUrl: any, image: any, size: string, i: any): any;
|
|
164
164
|
export function sanitizeHTML(string: any): string;
|
package/util.js
CHANGED
|
@@ -908,12 +908,12 @@ export function queryString (obj) {
|
|
|
908
908
|
return qs ? `?${qs}` : ''
|
|
909
909
|
}
|
|
910
910
|
|
|
911
|
-
export async function request (
|
|
911
|
+
export async function request (route, data, event, isLoading) {
|
|
912
912
|
/**
|
|
913
913
|
* Axios request to the route
|
|
914
|
-
* @param {Event} event - event to prevent default
|
|
915
914
|
* @param {string} route - e.g. 'post /api/user'
|
|
916
915
|
* @param {object} <data> - payload
|
|
916
|
+
* @param {Event} <event> - event to prevent default
|
|
917
917
|
* @param {array} <isLoading> - [isLoading, setIsLoading]
|
|
918
918
|
* @return {promise}
|
|
919
919
|
*/
|
|
@@ -925,7 +925,7 @@ export async function request (event, route, data, isLoading) {
|
|
|
925
925
|
// show loading
|
|
926
926
|
if (isLoading) {
|
|
927
927
|
if (isLoading[0]) return
|
|
928
|
-
else isLoading[1](
|
|
928
|
+
else isLoading[1](true)
|
|
929
929
|
}
|
|
930
930
|
|
|
931
931
|
// warning, not persisting through re-renders, but should be fine until loading is finished
|
|
@@ -950,7 +950,7 @@ export async function request (event, route, data, isLoading) {
|
|
|
950
950
|
])
|
|
951
951
|
|
|
952
952
|
// success
|
|
953
|
-
if (isLoading) isLoading[1](
|
|
953
|
+
if (isLoading) isLoading[1](false)
|
|
954
954
|
if (res.status == 'rejected') throw res.reason
|
|
955
955
|
return res.value.data
|
|
956
956
|
|