nitro-web 0.0.26 → 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 CHANGED
@@ -2,10 +2,14 @@ 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
+ }
12
+
9
13
  type LayoutProps = {
10
14
  config: Config;
11
15
  }
@@ -13,7 +17,7 @@ type LayoutProps = {
13
17
  type Settings = {
14
18
  afterApp?: () => void
15
19
  beforeApp: (config: Config) => Promise<object>
16
- beforeStoreUpdate: (prevStore: Store | null, newData: Store) => Store
20
+ // beforeStoreUpdate: (prevStore: Store | null, newData: Store) => Store
17
21
  isStatic?: boolean
18
22
  layouts: React.FC<LayoutProps>[]
19
23
  middleware: Record<string, (route: unknown, store: Store) => undefined | { redirect: string }>
@@ -22,7 +26,7 @@ type Settings = {
22
26
  }
23
27
 
24
28
  type Route = {
25
- component: React.FC<{ route?: Route; params?: object; location?: object }>
29
+ component: React.FC<{ route?: Route; params?: object; location?: object; config?: Config }>
26
30
  meta?: { title?: string }
27
31
  middleware: string[]
28
32
  name: string
@@ -30,26 +34,39 @@ type Route = {
30
34
  redirect?: string
31
35
  }
32
36
 
33
- 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')
34
39
  // Fetch state and init app
35
40
  const settings: Settings = {
36
41
  beforeApp: config.beforeApp || beforeApp,
37
- beforeStoreUpdate: config.beforeStoreUpdate || beforeStoreUpdate,
38
42
  isStatic: config.isStatic,
39
43
  layouts: layouts,
40
44
  middleware: Object.assign(defaultMiddleware, config.middleware || {}),
41
45
  name: config.name,
42
46
  titleSeparator: config.titleSeparator,
43
47
  }
44
- if (!settings.layouts) throw new Error('layouts are required')
45
- const initData = (await settings.beforeApp(config)) || {}
46
- beforeCreate(initData, settings.beforeStoreUpdate)
48
+
49
+ // Setup the jwt token
50
+ updateJwt(localStorage.getItem(injectedConfig.jwtName))
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)
47
56
 
48
57
  const root = ReactDOM.createRoot(document.getElementById('app') as HTMLElement)
49
- root.render(<App settings={settings} config={config} />)
58
+ root.render(<App settings={settings} config={config} storeContainer={storeContainer} />)
59
+ }
60
+
61
+ export function updateJwt(token?: string | null) {
62
+ // Update the jwt token in local storage and axios headers
63
+ const key = injectedConfig.jwtName
64
+ localStorage.setItem(key, token || '')
65
+ if (token) axios().defaults.headers.Authorization = `Bearer ${token}`
66
+ else delete axios().defaults.headers.Authorization
50
67
  }
51
68
 
52
- function App({ settings, config }: { settings: Settings, config: Config }): ReactNode {
69
+ function App({ settings, config, storeContainer }: { settings: Settings, config: Config, storeContainer: StoreContainer }): ReactNode {
53
70
  // const themeNormalised = theme
54
71
  const router = getRouter({ settings, config })
55
72
  // const theme = pick(themeNormalised, []) // e.g. 'topPanelHeight'
@@ -76,12 +93,12 @@ function App({ settings, config }: { settings: Settings, config: Config }): Reac
76
93
  }, [!!router])
77
94
 
78
95
  return (
79
- <Provider>
96
+ <storeContainer.Provider>
80
97
  {/* <ThemeProvider theme={themeNormalised}> */}
81
98
  { router && <RouterProvider router={router} /> }
82
99
  <AfterApp settings={settings} />
83
100
  {/* </ThemeProvider> */}
84
- </Provider>
101
+ </storeContainer.Provider>
85
102
  )
86
103
  }
87
104
 
