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.
- package/.editorconfig +9 -0
- package/.eslintrc.json +86 -0
- package/_example/.env-example +16 -0
- package/_example/client/config.ts +5 -0
- package/_example/client/css/index.css +35 -0
- package/_example/client/fonts/Roboto-Bold.ttf +0 -0
- package/_example/client/fonts/Roboto-BoldItalic.ttf +0 -0
- package/_example/client/fonts/Roboto-Italic.ttf +0 -0
- package/_example/client/fonts/Roboto-Medium.ttf +0 -0
- package/_example/client/fonts/Roboto-MediumItalic.ttf +0 -0
- package/_example/client/fonts/Roboto-Regular.ttf +0 -0
- package/_example/client/fonts/inter-v13-latin-300.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-500.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-600.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-700.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-800.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-900.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-regular.woff2 +0 -0
- package/_example/client/imgs/android-chrome-512x512.png +0 -0
- package/_example/client/imgs/favicon.png +0 -0
- package/_example/client/imgs/icons/calendar.svg +3 -0
- package/_example/client/imgs/icons/email.svg +6 -0
- package/_example/client/imgs/icons/eye-open.svg +4 -0
- package/_example/client/imgs/icons/eye.svg +5 -0
- package/_example/client/imgs/icons/filter.svg +7 -0
- package/_example/client/imgs/icons/left-circle.svg +3 -0
- package/_example/client/imgs/icons/left.svg +3 -0
- package/_example/client/imgs/icons/line-options.svg +5 -0
- package/_example/client/imgs/icons/line.svg +3 -0
- package/_example/client/imgs/icons/person.svg +7 -0
- package/_example/client/imgs/icons/plus-circle.svg +5 -0
- package/_example/client/imgs/icons/plus.svg +5 -0
- package/_example/client/imgs/icons/right-circle.svg +3 -0
- package/_example/client/imgs/icons/right.svg +3 -0
- package/_example/client/imgs/icons/search.svg +3 -0
- package/_example/client/imgs/icons/shield.svg +6 -0
- package/_example/client/imgs/icons/tick-circle-solid.svg +8 -0
- package/_example/client/imgs/icons/tick-circle.svg +6 -0
- package/_example/client/imgs/icons/tick.svg +5 -0
- package/_example/client/imgs/icons/up2-small.svg +4 -0
- package/_example/client/imgs/icons/up2.svg +4 -0
- package/_example/client/imgs/icons/updown.svg +6 -0
- package/_example/client/imgs/icons/v-big-dark.svg +3 -0
- package/_example/client/imgs/icons/v-dark.svg +3 -0
- package/_example/client/imgs/icons/v.svg +3 -0
- package/_example/client/imgs/icons/v2-active.svg +6 -0
- package/_example/client/imgs/icons/x1.svg +4 -0
- package/_example/client/imgs/logo/logo-white.svg +20 -0
- package/_example/client/imgs/logo/logo.svg +20 -0
- package/_example/client/imgs/no-image.jpg +0 -0
- package/_example/client/imgs/user.jpg +0 -0
- package/_example/client/index.html +12 -0
- package/_example/client/index.ts +47 -0
- package/_example/components/auth.api.js +1 -0
- package/_example/components/index.tsx +225 -0
- package/_example/components/partials/layouts.tsx +5 -0
- package/_example/components/settings.api.js +1 -0
- package/_example/server/config.js +120 -0
- package/_example/server/email/welcome.html +27 -0
- package/_example/server/index.js +32 -0
- package/_example/tailwind.config.js +84 -0
- package/_example/tsconfig.json +32 -0
- package/_example/types.d.ts +7 -0
- package/_example/webpack.config.js +4 -0
- package/client/app.js +300 -0
- package/client/css/components.css +84 -0
- package/client/css/fonts.css +67 -0
- package/client/imgs/icons/calendar.svg +3 -0
- package/client/imgs/icons/email.svg +6 -0
- package/client/imgs/icons/eye-open.svg +4 -0
- package/client/imgs/icons/eye.svg +5 -0
- package/client/imgs/icons/filter.svg +7 -0
- package/client/imgs/icons/left-circle.svg +3 -0
- package/client/imgs/icons/left.svg +3 -0
- package/client/imgs/icons/line-options.svg +5 -0
- package/client/imgs/icons/line.svg +3 -0
- package/client/imgs/icons/person.svg +7 -0
- package/client/imgs/icons/plus-circle.svg +5 -0
- package/client/imgs/icons/plus.svg +5 -0
- package/client/imgs/icons/right-circle.svg +3 -0
- package/client/imgs/icons/right.svg +3 -0
- package/client/imgs/icons/search.svg +3 -0
- package/client/imgs/icons/shield.svg +6 -0
- package/client/imgs/icons/tick-circle-solid.svg +8 -0
- package/client/imgs/icons/tick-circle.svg +6 -0
- package/client/imgs/icons/tick.svg +5 -0
- package/client/imgs/icons/up2-small.svg +4 -0
- package/client/imgs/icons/up2.svg +4 -0
- package/client/imgs/icons/updown.svg +6 -0
- package/client/imgs/icons/v-big-dark.svg +3 -0
- package/client/imgs/icons/v-dark.svg +3 -0
- package/client/imgs/icons/v.svg +3 -0
- package/client/imgs/icons/v2-active.svg +6 -0
- package/client/imgs/icons/x1.svg +4 -0
- package/client.js +42 -0
- package/components/auth/auth.api.js +419 -0
- package/components/auth/reset.jsx +88 -0
- package/components/auth/signin.jsx +74 -0
- package/components/auth/signup.jsx +62 -0
- package/components/billing/stripe.api.js +267 -0
- package/components/partials/element/accordion.jsx +82 -0
- package/components/partials/element/avatar.jsx +28 -0
- package/components/partials/element/button.jsx +66 -0
- package/components/partials/element/dropdown.jsx +185 -0
- package/components/partials/element/initials.jsx +56 -0
- package/components/partials/element/message.jsx +124 -0
- package/components/partials/element/modal.jsx +229 -0
- package/components/partials/element/sidebar.jsx +166 -0
- package/components/partials/element/tooltip.jsx +146 -0
- package/components/partials/element/topbar.jsx +25 -0
- package/components/partials/form/checkbox.jsx +74 -0
- package/components/partials/form/drop-handler.jsx +62 -0
- package/components/partials/form/drop.jsx +125 -0
- package/components/partials/form/form-error.jsx +21 -0
- package/components/partials/form/input-color.jsx +77 -0
- package/components/partials/form/input-currency.jsx +133 -0
- package/components/partials/form/input-date.jsx +223 -0
- package/components/partials/form/input.jsx +131 -0
- package/components/partials/form/location.jsx +212 -0
- package/components/partials/form/select.jsx +369 -0
- package/components/partials/form/toggle.jsx +46 -0
- package/components/partials/is-first-render.js +15 -0
- package/components/partials/layout/layout1.jsx +32 -0
- package/components/partials/layout/layout2.jsx +47 -0
- package/components/partials/not-found.jsx +7 -0
- package/components/partials/styleguide.jsx +252 -0
- package/components/settings/settings-account.jsx +143 -0
- package/components/settings/settings-business.jsx +121 -0
- package/components/settings/settings-team--member.jsx +108 -0
- package/components/settings/settings-team.jsx +76 -0
- package/components/settings/settings.api.js +54 -0
- package/package.json +175 -0
- package/readme.md +43 -0
- package/server/email/index.js +192 -0
- package/server/email/partials/email.css +153 -0
- package/server/email/partials/layout1.swig +92 -0
- package/server/email/partials/line.swig +8 -0
- package/server/email/partials/vert-10.swig +8 -0
- package/server/email/partials/vert-15.swig +8 -0
- package/server/email/partials/vert-20.swig +8 -0
- package/server/email/partials/vert-25.swig +8 -0
- package/server/email/partials/vert-30.swig +8 -0
- package/server/email/partials/vert-35.swig +8 -0
- package/server/email/partials/vert-50.swig +8 -0
- package/server/email/reset-password.html +21 -0
- package/server/email/welcome.html +21 -0
- package/server/models/company.js +76 -0
- package/server/models/user.js +45 -0
- package/server/router.js +355 -0
- package/server.js +20 -0
- package/util.js +1145 -0
- 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
|
+
}
|