nitro-web 0.0.1

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.
Files changed (152) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintrc.json +86 -0
  3. package/_example/.env-example +16 -0
  4. package/_example/client/config.ts +5 -0
  5. package/_example/client/css/index.css +35 -0
  6. package/_example/client/fonts/Roboto-Bold.ttf +0 -0
  7. package/_example/client/fonts/Roboto-BoldItalic.ttf +0 -0
  8. package/_example/client/fonts/Roboto-Italic.ttf +0 -0
  9. package/_example/client/fonts/Roboto-Medium.ttf +0 -0
  10. package/_example/client/fonts/Roboto-MediumItalic.ttf +0 -0
  11. package/_example/client/fonts/Roboto-Regular.ttf +0 -0
  12. package/_example/client/fonts/inter-v13-latin-300.woff2 +0 -0
  13. package/_example/client/fonts/inter-v13-latin-500.woff2 +0 -0
  14. package/_example/client/fonts/inter-v13-latin-600.woff2 +0 -0
  15. package/_example/client/fonts/inter-v13-latin-700.woff2 +0 -0
  16. package/_example/client/fonts/inter-v13-latin-800.woff2 +0 -0
  17. package/_example/client/fonts/inter-v13-latin-900.woff2 +0 -0
  18. package/_example/client/fonts/inter-v13-latin-regular.woff2 +0 -0
  19. package/_example/client/imgs/android-chrome-512x512.png +0 -0
  20. package/_example/client/imgs/favicon.png +0 -0
  21. package/_example/client/imgs/icons/calendar.svg +3 -0
  22. package/_example/client/imgs/icons/email.svg +6 -0
  23. package/_example/client/imgs/icons/eye-open.svg +4 -0
  24. package/_example/client/imgs/icons/eye.svg +5 -0
  25. package/_example/client/imgs/icons/filter.svg +7 -0
  26. package/_example/client/imgs/icons/left-circle.svg +3 -0
  27. package/_example/client/imgs/icons/left.svg +3 -0
  28. package/_example/client/imgs/icons/line-options.svg +5 -0
  29. package/_example/client/imgs/icons/line.svg +3 -0
  30. package/_example/client/imgs/icons/person.svg +7 -0
  31. package/_example/client/imgs/icons/plus-circle.svg +5 -0
  32. package/_example/client/imgs/icons/plus.svg +5 -0
  33. package/_example/client/imgs/icons/right-circle.svg +3 -0
  34. package/_example/client/imgs/icons/right.svg +3 -0
  35. package/_example/client/imgs/icons/search.svg +3 -0
  36. package/_example/client/imgs/icons/shield.svg +6 -0
  37. package/_example/client/imgs/icons/tick-circle-solid.svg +8 -0
  38. package/_example/client/imgs/icons/tick-circle.svg +6 -0
  39. package/_example/client/imgs/icons/tick.svg +5 -0
  40. package/_example/client/imgs/icons/up2-small.svg +4 -0
  41. package/_example/client/imgs/icons/up2.svg +4 -0
  42. package/_example/client/imgs/icons/updown.svg +6 -0
  43. package/_example/client/imgs/icons/v-big-dark.svg +3 -0
  44. package/_example/client/imgs/icons/v-dark.svg +3 -0
  45. package/_example/client/imgs/icons/v.svg +3 -0
  46. package/_example/client/imgs/icons/v2-active.svg +6 -0
  47. package/_example/client/imgs/icons/x1.svg +4 -0
  48. package/_example/client/imgs/logo/logo-white.svg +20 -0
  49. package/_example/client/imgs/logo/logo.svg +20 -0
  50. package/_example/client/imgs/no-image.jpg +0 -0
  51. package/_example/client/imgs/user.jpg +0 -0
  52. package/_example/client/index.html +12 -0
  53. package/_example/client/index.ts +47 -0
  54. package/_example/components/auth.api.js +1 -0
  55. package/_example/components/index.tsx +225 -0
  56. package/_example/components/partials/layouts.tsx +5 -0
  57. package/_example/components/settings.api.js +1 -0
  58. package/_example/server/config.js +120 -0
  59. package/_example/server/email/welcome.html +27 -0
  60. package/_example/server/index.js +32 -0
  61. package/_example/tailwind.config.js +84 -0
  62. package/_example/tsconfig.json +32 -0
  63. package/_example/types.d.ts +7 -0
  64. package/_example/webpack.config.js +4 -0
  65. package/client/app.js +300 -0
  66. package/client/css/components.css +84 -0
  67. package/client/css/fonts.css +67 -0
  68. package/client/imgs/icons/calendar.svg +3 -0
  69. package/client/imgs/icons/email.svg +6 -0
  70. package/client/imgs/icons/eye-open.svg +4 -0
  71. package/client/imgs/icons/eye.svg +5 -0
  72. package/client/imgs/icons/filter.svg +7 -0
  73. package/client/imgs/icons/left-circle.svg +3 -0
  74. package/client/imgs/icons/left.svg +3 -0
  75. package/client/imgs/icons/line-options.svg +5 -0
  76. package/client/imgs/icons/line.svg +3 -0
  77. package/client/imgs/icons/person.svg +7 -0
  78. package/client/imgs/icons/plus-circle.svg +5 -0
  79. package/client/imgs/icons/plus.svg +5 -0
  80. package/client/imgs/icons/right-circle.svg +3 -0
  81. package/client/imgs/icons/right.svg +3 -0
  82. package/client/imgs/icons/search.svg +3 -0
  83. package/client/imgs/icons/shield.svg +6 -0
  84. package/client/imgs/icons/tick-circle-solid.svg +8 -0
  85. package/client/imgs/icons/tick-circle.svg +6 -0
  86. package/client/imgs/icons/tick.svg +5 -0
  87. package/client/imgs/icons/up2-small.svg +4 -0
  88. package/client/imgs/icons/up2.svg +4 -0
  89. package/client/imgs/icons/updown.svg +6 -0
  90. package/client/imgs/icons/v-big-dark.svg +3 -0
  91. package/client/imgs/icons/v-dark.svg +3 -0
  92. package/client/imgs/icons/v.svg +3 -0
  93. package/client/imgs/icons/v2-active.svg +6 -0
  94. package/client/imgs/icons/x1.svg +4 -0
  95. package/client.js +42 -0
  96. package/components/auth/auth.api.js +419 -0
  97. package/components/auth/reset.jsx +88 -0
  98. package/components/auth/signin.jsx +74 -0
  99. package/components/auth/signup.jsx +62 -0
  100. package/components/billing/stripe.api.js +267 -0
  101. package/components/partials/element/accordion.jsx +82 -0
  102. package/components/partials/element/avatar.jsx +28 -0
  103. package/components/partials/element/button.jsx +66 -0
  104. package/components/partials/element/dropdown.jsx +185 -0
  105. package/components/partials/element/initials.jsx +56 -0
  106. package/components/partials/element/message.jsx +124 -0
  107. package/components/partials/element/modal.jsx +229 -0
  108. package/components/partials/element/sidebar.jsx +166 -0
  109. package/components/partials/element/tooltip.jsx +146 -0
  110. package/components/partials/element/topbar.jsx +25 -0
  111. package/components/partials/form/checkbox.jsx +74 -0
  112. package/components/partials/form/drop-handler.jsx +62 -0
  113. package/components/partials/form/drop.jsx +125 -0
  114. package/components/partials/form/form-error.jsx +21 -0
  115. package/components/partials/form/input-color.jsx +77 -0
  116. package/components/partials/form/input-currency.jsx +133 -0
  117. package/components/partials/form/input-date.jsx +223 -0
  118. package/components/partials/form/input.jsx +131 -0
  119. package/components/partials/form/location.jsx +212 -0
  120. package/components/partials/form/select.jsx +369 -0
  121. package/components/partials/form/toggle.jsx +46 -0
  122. package/components/partials/is-first-render.js +15 -0
  123. package/components/partials/layout/layout1.jsx +32 -0
  124. package/components/partials/layout/layout2.jsx +47 -0
  125. package/components/partials/not-found.jsx +7 -0
  126. package/components/partials/styleguide.jsx +252 -0
  127. package/components/settings/settings-account.jsx +143 -0
  128. package/components/settings/settings-business.jsx +121 -0
  129. package/components/settings/settings-team--member.jsx +108 -0
  130. package/components/settings/settings-team.jsx +76 -0
  131. package/components/settings/settings.api.js +54 -0
  132. package/package.json +175 -0
  133. package/readme.md +43 -0
  134. package/server/email/index.js +192 -0
  135. package/server/email/partials/email.css +153 -0
  136. package/server/email/partials/layout1.swig +92 -0
  137. package/server/email/partials/line.swig +8 -0
  138. package/server/email/partials/vert-10.swig +8 -0
  139. package/server/email/partials/vert-15.swig +8 -0
  140. package/server/email/partials/vert-20.swig +8 -0
  141. package/server/email/partials/vert-25.swig +8 -0
  142. package/server/email/partials/vert-30.swig +8 -0
  143. package/server/email/partials/vert-35.swig +8 -0
  144. package/server/email/partials/vert-50.swig +8 -0
  145. package/server/email/reset-password.html +21 -0
  146. package/server/email/welcome.html +21 -0
  147. package/server/models/company.js +76 -0
  148. package/server/models/user.js +45 -0
  149. package/server/router.js +355 -0
  150. package/server.js +20 -0
  151. package/util.js +1145 -0
  152. package/webpack.config.js +302 -0