@@ -182,14 +199,14 @@ function getRouter({ settings, config }: { settings: Settings, config: Config })
182
199
  children: layout.map((route) => {
183
200
  return {
184
201
  element: (
185
- <RouteComponent route={route} />
202
+ <RouteComponent route={route} config={config} />
186
203
  ),
187
204
  path: route.path,
188
205
  loader: async () => { // request
189
- // wait for container/exposedData to be setup
206
+ // wait for container/exposedStoreData to be setup
190
207
  if (!nonce) nonce = true && await setTimeoutPromise(() => {}, 0) // eslint-disable-line
191
208
  for (const key of route.middleware) {
192
- const error = settings.middleware[key](route, exposedData || {})
209
+ const error = settings.middleware[key](route, exposedStoreData || {})
193
210
  if (error && error.redirect) {
194
211
  return redirect(error.redirect)
195
212
  }
@@ -230,13 +247,13 @@ function RestoreScroll() {
230
247
  return (null)
231
248
  }
232
249
 
233
- function RouteComponent({ route }: { route: Route }) {
250
+ function RouteComponent({ route, config }: { route: Route, config: Config }) {
234
251
  const Component = route.component
235
252
  const params = useParams()
236
253
  const location = useLocation()
237
254
  document.title = route.meta?.title || ''
238
255
  return (
239
- <Component route={route} params={params} location={location} />
256
+ <Component route={route} params={params} location={location} config={config} />
240
257
  )
241
258
  }
242
259
 
@@ -261,7 +278,7 @@ async function beforeApp(config: Config) {
261
278
  // sharedStoreCache = window.prehot.sharedStoreCache
262
279
  // delete window.prehot
263
280
  // }
264
- if (!exposedData && !config.isStatic) {
281
+ if (!config.isStatic) {
265
282
  stateData = (await axios().get('/api/state', { 'axios-retry': { retries: 3 }, timeout: 4000 } as AxiosRequestConfig)).data
266
283
  apiAvailable = true
267
284
  }
@@ -269,29 +286,7 @@ async function beforeApp(config: Config) {
269
286
  console.error('We had trouble connecting to the API, please refresh')
270
287
  console.log(err)
271
288
  }
272
- return { ...(stateData || exposedData), apiAvailable }
273
- }
274
-
275
- function beforeStoreUpdate(prevStore: Store | null, newData: Store) {
276
- /**
277
- * Get store object (called on signup/signin/signout/state)
278
- * @param {object} store - existing store
279
- * @param {object} <newStoreData> - pass to override store with /login or /state request data
280
- * @return {object} store
281
- */
282
- if (!newData) return newData
283
- const store = {
284
- ...(prevStore || {
285
- message: undefined,
286
- user: undefined, // defined if user is signed in
287
- }),
288
- ...(newData || {}),
289
- }
290
-
291
- // Used to verify if the current cookie belongs to this user
292
- // E.g. signout > signin (to a different user on another tab)
293
- axios().defaults.headers.userid = store?.user?._id
294
- return store
289
+ return { ...stateData, apiAvailable }
295
290
  }
296
291
 
297
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
- 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'
@@ -48,5 +53,5 @@ export { Toggle } from '../components/partials/form/toggle'
48
53
  // Component Other
49
54
  export { IsFirstRender } from '../components/partials/is-first-render'
50
55
 
51
- // IsDemo environment variable
52
- export const isDemo = ISDEMO
56
+ // Expose the injected config
57
+ export const injectedConfig = { ...INJECTED_CONFIG } as import('types').Config
package/client/store.ts CHANGED
@@ -1,31 +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
 
7
+ export let preloadedStoreData: Store
8
+ export let exposedStoreData: Store
4
9
 
5
- export type BeforeUpdate = (prevStore: Store | null, newData: Store) => Store
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
+ }
6
19
 
7
- let initData: Store
8
- let beforeUpdate: BeforeUpdate = (prevStore, newData) => newData
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))
25
+ }
26
+ }
9
27
 
10
- const container = createContainer(() => {
11
- const [store, setStore] = useState(() => beforeUpdate(null, initData || exposedData || {}))
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
12
35
 
13
- // Wrap the setState function to always run beforeUpdate
14
- const wrappedSetStore = (updater: (prevStore: Store) => Store) => {
15
- if (typeof updater === 'function') {
16
- setStore((prevStore: Store) => beforeUpdate(prevStore, updater(prevStore)))
17
- } else {
18
- setStore((prevStore: Store) => beforeUpdate(prevStore, updater))
19
- }
36
+ // If newData.jwt is present, update the jwt token
37
+ if (newStore?.jwt) {
38
+ updateJwt(newStore.jwt)
39
+ delete newStore.jwt
20
40
  }
21
41
 
22
- exposedData = store
23
- return [store, wrappedSetStore]
24
- })
25
-
26
- export let exposedData: Store
27
- export const { Provider, useTracked } = container
28
- export function beforeCreate(_initData: Store, _beforeUpdate: BeforeUpdate) {
29
- initData = _initData // normally provided from a /login or /state request data
30
- 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
31
45
  }
@@ -1,15 +1,15 @@
1
1
  // @ts-nocheck
2
- import MongoStore from 'connect-mongo'
3
2
  import crypto from 'crypto'
4
- import expressSession from 'express-session'
5
3
  import passport from 'passport'
6
4
  import passportLocal from 'passport-local'
5
+ import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'
7
6
  import db from 'monastery'
7
+ import jsonwebtoken from 'jsonwebtoken'
8
8
  import { sendEmail } from 'nitro-web/server'
9
9
  import * as util from 'nitro-web/util'
10
- // import stripeController from '../billing/stripe.api.js'
11
10
 
12
11
  let config = {}
12
+ const JWT_SECRET = process.env.JWT_SECRET || 'replace_this_with_secure_env_secret'
13
13
 
14
14
  export default {
15
15
 
@@ -24,43 +24,19 @@ export default {
24
24
  },
25
25
 
26
26
  setup: function (middleware, _config) {
27
- // Setup passport handlers for reading and writing to req.session
28
27
  const that = this
29
28
  global.passport = passport
30
29
 
31
30
  // Set config values
32
- config = {
33
- env: _config.env,
34
- masterPassword: _config.masterPassword,
35
- }
36
- for (let key in config) {
37
- if (!config[key] && key != 'masterPassword') {
38
- throw new Error(`Missing config value for stripe.api.js: ${key}`)
39
- }
40
- }
41
-
42
- // After successful login, serialize the user into a session object
43
- passport.serializeUser((user, next) => {
44
- next(null, { _id: user._id })
45
- })
31
+ config = { env: _config.env, masterPassword: _config.masterPassword }
32
+ if (!config.env) throw new Error('Missing config value for: config.env')
46
33
 
47
- // After session read, get the user from the session object
48
- passport.deserializeUser(async (sessionObject, next) => {
49
- try {
50
- const user = await that._findUserFromProvider('deserialize', sessionObject)
51
- next(null, user)
52
- } catch (err) {
53
- next(err.message)
54
- }
55
- })
56
-
57
- // Setup passport local signin strategy
58
34
  passport.use(
59
35
  new passportLocal.Strategy(
60
- { usernameField: 'email' },
36
+ { usernameField: 'email' },
61
37
  async (email, password, next) => {
62
38
  try {
63
- const user = await that._findUserFromProvider('email', { email: email, password: password })
39
+ const user = await that._findUserFromProvider('email', { email, password })
64
40
  next(null, user)
65
41
  } catch (err) {
66
42
  next(err.message)
@@ -69,58 +45,45 @@ export default {
69
45
  )
70
46
  )
71
47
 
72
- // https://medium.com/swlh/everything-you-need-to-know-about-the-passport-jwt-passport-js-strategy-8b69f39014b0
73
- // https://github.com/mikenicholson/passport-jwt
74
- //
75
- // passport.use(new JwtStrategy.Strategy({
76
- // jwtFromRequest: JwtStrategy.ExtractJwt.fromAuthHeaderAsBearerToken(),
77
- // secretOrKey: '1fjw3h3jkdJD8sjA12dw53llapA2sjAjsv3nxaxzNBzz',
78
- // }, function(jwtPayload, done) {
79
- //
80
- // this._findUserFromProvider('email', { email: email, password: password }, done)
81
- // console.log(jwtPayload)
82
- // User.findOne({id: jwt_payload.sub}, function(err, user) {
83
- // if (err) {
84
- // return done(err, false);
85
- // }
86
- // if (user) {
87
- // return done(null, user);
88
- // } else {
89
- // return done(null, false);
90
- // // or you could create a new account
91
- // }
92
- // });
93
- // }));
94
-
95
- // Add session middleware
96
- middleware.order.splice(3, 0, 'session', 'passport', 'passportSession', 'passportError', 'blocked')
48
+ passport.use(
49
+ new JwtStrategy(
50
+ {
51
+ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
52
+ secretOrKey: JWT_SECRET,
53
+ },
54
+ async (payload, done) => {
55
+ try {
56
+ const user = await that._findUserFromProvider('deserialize', { _id: payload._id })
57
+ if (!user) return done(null, false)
58
+ return done(null, user)
59
+ } catch (err) {
60
+ return done(err, false)
61
+ }
62
+ }
63
+ )
64
+ )
65
+
66
+ middleware.order.splice(3, 0, 'passport', 'passportError', 'jwtAuth', 'blocked')
67
+
97
68
  Object.assign(middleware, {
98
69
  blocked: function (req, res, next) {
99
70
  if (req.user && req.user.loginActive === false) {
100
- req.logout()
101
- res.error('This user is not available.')
71
+ res.status(403).error('This user is not available.')
102
72
  } else {
103
73
  next()
104
74
  }
105
75
  },
76
+ jwtAuth: function(req, res, next) {
77
+ passport.authenticate('jwt', { session: false }, function(err, user) {
78
+ if (user) req.user = user
79
+ next()
80
+ })(req, res, next)
81
+ },
106
82
  passport: passport.initialize(),
107
83
  passportError: function (err, req, res, next) {
108
84
  if (!err) return next()
109
- req.logout()
110
85
  res.error(err)
111
86
  },
112
- passportSession: passport.session(),
113
- session: expressSession({
114
- secret: '092720e5ffc1237266b8517239cd81b6', // Changing invalidates cookies
115
- cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 },
116
- resave: false,
117
- saveUninitialized: false,
118
- store: MongoStore.create({
119
- clientPromise: db.onOpen((manager) => {
120
- return manager.client
121
- }),
122
- }),
123
- }),
124
87
  })
125
88
  },
126
89
 
@@ -131,31 +94,26 @@ export default {
131
94
  signup: async function (req, res) {
132
95
  try {
133
96
  let user = await this._userCreate(req.body)
134
- // Welcome email
135
97
  sendEmail({
136
98
  config: config,
137
99
  template: 'welcome',
138
100
  to: `${util.ucFirst(user.firstName)}<${user.email}>`,
139
- }).catch(err => {
140
- console.error(err)
141
- })
142
- // Login
143
- res.send(await this._signinAndGetState(req, user))
101
+ }).catch(console.error)
102
+ res.send(await this._signinAndGetState(user, req.query.desktop))
144
103
  } catch (err) {
145
104
  res.error(err)
146
105
  }
147
106
  },
148
107
 
149
108
  signin: function (req, res) {
150
- // console.log('api: signin')
151
- // console.log(req.body)
152
109
  if (!req.body.email) return res.error('email', 'The email you entered is incorrect.')
153
110
  if (!req.body.password) return res.error('password', 'The password you entered is incorrect.')
111
+
154
112
  passport.authenticate('local', { session: false }, async (err, user, info) => {
155
- if (err) return console.log(err) || res.error(err)
113
+ if (err) return res.error(err)
156
114
  if (!user && info) return res.error('email', info.message)
157
115
  try {
158
- const response = await this._signinAndGetState(req, user)
116
+ const response = await this._signinAndGetState(user, req.query.desktop)
159
117
  res.send(response)
160
118
  } catch (err) {
161
119
  res.error(err)
@@ -164,35 +122,29 @@ export default {
164
122
  },
165
123
 
166
124
  signout: function (req, res) {
167
- req.logout()
168
125
  res.json('{}')
169
126
  },
170
127
 
171
128
  resetInstructions: async function (req, res) {
172
129
  try {
173
- let email = (req.body.email||'').trim().toLowerCase()
174
- if (!email || !util.isString(email)) {
175
- throw { title: 'email', detail: 'The email you entered is incorrect.' }
176
- }
177
- // Find matching user and create new reset token
178
- let user = await db.user.findOne({ query: { email: email }, _privateData: true })
130
+ let email = (req.body.email || '').trim().toLowerCase()
131
+ if (!email || !util.isString(email)) throw { title: 'email', detail: 'The email you entered is incorrect.' }
132
+
133
+ let user = await db.user.findOne({ query: { email }, _privateData: true })
179
134
  if (!user) throw { title: 'email', detail: 'The email you entered is incorrect.' }
180
- // Create token
181
- let token = await this._tokenCreate(user._id)
182
- // Update user with token
183
- await db.user.update({ query: { email: email }, $set: { resetToken: token }})
184
- // Email.
135
+
136
+ let resetToken = await this._tokenCreate(user._id)
137
+ await db.user.update({ query: { email }, $set: { resetToken }})
138
+
185
139
  res.json({})
186
140
  sendEmail({
187
141
  config: config,
188
142
  template: 'reset-password',
189
143
  to: `${util.ucFirst(user.firstName)}<${email}>`,
190
144
  data: {
191
- token: token + (req.query.hasOwnProperty('desktop') ? '?desktop' : ''),
145
+ token: resetToken + (req.query.hasOwnProperty('desktop') ? '?desktop' : ''),
192
146
  },
193
- }).catch(err => {
194
- console.error('sendEmail(..) mailgun error', err)
195
- })
147
+ }).catch(err => console.error('sendEmail(..) mailgun error', err))
196
148
  } catch (err) {
197
149
  res.error(err)
198
150
  }
@@ -202,18 +154,11 @@ export default {
202
154
  try {
203
155
  const { token, password, password2 } = req.body
204
156
  const id = this._tokenParse(token)
205
- // Validate password
206
157
  this._validatePassword(password, password2)
207
- // Find matching user
208
- let user = await db.user.findOne({
209
- query: id,
210
- blacklist: ['-resetToken'],
211
- _privateData: true,
212
- })
213
- if (!user || user.resetToken !== token) {
214
- throw new Error('Sorry your email token is invalid or has already been used verify your email.')
215
- }
216
- // Update user with new password
158
+
159
+ let user = await db.user.findOne({ query: id, blacklist: ['-resetToken'], _privateData: true })
160
+ if (!user || user.resetToken !== token) throw new Error('Sorry your email token is invalid or has already been used.')
161
+
217
162
  await db.user.update({
218
163
  query: user._id,
219
164
  data: {
@@ -222,7 +167,7 @@ export default {
222
167
  },
223
168
  blacklist: ['-resetToken', '-password'],
224
169
  })
225
- res.send(await this._signinAndGetState(req, { ...user, resetToken: undefined }))
170
+ res.send(await this._signinAndGetState({ ...user, resetToken: undefined }, req.query.desktop))
226
171
  } catch (err) {
227
172
  res.error(err)
228
173
  }
@@ -264,27 +209,19 @@ export default {
264
209
  /* ---- Private fns ---------------- */
265
210
 
266
211
  _getState: async function (user) {
267
- // Format the initial state
268
- return {
212
+ // Initial state
213
+ return {
269
214
  user: user || null,
270
- // stripeProducts: await stripeController._getProducts(),
271
215
  }
272
216
  },
273
217
 
274
- _signinAndGetState: function (req, user) {
275
- // @return state
276
- return new Promise((resolve, reject) => {
277
- user.desktop = req.query.hasOwnProperty('desktop')
278
- if (user.loginActive !== false) {
279
- req.login(user, async (err) => {
280
- if (err) return reject(err)
281
- resolve(await this._getState(user))
282
- })
283
- } else {
284
- return reject('This user is not available.')
285
- // this._getState().then((state) => resolve(state))
286
- }
287
- })
218
+ _signinAndGetState: async function (user, isDesktop) {
219
+ if (user.loginActive === false) throw 'This user is not available.'
220
+ user.desktop = isDesktop
221
+
222
+ const jwt = jsonwebtoken.sign({ _id: user._id }, JWT_SECRET, { expiresIn: '30d' })
223
+ const state = await this._getState(user)
224
+ return { ...state, jwt }
288
225
  },
289
226
 
290
227
  _tokenCreate: function (id) {
@@ -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(event, 'post /api/reset-instructions', state, isLoading)
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={!!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,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(event, 'post /api/reset-password', state, isLoading)
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={!!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
  )
@@ -1,15 +1,15 @@
1
- import { Topbar, Field, Button, FormError, util } from 'nitro-web'
2
- import { Config, Errors } from 'nitro-web/types'
1
+ import { Topbar, Field, Button, FormError, util, injectedConfig, updateJwt } from 'nitro-web'
2
+ import { Errors } from 'nitro-web/types'
3
3
 
4
- export function Signin({ config }: { config: Config }) {
4
+ 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
  const [state, setState] = useState({
11
- email: config.env == 'development' ? config.placeholderEmail : '',
12
- password: config.env == 'development' ? '1234' : '',
11
+ email: injectedConfig.env == 'development' ? (injectedConfig.placeholderEmail || '') : '',
12
+ password: injectedConfig.env == 'development' ? '1234' : '',
13
13
  errors: [] as Errors,
14
14
  })
15
15
 
@@ -22,17 +22,19 @@ export function Signin({ config }: { config: Config }) {
22
22
  useEffect(() => {
23
23
  if (isSignout) {
24
24
  setStore(() => ({ user: null }))
25
- util.axios().get('/api/signout')
26
- .then(() => isLoading[1](''))
25
+ // util.axios().get('/api/signout')
26
+ Promise.resolve()
27
+ .then(() => isLoading[1](false))
28
+ .then(() => updateJwt())
27
29
  .then(() => navigate({ pathname: '/signin', search: location.search }, { replace: true }))
28
- .catch(err => (console.error(err), isLoading[1]('')))
30
+ .catch(err => (console.error(err), isLoading[1](false)))
29
31
  }
30
32
  }, [isSignout])
31
33
 
32
34
  async function onSubmit (e: React.FormEvent<HTMLFormElement>) {
33
35
  try {
34
- const data = await util.request(e, 'post /api/signin', state, isLoading)
35
- isLoading[1]('is-loading')
36
+ const data = await util.request('post /api/signin', state, e, isLoading)
37
+ isLoading[1](true)
36
38
  setStore(() => data)
37
39
  setTimeout(() => { // wait for setStore
38
40
  if (location.search.includes('redirect')) navigate(location.search.replace('?redirect=', ''))
@@ -65,7 +67,7 @@ export function Signin({ config }: { config: Config }) {
65
67
  <FormError state={state} className="pt-2" />
66
68
  </div>
67
69
 
68
- <Button class="w-full" isLoading={!!isLoading[0]} type="submit">Sign In</Button>
70
+ <Button class="w-full" isLoading={isLoading[0]} type="submit">Sign In</Button>
69
71
  </form>
70
72
  </div>
71
73
  )
@@ -1,22 +1,22 @@
1
- import { Button, Field, FormError, Topbar, util } from 'nitro-web'
2
- import { Config, Errors } from 'nitro-web/types'
1
+ import { Button, Field, FormError, Topbar, util, injectedConfig } from 'nitro-web'
2
+ import { Errors } from 'nitro-web/types'
3
3
 
4
- export function Signup({ config }: { config: Config}) {
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
- email: config.env === 'development' ? config.placeholderEmail : '',
10
- name: config.env === 'development' ? 'Bruce Wayne' : '',
11
- business: { name: config.env === 'development' ? 'Wayne Enterprises' : '' },
12
- password: config.env === 'development' ? '1234' : '',
9
+ email: injectedConfig.env === 'development' ? (injectedConfig.placeholderEmail || '') : '',
10
+ name: injectedConfig.env === 'development' ? 'Bruce Wayne' : '',
11
+ business: { name: injectedConfig.env === 'development' ? 'Wayne Enterprises' : '' },
12
+ password: injectedConfig.env === 'development' ? '1234' : '',
13
13
  errors: [] as Errors,
14
14
  })
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')
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({ config }: { config: Config}) {
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
  )
@@ -1,7 +1,7 @@
1
1
  // Component: https://tailwindui.com/components/application-ui/application-shells/sidebar#component-a69d85b6237ea2ad506c00ef1cd39a38
2
2
  import { Dialog, DialogBackdrop, DialogPanel, TransitionChild } from '@headlessui/react'
3
3
  import avatarImg from 'nitro-web/client/imgs/avatar.jpg'
4
- import { isDemo } from 'nitro-web'
4
+ import { injectedConfig } from 'nitro-web'
5
5
  import {
6
6
  Bars3Icon,
7
7
  HomeIcon,
@@ -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
@@ -85,7 +84,7 @@ function SidebarContents ({ Logo, menu, links, version }: SidebarProps) {
85
84
 
86
85
  const _menu = menu || [
87
86
  { name: 'Dashboard', to: '/', Icon: HomeIcon },
88
- { name: isDemo ? 'Design System' : 'Style Guide', to: '/styleguide', Icon: PaintBrushIcon },
87
+ { name: injectedConfig.isDemo ? 'Design System' : 'Style Guide', to: '/styleguide', Icon: PaintBrushIcon },
89
88
  { name: 'Pricing', to: '/pricing', Icon: UsersIcon },
90
89
  { name: 'Signout', to: '/signout', Icon: ArrowLeftCircleIcon },
91
90
  ]
@@ -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">
@@ -1,4 +1,4 @@
1
- import { Drop, Dropdown, Field, Select, Button, Checkbox, GithubLink, isDemo, Modal, Calendar } from 'nitro-web'
1
+ import { Drop, Dropdown, Field, Select, Button, Checkbox, GithubLink, Modal, Calendar, injectedConfig } from 'nitro-web'
2
2
  import { getCountryOptions, getCurrencyOptions, ucFirst } from 'nitro-web/util'
3
3
  import { CheckIcon } from '@heroicons/react/20/solid'
4
4
  import { Config } from 'nitro-web/types'
@@ -55,7 +55,7 @@ export function Styleguide({ config }: { config: Config }) {
55
55
  <div class="mb-10 text-left max-w-[1100px]">
56
56
  <GithubLink filename={__filename} />
57
57
  <div class="mb-7">
58
- <h1 class="h1">{isDemo ? 'Design System' : 'Style Guide'}</h1>
58
+ <h1 class="h1">{injectedConfig.isDemo ? 'Design System' : 'Style Guide'}</h1>
59
59
  <p>
60
60
  Components are styled using&nbsp;
61
61
  <a href="https://v3.tailwindcss.com/docs/configuration" class="underline" target="_blank" rel="noreferrer">TailwindCSS</a>.
@@ -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.26",
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 🚀",
@@ -33,18 +33,18 @@
33
33
  "bcrypt": "^5.0.0",
34
34
  "body-parser": "^1.19.0",
35
35
  "compression": "^1.7.4",
36
- "connect-mongo": "^5.1.0",
37
36
  "date-fns": "^3.6.0",
38
37
  "dateformat": "^3.0.3",
39
38
  "dotenv": "^14.3.2",
40
39
  "express": "^4.17.1",
41
40
  "express-fileupload": "^1.1.6",
42
- "express-session": "^1.17.0",
43
41
  "inline-css": "^4.0.2",
42
+ "jsonwebtoken": "^9.0.2",
44
43
  "nodemailer": "^6.5.0",
45
44
  "nodemailer-mailgun-transport": "^2.0.2",
46
45
  "nunjucks": "^3.2.2",
47
46
  "passport": "^0.4.1",
47
+ "passport-jwt": "^4.0.1",
48
48
  "passport-local": "^1.0.0",
49
49
  "sort-route-addresses-nodeps": "0.0.4"
50
50
  },
@@ -7,7 +7,6 @@ import { CSSInterpolation } from '@emotion/serialize'
7
7
  declare global {
8
8
  /** Webpack injected config variables */
9
9
  const INJECTED_CONFIG: Record<string, string|boolean|object>
10
- const ISDEMO: boolean
11
10
  /** Webpack svg loader */
12
11
  module '*.svg' {
13
12
  const content: React.FC<React.SVGProps<SVGElement>>
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
@@ -1,21 +1,25 @@
1
- // Expected config to be available
2
- export type Config = {
1
+ type InjectedConfig = {
2
+ awsUrl?: string
3
3
  clientUrl: string
4
4
  countries: { [key: string]: { numberFormats: { currency: string } } } // for input-currency.tsx
5
5
  currencies: { [key: string]: { symbol: string, digits: number } } // for input-currency.tsx
6
6
  env: string
7
+ googleMapsApiKey?: string
8
+ isDemo: boolean // implicitly defined by webpack
9
+ isStatic: boolean // implicitly defined by webpack
10
+ jwtName: string // implicitly defined by webpack
7
11
  name: string
12
+ placeholderEmail?: string
13
+ stripePublishableKey?: string
14
+ titleSeparator?: string
8
15
  version: string
16
+ }
9
17
 
10
- awsUrl?: string
18
+ export type Config = InjectedConfig & {
19
+ // Non-injectable config on the client
11
20
  beforeApp?: () => Promise<object>
12
21
  beforeStoreUpdate?: (prevStore: Store | null, newData: Store) => Store
13
- googleMapsApiKey?: string
14
- isStatic?: boolean
15
22
  middleware?: Record<string, (route: unknown, store: Store) => undefined | { redirect: string }>
16
- placeholderEmail?: string
17
- stripePublishableKey?: string
18
- titleSeparator?: string
19
23
  }
20
24
 
21
25
  export type User = {
@@ -45,6 +49,7 @@ export type Store = {
45
49
  message?: MessageObject | string | null
46
50
  user?: User | null,
47
51
  apiAvailable?: boolean
52
+ jwt?: string
48
53
  }
49
54
 
50
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