nitro-web 0.0.27 → 0.0.29

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 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
- import { injectedConfig } from './index'
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,12 @@ 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
- if (!settings.layouts) throw new Error('layouts are required')
50
- const initData = (await settings.beforeApp(config)) || {}
51
- beforeCreate(initData, settings.beforeStoreUpdate)
52
+ // Fetch the store data, and make it available to the store
53
+ const data = await settings.beforeApp(config)
54
+ Object.assign(preloadedStoreData, data)
52
55
 
53
56
  const root = ReactDOM.createRoot(document.getElementById('app') as HTMLElement)
54
- root.render(<App settings={settings} config={config} />)
57
+ root.render(<App settings={settings} config={config} storeContainer={storeContainer} />)
55
58
  }
56
59
 
57
60
  export function updateJwt(token?: string | null) {
@@ -62,7 +65,7 @@ export function updateJwt(token?: string | null) {
62
65
  else delete axios().defaults.headers.Authorization
63
66
  }
64
67
 
65
- function App({ settings, config }: { settings: Settings, config: Config }): ReactNode {
68
+ function App({ settings, config, storeContainer }: { settings: Settings, config: Config, storeContainer: StoreContainer }): ReactNode {
66
69
  // const themeNormalised = theme
67
70
  const router = getRouter({ settings, config })
68
71
  // const theme = pick(themeNormalised, []) // e.g. 'topPanelHeight'
@@ -89,12 +92,12 @@ function App({ settings, config }: { settings: Settings, config: Config }): Reac
89
92
  }, [!!router])
90
93
 
91
94
  return (
92
- <Provider>
95
+ <storeContainer.Provider>
93
96
  {/* <ThemeProvider theme={themeNormalised}> */}
94
97
  { router && <RouterProvider router={router} /> }
95
98
  <AfterApp settings={settings} />
96
99
  {/* </ThemeProvider> */}
97
- </Provider>
100
+ </storeContainer.Provider>
98
101
  )
99
102
  }
100
103
 
@@ -199,10 +202,10 @@ function getRouter({ settings, config }: { settings: Settings, config: Config })
199
202
  ),
200
203
  path: route.path,
