nitro-web 0.0.39 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/app.tsx +18 -11
- package/client/store.ts +3 -1
- package/components/auth/auth.api.js +2 -2
- package/components/auth/signin.tsx +1 -1
- package/components/partials/element/avatar.tsx +1 -2
- package/components/partials/element/dropdown.tsx +1 -2
- package/components/partials/form/field-date.tsx +5 -1
- package/components/partials/form/field.tsx +4 -1
- package/components/partials/styleguide.tsx +2 -2
- package/package.json +6 -2
- package/types/util.d.ts +615 -106
- package/types/util.d.ts.map +1 -1
- package/types.ts +5 -2
- package/util.js +870 -437
package/util.js
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
|
|
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 (!
|
|
45
|
-
|
|
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¶m2=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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
190
|
+
return dateformat(milliseconds, format || 'dS mmmm')
|
|
130
191
|
}
|
|
131
192
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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 (
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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 =
|
|
238
|
-
lastThis = this
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
394
|
+
|
|
395
|
+
return clone
|
|
271
396
|
}
|
|
272
397
|
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
277
|
-
|
|
406
|
+
if (typeof obj !== 'object') return undefined
|
|
407
|
+
|
|
408
|
+
const chunks = path.split('.')
|
|
409
|
+
/** @type {any} */
|
|
278
410
|
let target = obj
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
|
|
411
|
+
|
|
412
|
+
for (const chunk of chunks) {
|
|
413
|
+
if (target === null || target === undefined) return undefined
|
|
414
|
+
target = target[chunk]
|
|
283
415
|
}
|
|
284
|
-
|
|
416
|
+
|
|
417
|
+
return target
|
|
285
418
|
}
|
|
286
419
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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 =
|
|
434
|
+
let target = clone
|
|
435
|
+
|
|
301
436
|
for (let i = 0, l = chunks.length; i < l; i++) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
|
467
|
+
const shallowLen = obj === null ? void 0 : obj['length']
|
|
317
468
|
const isArrayLike = typeof shallowLen == 'number' && shallowLen >= 0
|
|
318
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
381
|
-
* @param {
|
|
382
|
-
* @
|
|
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
|
|
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 =
|
|
402
|
-
cfg.nullsAsUndefineds =
|
|
403
|
-
cfg.booleansAsIntegers =
|
|
404
|
-
cfg.allowEmptyArrays =
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
593
|
+
existingFormData.append(keyPrefix, '')
|
|
412
594
|
}
|
|
413
|
-
} else if (
|
|
595
|
+
} else if (typeof obj === 'boolean') {
|
|
414
596
|
if (cfg.booleansAsIntegers) {
|
|
415
|
-
|
|
597
|
+
existingFormData.append(keyPrefix, obj ? '1' : '0')
|
|
416
598
|
} else {
|
|
417
|
-
|
|
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 =
|
|
423
|
-
serialize(value, cfg,
|
|
604
|
+
const key = keyPrefix + '[' + (cfg.indices ? index : '') + ']'
|
|
605
|
+
serialize(value, cfg, existingFormData, key)
|
|
424
606
|
})
|
|
425
607
|
} else if (cfg.allowEmptyArrays) {
|
|
426
|
-
|
|
608
|
+
existingFormData.append(keyPrefix + '[]', '')
|
|
427
609
|
}
|
|
428
|
-
} else if (
|
|
429
|
-
|
|
430
|
-
} else if (
|
|
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 =
|
|
439
|
-
serialize(value, cfg,
|
|
620
|
+
const key = keyPrefix ? keyPrefix + '[' + prop + ']' : prop
|
|
621
|
+
serialize(value, cfg, existingFormData, key)
|
|
440
622
|
})
|
|
441
623
|
} else {
|
|
442
|
-
|
|
624
|
+
existingFormData.append(keyPrefix, /** @type {any} */(obj))
|
|
443
625
|
}
|
|
444
|
-
return
|
|
626
|
+
return existingFormData
|
|
445
627
|
}
|
|
446
|
-
return serialize(obj, cfg,
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
|
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
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
763
|
+
return [{ title: 'error', detail: errs }]
|
|
764
|
+
|
|
546
765
|
// Default error message
|
|
547
766
|
} else {
|
|
548
767
|
console.info('getResponseErrors(): ', errs)
|
|
549
|
-
|
|
768
|
+
return [{ title: 'error', detail: 'Oops there was an error' }]
|
|
550
769
|
}
|
|
551
|
-
return errors
|
|
552
770
|
}
|
|
553
771
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (typeof value == 'undefined') return
|
|
563
|
-
|
|
564
|
-
|
|
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 &&
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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 {
|
|
784
|
-
*
|
|
785
|
-
*
|
|
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
|
-
* @
|
|
788
|
-
* @
|
|
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 (
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
let value
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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 (
|
|
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
|
-
|
|
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]] =
|
|
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
|
-
|
|
834
|
-
|
|
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(
|
|
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
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
if (
|
|
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))
|
|
1208
|
+
if (obj.hasOwnProperty(key2) && key2.match(key)) output[key2] = obj[key2]
|
|
854
1209
|
}
|
|
855
1210
|
}
|
|
856
1211
|
}
|
|
857
|
-
return
|
|
1212
|
+
return output
|
|
858
1213
|
}
|
|
859
1214
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
|
|
870
|
-
if (
|
|
871
|
-
if (
|
|
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
|
-
|
|
1234
|
+
const searchParams = searchString.slice(1).split('&')
|
|
875
1235
|
|
|
876
1236
|
// Loop through each query paramter
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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] =
|
|
1245
|
+
obj[key] = decodeURIComponent(partArr[1])
|
|
888
1246
|
}
|
|
889
1247
|
})
|
|
890
1248
|
|
|
891
|
-
|
|
892
|
-
return
|
|
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
|
-
|
|
898
|
-
|
|
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'
|
|
904
|
-
|
|
1264
|
+
if (typeof obj[key] == 'undefined' || !obj[key]) continue
|
|
1265
|
+
newObj[key] = obj[key] + ''
|
|
905
1266
|
}
|
|
906
1267
|
}
|
|
907
|
-
let qs = new URLSearchParams(
|
|
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 =
|
|
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
|
-
//
|
|
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
|
-
|
|
943
|
-
|
|
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
|
-
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
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.
|
|
1048
|
-
|
|
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:' +
|
|
1051
|
-
(marginClass ? marginClass + ' {margin-right:' +
|
|
1052
|
-
(maxWidth ? '}' : '')
|
|
1053
|
-
|
|
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
|
|
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
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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 (
|
|
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
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
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 })
|