nitro-web 0.0.63 → 0.0.65

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.
@@ -11,28 +11,31 @@ import { isArray, pick, isString, ucFirst, fullNameSplit } from 'nitro-web/util'
11
11
  let authConfig = null
12
12
  const JWT_SECRET = process.env.JWT_SECRET || 'replace_this_with_secure_env_secret'
13
13
 
14
- export default {
15
- routes: {
16
- 'get /api/store': ['store'],
17
- 'get /api/signout': ['signout'],
18
- 'post /api/signin': ['signin'],
19
- 'post /api/signup': ['signup'],
20
- 'post /api/reset-instructions': ['resetInstructions'],
21
- 'post /api/reset-password': ['resetPassword'],
22
- 'delete /api/account/:uid': ['isUser', 'remove'],
23
- },
14
+ export const routes = {
15
+ // Routes
16
+ 'get /api/store': [store],
17
+ 'get /api/signout': [signout],
18
+ 'post /api/signin': [signin],
19
+ 'post /api/signup': [signup],
20
+ 'post /api/reset-instructions': [resetInstructions],
21
+ 'post /api/reset-password': [resetPassword],
22
+ 'post /api/invite-instructions': [inviteInstructions],
23
+ 'post /api/invite-accept': [resetPassword],
24
+ 'delete /api/account/:uid': [remove],
25
+
26
+ // Overridable helpers
24
27
  setup: setup,
25
- store: store,
26
- signup: signup,
27
- signin: signin,
28
- signout: signout,
29
- resetInstructions: resetInstructions,
30
- resetPassword: resetPassword,
31
- remove: remove,
28
+ findUserFromProvider: findUserFromProvider,
29
+ getStore: getStore,
30
+ signinAndGetStore: signinAndGetStore,
31
+ tokenCreate: tokenCreate,
32
+ tokenParse: tokenParse,
33
+ userCreate: userCreate,
34
+ validatePassword: validatePassword,
32
35
  }
33
36
 
34
37
  function setup(middleware, _config) {
35
- // Setup is called automatically when the server starts
38
+ // routes.setup is called automatically when express starts
36
39
  // Set config values
37
40
  const configKeys = ['clientUrl', 'emailFrom', 'env', 'name', 'mailgunDomain', 'mailgunKey', 'masterPassword', 'isNotMultiTenant']
38
41
  authConfig = pick(_config, configKeys)
@@ -45,7 +48,7 @@ function setup(middleware, _config) {
45
48
  { usernameField: 'email' },
46
49
  async (email, password, next) => {
47
50
  try {
48
- const user = await findUserFromProvider({ email }, password)
51
+ const user = await this.findUserFromProvider({ email }, password)
49
52
  next(null, user)
50
53
  } catch (err) {
51
54
  next(err.message)
@@ -62,7 +65,7 @@ function setup(middleware, _config) {
62
65
  },
63
66
  async (payload, done) => {
64
67
  try {
65
- const user = await findUserFromProvider({ _id: payload._id })
68
+ const user = await this.findUserFromProvider({ _id: payload._id })
66
69
  if (!user) return done(null, false)
67
70
  return done(null, user)
68
71
  } catch (err) {
@@ -97,24 +100,26 @@ function setup(middleware, _config) {
97
100
  }
98
101
 
99
102
  async function store(req, res) {
100
- res.json(await getStore(req.user))
103
+ res.json(await this.getStore(req.user))
101
104
  }
102
105
 
103
106
  async function signup(req, res) {
104
107
  try {
105
- let user = await userCreate(req.body)
108
+ const desktop = req.query.desktop
109
+ let user = await this.userCreate(req.body, this.findUserFromProvider)
106
110
  sendEmail({
107
111
  config: authConfig,
108
112
  template: 'welcome',
109
113
  to: `${ucFirst(user.firstName)}<${user.email}>`,
110
114
  }).catch(console.error)
111
- res.send(await signinAndGetState(user, req.query.desktop))
115
+ res.send(await this.signinAndGetStore(user, desktop, this.getStore))
112
116
  } catch (err) {
113
117
  res.error(err)
114
118
  }
115
119
  }
116
120
 
117
121
  function signin(req, res) {
122
+ const desktop = req.query.desktop
118
123
  if (!req.body.email) return res.error('email', 'The email you entered is incorrect.')
119
124
  if (!req.body.password) return res.error('password', 'The password you entered is incorrect.')
120
125
 
@@ -122,7 +127,7 @@ function signin(req, res) {
122
127
  if (err) return res.error(err)
123
128
  if (!user && info) return res.error('email', info.message)
124
129
  try {
125
- const response = await signinAndGetState(user, req.query.desktop)
130
+ const response = await this.signinAndGetStore(user, desktop, this.getStore)
126
131
  res.send(response)
127
132
  } catch (err) {
128
133
  res.error(err)
@@ -137,7 +142,7 @@ function signout(req, res) {
137
142
  async function resetInstructions(req, res) {
138
143
  try {
139
144
  let email = (req.body.email || '').trim().toLowerCase()
140
- if (!email || !isString(email)) throw { title: 'email', detail: 'The email you entered is incorrect.' }
145
+ if (!email) throw { title: 'email', detail: 'The email you entered is incorrect.' }
141
146
 
142
147
  let user = await db.user.findOne({ query: { email }, _privateData: true })
143
148
  if (!user) throw { title: 'email', detail: 'The email you entered is incorrect.' }
@@ -162,11 +167,13 @@ async function resetInstructions(req, res) {
162
167
  async function resetPassword(req, res) {
163
168
  try {
164
169
  const { token, password, password2 } = req.body
170
+ const name = req.path.includes('invite') ? 'inviteToken' : 'resetToken'
171
+ const desktop = req.query.desktop
165
172
  const id = tokenParse(token)
166
173
  await validatePassword(password, password2)
167
174
 
168
- let user = await db.user.findOne({ query: id, blacklist: ['-resetToken'], _privateData: true })
169
- if (!user || user.resetToken !== token) throw new Error('Sorry your email token is invalid or has already been used.')
175
+ let user = await db.user.findOne({ query: id, blacklist: ['-' + name], _privateData: true })
176
+ if (!user || user[name] !== token) throw new Error('Sorry your token is invalid or has already been used.')
170
177
 
171
178
  await db.user.update({
172
179
  query: user._id,
@@ -174,14 +181,53 @@ async function resetPassword(req, res) {
174
181
  password: await bcrypt.hash(password, 10),
175
182
  resetToken: '',
176
183
  },
177
- blacklist: ['-resetToken', '-password'],
184
+ blacklist: ['-' + name, '-password'],
178
185
  })
179
- res.send(await signinAndGetState({ ...user, resetToken: undefined }, req.query.desktop))
186
+ res.send(await this.signinAndGetStore({ ...user, [name]: undefined }, desktop, this.getStore))
180
187
  } catch (err) {
181
188
  res.error(err)
182
189
  }
183
190
  }
184
191
 
192
+ async function inviteInstructions(req, res) {
193
+ try {
194
+ // Check if user is admin here rather than in middleware (which may not exist yet)
195
+ if (req.user.type != 'admin') {
196
+ throw new Error('You are not authorized to invite users.')
197
+ }
198
+ const inviteToken = await tokenCreate()
199
+ const userData = await db.user.validate({
200
+ ...pick(req.body, ['email', 'firstName', 'lastName']),
201
+ status: 'invited',
202
+ inviteToken: inviteToken,
203
+ })
204
+
205
+ // Check if user already exists
206
+ if (await db.user.findOne({ query: { email: userData.email } })) {
207
+ throw { title: 'email', detail: 'User already exists.' }
208
+ }
209
+
210
+ // Create user
211
+ const user = await db.user.insert({
212
+ data: userData,
213
+ })
214
+
215
+ // Send email
216
+ res.send(user)
217
+ sendEmail({
218
+ config: authConfig,
219
+ template: 'invite-user',
220
+ to: `${ucFirst(userData.firstName)}<${userData.email}>`,
221
+ data: {
222
+ token: inviteToken + (req.query.hasOwnProperty('desktop') ? '?desktop' : ''),
223
+ },
224
+ }).catch(err => console.error('sendEmail(..) mailgun error', err))
225
+
226
+ } catch (err) {
227
+ return res.error(err)
228
+ }
229
+ }
230
+
185
231
  async function remove(req, res) {
186
232
  try {
187
233
  const uid = db.id(req.params.uid || 'badid')
@@ -208,7 +254,7 @@ async function remove(req, res) {
208
254
  }
209
255
  }
210
256
 
211
- /* ---- Auth helpers ------------------------- */
257
+ /* ---- Overridable helpers ------------------ */
212
258
 
213
259
  export async function findUserFromProvider(query, passwordToCheck) {
214
260
  /**
@@ -260,7 +306,7 @@ export async function getStore(user) {
260
306
  }
261
307
  }
262
308
 
263
- export async function signinAndGetState(user, isDesktop) {
309
+ export async function signinAndGetStore(user, isDesktop, getStore) {
264
310
  if (user.loginActive === false) throw 'This user is not available.'
265
311
  user.desktop = isDesktop
266
312
 
@@ -269,30 +315,7 @@ export async function signinAndGetState(user, isDesktop) {
269
315
  return { ...store, jwt }
270
316
  }
271
317
 
272
- export function tokenCreate(id) {
273
- return new Promise((resolve) => {
274
- crypto.randomBytes(16, (err, buff) => {
275
- let hash = buff.toString('hex') // 32 chars
276
- resolve(`${hash}${id || ''}:${Date.now()}`)
277
- })
278
- })
279
- }
280
-
281
- export function tokenParse(token) {
282
- let split = (token || '').split(':')
283
- let hash = split[0].slice(0, 32)
284
- let userId = split[0].slice(32)
285
- let time = split[1]
286
- if (!hash || !userId || !time) {
287
- throw { title: 'error', detail: 'Sorry your code is invalid.' }
288
- } else if (parseFloat(time) + 1000 * 60 * 60 * 24 < Date.now()) {
289
- throw { title: 'error', detail: 'Sorry your code has timed out.' }
290
- } else {
291
- return userId
292
- }
293
- }
294
-
295
- export async function userCreate({ name, business, email, password }) {
318
+ export async function userCreate({ name, business, email, password, findUserFromProvider }) {
296
319
  try {
297
320
  const options = { blacklist: ['-_id'] }
298
321
  const isMultiTenant = !authConfig.isNotMultiTenant
@@ -342,6 +365,29 @@ export async function userCreate({ name, business, email, password }) {
342
365
  }
343
366
  }
344
367
 
368
+ export function tokenCreate(id) {
369
+ return new Promise((resolve) => {
370
+ crypto.randomBytes(16, (err, buff) => {
371
+ let hash = buff.toString('hex') // 32 chars
372
+ resolve(`${hash}${id || ''}:${Date.now()}`)
373
+ })
374
+ })
375
+ }
376
+
377
+ export function tokenParse(token) {
378
+ let split = (token || '').split(':')
379
+ let hash = split[0].slice(0, 32)
380
+ let userId = split[0].slice(32)
381
+ let time = split[1]
382
+ if (!hash || !userId || !time) {
383
+ throw { title: 'error', detail: 'Sorry your code is invalid.' }
384
+ } else if (parseFloat(time) + 1000 * 60 * 60 * 24 < Date.now()) {
385
+ throw { title: 'error', detail: 'Sorry your code has timed out.' }
386
+ } else {
387
+ return userId
388
+ }
389
+ }
390
+
345
391
  export async function validatePassword(password='', password2) {
346
392
  // let hasLowerChar = password.match(/[a-z]/)
347
393
  // let hasUpperChar = password.match(/[A-Z]/)
@@ -359,4 +405,4 @@ export async function validatePassword(password='', password2) {
359
405
  } else if (typeof password2 != 'undefined' && password !== password2) {
360
406
  throw [{ title: 'password2', detail: 'Your passwords need to match.' }]
361
407
  }
362
- }
408
+ }
@@ -7,16 +7,14 @@ let stripe = undefined
7
7
  let stripeProducts = []
8
8
  let config = {}
9
9
 
10
- export default {
11
- routes: {
12
- 'post /api/stripe/webhook': ['stripeWebhook'],
13
- 'post /api/stripe/create-billing-portal-session': ['isUser', 'billingPortalSessionCreate'],
14
- 'get /api/stripe/upcoming-invoices': ['isUser', 'upcomingInvoicesFind'],
15
- },
10
+ export const routes = {
11
+ // Routes
12
+ 'post /api/stripe/webhook': [stripeWebhook],
13
+ 'post /api/stripe/create-billing-portal-session': [billingPortalSessionCreate],
14
+ 'get /api/stripe/upcoming-invoices': [upcomingInvoicesFind],
15
+
16
+ // Overridable helpers
16
17
  setup: setup,
17
- stripeWebhook: stripeWebhook,
18
- billingPortalSessionCreate: billingPortalSessionCreate,
19
- upcomingInvoicesFind: upcomingInvoicesFind,
20
18
  }
21
19
 
22
20
  function setup(middleware, _config) {
@@ -104,7 +102,80 @@ async function upcomingInvoicesFind(req, res) {
104
102
  }
105
103
  }
106
104
 
107
- /* Private webhook actions */
105
+ /* ---- Overridable helpers ------------------ */
106
+
107
+ async function error(req, res, err) {
108
+ if (err && err.response && err.response.body) console.log(err.response.body)
109
+ if (util.isString(err) && err.match(/Cannot find company with id/)) {
110
+ res.json({ user: 'no company found' })
111
+ } else res.error(err)
112
+ }
113
+
114
+ export async function getProducts() {
115
+ /**
116
+ * Returns all products and caches it on the app
117
+ * @returns {Array} products
118
+ */
119
+ try {
120
+ if (stripeProducts) return stripeProducts
121
+ if (!config.stripeSecretKey) {
122
+ stripeProducts = []
123
+ throw new Error('Missing process.env.stripeSecretKey for retrieving products')
124
+ }
125
+
126
+ let products = (await stripe.products.list({ limit: 100, active: true })).data
127
+ let prices = (await stripe.prices.list({ limit: 100, active: true, expand: ['data.tiers'] })).data
128
+
129
+ return (stripeProducts = products.map((product) => ({
130
+ // remove default_price when new pricing is ready
131
+ ...util.pick(product, ['id', 'created', 'default_price', 'description', 'name', 'metadata']),
132
+ type: product.name.match(/housing/i) ? 'project' : 'subscription', // overwriting, was 'service'
133
+ prices: prices
134
+ .filter((price) => price.product == product.id)
135
+ .map((price) => ({
136
+ ...util.pick(price, ['id', 'product', 'nickname', 'recurring', 'unit_amount', 'tiers', 'tiers_mode']),
137
+ interval: price.recurring?.interval, // 'year', 'month', undefined
138
+ })),
139
+ })))
140
+ } catch (err) {
141
+ console.error(new Error(err)) // when stripe throws errors, the callstack is missing.
142
+ return []
143
+ }
144
+ }
145
+
146
+ async function getUserFromEvent(event) {
147
+ // User retreived from the event's customer.
148
+ // The customer is created before the paymentIntent and subscriptionIntent is set up
149
+ let object = event.data.object
150
+ let customerId = object.object == 'customer'? object.id : object.customer
151
+ if (customerId) {
152
+ var user = await db.user.findOne({
153
+ query: { 'stripeCustomer.id': customerId },
154
+ populate: db.user.populate({}),
155
+ blacklist: false, // ['-company.users.inviteToken'],
156
+ })
157
+ }
158
+ if (!user) {
159
+ await db.log.insert({ data: {
160
+ date: Date.now(),
161
+ event: event.type,
162
+ message: `Cannot find user with id: ${customerId}.`,
163
+ }})
164
+ throw new Error(`Cannot find user with id: ${customerId}.`)
165
+ }
166
+ // populate company owner with user data (handy for _addSubscriptionBillingChange)
167
+ if (user.company?.users) {
168
+ user.company.users = user.company.users.map(o => {
169
+ if (o.role == 'owner' && o._id.toString() == user._id.toString()) {
170
+ o.firstName = user.firstName
171
+ o.name = user.name
172
+ o.email = user.email
173
+ }
174
+ return o
175
+ })
176
+ }
177
+ return user
178
+ }
108
179
 
109
180
  async function webhookCustomerCreatedUpdated(req, res, event) {
110
181
  try {
@@ -169,72 +240,6 @@ async function webhookSubUpdated(req, res, event) {
169
240
  }
170
241
  }
171
242
 
172
- async function getUserFromEvent(event) {
173
- // User retreived from the event's customer.
174
- // The customer is created before the paymentIntent and subscriptionIntent is set up
175
- let object = event.data.object
176
- let customerId = object.object == 'customer'? object.id : object.customer
177
- if (customerId) {
178
- var user = await db.user.findOne({
179
- query: { 'stripeCustomer.id': customerId },
180
- populate: db.user.populate({}),
181
- blacklist: false, // ['-company.users.inviteToken'],
182
- })
183
- }
184
- if (!user) {
185
- await db.log.insert({ data: {
186
- date: Date.now(),
187
- event: event.type,
188
- message: `Cannot find user with id: ${customerId}.`,
189
- }})
190
- throw new Error(`Cannot find user with id: ${customerId}.`)
191
- }
192
- // populate company owner with user data (handy for _addSubscriptionBillingChange)
193
- if (user.company?.users) {
194
- user.company.users = user.company.users.map(o => {
195
- if (o.role == 'owner' && o._id.toString() == user._id.toString()) {
196
- o.firstName = user.firstName
197
- o.name = user.name
198
- o.email = user.email
199
- }
200
- return o
201
- })
202
- }
203
- return user
204
- }
205
-
206
- export async function getProducts() {
207
- /**
208
- * Returns all products and caches it on the app
209
- * @returns {Array} products
210
- */
211
- try {
212
- if (stripeProducts) return stripeProducts
213
- if (!config.stripeSecretKey) {
214
- stripeProducts = []
215
- throw new Error('Missing process.env.stripeSecretKey for retrieving products')
216
- }
217
-
218
- let products = (await stripe.products.list({ limit: 100, active: true })).data
219
- let prices = (await stripe.prices.list({ limit: 100, active: true, expand: ['data.tiers'] })).data
220
-
221
- return (stripeProducts = products.map((product) => ({
222
- // remove default_price when new pricing is ready
223
- ...util.pick(product, ['id', 'created', 'default_price', 'description', 'name', 'metadata']),
224
- type: product.name.match(/housing/i) ? 'project' : 'subscription', // overwriting, was 'service'
225
- prices: prices
226
- .filter((price) => price.product == product.id)
227
- .map((price) => ({
228
- ...util.pick(price, ['id', 'product', 'nickname', 'recurring', 'unit_amount', 'tiers', 'tiers_mode']),
229
- interval: price.recurring?.interval, // 'year', 'month', undefined
230
- })),
231
- })))
232
- } catch (err) {
233
- console.error(new Error(err)) // when stripe throws errors, the callstack is missing.
234
- return []
235
- }
236
- }
237
-
238
243
  // async function createOrUpdateCustomer(user, paymentMethod=null) {
239
244
  // /**
240
245
  // * Creates or updates a stripe customer and saves it to the user
@@ -261,10 +266,3 @@ export async function getProducts() {
261
266
  // blacklist: ['-stripeCustomer'],
262
267
  // })
263
268
  // }
264
-
265
- async function error(req, res, err) {
266
- if (err && err.response && err.response.body) console.log(err.response.body)
267
- if (util.isString(err) && err.match(/Cannot find company with id/)) {
268
- res.json({ user: 'no company found' })
269
- } else res.error(err)
270
- }
@@ -24,7 +24,8 @@ export function Button({
24
24
  IconLeftEnd,
25
25
  IconRight,
26
26
  IconRightEnd,
27
- children,
27
+ children,
28
+ type='button',
28
29
  ...props
29
30
  }: Button) {
30
31
  // const size = (color.match(/xs|sm|md|lg/)?.[0] || 'md') as 'xs'|'sm'|'md'|'lg'
@@ -37,7 +38,7 @@ export function Button({
37
38
  const colors = {
38
39
  'primary': 'bg-primary hover:bg-primary-hover ring-transparent text-white [&>.loader]:border-white',
39
40
  'secondary': 'bg-secondary hover:bg-secondary-hover ring-transparent text-white [&>.loader]:border-white',
40
- 'black': 'bg-black hover:bg-gray-700 ring-transparent text-white [&>.loader]:border-white',
41
+ 'black': 'bg-black hover:bg-gray-800 ring-transparent text-white [&>.loader]:border-white',
41
42
  'dark': 'bg-gray-800 hover:bg-gray-700 ring-transparent text-white [&>.loader]:border-white',
42
43
  'white': 'bg-white hover:bg-gray-50 ring-gray-300 text-gray-900 [&>.loader]:border-black', // maybe change to text-foreground
43
44
  'clear': 'hover:bg-gray-50 ring-gray-300 text-foreground [&>.loader]:border-foreground !shadow-none',
@@ -62,7 +63,11 @@ export function Button({
62
63
  }
63
64
 
64
65
  return (
65
- <button class={twMerge(`${base} ${sizes[size]} ${appliedColor} ${contentLayout} ${loading} nitro-button ${className||''}`)} {...props}>
66
+ <button
67
+ type={type}
68
+ class={twMerge(`${base} ${sizes[size]} ${appliedColor} ${contentLayout} ${loading} nitro-button ${className||''}`)}
69
+ {...props}
70
+ >
66
71
  {IconLeft && getIcon(IconLeft)}
67
72
  {IconLeftEnd && getIcon(IconLeftEnd)}
68
73
  <span class={`${iconPosition == 'leftEnd' || iconPosition == 'rightEnd' ? 'flex-1' : ''}`}>{children}</span>
@@ -37,12 +37,16 @@ export type FiltersHandleType = {
37
37
  submit: (includePagination?: boolean) => void
38
38
  }
39
39
 
40
+ const debounceTime = 250
41
+
40
42
  export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
41
43
  filters, state, setState, elements, dropdownProps, buttonProps, buttonClassName, buttonText, buttonCounterClassName,
42
44
  }, ref) => {
43
45
  const location = useLocation()
46
+ const navigate = useNavigate()
44
47
  const stateRef = useRef(state)
45
- const [debouncedSubmit] = useState(() => debounce(submit, 150))
48
+ const [lastUpdated, setLastUpdated] = useState(0)
49
+ const [debouncedSubmit] = useState(() => debounce(submit, debounceTime))
46
50
  const count = Object.keys(state).length - (Object.keys(state).includes('page') ? 1 : 0)
47
51
  const Elements = {
48
52
  Button: elements?.Button || Button,
@@ -65,15 +69,18 @@ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
65
69
  }, [state])
66
70
 
67
71
  useEffect(() => {
68
- setState(() => ({
69
- ...queryObject(location.search),
70
- }))
72
+ // Only update the state if the filters haven't been input changed in the last 500ms
73
+ if (Date.now() - lastUpdated > (debounceTime + 250)) {
74
+ setState(() => ({
75
+ ...queryObject(location.search),
76
+ }))
77
+ }
71
78
  }, [location.search])
72
79
 
73
80
  function reset(e: React.MouseEvent<HTMLAnchorElement>, filter: FilterType) {
74
81
  e.preventDefault()
75
82
  setState((s) => omit(s, [filter.name]) as FilterState)
76
- debouncedSubmit()
83
+ onAfterChange()
77
84
  }
78
85
 
79
86
  function resetAll(e: React.MouseEvent<HTMLButtonElement>) {
@@ -81,18 +88,23 @@ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
81
88
  setState((s) => ({
82
89
  ...(s.page ? { page: s.page } : {}), // keep pagination
83
90
  } as FilterState))
84
- debouncedSubmit()
91
+ onAfterChange()
85
92
  }
86
93
 
87
- async function _onChange(e: {target: {name: string, value: unknown}}) {
94
+ async function onInputChange(e: {target: {name: string, value: unknown}}) {
88
95
  await onChange(setState, e)
96
+ onAfterChange()
97
+ }
98
+
99
+ function onAfterChange() {
100
+ setLastUpdated(Date.now())
89
101
  debouncedSubmit()
90
102
  }
91
103
 
92
- // Submit the filters and update the URL
104
+ // Update the URL by replacing the current entry in the history stack
93
105
  function submit(includePagination?: boolean) {
94
106
  const queryStr = queryString(omit(stateRef.current, includePagination ? [] : ['page']))
95
- history.pushState(null, '', location.pathname + queryStr)
107
+ navigate(location.pathname + queryStr, { replace: true })
96
108
  }
97
109
 
98
110
  if (!filters) return null
@@ -123,7 +135,7 @@ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
123
135
  type={filter.type}
124
136
  placeholder={filter.placeholder}
125
137
  state={state}
126
- onChange={_onChange}
138
+ onChange={onInputChange}
127
139
  />
128
140
  }
129
141
  {
@@ -134,7 +146,7 @@ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
134
146
  type="date"
135
147
  mode="range"
136
148
  state={state}
137
- onChange={_onChange}
149
+ onChange={onInputChange}
138
150
  placeholder={filter.placeholder || 'Select range...'}
139
151
  />
140
152
  }
@@ -146,7 +158,7 @@ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
146
158
  type="country"
147
159
  state={state}
148
160
  options={filter.enums || []}
149
- onChange={_onChange}
161
+ onChange={onInputChange}
150
162
  placeholder={filter.placeholder}
151
163
  />
152
164
  }
@@ -25,8 +25,8 @@ export function Message({ className }: { className?: string }) {
25
25
  'unauth': { type: 'error', text: 'You are unauthorised' },
26
26
  }
27
27
  const colorMap = {
28
- 'error': 'text-critical',
29
- 'warning': 'text-error',
28
+ 'error': 'text-danger',
29
+ 'warning': 'text-warning',
30
30
  'info': 'text-info',
31
31
  'success': 'text-success',
32
32
  }
@@ -70,7 +70,7 @@ export function Message({ className }: { className?: string }) {
70
70
  // Show message and hide it again after some time. Send back cleanup if store.message changes
71
71
  } else if (messageObject && now - 500 < messageObject.date) {
72
72
  const timeout1 = setTimeout(() => setVisible(true), 50)
73
- if (messageObject.timeout !== 0 && !devDontHide) var timeout2 = setTimeout(hide, messageObject.timeout || 5000)
73
+ if (messageObject.timeout !== 0 && !devDontHide) var timeout2 = setTimeout(hide, messageObject.timeout || 5000000)
74
74
  return () => {
75
75
  clearTimeout(timeout1)
76
76
  clearTimeout(timeout2)
@@ -97,24 +97,24 @@ export function Message({ className }: { className?: string }) {
97
97
  (visible ? 'translate-x-0 opacity-100' : 'translate-x-1 opacity-0')
98
98
  }>
99
99
  <div className="p-3">
100
- <div className="flex items-start">
101
- <div className="shrink-0">
102
- <CircleCheck aria-hidden="true" size={21} className={`${color} mt-0.5`} />
100
+ <div className="flex items-start gap-3 text-sm leading-[1.4em]">
101
+ <div className="flex items-center shrink-0 min-h-[1.4em]">
102
+ <CircleCheck aria-hidden="true" size={19} className={`${color}`} />
103
103
  </div>
104
- <div className="ml-3 flex-1 pt-0.5">
105
- <p className="text-sm font-medium text-gray-900">{typeof store.message === 'object' && store.message?.text}
104
+ <div className="flex flex-1 items-center min-h-[1.4em]">
105
+ <p className="font-medium text-gray-900">{typeof store.message === 'object' && store.message?.text}
106
106
  </p>
107
107
  {/* <p className="mt-1 text-sm text-gray-500">{store.message.text}</p> */}
108
108
  </div>
109
- <div className="ml-4 flex shrink-0">
109
+ <div className="flex items-center shrink-0 min-h-[1.4em]">
110
110
  <button
111
111
  type="button"
112
112
  onClick={hide}
113
- className="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2
113
+ className="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2
114
114
  focus:ring-indigo-500 focus:ring-offset-2"
115
115
  >
116
116
  <span className="sr-only">Close</span>
117
- <X aria-hidden="true" size={19} className="mt-0.5" />
117
+ <X aria-hidden="true" size={19} />
118
118
  </button>
119
119
  </div>
120
120
  </div>
@@ -5,13 +5,15 @@ type ModalProps = {
5
5
  show: boolean
6
6
  setShow: (show: boolean) => void
7
7
  children: React.ReactNode
8
+ className?: string
9
+ rootClassName?: string
10
+ dismissable?: boolean
8
11
  maxWidth?: string
9
12
  minHeight?: string
10
- dismissable?: boolean
11
13
  [key: string]: unknown
12
14
  }
13
15
 
14
- export function Modal({ show, setShow, children, maxWidth, minHeight, dismissable = true, ...props }: ModalProps) {
16
+ export function Modal({ show, setShow, children, maxWidth, minHeight, dismissable = true, className, rootClassName }: ModalProps) {
15
17
  const [state, setState] = useState(show ? 'open' : 'close')
16
18
  const containerEl = useRef<HTMLDivElement>(null)
17
19
  const isFirst = IsFirstRender()
@@ -33,8 +35,7 @@ export function Modal({ show, setShow, children, maxWidth, minHeight, dismissabl
33
35
  container: 'opacity-100 scale-[1] duration-200',
34
36
  },
35
37
  }
36
- const _state = states[state as keyof typeof states]
37
-
38
+ const stateObj = states[state as keyof typeof states]
38
39
 
39
40
  useEffect(() => {
40
41
  if (isFirst) return
@@ -64,15 +65,15 @@ export function Modal({ show, setShow, children, maxWidth, minHeight, dismissabl
64
65
  return (
65
66
  <div
66
67
  onClick={(e) => e.stopPropagation()}
67
- class={`${twMerge(`fixed top-0 w-[100vw] h-[100vh] z-[100] ${_state.root} ${props.className}`)} nitro-modal`}
68
+ class={`${twMerge(`fixed top-0 w-[100vw] h-[100vh] z-[100] ${stateObj.root} ${rootClassName||''}`)} nitro-modal`}
68
69
  >
69
- <div class={`!absolute inset-0 box-content bg-gray-500/70 transition-opacity ${_state.bg}`}></div>
70
- <div class={`relative h-[100vh] overflow-y-auto transition-[opacity,transform] ${_state.container}`}>
70
+ <div class={`!absolute inset-0 box-content bg-gray-500/70 transition-opacity ${stateObj.bg}`}></div>
71
+ <div class={`relative h-[100vh] overflow-y-auto transition-[opacity,transform] ${stateObj.container}`}>
71
72
  <div class="flex items-center justify-center min-h-full" onMouseDown={onClick}>
72
73
  <div
73
74
  ref={containerEl}
74
75
  style={{ maxWidth: maxWidth || '550px', minHeight: minHeight }}
75
- class={`relative w-full mx-6 mt-4 mb-8 bg-white rounded-lg shadow-lg ${props.className}`}
76
+ class={`relative w-full mx-6 mt-4 mb-8 bg-white rounded-lg shadow-lg p-9 ${className||''}`}
76
77
  >
77
78
  <div
78
79
  class="absolute top-0 right-0 p-3 m-1 cursor-pointer"
@@ -133,7 +133,7 @@ function FieldContainer({ children, className, error }: { children: React.ReactN
133
133
  return (
134
134
  <div css={style} className={twMerge(`mt-2.5 mb-6 mt-input-before mb-input-after grid grid-cols-1 nitro-field ${className || ''}`)}>
135
135
  {children}
136
- {error && <div class="mt-1.5 text-xs text-danger nitro-error">{error.detail}</div>}
136
+ {error && <div class="mt-1.5 text-xs text-danger-foreground nitro-error">{error.detail}</div>}
137
137
  </div>
138
138
  )
139
139
  }
@@ -149,7 +149,7 @@ function getInputClasses({ error, Icon, iconPos, type }: { error?: Error, Icon?:
149
149
  'placeholder:text-input-placeholder focus:outline focus:outline-2 focus:-outline-offset-2 ' +
150
150
  (iconPos == 'right' && Icon ? `${pl} ${prWithIcon} ` : (Icon ? `${plWithIcon} ${pr} ` : `${pl} ${pr} `)) +
151
151
  (error
152
- ? 'text-red-900 outline-danger focus:outline-danger '
152
+ ? 'text-danger-foreground outline-danger focus:outline-danger '
153
153
  : 'text-input outline-input-border focus:outline-input-border-focus ') +
154
154
  (iconPos == 'right' ? 'justify-self-start ' : 'justify-self-end ') +
155
155
  'nitro-input'
@@ -18,7 +18,7 @@ export function FormError({ state, fields, className }: FormError) {
18
18
  return (
19
19
  <>
20
20
  {error ? (
21
- <div class={`text-danger mt-1 text-sm nitro-error ${className||''}`}>
21
+ <div class={`text-danger-foreground mt-1 text-sm nitro-error ${className||''}`}>
22
22
  {error.detail}
23
23
  </div>
24
24
  ) : null}
@@ -151,7 +151,7 @@ function SelectBase({ inputId, minMenuWidth, name, prefix='', onChange, options,
151
151
  // isDisabled={true}
152
152
  // maxMenuHeight={200}
153
153
  />
154
- {error && <div class="mt-1.5 text-xs text-danger">{error.detail}</div>}
154
+ {error && <div class="mt-1.5 text-xs text-danger-foreground">{error.detail}</div>}
155
155
  </div>
156
156
  )
157
157
  }
@@ -257,7 +257,7 @@ const selectStyles = {
257
257
  // Input container objects
258
258
  input: {
259
259
  base: 'text-input',
260
- error: 'text-red-900',
260
+ error: 'text-danger-foreground',
261
261
  },
262
262
  multiValue: 'bg-primary text-white rounded items-center pl-2 pr-1.5 gap-1.5',
263
263
  multiValueLabel: 'text-xs',
@@ -265,10 +265,10 @@ const selectStyles = {
265
265
  placeholder: 'text-input-placeholder',
266
266
  singleValue: {
267
267
  base: 'text-input',
268
- error: 'text-red-900',
268
+ error: 'text-danger-foreground',
269
269
  },
270
270
  // Icon indicators
271
- clearIndicator: 'text-gray-500 p-1 rounded-md hover:bg-red-50 hover:text-red-800',
271
+ clearIndicator: 'text-gray-500 p-1 rounded-md hover:bg-red-50 hover:text-danger-foreground',
272
272
  dropdownIndicator: 'p-1 hover:bg-gray-100 text-gray-500 rounded-md hover:text-black',
273
273
  indicatorsContainer: 'p-1 px-2 gap-1',
274
274
  indicatorSeparator: 'py-0.5 before:content-[""] before:block before:bg-gray-100 before:w-px before:h-full',
@@ -279,7 +279,7 @@ const selectStyles = {
279
279
  option: {
280
280
  base: 'relative px-3 py-2 !flex items-center gap-2 cursor-default',
281
281
  hover: 'bg-gray-50',
282
- selected: '!bg-gray-100 text-primary-dark',
282
+ selected: '!bg-gray-100 text-dropdown-selected-foreground',
283
283
  },
284
284
  }
285
285
 
@@ -348,7 +348,7 @@ export function Styleguide({ className, elements, children }: StyleguideProps) {
348
348
  </div>
349
349
  </div>
350
350
 
351
- <Modal show={showModal1} setShow={setShowModal1} class="p-9">
351
+ <Modal show={showModal1} setShow={setShowModal1}>
352
352
  <h3 class="h3">Edit Profile</h3>
353
353
  <p class="mb-5">An example modal containing a basic form for editing profiles.</p>
354
354
  <form class="mb-8 text-left">
@@ -1,13 +1,9 @@
1
1
  // @ts-nocheck
2
2
  import db from 'monastery'
3
3
 
4
- export default {
5
- routes: {
6
- 'put /api/company/:cid': ['isCompanyUser', 'update'],
7
- 'put /api/user/:uid': ['isUser', 'updateUser'],
8
- },
9
- update: update,
10
- updateUser: updateUser,
4
+ export const routes = {
5
+ 'put /api/company/:cid': ['isCompanyUser', update],
6
+ 'put /api/user/:uid': ['isUser', updateUser],
11
7
  }
12
8
 
13
9
  async function update(req, res) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitro-web",
3
- "version": "0.0.63",
3
+ "version": "0.0.65",
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 🚀",
@@ -0,0 +1,23 @@
1
+ <!-- extends references the default nitro layout file -->
2
+ {% extends "partials/layout1.swig" %}
3
+
4
+ <!-- block content is the content of the email -->
5
+ {% block content %}
6
+
7
+ [[ subject = You've been invited to join IMG ]]
8
+ <b>%recipient.greet%</b>,<br/>
9
+ <br/>
10
+ You've been invited to join IMG.<br/>
11
+ <br/>
12
+ If you didn't expect to receive this email, feel free to disregard this email.<br/>
13
+ <br/>
14
+ <span mc:edit="button">
15
+ <a class="button" href="%recipient.domain%/invite/%recipient.token%" target="_blank">
16
+ Accept Invite
17
+ </a>
18
+ </span><br/>
19
+ <br/>
20
+ Thanks,<br/>
21
+ <b>The %recipient.configName% Team</b>
22
+
23
+ {% endblock %}
package/server/index.js CHANGED
@@ -18,16 +18,12 @@ export { setupRouter } from './router.js'
18
18
  // Export email utility
19
19
  export { sendEmail } from './email/index.js'
20
20
 
21
- // Export api default controllers
22
- export {
23
- default as auth,
24
- findUserFromProvider,
25
- signinAndGetState,
26
- userCreate,
27
- tokenCreate,
28
- tokenParse,
29
- validatePassword,
30
- getStore,
31
- } from '../components/auth/auth.api.js'
32
- export { default as settings } from '../components/settings/settings.api.js'
33
- export { default as stripe } from '../components/billing/stripe.api.js'
21
+ // Export API controllers
22
+ export * from '../components/auth/auth.api.js'
23
+ export * from '../components/settings/settings.api.js'
24
+ export * from '../components/billing/stripe.api.js'
25
+
26
+ export { routes as authRoutes } from '../components/auth/auth.api.js'
27
+ export { routes as settingsRoutes } from '../components/settings/settings.api.js'
28
+ export { routes as stripeRoutes } from '../components/billing/stripe.api.js'
29
+
package/server/router.js CHANGED
@@ -20,8 +20,9 @@ export async function setupRouter (config) {
20
20
  const expressApp = express()
21
21
  const server = http.createServer(expressApp)
22
22
  const apiRoutes = {}
23
- const controllers = {}
23
+ const controllers = {} // { controllerName: { ...routes, ...helpers } }
24
24
  const allMiddleware = { ...defaultMiddleware, ...(middleware || {}) }
25
+ const verbs = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'all']
25
26
 
26
27
  if (!env) {
27
28
  throw new Error('setupRouter: `config.env` missing')
@@ -40,23 +41,27 @@ export async function setupRouter (config) {
40
41
  let filepaths = getFiles(componentsDir, /\.api\.js$/)
41
42
  // console.log(filepaths, componentsDir)
42
43
  for (let filepath of filepaths) {
43
- let file = (await import(filepath)).default
44
- let name = filepath.replace(/^.*[\\\/]|\.api\.js$/g, '') // eslint-disable-line
45
- controllers[name] = file
44
+ const file = await import(filepath)
45
+ const routes = file.routes
46
+ const name = filepath.replace(/^.*[\\\/]|\.api\.js$/g, '') // eslint-disable-line
47
+ controllers[name] = routes // { ...routes, ...helpers }
46
48
 
47
- if (!file) {
48
- console.warn(`API warning: no default export found on file ${filepath}`)
49
+ if (!routes) {
50
+ console.warn(`API warning: no 'routes' export found on file ${filepath}`)
49
51
  continue
50
52
  }
51
53
 
52
- if (file.setup) file.setup(allMiddleware, config)
53
- if (file.routes) {
54
- util.each(file.routes, (_middleware, key) => {
54
+ if (routes?.setup) routes.setup.call(routes, allMiddleware, config)
55
+ if (routes) {
56
+ util.each(routes, (_middleware, key) => {
57
+ if (!key.match(/\s/)) return
58
+ const match = key.match(new RegExp(`^(${verbs.join('|')})\\s+(.*)$`, 'i'))
59
+ if (!match) throw new Error(`Invalid verb or path: ${key}`)
55
60
  apiRoutes[key] = {
56
61
  middleware: util.toArray(_middleware),
57
62
  filename: name,
58
- path: key.replace(/^[a-z]+\s+/, ''),
59
- verb: key.match(/^[a-z]+/)? key.match(/^[a-z]+/)[0] : 'get',
63
+ path: match[2],
64
+ verb: match[1],
60
65
  }
61
66
  })
62
67
  }
@@ -265,39 +270,23 @@ function getFiles (dir, regexp) {
265
270
  return paths
266
271
  }
267
272
 
268
- function resolveMiddleware (controllers, middleware, route, item, last) {
273
+ function resolveMiddleware (controllers, middleware, route, item) {
269
274
  /**
270
275
  * Resolves a placeholder string into a function
276
+ * @param {object} controllers - { controllerName: { routes, setup, ...other exported functions } }
277
+ * @param {object} middleware
271
278
  * @param {object} route
272
279
  * @param {fn|string} item
273
280
  * @param {boolean} last - last item
274
281
  * @return function(req, res){..}
275
282
  */
276
283
  if (util.isFunction(item)) {
277
- return item
284
+ return item.bind(controllers[route.filename])
278
285
 
279
- } else if (!util.isString(item)) {
280
- console.error('Invalid middleware item:', item)
286
+ } else if (typeof item !== 'string') {
287
+ console.error('Invalid middleware item on route:', route.path, item)
281
288
  return
282
289
 
283
- } else if (item.match(/\./) || last) { // e.g. user.read
284
- let arr = item.split('.')
285
- let controllerGroup = controllers[arr[1]? arr[0] : route.filename]
286
- let controllerName = arr[1] || arr[0]
287
- if (controllerGroup && controllerGroup[controllerName]) {
288
- if (middleware.endpointSwitcher && controllerGroup[controllerName + 'Desktop']) {
289
- return middleware.endpointSwitcher.bind(
290
- null,
291
- controllerGroup[controllerName].bind(controllerGroup),
292
- controllerGroup[controllerName + 'Desktop'].bind(controllerGroup)
293
- )
294
- } else {
295
- return controllerGroup[controllerName].bind(controllerGroup)
296
- }
297
- } else {
298
- console.error(`The controller '${item}' defined in '${route.filename}.api' doesn't exist.`)
299
- }
300
-
301
290
  } else if (middleware[item]) {
302
291
  return middleware[item]
303
292
 
@@ -308,13 +297,19 @@ function resolveMiddleware (controllers, middleware, route, item, last) {
308
297
  }
309
298
 
310
299
  const defaultMiddleware = {
311
- beforeAPIRoute: (req, res, next) => {
312
- res.set('version', req.version)
313
- next()
314
- },
300
+ order: [
301
+ // Express middleware runtime order
302
+ 'loadAssets',
303
+ 'modifyRequest',
304
+ 'parseUrlEncoded',
305
+ 'parseJson',
306
+ 'parseFile',
307
+ 'beforeAPIRoute',
308
+ ],
315
309
 
316
310
  modifyRequest: (req, res, next) => {
317
311
  // Handy boolean denoting that the request wants JSON returned
312
+ // global.start = new Date().getTime()
318
313
  req.json = req.xhr || req.accepts(['html', 'json']) == 'json'
319
314
  next()
320
315
  },
@@ -345,13 +340,8 @@ const defaultMiddleware = {
345
340
  })(req, res, () => { /*console.timeEnd('upload middleware'); */next() })
346
341
  },
347
342
 
348
- order: [
349
- // Express middleware runtime order
350
- 'loadAssets',
351
- 'modifyRequest',
352
- 'parseUrlEncoded',
353
- 'parseJson',
354
- 'parseFile',
355
- 'beforeAPIRoute',
356
- ],
343
+ beforeAPIRoute: (req, res, next) => {
344
+ res.set('version', req.version)
345
+ next()
346
+ },
357
347
  }
package/types/util.d.ts CHANGED
@@ -516,17 +516,16 @@ export function pick(obj: {
516
516
  /**
517
517
  *
518
518
  * Parses a query string into an object, or returns the last known matching cache
519
- * @param {string} searchString - location.search or location.href, e.g. '?page=1', 'https://...co.nz?page=1'
519
+ * @param {string} searchString - location.search e.g. '?page=1&book=my+%2B+book'
520
520
  * @param {boolean} [trueDefaults] - assign true to empty values
521
521
  * @returns {{[key: string]: string|true}} - e.g. { page: '1' }
522
- * UPDATE: removed array values, e.g. '?page=1&page=2' will return { page: '2' }
523
522
  */
524
523
  export function queryObject(searchString: string, trueDefaults?: boolean): {
525
524
  [key: string]: string | true;
526
525
  };
527
526
  /**
528
527
  * Parses a query string into an array of objects
529
- * @param {string} searchString - location.search or location.href, e.g. '?page=1', 'https://...co.nz?page=1'
528
+ * @param {string} searchString - location.search, e.g. '?page=1'
530
529
  * @returns {object[]} - e.g. [{ page: '1' }]
531
530
  */
532
531
  export function queryArray(searchString: string): object[];
@@ -583,13 +582,14 @@ export function s3Image(awsUrl: string, imageOrArray: Image[] | Image, size?: st
583
582
  export function sanitizeHTML(string: string): string;
584
583
  /**
585
584
  * Process scrollbar width once.
586
- * @param {string} paddingClass - class name to give padding to
587
- * @param {string} marginClass - class name to give margin to
588
- * @param {number} maxWidth - enclose css in a max-width media query
585
+ * @param {string} [paddingClass] - class name to give padding to
586
+ * @param {string} [marginClass] - class name to give margin to
587
+ * @param {number} [maxWidth] - enclose css in a max-width media query
588
+ * @param {string} [marginClassNegative] - class name to give negative margin to
589
589
  * @returns {number}
590
590
  *
591
591
  */
592
- export function scrollbar(paddingClass: string, marginClass: string, maxWidth: number): number;
592
+ export function scrollbar(paddingClass?: string, marginClass?: string, marginClassNegative?: string, maxWidth?: number): number;
593
593
  /**
594
594
  * Convert seconds to time
595
595
  * @param {number} seconds
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../util.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../util.js"],"names":[],"mappings":"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"}
package/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
1
3
  type InjectedConfig = {
2
4
  awsUrl?: string
3
5
  clientUrl: string
@@ -19,7 +21,7 @@ export type Config = InjectedConfig & {
19
21
  // Non-injectable config on the client
20
22
  beforeApp?: () => Promise<object>
21
23
  beforeStoreUpdate?: (prevStore: Store | null, newData: Store) => Store
22
- middleware?: Record<string, (route: unknown, store: Store) => undefined | { redirect: string }>
24
+ middleware?: {[key: string]: (route: any, store: any) => undefined | { redirect: string }}
23
25
  }
24
26
 
25
27
  export type User = {
package/util.js CHANGED
@@ -1224,44 +1224,34 @@ export function pick (obj, keys) {
1224
1224
  /**
1225
1225
  *
1226
1226
  * Parses a query string into an object, or returns the last known matching cache
1227
- * @param {string} searchString - location.search or location.href, e.g. '?page=1', 'https://...co.nz?page=1'
1227
+ * @param {string} searchString - location.search e.g. '?page=1&book=my+%2B+book'
1228
1228
  * @param {boolean} [trueDefaults] - assign true to empty values
1229
1229
  * @returns {{[key: string]: string|true}} - e.g. { page: '1' }
1230
- * UPDATE: removed array values, e.g. '?page=1&page=2' will return { page: '2' }
1231
1230
  */
1232
1231
  export function queryObject (searchString, trueDefaults) {
1233
- searchString = searchString.replace(/^[^?]+\?/, '?') // remove domain preceeding search string
1232
+ if (searchString.startsWith('?')) searchString = searchString.slice(1)
1234
1233
  const uniqueKey = searchString + (trueDefaults ? '-true' : '')
1235
- /** @type {{[key: string]: string|true}} */
1236
- let obj = {}
1237
1234
 
1238
1235
  if (searchString === '') return {}
1239
1236
  if (!queryObjectCache) queryObjectCache = {}
1240
1237
  if (queryObjectCache[uniqueKey]) return queryObjectCache[uniqueKey]
1241
1238
 
1242
- // Remove '?', and split each query parameter (ampersand-separated)
1243
- const searchParams = searchString.slice(1).split('&')
1244
-
1245
- // Loop through each query paramter
1246
- searchParams.map(function (part) {
1247
- const partArr = part.split('=') // Split into key/value
1248
- const key = partArr[0]
1249
- const isEmpty = !partArr[1] && partArr[1] != '0'
1250
-
1251
- if (trueDefaults === true) {
1252
- obj[key] = isEmpty ? true : decodeURIComponent(partArr[1])
1253
- } else {
1254
- obj[key] = decodeURIComponent(partArr[1])
1239
+ const params = new URLSearchParams(searchString)
1240
+ /** @type {{[key: string]: string|true}} */
1241
+ const result = Object.fromEntries(params.entries())
1242
+ if (trueDefaults) {
1243
+ for (const key in result) {
1244
+ if (!result[key] && result[key] !== '0') result[key] = true
1255
1245
  }
1256
- })
1246
+ }
1257
1247
 
1258
- queryObjectCache[uniqueKey] = obj
1259
- return obj
1248
+ queryObjectCache[uniqueKey] = result
1249
+ return result
1260
1250
  }
1261
1251
 
1262
1252
  /**
1263
1253
  * Parses a query string into an array of objects
1264
- * @param {string} searchString - location.search or location.href, e.g. '?page=1', 'https://...co.nz?page=1'
1254
+ * @param {string} searchString - location.search, e.g. '?page=1'
1265
1255
  * @returns {object[]} - e.g. [{ page: '1' }]
1266
1256
  */
1267
1257
  export function queryArray (searchString) {
@@ -1341,7 +1331,7 @@ export async function request (route, data, event, isLoading) {
1341
1331
 
1342
1332
  const [res] = await Promise.allSettled([
1343
1333
  axiosPromise,
1344
- setTimeoutPromise(() => {}, 200), // eslint-disable-line
1334
+ // setTimeoutPromise(() => {}, 200), // eslint-disable-line
1345
1335
  ])
1346
1336
 
1347
1337
  // success
@@ -1410,13 +1400,14 @@ export function sanitizeHTML (string) {
1410
1400
 
1411
1401
  /**
1412
1402
  * Process scrollbar width once.
1413
- * @param {string} paddingClass - class name to give padding to
1414
- * @param {string} marginClass - class name to give margin to
1415
- * @param {number} maxWidth - enclose css in a max-width media query
1403
+ * @param {string} [paddingClass] - class name to give padding to
1404
+ * @param {string} [marginClass] - class name to give margin to
1405
+ * @param {number} [maxWidth] - enclose css in a max-width media query
1406
+ * @param {string} [marginClassNegative] - class name to give negative margin to
1416
1407
  * @returns {number}
1417
1408
  *
1418
1409
  */
1419
- export function scrollbar (paddingClass, marginClass, maxWidth) {
1410
+ export function scrollbar (paddingClass, marginClass, marginClassNegative, maxWidth) {
1420
1411
  if (typeof window === 'undefined') return 0
1421
1412
  if (scrollbarCache || scrollbarCache === 0) {
1422
1413
  return scrollbarCache
@@ -1452,6 +1443,7 @@ export function scrollbar (paddingClass, marginClass, maxWidth) {
1452
1443
  (maxWidth ? '@media only screen and (max-width: ' + maxWidth + 'px) {' : '') +
1453
1444
  (paddingClass ? paddingClass + ' {padding-right:' + scrollbarCache + 'px}' : '') +
1454
1445
  (marginClass ? marginClass + ' {margin-right:' + scrollbarCache + 'px}' : '') +
1446
+ (marginClassNegative ? marginClassNegative + ' {margin-right:' + scrollbarCache * -1 + 'px}' : '') +
1455
1447
  (maxWidth ? '}' : '')
1456
1448
  document.head.appendChild(style)
1457
1449
  }