201
204
  loader: async () => { // request
202
- // wait for container/exposedData to be setup
205
+ // wait for container/exposedStoreData to be setup
203
206
  if (!nonce) nonce = true && await setTimeoutPromise(() => {}, 0) // eslint-disable-line
204
207
  for (const key of route.middleware) {
205
- const error = settings.middleware[key](route, exposedData || {})
208
+ const error = settings.middleware[key](route, exposedStoreData || {})
206
209
  if (error && error.redirect) {
207
210
  return redirect(error.redirect)
208
211
  }
@@ -266,51 +269,23 @@ async function beforeApp(config: Config) {
266
269
  * Gets called once before React is initialised
267
270
  * @return {promise} - newStoreData which is used for sharedStore, later merged with the config.store() defaults
268
271
  */
269
- let apiAvailable
270
- let stateData
272
+ let apiAvailable = false
273
+ let storeData = {}
271
274
  try {
272
275
  // Unload prehot data
273
276
  // if (window.prehot) {
274
277
  // sharedStoreCache = window.prehot.sharedStoreCache
275
278
  // delete window.prehot
276
279
  // }
277
- if (!exposedData && !config.isStatic) {
278
- stateData = (await axios().get('/api/state', { 'axios-retry': { retries: 3 }, timeout: 4000 } as AxiosRequestConfig)).data
280
+ if (!config.isStatic) {
281
+ storeData = (await axios().get('/api/store', { 'axios-retry': { retries: 3 }, timeout: 4000 } as AxiosRequestConfig)).data
279
282
  apiAvailable = true
280
283
  }
281
284
  } catch (err) {
282
285
  console.error('We had trouble connecting to the API, please refresh')
283
286
  console.log(err)
284
287
  }
285
- return { ...(stateData || exposedData), apiAvailable }
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
288
+ return { ...storeData, apiAvailable }
314
289
  }
315
290
 
316
291
  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
- export * from './app'
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 type BeforeUpdate = (prevStore: Store | null, newData: Store) => Store
7
+ export const preloadedStoreData: Store = {}
8
+ export let exposedStoreData: Store = preloadedStoreData
5
9
 
6
- let initData: Store
7
- let beforeUpdate: BeforeUpdate = (prevStore, newData) => newData
8
-
9
- const container = createContainer(() => {
10
- const [store, setStore] = useState(() => beforeUpdate(null, initData || exposedData || {}))
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
- // Wrap the setState function to always run beforeUpdate
13
- const wrappedSetStore = (updater: (prevStore: Store) => Store) => {
14
- if (typeof updater === 'function') {
15
- setStore((prevStore: Store) => beforeUpdate(prevStore, updater(prevStore)))
16
- } else {
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
- exposedData = store
22
- return [store, wrappedSetStore]
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
- export let exposedData: Store
26
- export const { Provider, useTracked } = container
27
- export function beforeCreate(_initData: Store, _beforeUpdate: BeforeUpdate) {
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
  }
@@ -14,7 +14,7 @@ const JWT_SECRET = process.env.JWT_SECRET || 'replace_this_with_secure_env_secre
14
14
  export default {
15
15
 
16
16
  routes: {
17
- 'get /api/state': ['state'],
17
+ 'get /api/store': ['store'],
18
18
  'get /api/signout': ['signout'],
19
19
  'post /api/signin': ['signin'],
20
20
  'post /api/signup': ['signup'],
@@ -87,8 +87,8 @@ export default {
87
87
  })
88
88
  },
89
89
 
90
- state: async function (req, res) {
91
- res.json(await this._getState(req.user))
90
+ store: async function (req, res) {
91
+ res.json(await this._getStore(req.user))
92
92
  },
93
93
 
94
94
  signup: async function (req, res) {
@@ -208,10 +208,10 @@ export default {
208
208
 
209
209
  /* ---- Private fns ---------------- */
210
210
 
211
- _getState: async function (user) {
212
- // Initial state
211
+ _getStore: async function (user) {
212
+ // Initial store
213
213
  return {
214
- user: user || null,
214
+ user: user || undefined,
215
215
  }
216
216
  },
217
217
 
@@ -220,8 +220,8 @@ export default {
220
220
  user.desktop = isDesktop
221
221
 
222
222
  const jwt = jsonwebtoken.sign({ _id: user._id }, JWT_SECRET, { expiresIn: '30d' })
223
- const state = await this._getState(user)
224
- return { ...state, jwt }
223
+ const store = await this._getStore(user)
224
+ return { ...store, jwt }
225
225
  },
226
226
 
227
227
  _tokenCreate: function (id) {
@@ -3,14 +3,14 @@ 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(event, 'post /api/reset-instructions', state, isLoading)
13
- setStore(s => ({ ...s, message: 'Done! Please check your email.' }))
12
+ await util.request('post /api/reset-instructions', state, event, isLoading)
13
+ setStore((s) => ({ ...s, message: 'Done! Please check your email.' }))
14
14
  navigate('/signin')
15
15
  } catch (e) {
16
16
  return setState({ ...state, errors: e as Errors })
@@ -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={!!isLoading[0]} type="submit">Email me a reset password link</Button>
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,8 +52,8 @@ export function ResetPassword() {
52
52
 
53
53
  async function onSubmit (event: React.FormEvent<HTMLFormElement>) {
54
54
  try {
55
- const data = await util.request(event, 'post /api/reset-password', state, isLoading)
56
- setStore(() => data)
55
+ const data = await util.request('post /api/reset-password', state, event, isLoading)
56
+ setStore((s) => ({ ...s, ...data }))
57
57
  navigate('/')
58
58
  } catch (e) {
59
59
  return setState({ ...state, errors: e as Errors })
@@ -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={!!isLoading[0]} type="submit">Reset Password</Button>
82
+ <Button class="w-full" isLoading={isLoading[0]} type="submit">Reset Password</Button>
83
83
  </form>
84
84
  </div>
85
85
  )
@@ -5,8 +5,9 @@ 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 ? 'is-loading' : '')
8
+ const isLoading = useState(isSignout)
9
9
  const [, setStore] = useTracked()
10
+
10
11
  const [state, setState] = useState({
11
12
  email: injectedConfig.env == 'development' ? (injectedConfig.placeholderEmail || '') : '',
12
13
  password: injectedConfig.env == 'development' ? '1234' : '',
@@ -21,21 +22,21 @@ export function Signin() {
21
22
 
22
23
  useEffect(() => {
23
24
  if (isSignout) {
24
- setStore(() => ({ user: null }))
25
+ setStore((s) => ({ ...s, user: undefined }))
25
26
  // util.axios().get('/api/signout')
26
27
  Promise.resolve()
27
- .then(() => isLoading[1](''))
28
+ .then(() => isLoading[1](false))
28
29
  .then(() => updateJwt())
29
30
  .then(() => navigate({ pathname: '/signin', search: location.search }, { replace: true }))
30
- .catch(err => (console.error(err), isLoading[1]('')))
31
+ .catch(err => (console.error(err), isLoading[1](false)))
31
32
  }
32
33
  }, [isSignout])
33
34
 
34
35
  async function onSubmit (e: React.FormEvent<HTMLFormElement>) {
35
36
  try {
36
- const data = await util.request(e, 'post /api/signin', state, isLoading)
37
- isLoading[1]('is-loading')
38
- setStore(() => data)
37
+ const data = await util.request('post /api/signin', state, e, isLoading)
38
+ isLoading[1](true)
39
+ setStore((s) => ({ ...s, ...data }))
39
40
  setTimeout(() => { // wait for setStore
40
41
  if (location.search.includes('redirect')) navigate(location.search.replace('?redirect=', ''))
41
42
  else navigate('/')
@@ -67,7 +68,7 @@ export function Signin() {
67
68
  <FormError state={state} className="pt-2" />
68
69
  </div>
69
70
 
70
- <Button class="w-full" isLoading={!!isLoading[0]} type="submit">Sign In</Button>
71
+ <Button class="w-full" isLoading={isLoading[0]} type="submit">Sign In</Button>
71
72
  </form>
72
73
  </div>
73
74
  )
@@ -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,9 +15,9 @@ export function Signup() {
15
15
 
16
16
  async function onSubmit (e: React.FormEvent<HTMLFormElement>) {
17
17
  try {
18
- const data = await util.request(e, 'post /api/signup', state, isLoading)
19
- isLoading[1]('is-loading')
20
- setStore(() => data)
18
+ const data = await util.request('post /api/signup', state, e, isLoading)
19
+ isLoading[1](true)
20
+ setStore((s) => ({ ...s, ...data }))
21
21
  setTimeout(() => navigate('/'), 0) // wait for setStore
22
22
  } catch (e) {
23
23
  return setState({ ...state, errors: e as Errors })
@@ -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={!!isLoading[0]} type="submit">Create Account</Button>
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 Props = {
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
- IconRight2?: React.ReactNode|'v'
12
+ IconRightEnd?: React.ReactNode|'v'
12
13
  children?: React.ReactNode|'v'
13
- [key: string]: unknown
14
14
  }
15
15
 
16
- export function Button({ color='primary', size='md', className, isLoading, IconLeft, IconRight, IconRight2, children, ...props }: Props) {
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' : IconRight2 ? 'right2' : 'none'
19
- const base = 'relative inline-block font-medium shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2'
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 types
22
- const primary = 'bg-primary text-white shadow-sm hover:bg-primary-hover focus-visible:outline-primary'
23
- const secondary = 'bg-secondary text-white shadow-sm hover:bg-secondary-hover focus-visible:outline-secondary'
24
- const white = 'bg-white text-gray-900 ring-1 ring-inset ring-gray-300 shadow-sm hover:bg-gray-50'
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
- // Icon position
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} ${colorAndSize} ${contentLayout} ${loading} ${className||''}`)} {...props}>
60
+ <button class={twMerge(`${base} ${colors[color]} ${sizes[size]} ${contentLayout} ${loading} ${className||''}`)} {...props}>
58
61
  {IconLeft && getIcon(IconLeft)}
59
- {children}
62
+ {IconLeftEnd && getIcon(IconLeftEnd)}
63
+ <span class={`${iconPosition == 'leftEnd' || iconPosition == 'rightEnd' ? 'flex-1' : ''}`}>{children}</span>
60
64
  {IconRight && getIcon(IconRight)}
61
- {IconRight2 && getIcon(IconRight2)}
65
+ {IconRightEnd && getIcon(IconRightEnd)}
62
66
  {
63
- isLoading &&
64
- <span class="!opacity-100 absolute inset-0 flex items-center justify-center">
65
- <span className="w-4 h-4 rounded-full animate-spin border-2 border-t-transparent border-white" />
66
- </span>
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
  )
@@ -74,7 +74,7 @@ export function Message() {
74
74
 
75
75
  function hide() {
76
76
  setVisible(false)
77
- setTimeout(() => setStore(s => ({ ...s, message: null })), 250)
77
+ setTimeout(() => setStore(s => ({ ...s, message: undefined })), 250)
78
78
  }
79
79
 
80
80
  return (
@@ -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, version }: SidebarProps) {
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} version={version} />
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} version={version} />
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, version }: SidebarProps) {
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 type="white" IconRight2="v" class="gap-x-3">Dropdown bottom-right</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 type="white" IconRight2="v" class="gap-x-3">Dropdown top-left</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=Element</Button></div>
111
- <div><Button IconRight="v">IconRight=&quot;v&quot;</Button></div>
112
- <div><Button IconRight2="v" className="w-[200px]">IconRight2=&quot;v&quot;</Button></div>
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(e, `put /api/user/${user._id}?files=true`, state, isLoading)
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(e, `delete /api/account/${state._id}`, null, isLoading)
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(e, `put /api/company/${user.company._id}`, state, isLoading)
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.27",
3
+ "version": "0.0.29",
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/tsconfig.json CHANGED
@@ -31,6 +31,7 @@
31
31
  "client",
32
32
  "components/**/*.tsx",
33
33
  "components/**/*.ts",
34
- "types"
34
+ "types",
35
+ "./types/core-only-globals.d.ts"
35
36
  ]
36
37
  }
@@ -0,0 +1,9 @@
1
+ import { Store } from 'nitro-web/types'
2
+ import { Dispatch, SetStateAction } from 'react'
3
+
4
+ // Core-only global, this global will be defined globally in the project (e.g. in ./client/index.ts)
5
+ declare global {
6
+ const useTracked: () => [Store, Dispatch<SetStateAction<Store>>]
7
+ }
8
+
9
+ export {}
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(event: any, route: any, data: any, isLoading: any): Promise<any>;
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/types.ts CHANGED
@@ -46,10 +46,10 @@ export type MessageObject = {
46
46
  }
47
47
 
48
48
  export type Store = {
49
- message?: MessageObject | string | null
50
- user?: User | null,
51
49
  apiAvailable?: boolean
52
50
  jwt?: string
51
+ message?: MessageObject | string
52
+ user?: User,
53
53
  }
54
54
 
55
55
  export type Svg = React.FC<React.SVGProps<SVGElement>>
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 (event, route, data, isLoading) {
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](' is-loading')
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