nitro-web 0.0.38 → 0.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/util.js CHANGED
@@ -1,12 +1,26 @@
1
- // @ts-nocheck
2
- import _axios from '@hokify/axios'
1
+ import _axios from 'axios'
3
2
  import axiosRetry from 'axios-retry'
4
3
  import dateformat from 'dateformat'
5
4
  import { loadStripe } from '@stripe/stripe-js/pure.js' // pure removes ping
6
- export const dateFormat = dateformat
7
5
 
6
+ /** @type {{[key: string]: {[key: string]: string|true}}} */
7
+ let queryObjectCache = {}
8
+
9
+ /** @type {Promise<import('@stripe/stripe-js').Stripe|null>|undefined} */
10
+ let stripeClientCache
11
+
12
+ /** @type {number|undefined} */
13
+ let scrollbarCache
14
+
15
+ /** @type {boolean|undefined} */
16
+ let axiosNonce
17
+
18
+ /**
19
+ * Returns an address monastery schema which Google autocomplete should return
20
+ */
8
21
  export function addressSchema () {
9
22
  // Google autocomplete should return the following object
23
+ /** @param {any} array @param {object} schema @returns {[]} */
10
24
  function arrayWithSchema (array, schema) {
11
25
  array.schema = schema
12
26
  return array
@@ -37,12 +51,16 @@ export function addressSchema () {
37
51
  }
38
52
  }
39
53
 
54
+ /**
55
+ * Returns an axios instance
56
+ * @returns {import('axios').AxiosStatic}
57
+ */
40
58
  export function axios () {
41
- // Remove mobile specific protocol and subdomain
42
- const clientOrigin = window.document.location.origin.replace(/^(capacitor|https):\/\/(mobile\.)?/, 'https://')
43
59
  // axios configurations on the client
44
- if (!axios._axiosNonce && typeof window !== 'undefined') {
45
- axios._axiosNonce = true
60
+ if (!axiosNonce && typeof window !== 'undefined') {
61
+ // Remove mobile specific protocol and subdomain
62
+ const clientOrigin = window.document.location.origin.replace(/^(capacitor|https):\/\/(mobile\.)?/, 'https://')
63
+ axiosNonce = true
46
64
  _axios.defaults.baseURL = clientOrigin
47
65
  _axios.defaults.headers.desktop = true
48
66
  _axios.defaults.withCredentials = true
@@ -52,16 +70,24 @@ export function axios () {
52
70
  return _axios
53
71
  }
54
72
 
73
+ /**
74
+ * Builds the url with params
75
+ * @param {string} url
76
+ * @param {{[key: string]: string}} parameters - Key value parameters
77
+ * @returns {string}, e.g. 'https://example.com?param1=value1&param2=value2'
78
+ */
55
79
  export function buildUrl (url, parameters) {
56
- /**
57
- * Builds the url with params
58
- * @param {string} url - String url
59
- * @param {object} parameters - Key value parameters
60
- */
61
80
  const params = Object.keys(parameters).map((p) => `${encodeURIComponent(p)}=${encodeURIComponent(parameters[p])}`)
62
81
  return [url, params.join('&')].join('?')
63
82
  }
64
83
 
84
+ /**
85
+ * Converts a string to camel case
86
+ * @param {string} str
87
+ * @param {boolean} [capitaliseFirst] - Capitalise the first letter
88
+ * @param {boolean} [allowNumber] - Allow numbers
89
+ * @returns {string}
90
+ */
65
91
  export function camelCase (str, capitaliseFirst, allowNumber) {
66
92
  let regex = (capitaliseFirst ? '(?:^[a-z0-9]|' : '(?:') + '[-]+[a-z0-9])'
67
93
  return str
@@ -77,20 +103,43 @@ export function camelCase (str, capitaliseFirst, allowNumber) {
77
103
  })
78
104
  }
79
105
 
106
+ /**
107
+ * Converts camel case to title case
108
+ * @param {string} str
109
+ * @param {boolean} [captialiseFirstOnly] - Capitalise the first letter only
110
+ * @returns {string}
111
+ */
80
112
  export function camelCaseToTitle (str, captialiseFirstOnly) {
81
113
  str = str.replace(/([A-Z]+)/g, ' $1').trim()
82
114
  if (captialiseFirstOnly) str = str.toLowerCase()
83
115
  return ucFirst(str)
84
116
  }
85
117
 
118
+ /**
119
+ * Converts camel case to hypen case
120
+ * @param {string} str
121
+ * @returns {string}
122
+ */
86
123
  export function camelCaseToHypen (str) {
87
124
  return str.replace(/[A-Z]|[0-9]+/g, m => '-' + m.toLowerCase())
88
125
  }
89
126
 
127
+ /**
128
+ * Capitalises a string
129
+ * @param {string} [str]
130
+ * @returns {string}
131
+ */
90
132
  export function capitalise (str) {
91
133
  return (str||'').replace(/(?:^|\s)\S/g, (a) => a.toUpperCase())
92
134
  }
93
135
 
136
+ /**
137
+ * Formats a currency string
138
+ * @param {number} cents
139
+ * @param {number} [decimals=2]
140
+ * @param {number} [decimalsMinimum]
141
+ * @returns {string}
142
+ */
94
143
  export function currency (cents, decimals=2, decimalsMinimum) {
95
144
  // Returns a formated currency string
96
145
  const num = Number(cents / 100)
@@ -101,75 +150,106 @@ export function currency (cents, decimals=2, decimalsMinimum) {
101
150
  })
102
151
  }
103
152
 
153
+ /**
154
+ * Converts a currency string to cents
155
+ * @param {string} currency string, e.g. '$1,234.00'
156
+ * @returns {string}
157
+ */
104
158
  export function currencyToCents (currency) {
105
159
  // Converts '$1,234.00' to '1234.00', then to '123400'
106
160
  let currencyString = Number(currency.replace(/[^0-9.]/g, '')).toFixed(2)
107
161
  return currencyString.replace(/\./g, '')
108
162
  }
109
163
 
164
+ /**
165
+ * Returns a formatted date string
166
+ * @param {number|Date} date - number can be in seconds or milliseconds (UTC)
167
+ * @param {string} [format] - e.g. "dd mmmm yy" (https://github.com/felixge/node-dateformat#mask-options)
168
+ * @param {string} [timezone] - convert a UTC date to a particular timezone.
169
+ * @returns {string}
170
+ *
171
+ * Note on the timezone conversion:
172
+ * Timezone conversion relies on parsing the toLocaleString result, e.g. 4/10/2012, 5:10:30 PM.
173
+ * A older browser may not accept en-US formatted date string to its Date constructor, and it may
174
+ * return unexpected result (it may ignore daylight saving).
175
+ */
110
176
  export function date (date, format, timezone) {
111
- /**
112
- * Returns a formatted date
113
- * @param {number|Date} date - number can be in seconds or milliseconds (UTC)
114
- * @param {string} format - e.g. "dd mmmm yy" (https://github.com/felixge/node-dateformat#mask-options)
115
- * @param {string} timezone - convert a UTC date to a particular timezone.
116
- *
117
- * Note on the timezone conversion:
118
- * Timezone conversion relies on parsing the toLocaleString result, e.g. 4/10/2012, 5:10:30 PM.
119
- * A older browser may not accept en-US formatted date string to its Date constructor, and it may
120
- * return unexpected result (it may ignore daylight saving).
121
- */
122
177
  if (!date || (!isNumber(date) && !isDate(date))) return 'Date?'
123
- else if (isNumber(date) && date < 9999999999) var milliseconds = date * 1000
124
- else if (isObject(date)) milliseconds = date.getTime()
125
- else milliseconds = date
178
+
179
+ // Get the milliseconds
180
+ let milliseconds = 0
181
+ if (typeof date === 'number') {
182
+ if (date < 9999999999) milliseconds = date * 1000
183
+ else milliseconds = date
184
+ } else if (isDate(date)) {
185
+ milliseconds = date.getTime()
186
+ }
126
187
  if (timezone) {
127
188
  milliseconds = new Date(new Date(milliseconds).toLocaleString('en-US', { timeZone: timezone })).getTime()
128
189
  }
129
- return dateFormat(milliseconds, format || 'dS mmmm')
190
+ return dateformat(milliseconds, format || 'dS mmmm')
130
191
  }
131
192
 
132
- export function debounce (func, wait, options) {
133
- /**
134
- * Creates a debounced function that delays invoking `func` until after `wait`
135
- * milliseconds have elapsed since the last time the debounced function was
136
- * invoked.
137
- * @param {function} func
138
- * @param {number} <wait=0> - number of milliseconds to delay
139
- * @param {boolean} <options.leading=false> - invoke on the leading edge of the timeout
140
- * @param {number} <options.maxWait> - maximum time `func` is allowed to be delayed before it's invoked
141
- * @param {boolean} <options.trailing=true> - invoke on the trailing edge of the timeout
142
- * @returns {Function}
143
- * @see lodash
144
- */
145
- var lastArgs,
146
- lastThis,
147
- maxWait,
148
- result,
149
- timerId,
150
- lastCallTime,
151
- lastInvokeTime = 0,
152
- leading = false,
153
- maxing = false,
154
- trailing = true
155
-
156
- wait = typeof wait == 'number'? wait : 0
157
- if (isObject(options)) {
193
+ /**
194
+ * Creates a debounced function that delays invoking `func` until after `wait`
195
+ * milliseconds have elapsed since the last time the debounced function was invoked.
196
+ *
197
+ * @param {Function} func - The function to debounce.
198
+ * @param {number} [wait=0] - Number of milliseconds to delay.
199
+ * @param {{
200
+ * leading?: boolean, // invoke on the leading edge of the timeout (default: false)
201
+ * maxWait?: number, // maximum time `func` is allowed to be delayed before it's invoked
202
+ * trailing?: boolean, // invoke on the trailing edge of the timeout (default: true)
203
+ * }} [options] - Options to control behavior.
204
+ * @returns {(...args: any[]) => any & {
205
+ * cancel: () => void,
206
+ * flush: () => any
207
+ * }} - A new debounced function with `cancel` and `flush` methods.
208
+ *
209
+ * @see https://lodash.com/docs/4.17.15#debounce
210
+ */
211
+ export function debounce(func, wait = 0, options) {
212
+ /** @type {any[]|undefined} */
213
+ let lastArgs
214
+ /** @type {any} */
215
+ let lastThis
216
+ /** @type {number|undefined} */
217
+ let maxWait
218
+ /** @type {any} */
219
+ let result
220
+ /** @type {ReturnType<typeof setTimeout>|undefined} */
221
+ let timerId
222
+ /** @type {number|undefined} */
223
+ let lastCallTime
224
+ let lastInvokeTime = 0
225
+ let leading = false
226
+ let maxing = false
227
+ let trailing = true
228
+
229
+ if (options) {
158
230
  leading = !!options.leading
159
231
  maxing = 'maxWait' in options
160
- maxWait = maxing ? Math.max(typeof options.maxWait == 'number'? options.maxWait : 0, wait) : maxWait
232
+ maxWait = maxing ? Math.max(typeof options.maxWait === 'number' ? options.maxWait : 0, wait) : undefined
161
233
  trailing = 'trailing' in options ? !!options.trailing : trailing
162
234
  }
163
235
 
236
+ /**
237
+ * @param {number} time
238
+ * @returns {any}
239
+ */
164
240
  function invokeFunc(time) {
165
- var args = lastArgs
166
- var thisArg = lastThis
241
+ const args = lastArgs
242
+ const thisArg = lastThis
167
243
  lastArgs = lastThis = undefined
168
244
  lastInvokeTime = time
169
- result = func.apply(thisArg, args)
245
+ result = func.apply(thisArg, args ? args : [])
170
246
  return result
171
247
  }
172
248
 
249
+ /**
250
+ * @param {number} time
251
+ * @returns {any}
252
+ */
173
253
  function leadingEdge(time) {
174
254
  // Reset any `maxWait` timer.
175
255
  lastInvokeTime = time
@@ -179,27 +259,42 @@ export function debounce (func, wait, options) {
179
259
  return leading ? invokeFunc(time) : result
180
260
  }
181
261
 
262
+ /**
263
+ * @param {number} time
264
+ * @returns {number}
265
+ */
182
266
  function remainingWait(time) {
183
- var timeSinceLastCall = time - lastCallTime,
184
- timeSinceLastInvoke = time - lastInvokeTime,
185
- timeWaiting = wait - timeSinceLastCall
267
+ const timeSinceLastCall = time - (lastCallTime ?? 0)
268
+ const timeSinceLastInvoke = time - lastInvokeTime
269
+ const timeWaiting = wait - timeSinceLastCall
186
270
  return maxing
187
- ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
271
+ ? Math.min(timeWaiting, (maxWait ?? 0) - timeSinceLastInvoke)
188
272
  : timeWaiting
189
273
  }
190
274
 
275
+ /**
276
+ * @param {number} time
277
+ * @returns {boolean}
278
+ */
191
279
  function shouldInvoke(time) {
192
- var timeSinceLastCall = time - lastCallTime,
193
- timeSinceLastInvoke = time - lastInvokeTime
280
+ const timeSinceLastCall = time - (lastCallTime ?? 0)
281
+ const timeSinceLastInvoke = time - lastInvokeTime
194
282
  // Either this is the first call, activity has stopped and we're at the
195
283
  // trailing edge, the system time has gone backwards and we're treating
196
284
  // it as the trailing edge, or we've hit the `maxWait` limit.
197
- return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
198
- (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
285
+ return (
286
+ lastCallTime === undefined ||
287
+ timeSinceLastCall >= wait ||
288
+ timeSinceLastCall < 0 ||
289
+ (maxing && timeSinceLastInvoke >= (maxWait ?? 0))
290
+ )
199
291
  }
200
292
 
293
+ /**
294
+ * @returns {any}
295
+ */
201
296
  function timerExpired() {
202
- var time = Date.now()
297
+ const time = Date.now()
203
298
  if (shouldInvoke(time)) {
204
299
  return trailingEdge(time)
205
300
  }
@@ -207,6 +302,10 @@ export function debounce (func, wait, options) {
207
302
  timerId = setTimeout(timerExpired, remainingWait(time))
208
303
  }
209
304
 
305
+ /**
306
+ * @param {number} time
307
+ * @returns {any}
308
+ */
210
309
  function trailingEdge(time) {
211
310
  timerId = undefined
212
311
  // Only invoke if we have `lastArgs` which means `func` has been
@@ -218,6 +317,10 @@ export function debounce (func, wait, options) {
218
317
  return result
219
318
  }
220
319
 
320
+ /**
321
+ * Cancel any pending debounced invocation.
322
+ * @returns {void}
323
+ */
221
324
  function cancel() {
222
325
  if (timerId !== undefined) {
223
326
  clearTimeout(timerId)
@@ -226,16 +329,26 @@ export function debounce (func, wait, options) {
226
329
  lastArgs = lastCallTime = lastThis = timerId = undefined
227
330
  }
228
331
 
332
+ /**
333
+ * Immediately invoke the debounced function if pending.
334
+ * @returns {any}
335
+ */
229
336
  function flush() {
230
337
  return timerId === undefined ? result : trailingEdge(Date.now())
231
338
  }
232
339
 
233
- function debounced() {
234
- var time = Date.now(),
235
- isInvoking = shouldInvoke(time)
340
+ /**
341
+ * The debounced function.
342
+ * @this {any}
343
+ * @param {...any} args
344
+ * @returns {any}
345
+ */
346
+ function debounced(...args) {
347
+ const time = Date.now()
348
+ const isInvoking = shouldInvoke(time)
236
349
 
237
- lastArgs = arguments
238
- lastThis = this // eslint-disable-line
350
+ lastArgs = args
351
+ lastThis = this
239
352
  lastCallTime = time
240
353
 
241
354
  if (isInvoking) {
@@ -243,10 +356,10 @@ export function debounce (func, wait, options) {
243
356
  return leadingEdge(lastCallTime)
244
357
  }
245
358
  if (maxing) {
246
- // Handle invocations in a tight loop.
247
- clearTimeout(timerId)
248
- timerId = setTimeout(timerExpired, wait)
249
- return invokeFunc(lastCallTime)
359
+ // Handle invocations in a tight loop.
360
+ clearTimeout(timerId)
361
+ timerId = setTimeout(timerExpired, wait)
362
+ return invokeFunc(lastCallTime)
250
363
  }
251
364
  }
252
365
  if (timerId === undefined) {
@@ -254,87 +367,137 @@ export function debounce (func, wait, options) {
254
367
  }
255
368
  return result
256
369
  }
370
+
257
371
  debounced.cancel = cancel
258
372
  debounced.flush = flush
259
373
  return debounced
260
374
  }
261
375
 
262
- export function deepCopy (obj) {
263
- // Deep clones an object
376
+ /**
377
+ * Deep clones an object or array, preserving its type
378
+ * @template T
379
+ * @param {T} obj - Object or array to deep clone
380
+ * @returns {T}
381
+ */
382
+ export function deepCopy(obj) {
264
383
  if (!obj) return obj
265
- let obj2 = Array.isArray(obj) ? [] : {}
266
- for (let key in obj) {
267
- let v = obj[key]
268
- obj2[key] = typeof v === 'object' && !isHex24(v) ? deepCopy(v) : v
384
+ if (typeof obj !== 'object') return obj
385
+
386
+ // Create a new instance based on the input type
387
+ /** @type {any} */
388
+ const clone = Array.isArray(obj) ? [] : {}
389
+
390
+ for (const key in obj) {
391
+ const value = obj[key]
392
+ clone[key] = typeof value === 'object' && !isHex24(value) ? deepCopy(value) : value
269
393
  }
270
- return obj2
394
+
395
+ return clone
271
396
  }
272
397
 
273
- export function deepFind (obj, path) {
274
- // Returns a nested value from a path URI e.g. owner.houses.0.color
398
+ /**
399
+ * Retrieves a nested value from an object or array from a dot-separated path.
400
+ * @param {object|any[]} obj - The source object or array.
401
+ * @param {string} path - Dot-separated path (e.g. "owner.houses.0.color").
402
+ * @returns {unknown}
403
+ */
404
+ export function deepFind(obj, path) {
275
405
  if (!obj) return undefined
276
- let last
277
- let chunks = (path || '').split('.')
406
+ if (typeof obj !== 'object') return undefined
407
+
408
+ const chunks = path.split('.')
409
+ /** @type {any} */
278
410
  let target = obj
279
- for (let i = 0, l = chunks.length; i < l; i++) {
280
- last = l === i + 1
281
- if (!last && !target[chunks[i]]) break
282
- else target = target[chunks[i]]
411
+
412
+ for (const chunk of chunks) {
413
+ if (target === null || target === undefined) return undefined
414
+ target = target[chunk]
283
415
  }
284
- return last ? target : undefined
416
+
417
+ return target
285
418
  }
286
419
 
287
- export function deepSave (obj, path, value) {
288
- /**
289
- * Save a deeply nested value without mutating original object
290
- * @param {object} obj
291
- * @param {string} path
292
- * @param {value|function(current-value) value - pass a function to access the current value
293
- * @return new object
294
- */
295
- if (isArray(obj)) obj = [...obj]
296
- else if (isObject(obj)) obj = {...obj}
297
- else return undefined
420
+ /**
421
+ * Saves a deeply nested value without mutating the original object.
422
+ * @template T
423
+ * @param {T} obj - The source object or array.
424
+ * @param {string} path - Dot-separated path to the nested property.
425
+ * @param {unknown|function} value - The value to set, or a function to compute it from the current value.
426
+ * @returns {T}
427
+ */
428
+ export function deepSave(obj, path, value) {
429
+ if (obj === null || obj === undefined) return obj
298
430
 
431
+ /** @type {any} */
432
+ let clone = Array.isArray(obj) ? [...obj] : { ...obj }
299
433
  let chunks = (path || '').split('.')
300
- let target = obj
434
+ let target = clone
435
+
301
436
  for (let i = 0, l = chunks.length; i < l; i++) {
302
- if (l === i + 1) { // Last
303
- target[chunks[i]] = isFunction(value) ? value(target[chunks[i]]) : value
304
- // console.log(target)
437
+ const key = chunks[i]
438
+ const isLast = i === l - 1
439
+
440
+ if (isLast) {
441
+ target[key] = typeof value === 'function' ? value(target[key]) : value
305
442
  } else {
306
- let isArray = chunks[i + 1].match(/^[0-9]+$/)
307
- let parentCopy = isArray ? [...(target[chunks[i]] || [])] : { ...(target[chunks[i]] || {}) }
308
- target = target[chunks[i]] = parentCopy
443
+ const nextIsArray = /^\d+$/.test(chunks[i + 1])
444
+ const current = target[key]
445
+
446
+ // If the next level doesn't exist, create an empty array/object
447
+ const parentCopy = nextIsArray
448
+ ? Array.isArray(current) ? [...current] : []
449
+ : isObject(current) ? { ...current } : {}
450
+
451
+ target = target[key] = parentCopy
309
452
  }
310
453
  }
311
- return obj
454
+
455
+ return clone
312
456
  }
313
457
 
458
+ /**
459
+ * Iterates over an object or array
460
+ * @param {{[key: string]: any}|[]|null} obj
461
+ * @param {function} iteratee
462
+ * @param {object} [context]
463
+ * @returns {object|[]|null}
464
+ */
314
465
  export function each (obj, iteratee, context) {
315
466
  // Similar to the underscore.each method
316
- const shallowLen = obj == null ? void 0 : obj['length']
467
+ const shallowLen = obj === null ? void 0 : obj['length']
317
468
  const isArrayLike = typeof shallowLen == 'number' && shallowLen >= 0
318
- if (isArrayLike) {
469
+ if (obj === null) {
470
+ return null
471
+ } else if (isArrayLike) {
319
472
  for (let i = 0, l = obj.length; i < l; i++) {
320
- iteratee.call(context || null, obj[i], i, obj)
473
+ iteratee.call(context || null, /** @type {any[]} */(obj)[i], i, obj)
321
474
  }
322
475
  } else {
323
476
  for (let key in obj) {
324
477
  if (!obj.hasOwnProperty(key)) continue
325
- iteratee.call(context || null, obj[key], key, obj)
478
+ iteratee.call(context || null, /** @type {{[key: string]: any}} */(obj)[key], key, obj)
326
479
  }
327
480
  }
328
481
  return obj
329
482
  }
330
483
 
484
+ /**
485
+ * Downloads a file
486
+ * @param {string|Blob|File} data
487
+ * @param {string} filename
488
+ * @param {string} [mime]
489
+ * @param {string} [bom]
490
+ * @returns {void}
491
+ *
492
+ * @link https://github.com/kennethjiang/js-file-download
493
+ */
331
494
  export function fileDownload (data, filename, mime, bom) {
332
- // @link https://github.com/kennethjiang/js-file-download
495
+ if (typeof window === 'undefined') return
333
496
  let blobData = (typeof bom !== 'undefined') ? [bom, data] : [data]
334
497
  let blob = new Blob(blobData, {type: mime || 'application/octet-stream'})
335
498
 
336
- if (typeof window.navigator.msSaveBlob !== 'undefined') {
337
- window.navigator.msSaveBlob(blob, filename)
499
+ if (typeof /** @type {any} */(window.navigator).msSaveBlob !== 'undefined') {
500
+ /** @type {any} */(window.navigator).msSaveBlob(blob, filename)
338
501
  } else {
339
502
  let blobURL = (window.URL && window.URL.createObjectURL)
340
503
  ? window.URL.createObjectURL(blob)
@@ -357,12 +520,23 @@ export function fileDownload (data, filename, mime, bom) {
357
520
  }
358
521
  }
359
522
 
523
+ /**
524
+ * Formats a string into a name
525
+ * @param {string} string
526
+ * @param {boolean} [ignoreHyphen]
527
+ * @returns {string}
528
+ */
360
529
  export function formatName (string, ignoreHyphen) {
361
530
  return ignoreHyphen
362
531
  ? ucFirst(string.toString().trim())
363
532
  : ucFirst(string.toString().trim().replace('-', ' '))
364
533
  }
365
534
 
535
+ /**
536
+ * Formats a string into a slug
537
+ * @param {string} string
538
+ * @returns {string}
539
+ */
366
540
  export function formatSlug (string) {
367
541
  return string
368
542
  .toString()
@@ -374,85 +548,101 @@ export function formatSlug (string) {
374
548
  .replace(/-+$/, '') // Remove trailing -
375
549
  }
376
550
 
377
- export function formData (obj, cfg, fd, pre) {
551
+ /**
552
+ * Serializes objects to FormData instances
553
+ * @param {object} obj
554
+ * @param {{ allowEmptyArrays?: boolean, indices?: boolean, nullsAsUndefineds?: boolean, booleansAsIntegers?: boolean }} [cfg] - config
555
+ * @param {FormData} [existingFormData]
556
+ * @param {string} [keyPrefix]
557
+ * @returns {FormData}
558
+ * @link https://github.com/therealparmesh/object-to-formdata
559
+ */
560
+ export function formData (obj, cfg, existingFormData, keyPrefix) {
378
561
  /**
379
562
  * Serializes objects to FormData instances
380
- * @param {object} obj
381
- * @param {object} cfg - config, e.g. { allowEmptyArrays: true, indices: true }
382
- * @link https://github.com/therealparmesh/object-to-formdata
563
+ * @param {{[key: string]: any}} obj
564
+ * @param {{ allowEmptyArrays?: boolean, indices?: boolean, nullsAsUndefineds?: boolean, booleansAsIntegers?: boolean }} [cfg] - config
565
+ * @param {FormData} [existingFormData]
566
+ * @param {string} [keyPrefix]
567
+ * @returns {FormData}
383
568
  */
384
- const isUndefined = (value) => value === undefined
385
- const isNull = (value) => value === null
386
- const isBoolean = (value) => typeof value === 'boolean'
387
- const isObject = (value) => value === Object(value)
388
- const isArray = (value) => Array.isArray(value)
389
- const isDate = (value) => value instanceof Date
390
-
391
- const isBlob = (value) =>
392
- value && typeof value.size === 'number' && typeof value.type === 'string' && typeof value.slice === 'function'
393
-
394
- const isFile = (value) =>
395
- isBlob(value) &&
396
- typeof value.name === 'string' &&
397
- (typeof value.lastModifiedDate === 'object' || typeof value.lastModified === 'number')
398
-
399
- const serialize = (obj, cfg, fd, pre) => {
569
+ const serialize = (obj, cfg, existingFormData, keyPrefix='') => {
400
570
  cfg = cfg || {}
401
- cfg.indices = isUndefined(cfg.indices) ? false : cfg.indices
402
- cfg.nullsAsUndefineds = isUndefined(cfg.nullsAsUndefineds) ? false : cfg.nullsAsUndefineds
403
- cfg.booleansAsIntegers = isUndefined(cfg.booleansAsIntegers) ? false : cfg.booleansAsIntegers
404
- cfg.allowEmptyArrays = isUndefined(cfg.allowEmptyArrays) ? false : cfg.allowEmptyArrays
405
- fd = fd || new FormData()
406
-
407
- if (isUndefined(obj)) {
408
- return fd
409
- } else if (isNull(obj)) {
571
+ cfg.indices = cfg.indices === undefined ? false : cfg.indices
572
+ cfg.nullsAsUndefineds = cfg.nullsAsUndefineds === undefined ? false : cfg.nullsAsUndefineds
573
+ cfg.booleansAsIntegers = cfg.booleansAsIntegers === undefined ? false : cfg.booleansAsIntegers
574
+ cfg.allowEmptyArrays = cfg.allowEmptyArrays === undefined ? false : cfg.allowEmptyArrays
575
+ existingFormData = existingFormData || new FormData()
576
+
577
+ const isBlob = typeof obj === 'object' &&
578
+ 'size' in obj && typeof obj.size === 'number' &&
579
+ 'type' in obj && typeof obj.type === 'string' &&
580
+ 'slice' in obj && typeof obj.slice === 'function'
581
+
582
+ const isFile = isBlob &&
583
+ 'name' in obj && typeof obj.name === 'string' &&
584
+ (
585
+ ('lastModifiedDate' in obj && typeof obj.lastModifiedDate === 'object') ||
586
+ ('lastModified' in obj && typeof obj.lastModified === 'number')
587
+ )
588
+
589
+ if (obj === undefined) {
590
+ return existingFormData
591
+ } else if (obj === null) {
410
592
  if (!cfg.nullsAsUndefineds) {
411
- fd.append(pre, '')
593
+ existingFormData.append(keyPrefix, '')
412
594
  }
413
- } else if (isBoolean(obj)) {
595
+ } else if (typeof obj === 'boolean') {
414
596
  if (cfg.booleansAsIntegers) {
415
- fd.append(pre, obj ? 1 : 0)
597
+ existingFormData.append(keyPrefix, obj ? '1' : '0')
416
598
  } else {
417
- fd.append(pre, obj)
599
+ existingFormData.append(keyPrefix, obj)
418
600
  }
419
- } else if (isArray(obj)) {
601
+ } else if (Array.isArray(obj)) {
420
602
  if (obj.length) {
421
603
  obj.forEach((value, index) => {
422
- const key = pre + '[' + (cfg.indices ? index : '') + ']'
423
- serialize(value, cfg, fd, key)
604
+ const key = keyPrefix + '[' + (cfg.indices ? index : '') + ']'
605
+ serialize(value, cfg, existingFormData, key)
424
606
  })
425
607
  } else if (cfg.allowEmptyArrays) {
426
- fd.append(pre + '[]', '')
608
+ existingFormData.append(keyPrefix + '[]', '')
427
609
  }
428
- } else if (isDate(obj)) {
429
- fd.append(pre, obj.toISOString())
430
- } else if (isObject(obj) && !isFile(obj) && !isBlob(obj)) {
610
+ } else if (obj instanceof Date) {
611
+ existingFormData.append(keyPrefix, obj.toISOString())
612
+ } else if (obj === Object(obj) && !isFile && !isBlob) {
431
613
  Object.keys(obj).forEach((prop) => {
432
614
  const value = obj[prop]
433
- if (isArray(value)) {
615
+ if (Array.isArray(value)) {
434
616
  while (prop.length > 2 && prop.lastIndexOf('[]') === prop.length - 2) {
435
617
  prop = prop.substring(0, prop.length - 2)
436
618
  }
437
619
  }
438
- const key = pre ? pre + '[' + prop + ']' : prop
439
- serialize(value, cfg, fd, key)
620
+ const key = keyPrefix ? keyPrefix + '[' + prop + ']' : prop
621
+ serialize(value, cfg, existingFormData, key)
440
622
  })
441
623
  } else {
442
- fd.append(pre, obj)
624
+ existingFormData.append(keyPrefix, /** @type {any} */(obj))
443
625
  }
444
- return fd
626
+ return existingFormData
445
627
  }
446
- return serialize(obj, cfg, fd, pre)
628
+ return serialize(obj, cfg, existingFormData, keyPrefix)
447
629
  }
448
630
 
631
+ /**
632
+ * Returns capitalized full name
633
+ * @param {{firstName: string, lastName: string}} object
634
+ * @returns {string}
635
+ */
449
636
  export function fullName (object) {
450
- // Returns full name
451
637
  return ucFirst(object.firstName) + ' ' + ucFirst(object.lastName)
452
638
  }
453
639
 
640
+ /**
641
+ * Splits a full name into first and last names
642
+ * @param {string} string
643
+ * @returns {string[]} e.g. ['John', 'Smith']
644
+ */
454
645
  export function fullNameSplit (string) {
455
- // Returns [firstName, lastName]
456
646
  string = string.trim().replace(/\s+/, ' ')
457
647
  if (string.match(/\s/)) {
458
648
  return [string.substring(0, string.lastIndexOf(' ')), string.substring(string.lastIndexOf(' ') + 1)]
@@ -461,6 +651,11 @@ export function fullNameSplit (string) {
461
651
  }
462
652
  }
463
653
 
654
+ /**
655
+ * Returns a list of country options
656
+ * @param {{ [key: string]: { name: string } }} countries
657
+ * @returns {{ value: string, label: string, flag: string }[]}
658
+ */
464
659
  export function getCountryOptions (countries) {
465
660
  const output = []
466
661
  for (const iso in countries) {
@@ -470,6 +665,11 @@ export function getCountryOptions (countries) {
470
665
  return output
471
666
  }
472
667
 
668
+ /**
669
+ * Returns a list of currency options
670
+ * @param {{ [iso: string]: { name: string } }} currencies
671
+ * @returns {{ value: string, label: string }[]}
672
+ */
473
673
  export function getCurrencyOptions (currencies) {
474
674
  const output = []
475
675
  for (const iso in currencies) {
@@ -482,11 +682,11 @@ export function getCurrencyOptions (currencies) {
482
682
  /**
483
683
  * Get the width of a prefix
484
684
  * @param {string} prefix
485
- * @param {number} paddingRight
685
+ * @param {number} [paddingRight=0]
486
686
  * @returns {number}
487
687
  */
488
688
  export function getPrefixWidth (prefix, paddingRight=0) {
489
- if (!prefix) return 0
689
+ if (!prefix || typeof window === 'undefined') return 0
490
690
  const span = document.createElement('span')
491
691
  span.classList.add('input-prefix')
492
692
  span.style.visibility = 'hidden'
@@ -497,8 +697,14 @@ export function getPrefixWidth (prefix, paddingRight=0) {
497
697
  return width
498
698
  }
499
699
 
700
+ /**
701
+ * Returns a list of project directories
702
+ * @param {{ join: (...args: string[]) => string }} path - e.g. `import path from 'path'`
703
+ * @param {string} [pwd]
704
+ * @returns {object}
705
+ */
500
706
  export function getDirectories (path, pwd) {
501
- const _pwd = pwd || process.env.PWD
707
+ const _pwd = pwd || process.env.PWD || ''
502
708
  return {
503
709
  clientDir: path.join(_pwd, process.env.clientDir || 'client', '/'),
504
710
  componentsDir: path.join(_pwd, process.env.componentsDir || 'components', '/'),
@@ -509,81 +715,125 @@ export function getDirectories (path, pwd) {
509
715
  }
510
716
  }
511
717
 
512
- export function getLink (obj, query) {
513
- /**
514
- * @param {object} obj - new query object
515
- * @param {object} <query> - current query object, window.location.search used otherwise
516
- * @return {string}
517
- */
518
- let newQueryObj = {...(query||queryObject(window.location.search))}
519
- for (let key in obj) {
520
- if (!obj[key]) delete newQueryObj[key]
521
- else newQueryObj[key] = obj[key]
522
- }
523
- return queryString(newQueryObj) || '?'
524
- }
525
-
718
+ /**
719
+ * Returns a Stripe client promise
720
+ * @param {string} stripePublishableKey
721
+ * @returns {Promise<import('@stripe/stripe-js').Stripe|null>}
722
+ */
526
723
  export function getStripeClientPromise (stripePublishableKey) {
527
- return global.stripeClientPromise || (global.stripeClientPromise = loadStripe(stripePublishableKey))
724
+ return stripeClientCache || (stripeClientCache = loadStripe(stripePublishableKey))
528
725
  }
529
726
 
727
+ /**
728
+ * Returns a list of response errors
729
+ *
730
+ * @typedef {{ title: string, detail: string }} NitroError
731
+ * @typedef {{ toJSON: () => { message: string } }} MongoError
732
+ * @typedef {{ response: { data: { errors?: NitroError[], error?: string, error_description?: string } } }} AxiosWithErrors
733
+ *
734
+ * @typedef {Error|NitroError[]|MongoError|AxiosWithErrors|string|any} NitroErrorRaw
735
+ *
736
+ * @param {NitroErrorRaw} errs
737
+ * @returns {NitroError[]}
738
+ */
530
739
  export function getResponseErrors (errs) {
740
+ // new Error
741
+ if (errs instanceof Error || errs === null) {
742
+ console.info('getResponseErrors(): ', errs)
743
+ return [{ title: 'error', detail: 'Oops there was an error' }]
744
+
745
+ // Array of error objects
746
+ } else if (Array.isArray(errs)) {
747
+ return errs
748
+
749
+ // Mongo errors (when called on the backend, now coming before Axios errors...)
750
+ } else if (typeof errs === 'object' && 'toJSON' in errs) {
751
+ return [{ title: 'error', detail: errs.toJSON().message }]
752
+
531
753
  // Axios response errors
532
- if (errs.response && errs.response.data && errs.response.data.errors) {
533
- var errors = errs.response.data.errors
534
- // Axios response error
535
- } else if (errs.response && errs.response.data && errs.response.data.error) {
536
- errors = [{ title: errs.response.data.error, detail: errs.response.data.error_description }]
537
- // Array
538
- } else if (isArray(errs)) {
539
- errors = errs
540
- // Error object
541
- } else if (errs.toJSON) {
542
- errors = [{ title: 'error', detail: errs.toJSON().message }]
754
+ } else if (typeof errs === 'object' && errs?.response?.data?.errors) {
755
+ return errs.response.data.errors
756
+
757
+ // // Axios response error
758
+ } else if (typeof errs === 'object' && errs?.response?.data?.error) {
759
+ return [{ title: errs.response.data.error, detail: errs.response.data.error_description || '' }]
760
+
543
761
  // String
544
762
  } else if (typeof errs === 'string') {
545
- errors = [{ title: 'error', detail: errs }]
763
+ return [{ title: 'error', detail: errs }]
764
+
546
765
  // Default error message
547
766
  } else {
548
767
  console.info('getResponseErrors(): ', errs)
549
- errors = [{ title: 'error', detail: 'Oops there was an error' }]
768
+ return [{ title: 'error', detail: 'Oops there was an error' }]
550
769
  }
551
- return errors
552
770
  }
553
771
 
554
- export function inArray (array, key, value) {
555
- /**
556
- * Property match inside an array of objects
557
- * (For a string/number value check just use [].includes(x))
558
- * @param {string} <key> - optional to match across on a colleciton of objects
559
- * @param {any} value
560
- */
561
- if (!array || typeof key == 'undefined') return false
562
- if (typeof value == 'undefined') return array.includes(key)
563
- for (let i = array.length; i--; ) {
564
- if (array[i] && array[i].hasOwnProperty(key) && array[i][key] == value) return array[i]
565
- }
772
+ /**
773
+ * Checks if a value is in an array (todo, update this to not use optional key)
774
+ * @param {any[]} array
775
+ * @param {unknown} [value]
776
+ * @param {string} [key] - optional, to match across on a colleciton of objects
777
+ * @returns {boolean}
778
+ */
779
+ export function inArray (array, value, key) {
780
+ if (!array || typeof value == 'undefined') return false
781
+ else if (typeof key == 'undefined') return array.includes(value)
782
+ else
783
+ for (let i = array.length; i--; ) {
784
+ if (typeof array[i] === 'object') {
785
+ /** @type {{[key: string]: unknown}} */
786
+ const item = array[i]
787
+ if (key in item && array[i][key] == value) return array[i]
788
+ }
789
+ }
566
790
  return false
567
791
  }
568
792
 
793
+ /**
794
+ * Checks if a variable is an array
795
+ * @param {unknown} variable
796
+ * @returns {boolean}
797
+ */
569
798
  export function isArray (variable) {
570
799
  return Array.isArray(variable)
571
800
  }
572
801
 
802
+ /**
803
+ * Checks if a variable is a date
804
+ * @param {unknown} variable
805
+ * @returns {boolean}
806
+ */
573
807
  export function isDate (variable) {
574
- return variable && typeof variable.getMonth === 'function'
808
+ return !!(typeof variable === 'object' && variable && 'getMonth' in variable)
575
809
  }
576
810
 
811
+ /**
812
+ * Checks if a variable is defined
813
+ * @param {unknown} variable
814
+ * @returns {boolean}
815
+ */
577
816
  export function isDefined (variable) {
578
817
  return typeof variable !== 'undefined'
579
818
  }
580
819
 
820
+ /**
821
+ * Checks if a variable is an email
822
+ * @param {string} email
823
+ * @returns {boolean}
824
+ */
581
825
  export function isEmail (email) {
582
826
  // eslint-disable-next-line
583
827
  const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
584
828
  return re.test(String(email).toLowerCase())
585
829
  }
586
830
 
831
+ /**
832
+ * Checks if an object is empty
833
+ * @param {{[key: string]: unknown}|null} [obj]
834
+ * @param {boolean} [truthyValuesOnly]
835
+ * @returns {boolean}
836
+ */
587
837
  export function isEmpty (obj, truthyValuesOnly) {
588
838
  // note req.files doesn't have a hasOwnProperty method
589
839
  if (obj === null || typeof obj === 'undefined') return true
@@ -593,10 +843,20 @@ export function isEmpty (obj, truthyValuesOnly) {
593
843
  return true
594
844
  }
595
845
 
846
+ /**
847
+ * Checks if a variable is a function
848
+ * @param {unknown} variable
849
+ * @returns {boolean}
850
+ */
596
851
  export function isFunction (variable) {
597
852
  return typeof variable === 'function' ? true : false
598
853
  }
599
854
 
855
+ /**
856
+ * Checks if a variable is a hex string
857
+ * @param {unknown} value
858
+ * @returns {boolean}
859
+ */
600
860
  export function isHex24 (value) {
601
861
  // Fast function to check if the length is exactly 24 and all characters are valid hexadecimal digits
602
862
  const str = (value||'').toString()
@@ -616,8 +876,13 @@ export function isHex24 (value) {
616
876
  return true
617
877
  }
618
878
 
879
+ /**
880
+ * Checks if a variable is a number
881
+ * @param {unknown} variable
882
+ * @returns {boolean}
883
+ */
619
884
  export function isNumber (variable) {
620
- return !isNaN(parseFloat(variable)) && isFinite(variable)
885
+ return !isNaN(parseFloat(/** @type {string} */(variable))) && isFinite(/** @type {number} */(variable))
621
886
  }
622
887
 
623
888
  /**
@@ -630,18 +895,40 @@ export function isObject (variable) {
630
895
  return variable !== null && typeof variable === 'object' && !(variable instanceof Array) ? true : false
631
896
  }
632
897
 
898
+ /**
899
+ * Checks if a variable is a regex
900
+ * @param {unknown} variable
901
+ * @returns {boolean}
902
+ */
633
903
  export function isRegex (variable) {
634
904
  return variable instanceof RegExp ? true : false
635
905
  }
636
906
 
907
+ /**
908
+ * Checks if a variable is a string
909
+ * @param {unknown} variable
910
+ * @returns {boolean}
911
+ */
637
912
  export function isString (variable) {
638
913
  return typeof variable === 'string' || variable instanceof String ? true : false
639
914
  }
640
915
 
916
+ /**
917
+ * Converts the first character of a string to lowercase
918
+ * @param {string} string
919
+ * @returns {string}
920
+ */
641
921
  export function lcFirst (string) {
642
922
  return string.charAt(0).toLowerCase() + string.slice(1)
643
923
  }
644
924
 
925
+ /**
926
+ * Trims a string to a maximum length, and removes any partial words at the end
927
+ * @param {string} string
928
+ * @param {number} [len=100]
929
+ * @param {boolean} [showEllipsis=true]
930
+ * @returns {string}
931
+ */
645
932
  export function maxLength (string, len, showEllipsis) {
646
933
  // Trims to a maximum length, and removes any partial words at the end
647
934
  len = len || 100
@@ -655,41 +942,48 @@ export function maxLength (string, len, showEllipsis) {
655
942
  + (showEllipsis ? '...' : '')
656
943
  }
657
944
 
658
- export function mongoAddKmsToBox (km, bottomLeft, topRight) {
659
- /**
660
- * Expands a mongodb lat/lng box in kms
661
- * @param {number} km
662
- * @param {Array[lng, lat]|Box} bottomLeft
663
- * @param {Array[lng, lat]} topRight
664
- * @return [bottomLeft, topRight]
665
- *
666
- * Handy box tester
667
- * https://www.keene.edu/campus/maps/tool/
668
- *
669
- * Returned Google places viewport (i.e. `place.geometry.viewport`)
670
- * {
671
- * Qa: {g: 174.4438160493033, h: 174.9684260722261} == [btmLng, topLng]
672
- * zb: {g: -37.05901990116617, h: -36.66060184426172} == [btmLat, topLat]
673
- * }
674
- *
675
- * We then convert above into `address.area.bottomLeft|topRight`
676
- *
677
- * Rangiora box
678
- * [[172.5608731356091,-43.34484397837406] (btm left)
679
- * [172.6497429548984,-43.28025140057695]] (top right)
680
- *
681
- * Auckland box
682
- * [[174.4438160493033,-37.05901990116617] (btm left)
683
- * [174.9684260722261,-36.66060184426172]] (top right)
684
- */
685
- if (bottomLeft && bottomLeft.bottomLeft) {
686
- topRight = bottomLeft.topRight
687
- bottomLeft = bottomLeft.bottomLeft
945
+ /**
946
+ * Expands a mongodb lng/lat box in kms, and returns the expanded box
947
+ * @typedef {[number, number]} Point - lng/lat
948
+ * @typedef {{bottomLeft: Point, topRight: Point}} Box
949
+ *
950
+ * @param {number} km
951
+ * @param {Point|Box} bottomLeftOrBox
952
+ * @param {Point} [topRight]
953
+ * @returns {[Point, Point]|null} (e.g. [bottomLeft, topRight])
954
+ *
955
+ * Handy box tester
956
+ * https://www.keene.edu/campus/maps/tool/
957
+ *
958
+ * Returned Google places viewport (i.e. `place.geometry.viewport`)
959
+ * {
960
+ * Qa: {g: 174.4438160493033, h: 174.9684260722261} == [btmLng, topLng]
961
+ * zb: {g: -37.05901990116617, h: -36.66060184426172} == [btmLat, topLat]
962
+ * }
963
+ *
964
+ * We then convert above into `address.area.bottomLeft|topRight`
965
+ *
966
+ * Rangiora box
967
+ * [[172.5608731356091,-43.34484397837406] (btm left)
968
+ * [172.6497429548984,-43.28025140057695]] (top right)
969
+ *
970
+ * Auckland box
971
+ * [[174.4438160493033,-37.05901990116617] (btm left)
972
+ * [174.9684260722261,-36.66060184426172]] (top right)
973
+ */
974
+ export function mongoAddKmsToBox (km, bottomLeftOrBox, topRight) {
975
+ if (typeof bottomLeftOrBox === 'object' && 'bottomLeft' in bottomLeftOrBox) {
976
+ topRight = bottomLeftOrBox.topRight
977
+ var bottomLeft = bottomLeftOrBox.bottomLeft
978
+ } else {
979
+ bottomLeft = bottomLeftOrBox
688
980
  }
689
981
  if (!bottomLeft || !topRight) {
690
982
  return null
691
983
  }
984
+ /** @param {number} lat @param {number} kms @returns {number} */
692
985
  let lat = (lat, kms) => lat + (kms / 6371) * (180 / Math.PI)
986
+ /** @param {number} lng @param {number} lat @param {number} kms @returns {number} */
693
987
  let lng = (lng, lat, kms) => lng + (kms / 6371) * (180 / Math.PI) / Math.cos(lat * Math.PI/180)
694
988
  return [
695
989
  [lng(bottomLeft[0], bottomLeft[1], -km), lat(bottomLeft[1], -km)],
@@ -697,28 +991,38 @@ export function mongoAddKmsToBox (km, bottomLeft, topRight) {
697
991
  ]
698
992
  }
699
993
 
994
+ /**
995
+ * Returns a mongo query to find documents within a passed address
996
+ * @param {{
997
+ * area?: {bottomLeft: [number, number], topRight: [number, number]}
998
+ * location?: {coordinates: [number, number]}
999
+ * }} address
1000
+ * @param {number} km
1001
+ * @param {string} prefix
1002
+ * @returns {Object}
1003
+ */
700
1004
  export function mongoDocWithinPassedAddress (address, km, prefix) {
701
- let type = ''
1005
+ // let type = ''
702
1006
  let areaSize = 5
703
- if (type == 'geoNear') {
704
- // NOT USING
705
- // Must be the first stage in an aggregate pipeline
706
- if (address.area) {
707
- areaSize = mongoPointDifference(address.area.bottomLeft, address.area.topRight)
708
- }
709
- // console.log('kms', (areaSize / 2))
710
- return {
711
- $geoNear: {
712
- near: {
713
- type: 'Point',
714
- coordinates: [address.location.coordinates[0], address.location.coordinates[1]],
715
- },
716
- distanceField: 'distance',
717
- maxDistance: ((areaSize / 2) + km) / 6371, // km / earth's radius in km = radians
718
- spherical: true,
719
- },
720
- }
721
- } else if (address.area) {
1007
+ // if (type == 'geoNear') {
1008
+ // // NOT USING
1009
+ // // Must be the first stage in an aggregate pipeline
1010
+ // if (address.area) {
1011
+ // areaSize = mongoPointDifference(address.area.bottomLeft, address.area.topRight)
1012
+ // }
1013
+ // // console.log('kms', (areaSize / 2))
1014
+ // return {
1015
+ // $geoNear: {
1016
+ // near: {
1017
+ // type: 'Point',
1018
+ // coordinates: [address.location.coordinates[0], address.location.coordinates[1]],
1019
+ // },
1020
+ // distanceField: 'distance',
1021
+ // maxDistance: ((areaSize / 2) + km) / 6371, // km / earth's radius in km = radians
1022
+ // spherical: true,
1023
+ // },
1024
+ // }
1025
+ if ('area' in address && address.area) {
722
1026
  let box = mongoAddKmsToBox(km, address.area)
723
1027
  return {
724
1028
  [`${prefix}location`]: {
@@ -727,7 +1031,7 @@ export function mongoDocWithinPassedAddress (address, km, prefix) {
727
1031
  },
728
1032
  },
729
1033
  }
730
- } else {
1034
+ } else if ('location' in address && address.location) {
731
1035
  return {
732
1036
  [`${prefix}location`]: {
733
1037
  $geoWithin: {
@@ -738,17 +1042,20 @@ export function mongoDocWithinPassedAddress (address, km, prefix) {
738
1042
  },
739
1043
  },
740
1044
  }
1045
+ } else {
1046
+ throw new Error('Missing address.location or address.area')
741
1047
  }
742
1048
  }
743
1049
 
1050
+ /**
1051
+ * Find the distance in km between to points
1052
+ * @param {number[]} point1 - [lng, lat] ([192.2132.., 212.23323..])
1053
+ * @param {number[]} point2 - [lng, lat] ([192.2132.., 212.23323..])
1054
+ * @return {number} kms
1055
+ */
744
1056
  export function mongoPointDifference (point1, point2) {
745
- /**
746
- * Find the distance in km between to points
747
- * @param {array} point1 - [192.2132.., 212.23323..]
748
- * @param {array} point2 - [192.2132.., 212.23323..]
749
- * @return {number} kms
750
- */
751
1057
  let R = 6371 // km
1058
+ /** @param {number} degrees @returns {number} */
752
1059
  let mongoDegreesToRadians = (degrees) => degrees * (Math.PI / 180)
753
1060
  let dLat = mongoDegreesToRadians(point2[1]-point1[1])
754
1061
  let dLon = mongoDegreesToRadians(point2[0]-point1[0])
@@ -759,16 +1066,29 @@ export function mongoPointDifference (point1, point2) {
759
1066
  Math.sin(dLon/2) * Math.cos(lat1) * Math.cos(lat2)
760
1067
  let c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
761
1068
  let d = R * c
762
- return d.toFixed(1)
1069
+ return parseFloat(d.toFixed(1))
763
1070
  }
764
1071
 
1072
+ /**
1073
+ * Maps over an object
1074
+ * @param {{ [key: string]: any }} object
1075
+ * @param {(value: any, key: string) => any} fn
1076
+ */
765
1077
  export function objectMap (object, fn) {
1078
+ /** @type {{ [key: string]: any }} */
1079
+ const result = {}
766
1080
  return Object.keys(object).reduce(function(result, key) {
767
1081
  result[key] = fn(object[key], key)
768
1082
  return result
769
- }, {})
1083
+ }, result)
770
1084
  }
771
1085
 
1086
+ /**
1087
+ * Omits fields from an object
1088
+ * @param {{ [key: string]: unknown }} obj
1089
+ * @param {string[]} fields
1090
+ * @returns {{ [key: string]: unknown }}
1091
+ */
772
1092
  export function omit (obj, fields) {
773
1093
  const shallowCopy = Object.assign({}, obj)
774
1094
  for (let i=0; i<fields.length; i+=1) {
@@ -780,40 +1100,63 @@ export function omit (obj, fields) {
780
1100
 
781
1101
  /**
782
1102
  * Updates state from an input event, you can also update deep state properties
783
- * @param {Event|Array[{string},{string|number|fn}]}
784
- * {Event} - pass the event object e.g. <input onChange={(e) => onChange.call(setState, e)}>
785
- * {Array} - pass an array with [path, value] e.g. <input onChange={(e) => onChange.call(setState, e, ['name', 'Joe'])}>
1103
+ * @param {(
1104
+ * ChangeEvent | // e.g. <input onChange={(e) => onChange.call(setState, e)}>
1105
+ * PathValue // e.g. <input onChange={(e) => onChange.call(setState, e, ['address.name', 'Joe'])}> // [id, value]
1106
+ * )} eventOrPathValue
786
1107
  * @param {Function} [beforeSetState] - optional function to run before setting the state
787
- * @this {Function} setState
788
- * @return {Promise({state, chunks, target})}
1108
+ * @returns {Promise<object>}
1109
+ * @this {function}
1110
+ *
1111
+ * @typedef {import('react').ChangeEvent} ChangeEvent
1112
+ * @typedef {[
1113
+ * string, // can be a path (e.g. 'name.first')
1114
+ * function|unknown, // value or function to set the value to
1115
+ * ]} PathValue
789
1116
  */
790
- export function onChange (event, beforeSetState) {
1117
+ export function onChange (eventOrPathValue, beforeSetState) {
791
1118
  if (!isFunction(this)) {
792
1119
  throw new Error('Missing setState, please either call or bind setState to the function. E.g. onChange.call(setState, e)')
793
1120
  }
794
- let elem = event.target ? event.target : { id: event[0], value: event[1] }
795
- let chunks = (elem.id || elem.name).split('.')
796
- let value = elem.files
797
- ? elem.files[0]
798
- : elem.type === 'checkbox'
799
- ? elem.checked
800
- : isDefined(elem._value)
801
- ? elem._value
802
- : elem.value
803
-
1121
+
1122
+ /** @type {unknown|function} */
1123
+ let value
1124
+ /** @type {string[]} */
1125
+ let chunks = []
1126
+ /** @type {boolean} */
1127
+ let hasFiles
1128
+
1129
+ if (eventOrPathValue instanceof Event && eventOrPathValue.target) {
1130
+ const element = /** @type {HTMLInputElement & {_value?: unknown}} */(eventOrPathValue.target) // we need to assume this is an input
1131
+ chunks = (element.id || element.name).split('.')
1132
+ hasFiles = !!element.files
1133
+ value = element.files
1134
+ ? element.files[0]
1135
+ : typeof element._value !== 'undefined'
1136
+ ? element._value
1137
+ : element.type === 'checkbox'
1138
+ ? element.checked
1139
+ : element.value
1140
+
1141
+ } else if (Array.isArray(eventOrPathValue)) {
1142
+ chunks = eventOrPathValue[0].split('.')
1143
+ value = eventOrPathValue[1]
1144
+ }
1145
+
804
1146
  // Removing leading zero(s) on number fields
805
- // if (elem.type == 'number' && !isFunction(value) && (value||'').match(/^0+([1-9])/)) {
1147
+ // if (element.type == 'number' && !isFunction(value) && (value||'').match(/^0+([1-9])/)) {
806
1148
  // value = value.replace(/^0+([1-9])/, '$1')
807
1149
  // }
808
1150
 
809
1151
  // Update state
810
1152
  return new Promise((resolve) => {
811
- this((state) => {
812
- const newState = { ...state, ...(elem.files ? { hasFiles: true } : {}) }
1153
+ this((/** @type object */ state) => {
1154
+ /** @type {{[key: string]: any}} */
1155
+ const newState = { ...state, ...(hasFiles ? { hasFiles } : {}) }
813
1156
  let target = newState
814
1157
  for (var i = 0, l = chunks.length; i < l; i++) {
815
1158
  if (l === i + 1) { // Last
816
- target[chunks[i]] = isFunction(value) ? value(state) : value
1159
+ target[chunks[i]] = typeof value === 'function' ? value(state) : value
817
1160
  // console.log(target)
818
1161
  } else {
819
1162
  let isArray = chunks[i + 1].match(/^[0-9]+$/)
@@ -830,97 +1173,118 @@ export function onChange (event, beforeSetState) {
830
1173
  })
831
1174
  }
832
1175
 
833
- export function pad (num, padLeft, fixedRight) {
834
- num = parseFloat(num || 0)
1176
+ /**
1177
+ * Pads a number
1178
+ * @param {number} [num=0]
1179
+ * @param {number} [padLeft=0]
1180
+ * @param {number} [fixedRight]
1181
+ * @returns {string}
1182
+ */
1183
+ export function pad (num=0, padLeft=0, fixedRight) {
1184
+ num = parseFloat(num + '')
835
1185
  if (fixedRight || fixedRight === 0) {
836
- return num.toFixed(fixedRight).padStart((padLeft||0) + fixedRight + 1, '0')
1186
+ return num.toFixed(fixedRight).padStart(padLeft + fixedRight + 1, '0')
837
1187
  } else {
838
1188
  if (padLeft && `${num}`.match('.')) padLeft += (`${num}`.split('.')[1]||'').length + 1
839
1189
  return `${num}`.padStart(padLeft, '0')
840
1190
  }
841
1191
  }
842
1192
 
1193
+ /**
1194
+ * Picks fields from an object
1195
+ * @param {{ [key: string]: any }} obj
1196
+ * @param {string|RegExp|string[]|RegExp[]} keys
1197
+ */
843
1198
  export function pick (obj, keys) {
844
1199
  // Similiar to underscore.pick
845
- // @param {string[] | regex[]} keys
846
1200
  if (!isObject(obj) && !isFunction(obj)) return {}
847
- keys = toArray(keys)
848
- let res = {}
849
- for (let key of keys) {
850
- if (isString(key) && obj.hasOwnProperty(key)) res[key] = obj[key]
851
- if (isRegex(key)) {
1201
+ const keysArr = toArray(keys)
1202
+ /** @type {{ [key: string]: unknown }} */
1203
+ let output = {}
1204
+ for (let key of keysArr) {
1205
+ if (typeof key === 'string' && obj.hasOwnProperty(key)) output[key] = obj[key]
1206
+ else if (key instanceof RegExp ) {
852
1207
  for (let key2 in obj) {
853
- if (obj.hasOwnProperty(key2) && key2.match(key)) res[key2] = obj[key2]
1208
+ if (obj.hasOwnProperty(key2) && key2.match(key)) output[key2] = obj[key2]
854
1209
  }
855
1210
  }
856
1211
  }
857
- return res
1212
+ return output
858
1213
  }
859
1214
 
860
- export function queryObject (search, assignTrue) {
861
- /*
862
- * Parses a query string into an object, or returns the last known matching cache
863
- * @param {string} search - location.search or location.href, e.g. '?page=1', 'https://...co.nz?page=1'
864
- * @param {boolean} assignTrue - assign true to empty values
865
- * @return {object} e.g. { page: 1 }
866
- */
867
- search = search.replace(/^[^?]+\?/, '?') // remove domain preceeding search string
1215
+ /**
1216
+ *
1217
+ * Parses a query string into an object, or returns the last known matching cache
1218
+ * @param {string} searchString - location.search or location.href, e.g. '?page=1', 'https://...co.nz?page=1'
1219
+ * @param {boolean} [trueDefaults] - assign true to empty values
1220
+ * @returns {{[key: string]: string|true}} - e.g. { page: '1' }
1221
+ * UPDATE: removed array values, e.g. '?page=1&page=2' will return { page: '2' }
1222
+ */
1223
+ export function queryObject (searchString, trueDefaults) {
1224
+ searchString = searchString.replace(/^[^?]+\?/, '?') // remove domain preceeding search string
1225
+ const uniqueKey = searchString + (trueDefaults ? '-true' : '')
1226
+ /** @type {{[key: string]: string|true}} */
868
1227
  let obj = {}
869
- if (search === '') return obj
870
- if (!queryObject.queryObjectCache) queryObject.queryObjectCache = {}
871
- if (queryObject.queryObjectCache[search]) return { ...queryObject.queryObjectCache[search] }
1228
+
1229
+ if (searchString === '') return {}
1230
+ if (!queryObjectCache) queryObjectCache = {}
1231
+ if (queryObjectCache[uniqueKey]) return queryObjectCache[uniqueKey]
872
1232
 
873
1233
  // Remove '?', and split each query parameter (ampersand-separated)
874
- search = search.slice(1).split('&')
1234
+ const searchParams = searchString.slice(1).split('&')
875
1235
 
876
1236
  // Loop through each query paramter
877
- search.map(function (part) {
878
- part = part.split('=') // Split into key/value
879
- let key = part[0]
880
- let value = !part[1] && part[1] !== 0 ? (assignTrue ? true : '') : decodeURIComponent(part[1])
881
-
882
- // Key already exists
883
- if (obj[key]) {
884
- obj[key] = toArray([obj[key]])
885
- obj[key].push(value)
1237
+ searchParams.map(function (part) {
1238
+ const partArr = part.split('=') // Split into key/value
1239
+ const key = partArr[0]
1240
+ const isEmpty = !partArr[1] && partArr[1] != '0'
1241
+
1242
+ if (trueDefaults === true) {
1243
+ obj[key] = isEmpty ? true : decodeURIComponent(partArr[1])
886
1244
  } else {
887
- obj[key] = value
1245
+ obj[key] = decodeURIComponent(partArr[1])
888
1246
  }
889
1247
  })
890
1248
 
891
- queryObject.queryObjectCache = { [search]: obj }
892
- return { ...obj }
1249
+ queryObjectCache[uniqueKey] = obj
1250
+ return obj
893
1251
  }
894
1252
 
1253
+ /**
1254
+ * Parses an object and returns a query string
1255
+ * @param {{[key: string]: unknown}} [obj] - query object
1256
+ * @returns {string}
1257
+ */
895
1258
  export function queryString (obj) {
896
- /*
897
- * Parses an object and returns a query string
898
- * @param {object} obj - query object
899
- */
900
- obj = { ...(obj||{}) }
1259
+ /** @type {{[key: string]: string}} */
1260
+ const newObj = {}
1261
+
901
1262
  for (let key in obj) {
902
1263
  if (obj.hasOwnProperty(key)) {
903
- if (typeof obj[key] == 'undefined') delete obj[key]
904
- else if (!obj[key]) delete obj[key]
1264
+ if (typeof obj[key] == 'undefined' || !obj[key]) continue
1265
+ newObj[key] = obj[key] + ''
905
1266
  }
906
1267
  }
907
- let qs = new URLSearchParams(obj).toString()
1268
+ let qs = new URLSearchParams(newObj).toString()
908
1269
  return qs ? `?${qs}` : ''
909
1270
  }
910
1271
 
1272
+ /**
1273
+ * Axios request to the route
1274
+ * @param {string} route - e.g. 'post /api/user'
1275
+ * @param {{ [key: string]: any }} [data] - payload
1276
+ * @param {{preventDefault?: function}} [event] - event to prevent default
1277
+ * @param {[boolean, (value: boolean) => void]} [isLoading] - [isLoading, setIsLoading]
1278
+ * @returns {Promise<any>}
1279
+ */
911
1280
  export async function request (route, data, event, isLoading) {
912
- /**
913
- * Axios request to the route
914
- * @param {string} route - e.g. 'post /api/user'
915
- * @param {object} <data> - payload
916
- * @param {Event} <event> - event to prevent default
917
- * @param {array} <isLoading> - [isLoading, setIsLoading]
918
- * @return {promise}
919
- */
920
1281
  try {
921
1282
  if (event?.preventDefault) event.preventDefault()
922
1283
  const uri = route.replace(/^(post|put|delete|get) /, '')
923
- const method = (route.match(/^(post|put|delete|get) /)?.[1] || 'post').trim()
1284
+ const method =
1285
+ /** @type {'post'|'put'|'delete'|'get'} */ (
1286
+ (route.match(/^(post|put|delete|get) /)?.[1] || 'post').trim()
1287
+ )
924
1288
 
925
1289
  // show loading
926
1290
  if (isLoading) {
@@ -932,20 +1296,26 @@ export async function request (route, data, event, isLoading) {
932
1296
  data = data || {}
933
1297
  delete data.errors
934
1298
 
935
- // has files, if yes, convert to form data
936
- let hasFiles
1299
+ // Find out if the data has files?
1300
+ let hasFiles = false
1301
+ /** @param {unknown} o */
937
1302
  let recurse = (o) => {
938
1303
  if (o instanceof File || hasFiles) hasFiles = true
939
1304
  else if (o && typeof o === 'object') each(o, recurse)
940
1305
  }
941
1306
  recurse(data)
942
- if (hasFiles) {
943
- data = formData(data, { allowEmptyArrays: true, indices: true })
944
- }
1307
+
1308
+ // If yes, convert to form data
1309
+ /** @type {FormData|undefined} */
1310
+ const formData2 = hasFiles ? formData(data, { allowEmptyArrays: true, indices: true }) : undefined
945
1311
 
946
1312
  // send the request
1313
+ const axiosPromise = (method === 'get' || method === 'delete')
1314
+ ? axios()[method](uri, { withCredentials: true })
1315
+ : axios()[method](uri, formData2 || data, { withCredentials: true })
1316
+
947
1317
  const [res] = await Promise.allSettled([
948
- axios()[method](uri, data, { withCredentials: true }),
1318
+ axiosPromise,
949
1319
  setTimeoutPromise(() => {}, 200), // eslint-disable-line
950
1320
  ])
951
1321
 
@@ -960,6 +1330,11 @@ export async function request (route, data, event, isLoading) {
960
1330
  }
961
1331
  }
962
1332
 
1333
+ /**
1334
+ * Removes undefined from an array or object
1335
+ * @param {[]|{[key: string]: any}} variable
1336
+ * @returns {[]|{[key: string]: any}}
1337
+ */
963
1338
  export function removeUndefined (variable) {
964
1339
  // Removes undefined from an array or object
965
1340
  if (Array.isArray(variable)) {
@@ -968,55 +1343,58 @@ export function removeUndefined (variable) {
968
1343
  }
969
1344
  } else {
970
1345
  Object.keys(variable).forEach((key) => {
971
- if (variable[key] === undefined) delete variable[key]
1346
+ if (key in variable && variable[key] === undefined) delete variable[key]
972
1347
  })
973
1348
  }
974
1349
  return variable
975
1350
  }
976
1351
 
977
- export function s3Image (awsUrl, image, size='full', i) {
978
- /**
979
- * Build image URL from image array or object
980
- * @param {string} awsUrl - e.g. 'https://s3.amazonaws.com/...'
981
- * @param {array|object} image - file object/array
982
- * @param {string} <size> - overrides to 'full' when the image sizes are still processing
983
- * @param {integer} <i> - array index
984
- */
1352
+ /**
1353
+ * Build image URL from image array or object
1354
+ * @typedef {{path: string, bucket: string, base64?: string, date?: number}} Image
1355
+ * @param {string} awsUrl - e.g. 'https://s3.amazonaws.com/...'
1356
+ * @param {Image[]|Image} imageOrArray - file object/array
1357
+ * @param {string} [size] - overrides to 'full' when the image sizes are still processing
1358
+ * @param {number} [i] - array index
1359
+ * @returns {string}
1360
+ */
1361
+ export function s3Image (awsUrl, imageOrArray, size='full', i) {
985
1362
  let lambdaDelay = 7000
986
1363
  let usingMilliseconds = true
987
1364
 
988
- if (!isObject(image)) image = (image && image[i]) ? image[i] : null
1365
+ const image = /**@type {Image}*/(Array.isArray(imageOrArray) ? imageOrArray[i||0] : imageOrArray)
989
1366
  if (!image) return ''
990
1367
  // Alway use preview if available
991
1368
  if (image.base64) return image.base64
992
1369
  // Wait a moment before the different sizes are generated by lambda
993
- if (((usingMilliseconds ? image.date : image.date * 1000) + lambdaDelay) > new Date()) size = 'full'
1370
+ if (((usingMilliseconds ? (image.date || 0) : (image.date || 0) * 1000) + lambdaDelay) > new Date().getTime()) size = 'full'
994
1371
  let key = size == 'full' ? image.path : `${size}/${image.path.replace(/^full\/|\.[a-z0-9]{3,4}$/ig, '')}.jpg`
995
1372
  return awsUrl + image.bucket + '/' + key
996
1373
  }
997
1374
 
1375
+ /**
1376
+ * Sanitize and encode all HTML in a user-submitted string
1377
+ * @param {string} string
1378
+ * @returns {string}
1379
+ */
998
1380
  export function sanitizeHTML (string) {
999
- /*
1000
- * Sanitize and encode all HTML in a user-submitted string
1001
- * (c) 2018 Chris Ferdinandi, MIT License, https://gomakethings.com
1002
- * @param {String} str The user-submitted string
1003
- * @return {String} str The sanitized string
1004
- */
1005
1381
  var temp = document.createElement('div')
1006
1382
  temp.textContent = string
1007
1383
  return temp.innerHTML
1008
1384
  }
1009
1385
 
1386
+ /**
1387
+ * Process scrollbar width once.
1388
+ * @param {string} paddingClass - class name to give padding to
1389
+ * @param {string} marginClass - class name to give margin to
1390
+ * @param {number} maxWidth - enclose css in a max-width media query
1391
+ * @returns {number}
1392
+ *
1393
+ */
1010
1394
  export function scrollbar (paddingClass, marginClass, maxWidth) {
1011
- /**
1012
- * Process scrollbar width once.
1013
- * @param {string} paddingClass - class name to give padding to
1014
- * @param {string} marginClass - class name to give margin to
1015
- * @param {number} maxWidth - enclose css in a max-width media query
1016
- * @return width.
1017
- */
1018
- if (scrollbar.width || scrollbar.width === 0) {
1019
- return scrollbar.width
1395
+ if (typeof window === 'undefined') return 0
1396
+ if (scrollbarCache || scrollbarCache === 0) {
1397
+ return scrollbarCache
1020
1398
  }
1021
1399
 
1022
1400
  var outer = document.createElement('div')
@@ -1039,25 +1417,30 @@ export function scrollbar (paddingClass, marginClass, maxWidth) {
1039
1417
  var widthWithScroll = inner.offsetWidth
1040
1418
 
1041
1419
  // Remove divs
1042
- outer.parentNode.removeChild(outer)
1043
- scrollbar.width = widthNoScroll - widthWithScroll
1420
+ if (outer.parentNode) outer.parentNode.removeChild(outer)
1421
+ scrollbarCache = widthNoScroll - widthWithScroll
1044
1422
 
1045
- // Add padding class.
1423
+ // Add padding class. (CHANGED, need to test)
1046
1424
  if (paddingClass || marginClass) {
1047
- document.head.appendChild( // double check this, was jquery
1048
- '<style type="text/css">' +
1425
+ const style = document.createElement('style')
1426
+ style.textContent =
1049
1427
  (maxWidth ? '@media only screen and (max-width: ' + maxWidth + 'px) {' : '') +
1050
- (paddingClass ? paddingClass + ' {padding-right:' + scrollbar.width + 'px}' : '') +
1051
- (marginClass ? marginClass + ' {margin-right:' + scrollbar.width + 'px}' : '') +
1052
- (maxWidth ? '}' : '') +
1053
- '</style>'
1054
- )
1428
+ (paddingClass ? paddingClass + ' {padding-right:' + scrollbarCache + 'px}' : '') +
1429
+ (marginClass ? marginClass + ' {margin-right:' + scrollbarCache + 'px}' : '') +
1430
+ (maxWidth ? '}' : '')
1431
+ document.head.appendChild(style)
1055
1432
  }
1056
1433
 
1057
1434
  // return.
1058
- return scrollbar.width
1435
+ return scrollbarCache
1059
1436
  }
1060
1437
 
1438
+ /**
1439
+ * Convert seconds to time
1440
+ * @param {number} seconds
1441
+ * @param {boolean} [padMinute]
1442
+ * @returns {string}
1443
+ */
1061
1444
  export function secondsToTime (seconds, padMinute) {
1062
1445
  seconds = Math.round(seconds)
1063
1446
  let hours = Math.floor(seconds / (60 * 60))
@@ -1067,79 +1450,67 @@ export function secondsToTime (seconds, padMinute) {
1067
1450
  let secs = Math.ceil(divisor_for_seconds)
1068
1451
  let data = {
1069
1452
  h: (hours + ''),
1070
- m: (minutes + '').padStart(padMinute? 2 : 1, 0),
1453
+ m: (minutes + '').padStart(padMinute ? 2 : 1, '0'),
1071
1454
  s: (secs + ''),
1072
1455
  }
1073
1456
  return data.h + ':' + data.m
1074
1457
  }
1075
1458
 
1459
+ /**
1460
+ * Promise wrapper for setTimeout
1461
+ * @param {function} func
1462
+ * @param {number} milliseconds
1463
+ * @returns {Promise<void>}
1464
+ */
1076
1465
  export function setTimeoutPromise (func, milliseconds) {
1077
1466
  return new Promise(function(resolve) {
1078
1467
  setTimeout(() => resolve(func()), milliseconds)
1079
1468
  })
1080
1469
  }
1081
1470
 
1471
+ /**
1472
+ * Shows a global error
1473
+ * @param {function} setStore
1474
+ * @param {NitroErrorRaw} errs
1475
+ */
1082
1476
  export function showError (setStore, errs) {
1083
- /**
1084
- * Shows a global error
1085
- * @params {function} setStore
1086
- * @params {Array|Error|String|Axios Object} errs
1087
- */
1088
1477
  let detail = getResponseErrors(errs)[0].detail
1089
- setStore((o) => ({ ...o, message: { type: 'error', text: detail } }))
1478
+ setStore((/** @type {{[key: string]: any}} */o) => ({ ...o, message: { type: 'error', text: detail } }))
1090
1479
  }
1091
1480
 
1092
- export function sortByKey (objects, key) {
1093
- return objects.sort(function (a, b) {
1094
- var textA = (a[key] || '').toUpperCase()
1095
- var textB = (b[key] || '').toUpperCase()
1481
+ /**
1482
+ * Sort an array of objects by a key
1483
+ * @param {{[key: string]: any}[]} collection
1484
+ * @param {string} key
1485
+ * @returns {object[]}
1486
+ */
1487
+ export function sortByKey (collection, key) {
1488
+ return collection.sort(function (a, b) {
1489
+ var textA = (key in a ? a[key] : '').toUpperCase()
1490
+ var textB = (key in b ? b[key] : '').toUpperCase()
1096
1491
  return textA < textB ? -1 : textA > textB ? 1 : 0
1097
1492
  })
1098
1493
  }
1099
1494
 
1100
- export function sortFromQuery (req, sortMap, sortByDefault='createdAt') {
1101
- /**
1102
- *
1103
- * Return a mongodb sort pipeline stage using approved `req.query.sort-by|sort` values
1104
- * @param {object} req - requires req.query.sort-by|sort
1105
- * @param {object} sortMap - e.g. { name: ['user.firstName', ..], .. }
1106
- * @param {string} <sortDefault> - e.g. 'createdAt' (default)
1107
- * @return {object} - e.g. { 'user.firstName': -1 }
1108
- * @see used in karpark
1109
- */
1110
- let sort = req.query.sort == 'asc' ? 1 : -1
1111
- let sortBy = req.query['sort-by']
1112
- sortByDefault = sortByDefault || 'createdAt'
1113
-
1114
- // Convert { name: ['user.firstName'] } to { name: { 'user.firstName': 1 }}
1115
- for (let key in sortMap) {
1116
- sortMap[key] = sortMap[key].reduce((o, name) => {
1117
- o[name] = sort
1118
- return o
1119
- }, {})
1120
- }
1121
-
1122
- if (sortMap[sortBy]) return sortMap[sortBy]
1123
- else return { [sortByDefault]: sort }
1124
- }
1125
-
1126
- export function throttle (func, wait, options) {
1127
- /**
1495
+ /**
1128
1496
  * Creates a throttled function that only invokes `func` at most once per every `wait` milliseconds
1129
- * @param {function} func
1130
- * @param {number} <wait=0> - the number of milliseconds to throttle invocations to
1131
- * @param {boolean} <options.leading=true> - invoke on the leading edge of the timeout
1132
- * @param {boolean} <options.trailing=true> - invoke on the trailing edge of the timeout
1133
- * @returns {Function}
1134
- * @example const throttled = util.throttle(updatePosition, 100)
1135
- * @see lodash
1136
- */
1497
+ * @param {function} func
1498
+ * @param {number} [wait=0] - the number of milliseconds to throttle invocations to
1499
+ * @param {{
1500
+ * leading?: boolean, // invoke on the leading edge of the timeout
1501
+ * trailing?: boolean, // invoke on the trailing edge of the timeout
1502
+ * }} [options] - options object
1503
+ * @returns {function}
1504
+ * @example const throttled = util.throttle(updatePosition, 100)
1505
+ * @see lodash
1506
+ */
1507
+ export function throttle (func, wait, options) {
1137
1508
  let leading = true
1138
1509
  let trailing = true
1139
1510
  if (typeof func != 'function') {
1140
1511
  throw new TypeError('Expected a function')
1141
1512
  }
1142
- if (isObject(options)) {
1513
+ if (options) {
1143
1514
  leading = 'leading' in options ? !!options.leading : leading
1144
1515
  trailing = 'trailing' in options ? !!options.trailing : trailing
1145
1516
  }
@@ -1150,18 +1521,80 @@ export function throttle (func, wait, options) {
1150
1521
  })
1151
1522
  }
1152
1523
 
1524
+ /**
1525
+ * Convert a variable to an array, if not already an array.
1526
+ * @template T
1527
+ * @param {T | undefined} variable
1528
+ * @returns {(T extends any[] ? T : T[])}
1529
+ */
1153
1530
  export function toArray (variable) {
1154
- // converts a variable to an array, if not already so
1155
- if (typeof variable === 'undefined') return []
1156
- return Array.isArray(variable) ? variable : [variable]
1531
+ if (variable === undefined) {
1532
+ // TypeScript can’t infer conditional return types from runtime empty array
1533
+ // So we force-cast it to the generic fallback type
1534
+ return /** @type {T extends any[] ? T : T[]} */([])
1535
+ }
1536
+ return /** @type {T extends any[] ? T : T[]} */(Array.isArray(variable) ? variable : [variable])
1157
1537
  }
1158
1538
 
1539
+ /**
1540
+ * Trim a string and replace multiple newlines with double newlines
1541
+ * @param {string} string
1542
+ * @returns {string}
1543
+ */
1159
1544
  export function trim (string) {
1160
1545
  if (!string || !isString(string)) return ''
1161
1546
  return string.trim().replace(/\n\s+\n/g, '\n\n')
1162
1547
  }
1163
1548
 
1549
+ /**
1550
+ * Capitalize the first letter of a string
1551
+ * @param {string} string
1552
+ * @returns {string}
1553
+ */
1164
1554
  export function ucFirst (string) {
1165
1555
  if (!string) return ''
1166
1556
  return string.charAt(0).toUpperCase() + string.slice(1)
1167
- }
1557
+ }
1558
+
1559
+
1560
+
1561
+
1562
+ //--------------------------------
1563
+ /* eslint-disable @typescript-eslint/no-unused-vars */
1564
+
1565
+ // const axios2 = axios()
1566
+ // const addressSchema2 = addressSchema()
1567
+ // const url = buildUrl('https://example.com', { param1: 'value1', param2: 'value2' })
1568
+ // const camelCase2 = camelCase('camelCase')
1569
+ // const camelCaseToTitle2 = camelCaseToTitle('camelCase')
1570
+ // const camelCaseToHypen2 = camelCaseToHypen('camelCase')
1571
+ // const capitalise2 = capitalise()
1572
+ // const currency2 = currency(1234)
1573
+ // const currencyToCents2 = currencyToCents('$12.34')
1574
+ // const date2 = date(1234567890)
1575
+ // const debounce2 = debounce(() => {}, 1000)
1576
+ // const deepCopy2 = deepCopy([])
1577
+ // const deepFind2 = deepFind({})
1578
+ // const deepSave2 = deepSave({})
1579
+ // const each2 = each({})
1580
+ // const fileDownload2 = fileDownload('data', 'filename.txt')
1581
+ // const formatName2 = formatName('John Smith')
1582
+ // const formatSlug2 = formatSlug('John Smith')
1583
+ // const formData2 = formData({})
1584
+ // const fullName2 = fullName({ firstName: 'John', lastName: 'Smith' })
1585
+ // const [firstName, lastName] = fullNameSplit('name')
1586
+ // const countries2 = getCountryOptions({ US: { name: 'United States' }, GB: { name: 'United Kingdom' } })
1587
+ // const currencies2 = getCurrencyOptions({ USD: { name: 'United States Dollar' }, GBP: { name: 'British Pound' } })
1588
+ // const width = getPrefixWidth('£')
1589
+ // const directories2 = getDirectories({ join: (...args) => args.join('/') }, 'pwd')
1590
+ // const stripeClient = getStripeClientPromise('pk_test')
1591
+ // const getResponseErrors2 = getResponseErrors(new Error('test'))
1592
+ // const inArray2 = inArray([1, 2, 3], 's')
1593
+ // const isArray2 = isArray([1, 2, 3])
1594
+
1595
+ // const isObject2 = isObject({})
1596
+
1597
+ // const onChange2 = onChange.call(() => {}, { target: {} })
1598
+
1599
+ // const queryObject2 = queryObject('?page=1&perPage=10')
1600
+ // const queryString2 = queryString({ page: 1, perPage: 10 })