nitro-web 0.0.87 → 0.0.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/index.ts +0 -3
- package/components/auth/auth.api.js +411 -0
- package/components/auth/reset.tsx +86 -0
- package/components/auth/signin.tsx +76 -0
- package/components/auth/signup.tsx +62 -0
- package/components/billing/stripe.api.js +268 -0
- package/components/dashboard/dashboard.tsx +32 -0
- package/components/partials/element/accordion.tsx +102 -0
- package/components/partials/element/avatar.tsx +40 -0
- package/components/partials/element/button.tsx +98 -0
- package/components/partials/element/calendar.tsx +125 -0
- package/components/partials/element/dropdown.tsx +248 -0
- package/components/partials/element/filters.tsx +194 -0
- package/components/partials/element/github-link.tsx +16 -0
- package/components/partials/element/initials.tsx +66 -0
- package/components/partials/element/message.tsx +141 -0
- package/components/partials/element/modal.tsx +90 -0
- package/components/partials/element/sidebar.tsx +195 -0
- package/components/partials/element/tooltip.tsx +154 -0
- package/components/partials/element/topbar.tsx +15 -0
- package/components/partials/form/checkbox.tsx +150 -0
- package/components/partials/form/drop-handler.tsx +68 -0
- package/components/partials/form/drop.tsx +141 -0
- package/components/partials/form/field-color.tsx +86 -0
- package/components/partials/form/field-currency.tsx +158 -0
- package/components/partials/form/field-date.tsx +252 -0
- package/components/partials/form/field.tsx +231 -0
- package/components/partials/form/form-error.tsx +27 -0
- package/components/partials/form/location.tsx +225 -0
- package/components/partials/form/select.tsx +360 -0
- package/components/partials/is-first-render.ts +14 -0
- package/components/partials/not-found.tsx +7 -0
- package/components/partials/styleguide.tsx +407 -0
- package/package.json +2 -1
- package/types/globals.d.ts +0 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
// fill-current tw class for lucide icons (https://github.com/lucide-icons/lucide/discussions/458)
|
|
3
|
+
import { css } from 'twin.macro'
|
|
4
|
+
import { FieldCurrency, FieldCurrencyProps, FieldColor, FieldColorProps, FieldDate, FieldDateProps } from 'nitro-web'
|
|
5
|
+
import { twMerge, getErrorFromState, deepFind } from 'nitro-web/util'
|
|
6
|
+
import { Errors, type Error } from 'nitro-web/types'
|
|
7
|
+
import { MailIcon, CalendarIcon, FunnelIcon, SearchIcon, EyeIcon, EyeOffIcon } from 'lucide-react'
|
|
8
|
+
import { memo } from 'react'
|
|
9
|
+
|
|
10
|
+
type FieldType = 'text' | 'password' | 'email' | 'filter' | 'search' | 'textarea' | 'currency' | 'date' | 'color'
|
|
11
|
+
type InputProps = React.InputHTMLAttributes<HTMLInputElement>
|
|
12
|
+
type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
|
13
|
+
type FieldExtraProps = {
|
|
14
|
+
/** field name or path on state (used to match errors), e.g. 'date', 'company.email' */
|
|
15
|
+
name: string
|
|
16
|
+
/** name is applied if id is not provided */
|
|
17
|
+
id?: string
|
|
18
|
+
/** state object to get the value, and check errors against */
|
|
19
|
+
state?: { errors?: Errors, [key: string]: any }
|
|
20
|
+
/** type of the field */
|
|
21
|
+
type?: FieldType
|
|
22
|
+
/** icon to show in the input */
|
|
23
|
+
icon?: React.ReactNode
|
|
24
|
+
iconPos?: 'left' | 'right'
|
|
25
|
+
/** Dependencies to break the implicit memoization of onChange/onInputChange */
|
|
26
|
+
deps?: unknown[]
|
|
27
|
+
placeholder?: string
|
|
28
|
+
/** title used to find related error messages */
|
|
29
|
+
errorTitle?: string|RegExp
|
|
30
|
+
}
|
|
31
|
+
type IconWrapperProps = {
|
|
32
|
+
iconPos: 'left' | 'right'
|
|
33
|
+
icon?: React.ReactNode
|
|
34
|
+
[key: string]: unknown
|
|
35
|
+
}
|
|
36
|
+
// Discriminated union (https://stackoverflow.com/a/77351290/1900648)
|
|
37
|
+
export type FieldProps = (
|
|
38
|
+
| ({ type?: 'text' | 'password' | 'email' | 'filter' | 'search' } & InputProps & FieldExtraProps)
|
|
39
|
+
| ({ type: 'textarea' } & TextareaProps & FieldExtraProps)
|
|
40
|
+
| ({ type: 'currency' } & FieldCurrencyProps & FieldExtraProps)
|
|
41
|
+
| ({ type: 'color' } & FieldColorProps & FieldExtraProps)
|
|
42
|
+
| ({ type: 'date' } & FieldDateProps & FieldExtraProps)
|
|
43
|
+
)
|
|
44
|
+
type IsFieldCachedProps = {
|
|
45
|
+
name: string
|
|
46
|
+
state?: FieldProps['state']
|
|
47
|
+
deps?: FieldProps['deps']
|
|
48
|
+
errorTitle?: FieldProps['errorTitle']
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const Field = memo(FieldBase, (prev, next) => {
|
|
52
|
+
return isFieldCached(prev, next)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
function FieldBase({ state, icon, iconPos: ip, errorTitle, ...props }: FieldProps) {
|
|
56
|
+
// `type` must be kept as props.type for TS to be happy and follow the conditions below
|
|
57
|
+
let value!: string
|
|
58
|
+
let Icon!: React.ReactNode
|
|
59
|
+
const error = getErrorFromState(state, errorTitle || props.name)
|
|
60
|
+
const type = props.type
|
|
61
|
+
const iconPos = ip == 'left' || (type == 'color' && !ip) ? 'left' : 'right'
|
|
62
|
+
const id = props.id || props.name
|
|
63
|
+
|
|
64
|
+
if (!props.name) {
|
|
65
|
+
throw new Error('Field component requires a `name` prop')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Input type
|
|
69
|
+
const [inputType, setInputType] = useState(() => { // eslint-disable-line
|
|
70
|
+
return type == 'password' ? 'password' : (type == 'textarea' ? 'textarea' : 'text')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Value: Input is always controlled if state is passed in
|
|
74
|
+
if (typeof props.value !== 'undefined') value = props.value as string
|
|
75
|
+
else if (typeof state == 'object') {
|
|
76
|
+
const v = deepFind(state, props.name) as string | undefined
|
|
77
|
+
value = v ?? ''
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Icon
|
|
81
|
+
if (type == 'password') {
|
|
82
|
+
Icon = <IconWrapper
|
|
83
|
+
iconPos={iconPos}
|
|
84
|
+
icon={icon || inputType == 'password' ? <EyeOffIcon /> : <EyeIcon />}
|
|
85
|
+
onClick={() => setInputType(o => o == 'password' ? 'text' : 'password')}
|
|
86
|
+
className="size-[15px] size-input-icon pointer-events-auto"
|
|
87
|
+
/>
|
|
88
|
+
} else if (type == 'email') {
|
|
89
|
+
Icon = <IconWrapper iconPos={iconPos} icon={icon || <MailIcon />} className="size-[14px] size-input-icon" />
|
|
90
|
+
} else if (type == 'filter') {
|
|
91
|
+
Icon = <IconWrapper iconPos={iconPos} icon={icon || <FunnelIcon />} className="size-[14px] size-input-icon" />
|
|
92
|
+
} else if (type == 'search') {
|
|
93
|
+
Icon = <IconWrapper iconPos={iconPos} icon={icon || <SearchIcon />} className="size-[14px] size-input-icon" />
|
|
94
|
+
} else if (type == 'color') {
|
|
95
|
+
Icon = <IconWrapper iconPos={iconPos} icon={icon || <ColorSvg hex={value}/>} className="size-[17px]" />
|
|
96
|
+
} else if (type == 'date') {
|
|
97
|
+
Icon = <IconWrapper iconPos={iconPos} icon={icon || <CalendarIcon />} className="size-[14px] size-input-icon" />
|
|
98
|
+
} else if (icon) {
|
|
99
|
+
Icon = <IconWrapper iconPos={iconPos} icon={icon} className="size-[14px] size-input-icon" />
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Classname
|
|
103
|
+
const inputClassName = getInputClasses({ error, Icon, iconPos, type })
|
|
104
|
+
const commonProps = { id: id, value: value, className: inputClassName }
|
|
105
|
+
|
|
106
|
+
// Type has to be referenced as props.type for TS to be happy
|
|
107
|
+
if (!type || type == 'text' || type == 'password' || type == 'email' || type == 'filter' || type == 'search') {
|
|
108
|
+
return (
|
|
109
|
+
<FieldContainer error={error} className={props.className}>
|
|
110
|
+
{Icon}<input {...props} {...commonProps} type={inputType} />
|
|
111
|
+
</FieldContainer>
|
|
112
|
+
)
|
|
113
|
+
} else if (type == 'textarea') {
|
|
114
|
+
return (
|
|
115
|
+
<FieldContainer error={error} className={props.className}>
|
|
116
|
+
{Icon}<textarea {...props} {...commonProps} />
|
|
117
|
+
</FieldContainer>
|
|
118
|
+
)
|
|
119
|
+
} else if (type == 'currency') {
|
|
120
|
+
return (
|
|
121
|
+
<FieldContainer error={error} className={props.className}>
|
|
122
|
+
{Icon}<FieldCurrency {...props} {...commonProps} />
|
|
123
|
+
</FieldContainer>
|
|
124
|
+
)
|
|
125
|
+
} else if (type == 'color') {
|
|
126
|
+
return (
|
|
127
|
+
<FieldContainer error={error} className={props.className}>
|
|
128
|
+
<FieldColor {...props} {...commonProps} Icon={Icon} />
|
|
129
|
+
</FieldContainer>
|
|
130
|
+
)
|
|
131
|
+
} else if (type == 'date') {
|
|
132
|
+
return (
|
|
133
|
+
<FieldContainer error={error} className={props.className}>
|
|
134
|
+
<FieldDate {...props} {...commonProps} Icon={Icon} />
|
|
135
|
+
</FieldContainer>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function FieldContainer({ children, className, error }: { children: React.ReactNode, className?: string, error?: Error }) {
|
|
141
|
+
return (
|
|
142
|
+
<div css={style} className={'mt-2.5 mb-6 ' + twMerge(`mt-input-before mb-input-after grid grid-cols-1 nitro-field ${className || ''}`)}>
|
|
143
|
+
{children}
|
|
144
|
+
{error && <div class="mt-1.5 text-xs text-danger-foreground nitro-error">{error.detail}</div>}
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getInputClasses({ error, Icon, iconPos, type }: { error?: Error, Icon?: React.ReactNode, iconPos: string, type?: string }) {
|
|
150
|
+
// not twMerge
|
|
151
|
+
const px = 'px-[12px]'
|
|
152
|
+
const py = 'py-[9px] py-input-y'
|
|
153
|
+
return (
|
|
154
|
+
'block col-start-1 row-start-1 w-full rounded-md bg-white text-input-base outline outline-1 -outline-offset-1 ' +
|
|
155
|
+
'placeholder:text-input-placeholder focus:outline focus:outline-2 focus:-outline-offset-2 ' + `${py} ${px} ` +
|
|
156
|
+
(iconPos == 'right' && Icon ? 'pr-[32px] pr-input-x-icon pl-input-x ' : '') +
|
|
157
|
+
(iconPos == 'left' && Icon ? 'pl-[32px] pl-input-x-icon pr-input-x ' : 'px-input-x ') +
|
|
158
|
+
(iconPos == 'left' && Icon && type == 'color' ? 'indent-[5px] ' : '') +
|
|
159
|
+
(error
|
|
160
|
+
? 'text-danger-foreground outline-danger focus:outline-danger '
|
|
161
|
+
: 'text-input outline-input-border focus:outline-input-border-focus ') +
|
|
162
|
+
(iconPos == 'right' ? 'justify-self-start ' : 'justify-self-end ') +
|
|
163
|
+
'nitro-input'
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function IconWrapper({ icon, iconPos, ...props }: IconWrapperProps) {
|
|
168
|
+
return (
|
|
169
|
+
!!icon &&
|
|
170
|
+
<div
|
|
171
|
+
{...props}
|
|
172
|
+
className={
|
|
173
|
+
'z-[0] col-start-1 row-start-1 self-center text-[#c6c8ce] text-input-icon select-none [&>svg]:size-full ' +
|
|
174
|
+
(iconPos == 'right' ? 'justify-self-end mr-[12px] mr-input-x ' : 'justify-self-start ml-[12px] ml-input-x ') +
|
|
175
|
+
props.className || ''
|
|
176
|
+
}
|
|
177
|
+
>{icon}</div>
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function ColorSvg({ hex }: { hex?: string }) {
|
|
182
|
+
return (
|
|
183
|
+
<span class="block size-full rounded-md" style={{ backgroundColor: hex ? hex : '#f1f1f1' }}></span>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function isFieldCached(prev: IsFieldCachedProps, next: IsFieldCachedProps) {
|
|
188
|
+
// Check if the field is cached, onChange/onInputChange doesn't affect the cache
|
|
189
|
+
const path = prev.name
|
|
190
|
+
const prevState = prev.state || {}
|
|
191
|
+
const nextState = next.state || {}
|
|
192
|
+
const errorTitle = next.errorTitle || path
|
|
193
|
+
|
|
194
|
+
// Check if any prop has changed, except `onChange`/`onInputChange`
|
|
195
|
+
const allKeys = new Set([...Object.keys(prev), ...Object.keys(next)])
|
|
196
|
+
for (const k of allKeys) {
|
|
197
|
+
if (k === 'state' || k === 'onChange' || k === 'onInputChange') continue
|
|
198
|
+
if (prev[k as keyof typeof prev] !== next[k as keyof typeof next]) {
|
|
199
|
+
// console.log(4, 'changed', path, k)
|
|
200
|
+
return false
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// If `deps` have changed, handy for onChange/onInputChange, re-render!
|
|
205
|
+
if ((next.deps?.length !== prev.deps?.length) || next.deps?.some((v, i) => v !== prev.deps?.[i])) return false
|
|
206
|
+
|
|
207
|
+
// If the state value has changed, re-render!
|
|
208
|
+
if (deepFind(prevState, path) !== deepFind(nextState, path)) return false
|
|
209
|
+
|
|
210
|
+
// If the state error has changed, re-render!
|
|
211
|
+
if (getErrorFromState(prevState, errorTitle) !== getErrorFromState(nextState, errorTitle)) return false
|
|
212
|
+
|
|
213
|
+
// All good, use cached version
|
|
214
|
+
return true
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const style = css`
|
|
218
|
+
input {
|
|
219
|
+
appearance: textfield;
|
|
220
|
+
-moz-appearance: textfield;
|
|
221
|
+
}
|
|
222
|
+
input::-webkit-outer-spin-button,
|
|
223
|
+
input::-webkit-inner-spin-button {
|
|
224
|
+
-webkit-appearance: none;
|
|
225
|
+
margin: 0;
|
|
226
|
+
}
|
|
227
|
+
/* tw4 we can use calc to determine the padding-left with css variables...
|
|
228
|
+
.inputt {
|
|
229
|
+
padding-left: calc(var(--input-x) * 2);
|
|
230
|
+
} */
|
|
231
|
+
`
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Errors } from 'nitro-web/types'
|
|
2
|
+
|
|
3
|
+
type FormError = {
|
|
4
|
+
state: { errors?: Errors },
|
|
5
|
+
// display all errors except these field titles, e.g. ['name', 'address']
|
|
6
|
+
fields?: Array<string>,
|
|
7
|
+
className?: string,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function FormError({ state, fields, className }: FormError) {
|
|
11
|
+
// A catch all error element that should be placed next to the submit button
|
|
12
|
+
let error: { title: string, detail: string } | undefined
|
|
13
|
+
for (const item of state.errors || []) {
|
|
14
|
+
if (!item.title || item.title.match(/^(error|invalid)$/i) || (fields && !fields.includes(item.title))) {
|
|
15
|
+
error = item
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
{error ? (
|
|
21
|
+
<div class={`text-danger-foreground mt-1 text-sm nitro-error ${className||''}`}>
|
|
22
|
+
{error.detail}
|
|
23
|
+
</div>
|
|
24
|
+
) : null}
|
|
25
|
+
</>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// todo: finish tailwind conversion
|
|
3
|
+
import * as util from 'nitro-web/util'
|
|
4
|
+
|
|
5
|
+
type LocationProps = {
|
|
6
|
+
clear: boolean
|
|
7
|
+
id?: string
|
|
8
|
+
name: string
|
|
9
|
+
onInput?: (place: Place) => void
|
|
10
|
+
onSelect?: (place: Place) => void
|
|
11
|
+
placeholder?: string
|
|
12
|
+
placeTypes?: string[]
|
|
13
|
+
value?: Place
|
|
14
|
+
googleMapsApiKey: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function Location({ clear, id, name, onInput, onSelect, placeholder, placeTypes, value, googleMapsApiKey }: LocationProps) {
|
|
18
|
+
/**
|
|
19
|
+
* Get location or area of place (requires both 'maps javascript' and 'places' APIs)
|
|
20
|
+
*
|
|
21
|
+
* @param {boolean} clear - clear input after select
|
|
22
|
+
* @param {function(place)} onInput - called when the input value changes, with an
|
|
23
|
+
* empty place, e.g. {full: '...', fullModified: true}
|
|
24
|
+
* @param {function(place)} onSelect - called when a place is selected
|
|
25
|
+
* @param {object} value - {full, line1, ..etc}
|
|
26
|
+
*
|
|
27
|
+
* Handy box tester (see also util.mongoAddKmsToBox())
|
|
28
|
+
* https://www.keene.edu/campus/maps/tool/
|
|
29
|
+
*
|
|
30
|
+
* Returned Google places viewport (area), i.e. `place.geometry.viewport`
|
|
31
|
+
* {
|
|
32
|
+
* Qa: {g: 174.4438160493033, h: 174.9684260722261} == [btmLng, topLng]
|
|
33
|
+
* zb: {g: -37.05901990116617, h: -36.66060184426172} == [btmLat, topLat]
|
|
34
|
+
* }
|
|
35
|
+
*/
|
|
36
|
+
const inputRef = useRef(null)
|
|
37
|
+
const full = (value || {}).full || ''
|
|
38
|
+
const [inputValue, setInputValue] = useState(full)
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!onSelect) console.error('Please pass `onSelect` to location.jsx')
|
|
42
|
+
let autoComplete
|
|
43
|
+
loadGoogleMaps(googleMapsApiKey).then(() => {
|
|
44
|
+
if (inputRef.current) {
|
|
45
|
+
autoComplete = new window.google.maps.places.Autocomplete(inputRef.current, {
|
|
46
|
+
types: placeTypes ? placeTypes : ['address'],
|
|
47
|
+
componentRestrictions: { country: ['nz'] },
|
|
48
|
+
})
|
|
49
|
+
autoComplete.setFields(['address_components', 'formatted_address', 'geometry'])
|
|
50
|
+
autoComplete.addListener('place_changed', onPlaceSelect)
|
|
51
|
+
inputRef.current.addEventListener('keydown', onKeyDown)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
return () => {
|
|
55
|
+
// It seems like autoComplete cleans up both listeners, handy links if needing to remove sooner..
|
|
56
|
+
// Cleanup listners: https://stackoverflow.com/a/22862011/1900648
|
|
57
|
+
// Cleanup .pac-container: https://stackoverflow.com/a/21419890/1900648
|
|
58
|
+
for (const elem of document.getElementsByClassName('pac-container')) elem.remove()
|
|
59
|
+
}
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (full !== inputValue) setInputValue(full)
|
|
64
|
+
}, [full])
|
|
65
|
+
|
|
66
|
+
function formatAddressObject(place) {
|
|
67
|
+
console.log(place)
|
|
68
|
+
var addressMap = {
|
|
69
|
+
city: ['locality'],
|
|
70
|
+
country: ['country'],
|
|
71
|
+
number: ['street_number'],
|
|
72
|
+
postcode: ['postal_code'],
|
|
73
|
+
region: [
|
|
74
|
+
'administrative_area_level_1',
|
|
75
|
+
'administrative_area_level_2',
|
|
76
|
+
'administrative_area_level_3',
|
|
77
|
+
'administrative_area_level_4',
|
|
78
|
+
'administrative_area_level_5',
|
|
79
|
+
],
|
|
80
|
+
street: ['street_address', 'route'],
|
|
81
|
+
suburb: [
|
|
82
|
+
'sublocality',
|
|
83
|
+
'sublocality_level_1',
|
|
84
|
+
'sublocality_level_2',
|
|
85
|
+
'sublocality_level_3',
|
|
86
|
+
'sublocality_level_4',
|
|
87
|
+
],
|
|
88
|
+
unit: ['subpremise'],
|
|
89
|
+
}
|
|
90
|
+
var address = {
|
|
91
|
+
city: '',
|
|
92
|
+
country: '',
|
|
93
|
+
number: '',
|
|
94
|
+
postcode: '',
|
|
95
|
+
region: '',
|
|
96
|
+
street: '',
|
|
97
|
+
suburb: '',
|
|
98
|
+
unit: '',
|
|
99
|
+
}
|
|
100
|
+
place.address_components.forEach((component) => {
|
|
101
|
+
for (var key in addressMap) {
|
|
102
|
+
if (addressMap[key].indexOf(component.types[0]) !== -1) {
|
|
103
|
+
address[key] = component.long_name
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
if (!address.city) {
|
|
108
|
+
address.city = address.suburb
|
|
109
|
+
address.suburb = ''
|
|
110
|
+
}
|
|
111
|
+
return address
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function onPlaceSelect() {
|
|
115
|
+
const place = this.getPlace()
|
|
116
|
+
if (!place.geometry) return
|
|
117
|
+
if (clear) setInputValue('')
|
|
118
|
+
else setInputValue(place.formatted_address)
|
|
119
|
+
const addressObject = formatAddressObject(place)
|
|
120
|
+
onSelect({
|
|
121
|
+
city: addressObject.city,
|
|
122
|
+
country: addressObject.country,
|
|
123
|
+
line1: [[addressObject.unit, addressObject.number].filter(o=>o).join('/'), addressObject.street].join(' '),
|
|
124
|
+
line2: [addressObject.suburb, addressObject.postcode].filter(o=>o).join(', '),
|
|
125
|
+
full: place.formatted_address,
|
|
126
|
+
number: addressObject.number,
|
|
127
|
+
postcode: addressObject.postcode,
|
|
128
|
+
suburb: addressObject.suburb,
|
|
129
|
+
location: {
|
|
130
|
+
coordinates: [place.geometry.location.lng(), place.geometry.location.lat()],
|
|
131
|
+
type: 'Point',
|
|
132
|
+
},
|
|
133
|
+
unit: addressObject.unit,
|
|
134
|
+
area: !util.deepFind(place, 'geometry.viewport') ? undefined : {
|
|
135
|
+
bottomLeft: [
|
|
136
|
+
place.geometry.viewport.getSouthWest().lng(),
|
|
137
|
+
place.geometry.viewport.getSouthWest().lat(),
|
|
138
|
+
],
|
|
139
|
+
topRight: [
|
|
140
|
+
place.geometry.viewport.getNorthEast().lng(),
|
|
141
|
+
place.geometry.viewport.getNorthEast().lat(),
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function onChange(event) {
|
|
148
|
+
// On input change
|
|
149
|
+
setInputValue(event.target.value)
|
|
150
|
+
if (onInput) onInput({
|
|
151
|
+
full: event.target.value,
|
|
152
|
+
fullModified: true,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function onFocus(event) {
|
|
157
|
+
// Required to disable the chrome autocomplete, https://stackoverflow.com/a/57131179/4553162
|
|
158
|
+
if (event.target.autocomplete) {
|
|
159
|
+
event.target.autocomplete = 'off'
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function onKeyDown(event) {
|
|
164
|
+
// Stop form submission if there is a google autocomplete dropdown opened
|
|
165
|
+
let prevented
|
|
166
|
+
if (event.key === 'Enter') {
|
|
167
|
+
for (const el of document.getElementsByClassName('pac-container')) {
|
|
168
|
+
if (el.offsetParent !== null && !prevented) { // google autocomplete opened somewhere
|
|
169
|
+
event.preventDefault()
|
|
170
|
+
prevented = true
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<input
|
|
178
|
+
id={id||name}
|
|
179
|
+
name={name||id}
|
|
180
|
+
onChange={onChange}
|
|
181
|
+
onFocus={onFocus}
|
|
182
|
+
placeholder={placeholder}
|
|
183
|
+
ref={inputRef}
|
|
184
|
+
type="text"
|
|
185
|
+
value={inputValue}
|
|
186
|
+
/>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function loadGoogleMaps(googleMapsApiKey) {
|
|
191
|
+
// Requires both 'maps javascript api' and 'places api' within Goolge Cloud Platform
|
|
192
|
+
if (!window.initMap) {
|
|
193
|
+
window.initMap = () => {/*noop to prevent warning*/}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return new Promise((res) => {
|
|
197
|
+
const scriptId = 'googleMapsUrl'
|
|
198
|
+
let script = document.getElementById(scriptId)
|
|
199
|
+
// script not yet inserted
|
|
200
|
+
if (script === null) {
|
|
201
|
+
script = document.createElement('script')
|
|
202
|
+
script.type = 'text/javascript'
|
|
203
|
+
script.id = scriptId
|
|
204
|
+
script.src = `https://maps.googleapis.com/maps/api/js?key=${googleMapsApiKey}`+
|
|
205
|
+
'&libraries=places&callback=initMap'
|
|
206
|
+
script.onload = () => res()
|
|
207
|
+
document.getElementsByTagName('head')[0].appendChild(script)
|
|
208
|
+
// script has already been inserted
|
|
209
|
+
} else {
|
|
210
|
+
// script has already loaded
|
|
211
|
+
if (window.google) {
|
|
212
|
+
res()
|
|
213
|
+
// script hasn't been loaded yet
|
|
214
|
+
} else {
|
|
215
|
+
const cachedCallback = script.onload
|
|
216
|
+
script.onload = () => {
|
|
217
|
+
cachedCallback()
|
|
218
|
+
res()
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Styles are in custom.css
|