package/util.js ADDED
@@ -0,0 +1,1145 @@
1
+ import _axios from '@hokify/axios'
2
+ import axiosRetry from 'axios-retry'
3
+ import dateformat from 'dateformat'
4
+ import { loadStripe } from '@stripe/stripe-js/pure.js' // pure removes ping
5
+ export const dateFormat = dateformat
6
+
7
+ export function addressSchema () {
8
+ // Google autocomplete should return the following object
9
+ function arrayWithSchema (array, schema) {
10
+ array.schema = schema
11
+ return array
12
+ }
13
+ return {
14
+ city: { type: 'string' },
15
+ country: { type: 'string', default: 'New Zealand' },
16
+ full: { type: 'string', index: 'text' },
17
+ line1: { type: 'string' },
18
+ line2: { type: 'string' },
19
+ number: { type: 'string' },
20
+ postcode: { type: 'string' },
21
+ suburb: { type: 'string' },
22
+ unit: { type: 'string' },
23
+ // Google places viewport
24
+ area: {
25
+ bottomLeft: [{ type: 'number' }], // lng, lat
26
+ topRight: [{ type: 'number' }], // lng, lat
27
+ },
28
+ location: {
29
+ index: '2dsphere',
30
+ type: { type: 'string', default: 'Point' },
31
+ coordinates: arrayWithSchema(
32
+ [{ type: 'number' }], // lng, lat
33
+ { minLength: 2 }
34
+ ),
35
+ },
36
+ }
37
+ }
38
+
39
+ export function axios () {
40
+ // Remove mobile specific protocol and subdomain
41
+ const clientOrigin = window.document.location.origin.replace(/^(capacitor|https):\/\/(mobile\.)?/, 'https://')
42
+ // axios configurations on the client
43
+ if (!axios._axiosNonce && typeof window !== 'undefined') {
44
+ axios._axiosNonce = true
45
+ _axios.defaults.baseURL = clientOrigin
46
+ _axios.defaults.headers.desktop = true
47
+ _axios.defaults.withCredentials = true
48
+ _axios.defaults.timeout = 60000
49
+ axiosRetry(_axios, { retries: 0, retryDelay: (/*i*/) => 300 })
50
+ }
51
+ return _axios
52
+ }
53
+
54
+ export function buildUrl (url, parameters) {
55
+ /**
56
+ * Builds the url with params
57
+ * @param {string} url - String url
58
+ * @param {object} parameters - Key value parameters
59
+ */
60
+ const params = Object.keys(parameters).map((p) => `${encodeURIComponent(p)}=${encodeURIComponent(parameters[p])}`)
61
+ return [url, params.join('&')].join('?')
62
+ }
63
+
64
+ export function camelCase (str, capitaliseFirst, allowNumber) {
65
+ let regex = (capitaliseFirst ? '(?:^[a-z0-9]|' : '(?:') + '[-]+[a-z0-9])'
66
+ return str
67
+ .toString()
68
+ .toLowerCase()
69
+ .replace(allowNumber ? /^[^a-zA-Z0-9]+/ : /^[^a-zA-Z]+/, '') // Allow only letters at start.
70
+ .replace(/(_|\s|\()+/g, '-') // Underscores, spaces, and curly braces to -
71
+ .replace(/-+/g, '-') // Replace multiple - with single -
72
+ .replace(/[^a-zA-Z0-9-_]+/g, '') // Remove bad characters.
73
+ .replace(/-+$/, '') // Remove trailing -
74
+ .replace(new RegExp(regex, 'g'), function(match) {
75
+ return match.toUpperCase().replace(/[-]+/g, '')
76
+ })
77
+ }
78
+
79
+ export function camelCaseToTitle (str, captialiseFirstOnly) {
80
+ str = str.replace(/([A-Z]+)/g, ' $1').trim()
81
+ if (captialiseFirstOnly) str = str.toLowerCase()
82
+ return ucFirst(str)
83
+ }
84
+
85
+ export function camelCaseToHypen (str) {
86
+ return str.replace(/[A-Z]|[0-9]+/g, m => '-' + m.toLowerCase())
87
+ }
88
+
89
+ export function capitalise (str) {
90
+ return (str||'').replace(/(?:^|\s)\S/g, (a) => a.toUpperCase())
91
+ }
92
+
93
+ export function currency (cents, decimals=2, decimalsMinimum) {
94
+ // Returns a formated currency string
95
+ const num = Number(cents / 100)
96
+ if (!isNumber(num)) return '$0.00'
97
+ return '$' + num.toLocaleString(undefined, {
98
+ minimumFractionDigits: typeof decimalsMinimum == 'undefined' ? decimals : decimalsMinimum,
99
+ maximumFractionDigits: decimals,
100
+ })
101
+ }
102
+
103
+ export function currencyToCents (currency) {
104
+ // Converts '$1,234.00' to '1234.00', then to '123400'
105
+ let currencyString = Number(currency.replace(/[^0-9.]/g, '')).toFixed(2)
106
+ return currencyString.replace(/\./g, '')
107
+ }
108
+
109
+ export function date (date, format, timezone) {
110
+ /**
111
+ * Returns a formatted date
112
+ * @param {number|Date} date - number can be in seconds or milliseconds (UTC)
113
+ * @param {string} format - e.g. "dd mmmm yy" (https://github.com/felixge/node-dateformat#mask-options)
114
+ * @param {string} timezone - convert a UTC date to a particular timezone.
115
+ *
116
+ * Note on the timezone conversion:
117
+ * Timezone conversion relies on parsing the toLocaleString result, e.g. 4/10/2012, 5:10:30 PM.
118
+ * A older browser may not accept en-US formatted date string to its Date constructor, and it may
119
+ * return unexpected result (it may ignore daylight saving).
120
+ */
121
+ if (!date || (!isNumber(date) && !isDate(date))) return 'Date?'
122
+ else if (isNumber(date) && date < 9999999999) var milliseconds = date * 1000
123
+ else if (isObject(date)) milliseconds = date.getTime()
124
+ else milliseconds = date
125
+ if (timezone) {
126
+ milliseconds = new Date(new Date(milliseconds).toLocaleString('en-US', { timeZone: timezone })).getTime()
127
+ }
128
+ return dateFormat(milliseconds, format || 'dS mmmm')
129
+ }
130
+
131
+ export function debounce (func, wait, options) {
132
+ /**
133
+ * Creates a debounced function that delays invoking `func` until after `wait`
134
+ * milliseconds have elapsed since the last time the debounced function was
135
+ * invoked.
136
+ * @param {function} func
137
+ * @param {number} <wait=0> - number of milliseconds to delay
138
+ * @param {boolean} <options.leading=false> - invoke on the leading edge of the timeout
139
+ * @param {number} <options.maxWait> - maximum time `func` is allowed to be delayed before it's invoked
140
+ * @param {boolean} <options.trailing=true> - invoke on the trailing edge of the timeout
141
+ * @returns {Function}
142
+ * @see lodash
143
+ */
144
+ var lastArgs,
145
+ lastThis,
146
+ maxWait,
147
+ result,
148
+ timerId,
149
+ lastCallTime,
150
+ lastInvokeTime = 0,
151
+ leading = false,
152
+ maxing = false,
153
+ trailing = true
154
+
155
+ wait = typeof wait == 'number'? wait : 0
156
+ if (isObject(options)) {
157
+ leading = !!options.leading
158
+ maxing = 'maxWait' in options
159
+ maxWait = maxing ? Math.max(typeof options.maxWait == 'number'? options.maxWait : 0, wait) : maxWait
160
+ trailing = 'trailing' in options ? !!options.trailing : trailing
161
+ }
162
+
163
+ function invokeFunc(time) {
164
+ var args = lastArgs
165
+ var thisArg = lastThis
166
+ lastArgs = lastThis = undefined
167
+ lastInvokeTime = time
168
+ result = func.apply(thisArg, args)
169
+ return result
170
+ }
171
+
172
+ function leadingEdge(time) {
173
+ // Reset any `maxWait` timer.
174
+ lastInvokeTime = time
175
+ // Start the timer for the trailing edge.
176
+ timerId = setTimeout(timerExpired, wait)
177
+ // Invoke the leading edge.
178
+ return leading ? invokeFunc(time) : result
179
+ }
180
+
181
+ function remainingWait(time) {
182
+ var timeSinceLastCall = time - lastCallTime,
183
+ timeSinceLastInvoke = time - lastInvokeTime,
184
+ timeWaiting = wait - timeSinceLastCall
185
+ return maxing
186
+ ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
187
+ : timeWaiting
188
+ }
189
+
190
+ function shouldInvoke(time) {
191
+ var timeSinceLastCall = time - lastCallTime,
192
+ timeSinceLastInvoke = time - lastInvokeTime
193
+ // Either this is the first call, activity has stopped and we're at the
194
+ // trailing edge, the system time has gone backwards and we're treating
195
+ // it as the trailing edge, or we've hit the `maxWait` limit.
196
+ return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
197
+ (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
198
+ }
199
+
200
+ function timerExpired() {
201
+ var time = Date.now()
202
+ if (shouldInvoke(time)) {
203
+ return trailingEdge(time)
204
+ }
205
+ // Restart the timer.
206
+ timerId = setTimeout(timerExpired, remainingWait(time))
207
+ }
208
+
209
+ function trailingEdge(time) {
210
+ timerId = undefined
211
+ // Only invoke if we have `lastArgs` which means `func` has been
212
+ // debounced at least once.
213
+ if (trailing && lastArgs) {
214
+ return invokeFunc(time)
215
+ }
216
+ lastArgs = lastThis = undefined
217
+ return result
218
+ }
219
+
220
+ function cancel() {
221
+ if (timerId !== undefined) {
222
+ clearTimeout(timerId)
223
+ }
224
+ lastInvokeTime = 0
225
+ lastArgs = lastCallTime = lastThis = timerId = undefined
226
+ }
227
+
228
+ function flush() {
229
+ return timerId === undefined ? result : trailingEdge(Date.now())
230
+ }
231
+
232
+ function debounced() {
233
+ var time = Date.now(),
234
+ isInvoking = shouldInvoke(time)
235
+
236
+ lastArgs = arguments
237
+ lastThis = this // eslint-disable-line
238
+ lastCallTime = time
239
+
240
+ if (isInvoking) {
241
+ if (timerId === undefined) {
242
+ return leadingEdge(lastCallTime)
243
+ }
244
+ if (maxing) {
245
+ // Handle invocations in a tight loop.
246
+ clearTimeout(timerId)
247
+ timerId = setTimeout(timerExpired, wait)
248
+ return invokeFunc(lastCallTime)
249
+ }
250
+ }
251
+ if (timerId === undefined) {
252
+ timerId = setTimeout(timerExpired, wait)
253
+ }
254
+ return result
255
+ }
256
+ debounced.cancel = cancel
257
+ debounced.flush = flush
258
+ return debounced
259
+ }
260
+
261
+ export function deepCopy (obj) {
262
+ // Deep clones an object
263
+ if (!obj) return obj
264
+ let obj2 = Array.isArray(obj) ? [] : {}
265
+ for (let key in obj) {
266
+ let v = obj[key]
267
+ obj2[key] = typeof v === 'object' && !isHex24(v) ? deepCopy(v) : v
268
+ }
269
+ return obj2
270
+ }
271
+
272
+ export function deepFind (obj, path) {
273
+ // Returns a nested value from a path URI e.g. owner.houses.0.color
274
+ if (!obj) return undefined
275
+ let last
276
+ let chunks = (path || '').split('.')
277
+ let target = obj
278
+ for (let i = 0, l = chunks.length; i < l; i++) {
279
+ last = l === i + 1
280
+ if (!last && !target[chunks[i]]) break
281
+ else target = target[chunks[i]]
282
+ }
283
+ return last ? target : undefined
284
+ }
285
+
286
+ export function deepSave (obj, path, value) {
287
+ /**
288
+ * Save a deeply nested value without mutating original object
289
+ * @param {object} obj
290
+ * @param {string} path
291
+ * @param {value|function(current-value) value - pass a function to access the current value
292
+ * @return new object
293
+ */
294
+ if (isArray(obj)) obj = [...obj]
295
+ else if (isObject(obj)) obj = {...obj}
296
+ else return undefined
297
+
298
+ let chunks = (path || '').split('.')
299
+ let target = obj
300
+ for (let i = 0, l = chunks.length; i < l; i++) {
301
+ if (l === i + 1) { // Last
302
+ target[chunks[i]] = isFunction(value) ? value(target[chunks[i]]) : value
303
+ // console.log(target)
304
+ } else {
305
+ let isArray = chunks[i + 1].match(/^[0-9]+$/)
306
+ let parentCopy = isArray ? [...(target[chunks[i]] || [])] : { ...(target[chunks[i]] || {}) }
307
+ target = target[chunks[i]] = parentCopy
308
+ }
309
+ }
310
+ return obj
311
+ }
312
+
313
+ export function each (obj, iteratee, context) {
314
+ // Similar to the underscore.each method
315
+ const shallowLen = obj == null ? void 0 : obj['length']
316
+ const isArrayLike = typeof shallowLen == 'number' && shallowLen >= 0
317
+ if (isArrayLike) {
318
+ for (let i = 0, l = obj.length; i < l; i++) {
319
+ iteratee.call(context || null, obj[i], i, obj)
320
+ }
321
+ } else {
322
+ for (let key in obj) {
323
+ if (!obj.hasOwnProperty(key)) continue
324
+ iteratee.call(context || null, obj[key], key, obj)
325
+ }
326
+ }
327
+ return obj
328
+ }
329
+
330
+ export function fileDownload (data, filename, mime, bom) {
331
+ // @link https://github.com/kennethjiang/js-file-download
332
+ let blobData = (typeof bom !== 'undefined') ? [bom, data] : [data]
333
+ let blob = new Blob(blobData, {type: mime || 'application/octet-stream'})
334
+
335
+ if (typeof window.navigator.msSaveBlob !== 'undefined') {
336
+ window.navigator.msSaveBlob(blob, filename)
337
+ } else {
338
+ let blobURL = (window.URL && window.URL.createObjectURL)
339
+ ? window.URL.createObjectURL(blob)
340
+ : window.webkitURL.createObjectURL(blob)
341
+ let tempLink = document.createElement('a')
342
+ tempLink.style.display = 'none'
343
+ tempLink.href = blobURL
344
+ tempLink.setAttribute('download', filename)
345
+ if (typeof tempLink.download === 'undefined') {
346
+ tempLink.setAttribute('target', '_blank')
347
+ }
348
+ document.body.appendChild(tempLink)
349
+ tempLink.click()
350
+
351
+ // Fixes "webkit blob resource error 1"
352
+ setTimeout(function() {
353
+ document.body.removeChild(tempLink)
354
+ window.URL.revokeObjectURL(blobURL)
355
+ }, 200)
356
+ }
357
+ }
358
+
359
+ export function formatName (string, ignoreHyphen) {
360
+ return ignoreHyphen
361
+ ? ucFirst(string.toString().trim())
362
+ : ucFirst(string.toString().trim().replace('-', ' '))
363
+ }
364
+
365
+ export function formatSlug (string) {
366
+ return string
367
+ .toString()
368
+ .toLowerCase()
369
+ .replace(/^[^a-zA-Z]+/, '') // Allow only letters at start.
370
+ .replace(/\s+/g, '-') // Spaces to -
371
+ .replace(/[^a-zA-Z0-9-_]+/g, '') // Remove bad characters.
372
+ .replace(/-+/g, '-') // Replace multiple - with single -
373
+ .replace(/-+$/, '') // Remove trailing -
374
+ }
375
+
376
+ export function formData (obj, cfg, fd, pre) {
377
+ /**
378
+ * Serializes objects to FormData instances
379
+ * @param {object} obj
380
+ * @param {object} cfg - config, e.g. { allowEmptyArrays: true, indices: true }
381
+ * @link https://github.com/therealparmesh/object-to-formdata
382
+ */
383
+ const isUndefined = (value) => value === undefined
384
+ const isNull = (value) => value === null
385
+ const isBoolean = (value) => typeof value === 'boolean'
386
+ const isObject = (value) => value === Object(value)
387
+ const isArray = (value) => Array.isArray(value)
388
+ const isDate = (value) => value instanceof Date
389
+
390
+ const isBlob = (value) =>
391
+ value && typeof value.size === 'number' && typeof value.type === 'string' && typeof value.slice === 'function'
392
+
393
+ const isFile = (value) =>
394
+ isBlob(value) &&
395
+ typeof value.name === 'string' &&
396
+ (typeof value.lastModifiedDate === 'object' || typeof value.lastModified === 'number')
397
+
398
+ const serialize = (obj, cfg, fd, pre) => {
399
+ cfg = cfg || {}
400
+ cfg.indices = isUndefined(cfg.indices) ? false : cfg.indices
401
+ cfg.nullsAsUndefineds = isUndefined(cfg.nullsAsUndefineds) ? false : cfg.nullsAsUndefineds
402
+ cfg.booleansAsIntegers = isUndefined(cfg.booleansAsIntegers) ? false : cfg.booleansAsIntegers
403
+ cfg.allowEmptyArrays = isUndefined(cfg.allowEmptyArrays) ? false : cfg.allowEmptyArrays
404
+ fd = fd || new FormData()
405
+
406
+ if (isUndefined(obj)) {
407
+ return fd
408
+ } else if (isNull(obj)) {
409
+ if (!cfg.nullsAsUndefineds) {
410
+ fd.append(pre, '')
411
+ }
412
+ } else if (isBoolean(obj)) {
413
+ if (cfg.booleansAsIntegers) {
414
+ fd.append(pre, obj ? 1 : 0)
415
+ } else {
416
+ fd.append(pre, obj)
417
+ }
418
+ } else if (isArray(obj)) {
419
+ if (obj.length) {
420
+ obj.forEach((value, index) => {
421
+ const key = pre + '[' + (cfg.indices ? index : '') + ']'
422
+ serialize(value, cfg, fd, key)
423
+ })
424
+ } else if (cfg.allowEmptyArrays) {
425
+ fd.append(pre + '[]', '')
426
+ }
427
+ } else if (isDate(obj)) {
428
+ fd.append(pre, obj.toISOString())
429
+ } else if (isObject(obj) && !isFile(obj) && !isBlob(obj)) {
430
+ Object.keys(obj).forEach((prop) => {
431
+ const value = obj[prop]
432
+ if (isArray(value)) {
433
+ while (prop.length > 2 && prop.lastIndexOf('[]') === prop.length - 2) {
434
+ prop = prop.substring(0, prop.length - 2)
435
+ }
436
+ }
437
+ const key = pre ? pre + '[' + prop + ']' : prop
438
+ serialize(value, cfg, fd, key)
439
+ })
440
+ } else {
441
+ fd.append(pre, obj)
442
+ }
443
+ return fd
444
+ }
445
+ return serialize(obj, cfg, fd, pre)
446
+ }
447
+
448
+ export function fullName (object) {
449
+ // Returns full name
450
+ return ucFirst(object.firstName) + ' ' + ucFirst(object.lastName)
451
+ }
452
+
453
+ export function fullNameSplit (string) {
454
+ // Returns [firstName, lastName]
455
+ string = string.trim().replace(/\s+/, ' ')
456
+ if (string.match(/\s/)) {
457
+ return [string.substring(0, string.lastIndexOf(' ')), string.substring(string.lastIndexOf(' ') + 1)]
458
+ } else {
459
+ return [string, '']
460
+ }
461
+ }
462
+
463
+ export function getCountryOptions (countries) {
464
+ const output = []
465
+ for (const iso in countries) {
466
+ const name = countries[iso].name
467
+ output.push({ value: iso, label: name, flag: iso.toUpperCase() })
468
+ }
469
+ return output
470
+ }
471
+
472
+ export function getCurrencyOptions (currencies) {
473
+ const output = []
474
+ for (const iso in currencies) {
475
+ const name = currencies[iso].name
476
+ output.push({ value: iso, label: name })
477
+ }
478
+ return output
479
+ }
480
+
481
+ export function getCurrencyPrefixWidth (prefix, paddingRight=0) {
482
+ if (!prefix) return
483
+ const span = document.createElement('span')
484
+ span.classList.add('input-prefix')
485
+ span.style.visibility = 'hidden'
486
+ span.textContent = prefix
487
+ document.body.appendChild(span)
488
+ const width = span.offsetWidth + paddingRight
489
+ document.body.removeChild(span)
490
+ return width
491
+ }
492
+
493
+ export function getLink (obj, query) {
494
+ /**
495
+ * @param {object} obj - new query object
496
+ * @param {object} <query> - current query object, window.location.search used otherwise
497
+ * @return {string}
498
+ */
499
+ let newQueryObj = {...(query||queryObject(window.location.search))}
500
+ for (let key in obj) {
501
+ if (!obj[key]) delete newQueryObj[key]
502
+ else newQueryObj[key] = obj[key]
503
+ }
504
+ return queryString(newQueryObj) || '?'
505
+ }
506
+
507
+ export function getStripeClientPromise (stripePublishableKey) {
508
+ return global.stripeClientPromise || (global.stripeClientPromise = loadStripe(stripePublishableKey))
509
+ }
510
+
511
+ export function getResponseErrors (errs) {
512
+ // Axios response errors
513
+ if (errs.response && errs.response.data && errs.response.data.errors) {
514
+ var errors = errs.response.data.errors
515
+ // Axios response error
516
+ } else if (errs.response && errs.response.data && errs.response.data.error) {
517
+ errors = [{ title: errs.response.data.error, detail: errs.response.data.error_description }]
518
+ // Array
519
+ } else if (isArray(errs)) {
520
+ errors = errs
521
+ // Error object
522
+ } else if (errs.toJSON) {
523
+ errors = [{ title: 'error', detail: errs.toJSON().message }]
524
+ // String
525
+ } else if (typeof errs === 'string') {
526
+ errors = [{ title: 'error', detail: errs }]
527
+ // Default error message
528
+ } else {
529
+ console.info('getResponseErrors(): ', errs)
530
+ errors = [{ title: 'error', detail: 'Oops there was an error' }]
531
+ }
532
+ return errors
533
+ }
534
+
535
+ export function inArray (array, key, value) {
536
+ /**
537
+ * Property match inside an array of objects
538
+ * (For a string/number value check just use [].includes(x))
539
+ * @param {string} <key> - optional to match across on a colleciton of objects
540
+ * @param {any} value
541
+ */
542
+ if (!array || typeof key == 'undefined') return false
543
+ if (typeof value == 'undefined') return array.includes(key)
544
+ for (let i = array.length; i--; ) {
545
+ if (array[i] && array[i].hasOwnProperty(key) && array[i][key] == value) return array[i]
546
+ }
547
+ return false
548
+ }
549
+
550
+ export function isArray (variable) {
551
+ return Array.isArray(variable)
552
+ }
553
+
554
+ export function isDate (variable) {
555
+ return variable && typeof variable.getMonth === 'function'
556
+ }
557
+
558
+ export function isDefined (variable) {
559
+ return typeof variable !== 'undefined'
560
+ }
561
+
562
+ export function isEmail (email) {
563
+ // eslint-disable-next-line
564
+ 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,}))$/
565
+ return re.test(String(email).toLowerCase())
566
+ }
567
+
568
+ export function isEmpty (obj, truthyValuesOnly) {
569
+ // note req.files doesn't have a hasOwnProperty method
570
+ if (obj === null || typeof obj === 'undefined') return true
571
+ for (let prop in obj) {
572
+ if (obj[prop] || (!truthyValuesOnly && obj.hasOwnProperty && obj.hasOwnProperty(prop))) return false
573
+ }
574
+ return true
575
+ }
576
+
577
+ export function isFunction (variable) {
578
+ return typeof variable === 'function' ? true : false
579
+ }
580
+
581
+ export function isHex24 (value) {
582
+ // Fast function to check if the length is exactly 24 and all characters are valid hexadecimal digits
583
+ const str = (value||'').toString()
584
+ if (str.length !== 24) return false
585
+ else if (Array.isArray(value)) return false
586
+
587
+ // Check if all characters are valid hexadecimal digits
588
+ for (let i=24; i--;) {
589
+ const charCode = str.charCodeAt(i)
590
+ const isDigit = charCode >= 48 && charCode <= 57 // '0' to '9'
591
+ const isLowerHex = charCode >= 97 && charCode <= 102 // 'a' to 'f'
592
+ const isUpperHex = charCode >= 65 && charCode <= 70 // 'A' to 'F'
593
+ if (!isDigit && !isLowerHex && !isUpperHex) {
594
+ return false
595
+ }
596
+ }
597
+ return true
598
+ }
599
+
600
+ export function isNumber (variable) {
601
+ return !isNaN(parseFloat(variable)) && isFinite(variable)
602
+ }
603
+
604
+ export function isObject (variable) {
605
+ // Excludes null and array's
606
+ return variable !== null && typeof variable === 'object' && !(variable instanceof Array) ? true : false
607
+ }
608
+
609
+ export function isRegex (variable) {
610
+ return variable instanceof RegExp ? true : false
611
+ }
612
+
613
+ export function isString (variable) {
614
+ return typeof variable === 'string' || variable instanceof String ? true : false
615
+ }
616
+
617
+ export function lcFirst (string) {
618
+ return string.charAt(0).toLowerCase() + string.slice(1)
619
+ }
620
+
621
+ export function maxLength (string, len, showEllipsis) {
622
+ // Trims to a maximum length, and removes any partial words at the end
623
+ len = len || 100
624
+ if (!string) return ''
625
+ if (string.length <= len) return string
626
+ if (showEllipsis) len -= 3
627
+ // trim the string to the maximum length
628
+ var trimmed = string.substr(0, len || 100)
629
+ // re-trim if we are in the middle of a word
630
+ return trimmed.substr(0, Math.min(trimmed.length, trimmed.lastIndexOf(' ')))
631
+ + (showEllipsis ? '...' : '')
632
+ }
633
+
634
+ export function mongoAddKmsToBox (km, bottomLeft, topRight) {
635
+ /**
636
+ * Expands a mongodb lat/lng box in kms
637
+ * @param {number} km
638
+ * @param {Array[lng, lat]|Box} bottomLeft
639
+ * @param {Array[lng, lat]} topRight
640
+ * @return [bottomLeft, topRight]
641
+ *
642
+ * Handy box tester
643
+ * https://www.keene.edu/campus/maps/tool/
644
+ *
645
+ * Returned Google places viewport (i.e. `place.geometry.viewport`)
646
+ * {
647
+ * Qa: {g: 174.4438160493033, h: 174.9684260722261} == [btmLng, topLng]
648
+ * zb: {g: -37.05901990116617, h: -36.66060184426172} == [btmLat, topLat]
649
+ * }
650
+ *
651
+ * We then convert above into `address.area.bottomLeft|topRight`
652
+ *
653
+ * Rangiora box
654
+ * [[172.5608731356091,-43.34484397837406] (btm left)
655
+ * [172.6497429548984,-43.28025140057695]] (top right)
656
+ *
657
+ * Auckland box
658
+ * [[174.4438160493033,-37.05901990116617] (btm left)
659
+ * [174.9684260722261,-36.66060184426172]] (top right)
660
+ */
661
+ if (bottomLeft && bottomLeft.bottomLeft) {
662
+ topRight = bottomLeft.topRight
663
+ bottomLeft = bottomLeft.bottomLeft
664
+ }
665
+ if (!bottomLeft || !topRight) {
666
+ return null
667
+ }
668
+ let lat = (lat, kms) => lat + (kms / 6371) * (180 / Math.PI)
669
+ let lng = (lng, lat, kms) => lng + (kms / 6371) * (180 / Math.PI) / Math.cos(lat * Math.PI/180)
670
+ return [
671
+ [lng(bottomLeft[0], bottomLeft[1], -km), lat(bottomLeft[1], -km)],
672
+ [lng(topRight[0], -topRight[1], km), lat(topRight[1], km)],
673
+ ]
674
+ }
675
+
676
+ export function mongoDocWithinPassedAddress (address, km, prefix) {
677
+ let type = ''
678
+ let areaSize = 5
679
+ if (type == 'geoNear') {
680
+ // NOT USING
681
+ // Must be the first stage in an aggregate pipeline
682
+ if (address.area) {
683
+ areaSize = mongoPointDifference(address.area.bottomLeft, address.area.topRight)
684
+ }
685
+ // console.log('kms', (areaSize / 2))
686
+ return {
687
+ $geoNear: {
688
+ near: {
689
+ type: 'Point',
690
+ coordinates: [address.location.coordinates[0], address.location.coordinates[1]],
691
+ },
692
+ distanceField: 'distance',
693
+ maxDistance: ((areaSize / 2) + km) / 6371, // km / earth's radius in km = radians
694
+ spherical: true,
695
+ },
696
+ }
697
+ } else if (address.area) {
698
+ let box = mongoAddKmsToBox(km, address.area)
699
+ return {
700
+ [`${prefix}location`]: {
701
+ $geoWithin: {
702
+ $box: box,// [[lng lat], [lng lat]]
703
+ },
704
+ },
705
+ }
706
+ } else {
707
+ return {
708
+ [`${prefix}location`]: {
709
+ $geoWithin: {
710
+ $centerSphere: [
711
+ [address.location.coordinates[0], address.location.coordinates[1]], // lng lat
712
+ (areaSize + km) / 6371, // km / earth's radius in km = radians
713
+ ],
714
+ },
715
+ },
716
+ }
717
+ }
718
+ }
719
+
720
+ export function mongoPointDifference (point1, point2) {
721
+ /**
722
+ * Find the distance in km between to points
723
+ * @param {array} point1 - [192.2132.., 212.23323..]
724
+ * @param {array} point2 - [192.2132.., 212.23323..]
725
+ * @return {number} kms
726
+ */
727
+ let R = 6371 // km
728
+ let mongoDegreesToRadians = (degrees) => degrees * (Math.PI / 180)
729
+ let dLat = mongoDegreesToRadians(point2[1]-point1[1])
730
+ let dLon = mongoDegreesToRadians(point2[0]-point1[0])
731
+ let lat1 = mongoDegreesToRadians(point1[1])
732
+ let lat2 = mongoDegreesToRadians(point2[1])
733
+
734
+ let a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.sin(dLon/2) *
735
+ Math.sin(dLon/2) * Math.cos(lat1) * Math.cos(lat2)
736
+ let c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
737
+ let d = R * c
738
+ return d.toFixed(1)
739
+ }
740
+
741
+ export function objectMap (object, fn) {
742
+ return Object.keys(object).reduce(function(result, key) {
743
+ result[key] = fn(object[key], key)
744
+ return result
745
+ }, {})
746
+ }
747
+
748
+ export function omit (obj, fields) {
749
+ const shallowCopy = Object.assign({}, obj)
750
+ for (let i=0; i<fields.length; i+=1) {
751
+ const key = fields[i]
752
+ delete shallowCopy[key]
753
+ }
754
+ return shallowCopy
755
+ }
756
+
757
+ export function onChange (setState, event, beforeSetState) {
758
+ /**
759
+ * Updates state from an input event, you can also update deep state properties
760
+ *
761
+ * @param {function} setState
762
+ * @param {Empty | Event | Array[{string}, {string|number|fn}]}
763
+ * {Empty} - pass undefined to return a reusable function, e.g. const _onChange = onChange(setState)
764
+ * {Event} - pass the event object, e.g. <input onChange={_onChange}>
765
+ * {Array} - pass an array with [path, value], e.g. <input onChange={() => _onChange(['name', 'Joe'])}>
766
+ * @param {function} beforeSetState - optional function to run before setting the state
767
+ *
768
+ * @return {Function | Promise({state, chunks, target})}
769
+ */
770
+ if (typeof event === 'undefined') {
771
+ return onChange.bind(this, setState)
772
+ }
773
+ let elem = event.target ? event.target : { id: event[0], value: event[1] }
774
+ let chunks = (elem.id || elem.name).split('.')
775
+ let value = elem.files
776
+ ? elem.files[0]
777
+ : elem.type === 'checkbox'
778
+ ? elem.checked
779
+ : isDefined(elem._value)
780
+ ? elem._value
781
+ : elem.value
782
+
783
+ // Removing leading zero(s) on number fields
784
+ // if (elem.type == 'number' && !isFunction(value) && (value||'').match(/^0+([1-9])/)) {
785
+ // value = value.replace(/^0+([1-9])/, '$1')
786
+ // }
787
+
788
+ // Update state
789
+ return new Promise((resolve) => {
790
+ setState((state) => {
791
+ const newState = { ...state, ...(elem.files ? { hasFiles: true } : {}) }
792
+ let target = newState
793
+ for (var i = 0, l = chunks.length; i < l; i++) {
794
+ if (l === i + 1) { // Last
795
+ target[chunks[i]] = isFunction(value) ? value(state) : value
796
+ // console.log(target)
797
+ } else {
798
+ let isArray = chunks[i + 1].match(/^[0-9]+$/)
799
+ let parentCopy = isArray ? [...(target[chunks[i]] || [])] : { ...(target[chunks[i]] || {}) }
800
+ target = target[chunks[i]] = parentCopy
801
+ }
802
+ }
803
+ if (beforeSetState) {
804
+ beforeSetState({ newState: newState, fieldName: chunks[i], parent: target })
805
+ }
806
+ resolve(newState)
807
+ return newState
808
+ })
809
+ })
810
+ }
811
+
812
+ export function pad (num, padLeft, fixedRight) {
813
+ num = parseFloat(num || 0)
814
+ if (fixedRight || fixedRight === 0) {
815
+ return num.toFixed(fixedRight).padStart((padLeft||0) + fixedRight + 1, '0')
816
+ } else {
817
+ if (padLeft && `${num}`.match('.')) padLeft += (`${num}`.split('.')[1]||'').length + 1
818
+ return `${num}`.padStart(padLeft, '0')
819
+ }
820
+ }
821
+
822
+ export function pick (obj, keys) {
823
+ // Similiar to underscore.pick
824
+ // @param {string[] | regex[]} keys
825
+ if (!isObject(obj) && !isFunction(obj)) return {}
826
+ keys = toArray(keys)
827
+ let res = {}
828
+ for (let key of keys) {
829
+ if (isString(key) && obj.hasOwnProperty(key)) res[key] = obj[key]
830
+ if (isRegex(key)) {
831
+ for (let key2 in obj) {
832
+ if (obj.hasOwnProperty(key2) && key2.match(key)) res[key2] = obj[key2]
833
+ }
834
+ }
835
+ }
836
+ return res
837
+ }
838
+
839
+ export function queryObject (search, assignTrue) {
840
+ /*
841
+ * Parses a query string into an object, or returns the last known matching cache
842
+ * @param {string} search - location.search or location.href, e.g. '?page=1', 'https://...co.nz?page=1'
843
+ * @param {boolean} assignTrue - assign true to empty values
844
+ * @return {object} e.g. { page: 1 }
845
+ */
846
+ search = search.replace(/^[^?]+\?/, '?') // remove domain preceeding search string
847
+ let obj = {}
848
+ if (search === '') return obj
849
+ if (!queryObject.queryObjectCache) queryObject.queryObjectCache = {}
850
+ if (queryObject.queryObjectCache[search]) return { ...queryObject.queryObjectCache[search] }
851
+
852
+ // Remove '?', and split each query parameter (ampersand-separated)
853
+ search = search.slice(1).split('&')
854
+
855
+ // Loop through each query paramter
856
+ search.map(function (part) {
857
+ part = part.split('=') // Split into key/value
858
+ let key = part[0]
859
+ let value = !part[1] && part[1] !== 0 ? (assignTrue ? true : '') : decodeURIComponent(part[1])
860
+
861
+ // Key already exists
862
+ if (obj[key]) {
863
+ obj[key] = toArray([obj[key]])
864
+ obj[key].push(value)
865
+ } else {
866
+ obj[key] = value
867
+ }
868
+ })
869
+
870
+ queryObject.queryObjectCache = { [search]: obj }
871
+ return { ...obj }
872
+ }
873
+
874
+ export function queryString (obj) {
875
+ /*
876
+ * Parses an object and returns a query string
877
+ * @param {object} obj - query object
878
+ */
879
+ obj = { ...(obj||{}) }
880
+ for (let key in obj) {
881
+ if (obj.hasOwnProperty(key)) {
882
+ if (typeof obj[key] == 'undefined') delete obj[key]
883
+ else if (!obj[key]) delete obj[key]
884
+ }
885
+ }
886
+ let qs = new URLSearchParams(obj).toString()
887
+ return qs ? `?${qs}` : ''
888
+ }
889
+
890
+ export async function request (event, route, data, isLoading) {
891
+ /**
892
+ * Axios request to the route
893
+ * @param {Event} event - event to prevent default
894
+ * @param {string} route - e.g. 'post /api/user'
895
+ * @param {object} <data> - payload
896
+ * @param {array} <isLoading> - [isLoading, setIsLoading]
897
+ * @return {promise}
898
+ */
899
+ try {
900
+ if (event?.preventDefault) event.preventDefault()
901
+ const uri = route.replace(/^(post|put|delete|get) /, '')
902
+ const method = (route.match(/^(post|put|delete|get) /)?.[1] || 'post').trim()
903
+
904
+ // show loading
905
+ if (isLoading) {
906
+ if (isLoading[0]) return
907
+ else isLoading[1](' is-loading')
908
+ }
909
+
910
+ // warning, not persisting through re-renders, but should be fine until loading is finished
911
+ data = data || {}
912
+ delete data.errors
913
+
914
+ // has files, if yes, convert to form data
915
+ let hasFiles
916
+ let recurse = (o) => {
917
+ if (o instanceof File || hasFiles) hasFiles = true
918
+ else if (o && typeof o === 'object') each(o, recurse)
919
+ }
920
+ recurse(data)
921
+ if (hasFiles) {
922
+ data = formData(data, { allowEmptyArrays: true, indices: true })
923
+ }
924
+
925
+ // send the request
926
+ const [res] = await Promise.allSettled([
927
+ axios()[method](uri, data, { withCredentials: true }),
928
+ setTimeoutPromise(() => {}, 200), // eslint-disable-line
929
+ ])
930
+
931
+ // success
932
+ if (isLoading) isLoading[1]('')
933
+ if (res.status == 'rejected') throw res.reason
934
+ return res.value.data
935
+
936
+ } catch (errs) {
937
+ throw getResponseErrors(errs)
938
+ }
939
+ }
940
+
941
+ export function removeUndefined (variable) {
942
+ // Removes undefined from an array or object
943
+ if (Array.isArray(variable)) {
944
+ for (let i = variable.length; i--; ) {
945
+ if (variable[i] === undefined) variable.splice(i, 1)
946
+ }
947
+ } else {
948
+ Object.keys(variable).forEach((key) => {
949
+ if (variable[key] === undefined) delete variable[key]
950
+ })
951
+ }
952
+ return variable
953
+ }
954
+
955
+ export function s3Image (awsUrl, image, size='full', i) {
956
+ /**
957
+ * Build image URL from image array or object
958
+ * @param {string} awsUrl - e.g. 'https://s3.amazonaws.com/...'
959
+ * @param {array|object} image - file object/array
960
+ * @param {string} <size> - overrides to 'full' when the image sizes are still processing
961
+ * @param {integer} <i> - array index
962
+ */
963
+ let lambdaDelay = 7000
964
+ let usingMilliseconds = true
965
+
966
+ if (!isObject(image)) image = (image && image[i]) ? image[i] : null
967
+ if (!image) return '/assets/imgs/no-image.jpg'
968
+ // Alway use preview if available
969
+ if (image.base64) return image.base64
970
+ // Wait a moment before the different sizes are generated by lambda
971
+ if (((usingMilliseconds ? image.date : image.date * 1000) + lambdaDelay) > new Date()) size = 'full'
972
+ let key = size == 'full' ? image.path : `${size}/${image.path.replace(/^full\/|\.[a-z0-9]{3,4}$/ig, '')}.jpg`
973
+ return awsUrl + image.bucket + '/' + key
974
+ }
975
+
976
+ export function sanitizeHTML (string) {
977
+ /*
978
+ * Sanitize and encode all HTML in a user-submitted string
979
+ * (c) 2018 Chris Ferdinandi, MIT License, https://gomakethings.com
980
+ * @param {String} str The user-submitted string
981
+ * @return {String} str The sanitized string
982
+ */
983
+ var temp = document.createElement('div')
984
+ temp.textContent = string
985
+ return temp.innerHTML
986
+ }
987
+
988
+ export function scrollbar (paddingClass, marginClass, maxWidth) {
989
+ /**
990
+ * Process scrollbar width once.
991
+ * @param {string} paddingClass - class name to give padding to
992
+ * @param {string} marginClass - class name to give margin to
993
+ * @param {number} maxWidth - enclose css in a max-width media query
994
+ * @return width.
995
+ */
996
+ if (scrollbar.width || scrollbar.width === 0) {
997
+ return scrollbar.width
998
+ }
999
+
1000
+ var outer = document.createElement('div')
1001
+ outer.style.visibility = 'hidden'
1002
+ outer.style.width = '100px'
1003
+ outer.style.margin = '0px'
1004
+ outer.style.padding = '0px'
1005
+ outer.style.border = '0'
1006
+ document.body.appendChild(outer)
1007
+
1008
+ var widthNoScroll = outer.offsetWidth
1009
+ // force scrollbars
1010
+ outer.style.overflow = 'scroll'
1011
+
1012
+ // add innerdiv
1013
+ var inner = document.createElement('div')
1014
+ inner.style.width = '100%'
1015
+ outer.appendChild(inner)
1016
+
1017
+ var widthWithScroll = inner.offsetWidth
1018
+
1019
+ // Remove divs
1020
+ outer.parentNode.removeChild(outer)
1021
+ scrollbar.width = widthNoScroll - widthWithScroll
1022
+
1023
+ // Add padding class.
1024
+ if (paddingClass || marginClass) {
1025
+ document.head.appendChild( // double check this, was jquery
1026
+ '<style type="text/css">' +
1027
+ (maxWidth ? '@media only screen and (max-width: ' + maxWidth + 'px) {' : '') +
1028
+ (paddingClass ? paddingClass + ' {padding-right:' + scrollbar.width + 'px}' : '') +
1029
+ (marginClass ? marginClass + ' {margin-right:' + scrollbar.width + 'px}' : '') +
1030
+ (maxWidth ? '}' : '') +
1031
+ '</style>'
1032
+ )
1033
+ }
1034
+
1035
+ // return.
1036
+ return scrollbar.width
1037
+ }
1038
+
1039
+ export function secondsToTime (seconds, padMinute) {
1040
+ seconds = Math.round(seconds)
1041
+ let hours = Math.floor(seconds / (60 * 60))
1042
+ let divisor_for_minutes = seconds % (60 * 60)
1043
+ let minutes = Math.floor(divisor_for_minutes / 60)
1044
+ let divisor_for_seconds = divisor_for_minutes % 60
1045
+ let secs = Math.ceil(divisor_for_seconds)
1046
+ let data = {
1047
+ h: (hours + ''),
1048
+ m: (minutes + '').padStart(padMinute? 2 : 1, 0),
1049
+ s: (secs + ''),
1050
+ }
1051
+ return data.h + ':' + data.m
1052
+ }
1053
+
1054
+ export function setTimeoutPromise (func, milliseconds) {
1055
+ return new Promise(function(resolve) {
1056
+ setTimeout(() => resolve(func()), milliseconds)
1057
+ })
1058
+ }
1059
+
1060
+ export function showError (setStore, errs) {
1061
+ /**
1062
+ * Shows a global error
1063
+ * @params {function} setStore
1064
+ * @params {Array|Error|String|Axios Object} errs
1065
+ */
1066
+ let detail = getResponseErrors(errs)[0].detail
1067
+ setStore((o) => ({ ...o, message: { type: 'error', text: detail } }))
1068
+ }
1069
+
1070
+ export function sortByKey (objects, key) {
1071
+ return objects.sort(function (a, b) {
1072
+ var textA = (a[key] || '').toUpperCase()
1073
+ var textB = (b[key] || '').toUpperCase()
1074
+ return textA < textB ? -1 : textA > textB ? 1 : 0
1075
+ })
1076
+ }
1077
+
1078
+ export function sortFromQuery (req, sortMap, sortByDefault='createdAt') {
1079
+ /**
1080
+ *
1081
+ * Return a mongodb sort pipeline stage using approved `req.query.sort-by|sort` values
1082
+ * @param {object} req - requires req.query.sort-by|sort
1083
+ * @param {object} sortMap - e.g. { name: ['user.firstName', ..], .. }
1084
+ * @param {string} <sortDefault> - e.g. 'createdAt' (default)
1085
+ * @return {object} - e.g. { 'user.firstName': -1 }
1086
+ * @see used in karpark
1087
+ */
1088
+ let sort = req.query.sort == 'asc' ? 1 : -1
1089
+ let sortBy = req.query['sort-by']
1090
+ sortByDefault = sortByDefault || 'createdAt'
1091
+
1092
+ // Convert { name: ['user.firstName'] } to { name: { 'user.firstName': 1 }}
1093
+ for (let key in sortMap) {
1094
+ sortMap[key] = sortMap[key].reduce((o, name) => {
1095
+ o[name] = sort
1096
+ return o
1097
+ }, {})
1098
+ }
1099
+
1100
+ if (sortMap[sortBy]) return sortMap[sortBy]
1101
+ else return { [sortByDefault]: sort }
1102
+ }
1103
+
1104
+ export function throttle (func, wait, options) {
1105
+ /**
1106
+ * Creates a throttled function that only invokes `func` at most once per every `wait` milliseconds
1107
+ * @param {function} func
1108
+ * @param {number} <wait=0> - the number of milliseconds to throttle invocations to
1109
+ * @param {boolean} <options.leading=true> - invoke on the leading edge of the timeout
1110
+ * @param {boolean} <options.trailing=true> - invoke on the trailing edge of the timeout
1111
+ * @returns {Function}
1112
+ * @example const throttled = util.throttle(updatePosition, 100)
1113
+ * @see lodash
1114
+ */
1115
+ let leading = true
1116
+ let trailing = true
1117
+ if (typeof func != 'function') {
1118
+ throw new TypeError('Expected a function')
1119
+ }
1120
+ if (isObject(options)) {
1121
+ leading = 'leading' in options ? !!options.leading : leading
1122
+ trailing = 'trailing' in options ? !!options.trailing : trailing
1123
+ }
1124
+ return debounce(func, wait, {
1125
+ 'leading': leading,
1126
+ 'maxWait': wait,
1127
+ 'trailing': trailing,
1128
+ })
1129
+ }
1130
+
1131
+ export function toArray (variable) {
1132
+ // converts a variable to an array, if not already so
1133
+ if (typeof variable === 'undefined') return []
1134
+ return Array.isArray(variable) ? variable : [variable]
1135
+ }
1136
+
1137
+ export function trim (string) {
1138
+ if (!string || !isString(string)) return ''
1139
+ return string.trim().replace(/\n\s+\n/g, '\n\n')
1140
+ }
1141
+
1142
+ export function ucFirst (string) {
1143
+ if (!string) return ''
1144
+ return string.charAt(0).toUpperCase() + string.slice(1)
1145
+ }