@colabcommerce/elements 0.9.1 → 0.9.3
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/dist/QuoteForm.js +24 -0
- package/dist/Store.js +37 -0
- package/dist/StoreLocator.js +34 -0
- package/dist/StoreLocatorProvider.js +1 -0
- package/dist/elements.css +1 -0
- package/dist/index-DvX0QvFh.js +31 -0
- package/dist/navigation-DpGLbcKb.js +1 -0
- package/dist/store-locator-Bto20jHS.js +1 -0
- package/dist/styles.js +1 -0
- package/dist/translations-6mspyPRw.js +1 -0
- package/dist/useStoreLocator.js +1 -0
- package/dist/useStoreLocatorConfig-r4Y_2t-M.js +1 -0
- package/package.json +5 -1
- package/.pnp.cjs +0 -16484
- package/.pnp.loader.mjs +0 -2126
- package/.yarn/install-state.gz +0 -0
- package/.yarn/releases/yarn-4.12.0.cjs +0 -942
- package/.yarnrc.yml +0 -1
- package/cypress/fixtures/example.json +0 -5
- package/cypress/support/commands.js +0 -25
- package/cypress/support/component-index.html +0 -15
- package/cypress/support/component.js +0 -26
- package/eslint.config.js +0 -32
- package/index.html +0 -13
- package/playground/index.html +0 -14
- package/playground/main.jsx +0 -36
- package/src/App.css +0 -0
- package/src/App.jsx +0 -65
- package/src/components/CollapsibleStoreHours/index.jsx +0 -269
- package/src/components/HoursList/index.jsx +0 -225
- package/src/components/LeadForm/index.jsx +0 -241
- package/src/components/MessageDialog/index.jsx +0 -169
- package/src/components/QuoteForm/index.jsx +0 -82
- package/src/components/QuoteFormSearch/index.jsx +0 -276
- package/src/components/QuoteFormStoreList/index.jsx +0 -65
- package/src/components/QuoteFormStoreListItem/index.jsx +0 -134
- package/src/components/QuoteLeadForm/index.jsx +0 -16
- package/src/components/QuoteMap/index.jsx +0 -96
- package/src/components/QuoteMapMarker/index.jsx +0 -56
- package/src/components/StaticMap/index.jsx +0 -24
- package/src/components/Store/index.jsx +0 -44
- package/src/components/StoreContact/index.jsx +0 -96
- package/src/components/StoreInfo/index.jsx +0 -50
- package/src/components/StoreList/index.jsx +0 -59
- package/src/components/StoreListItem/index.jsx +0 -99
- package/src/components/StoreListItem/indexStoreListItem.cy.jsx +0 -30
- package/src/components/StoreListNoneFound/index.jsx +0 -16
- package/src/components/StoreLocator/index.jsx +0 -43
- package/src/components/StoreLocatorMap/index.jsx +0 -93
- package/src/components/StoreLocatorMapMarker/index.jsx +0 -55
- package/src/components/StoreLocatorMessageDialog/index.jsx +0 -20
- package/src/components/StoreLocatorSearch/index.jsx +0 -316
- package/src/components/StoreMap/index.jsx +0 -30
- package/src/components/StoreMeta/index.jsx +0 -7
- package/src/components/StoreProducts/index.jsx +0 -112
- package/src/components/ui/Badge/index.jsx +0 -46
- package/src/components/ui/Button/index.jsx +0 -56
- package/src/components/ui/Button/indexButton.cy.jsx +0 -9
- package/src/components/ui/Card/index.jsx +0 -90
- package/src/components/ui/Input/index.jsx +0 -19
- package/src/components/ui/Input/indexInput.cy.jsx +0 -9
- package/src/components/ui/LoadingPuff/index.jsx +0 -10
- package/src/components/ui/Panel/index.jsx +0 -23
- package/src/components/ui/PhoneNumberInput/index.jsx +0 -17
- package/src/contexts/quote-form.jsx +0 -94
- package/src/contexts/store-locator.jsx +0 -83
- package/src/contexts/store.jsx +0 -59
- package/src/contexts/translations.jsx +0 -11
- package/src/dist.css +0 -229
- package/src/entries/QuoteForm.js +0 -2
- package/src/entries/Store.js +0 -2
- package/src/entries/StoreLocator.js +0 -2
- package/src/entries/StoreLocatorProvider.js +0 -2
- package/src/entries/styles.js +0 -2
- package/src/entries/useStoreLocator.js +0 -2
- package/src/i18n/defaultResources.js +0 -19
- package/src/i18n/index.js +0 -44
- package/src/i18n/mergeResources.js +0 -22
- package/src/index.css +0 -214
- package/src/lib/addressComponentsToAddress.js +0 -43
- package/src/lib/productSchema.js +0 -6
- package/src/lib/useGeolocation.js +0 -266
- package/src/lib/useHours.js +0 -205
- package/src/lib/usePlacesAutocomplete.js +0 -288
- package/src/lib/useProductAvailability.js +0 -38
- package/src/lib/useRudderAnalytics.js +0 -50
- package/src/lib/useSearchResults.js +0 -102
- package/src/lib/useStoreLocatorConfig.js +0 -50
- package/src/lib/utils/cn.js +0 -6
- package/src/lib/utils/measure.js +0 -31
- package/src/locales/en/default.json +0 -58
- package/src/locales/es/default.json +0 -58
- package/src/locales/fr/default.json +0 -58
- package/src/locales/it/default.json +0 -58
- package/src/main.jsx +0 -10
- package/vite.config.js +0 -67
- /package/{public → dist}/vite.svg +0 -0
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
const modules = import.meta.glob("../locales/*/*.json", { eager: true })
|
|
2
|
-
|
|
3
|
-
export function getDefaultResources() {
|
|
4
|
-
const resources = {}
|
|
5
|
-
|
|
6
|
-
for (const path in modules) {
|
|
7
|
-
const match = path.match(/\.\.\/locales\/([^/]+)\/([^/]+)\.json$/)
|
|
8
|
-
if (!match) continue
|
|
9
|
-
|
|
10
|
-
const [, lng, ns] = match
|
|
11
|
-
const mod = modules[path]
|
|
12
|
-
const json = mod.default ?? mod
|
|
13
|
-
|
|
14
|
-
resources[lng] ||= {}
|
|
15
|
-
resources[lng][ns] = json
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return resources
|
|
19
|
-
}
|
package/src/i18n/index.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import i18n from "i18next"
|
|
2
|
-
import { initReactI18next } from "react-i18next"
|
|
3
|
-
import { getDefaultResources } from "./defaultResources"
|
|
4
|
-
import { mergeResources } from "./mergeResources"
|
|
5
|
-
|
|
6
|
-
export function initLibraryI18n(opts = {}) {
|
|
7
|
-
const defaults = opts.disableDefaults ? {} : getDefaultResources()
|
|
8
|
-
const resources = mergeResources(defaults, opts.resources)
|
|
9
|
-
|
|
10
|
-
if (!i18n.isInitialized) {
|
|
11
|
-
i18n
|
|
12
|
-
.use(initReactI18next)
|
|
13
|
-
.init({
|
|
14
|
-
resources,
|
|
15
|
-
lng: opts.lng,
|
|
16
|
-
fallbackLng: opts.fallbackLng ?? "en",
|
|
17
|
-
ns: opts.ns ?? Object.keys(resources[opts.fallbackLng ?? "en"] || { common: true }),
|
|
18
|
-
defaultNS: opts.defaultNS ?? "common",
|
|
19
|
-
interpolation: { escapeValue: false },
|
|
20
|
-
...opts.i18next,
|
|
21
|
-
})
|
|
22
|
-
} else {
|
|
23
|
-
// If already initialized, just add/overwrite resources safely
|
|
24
|
-
for (const [lng, namespaces] of Object.entries(resources)) {
|
|
25
|
-
for (const [ns, bundle] of Object.entries(namespaces)) {
|
|
26
|
-
i18n.addResourceBundle(lng, ns, bundle, true, true) // deep merge + overwrite
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (opts.lng) i18n.changeLanguage(opts.lng)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return i18n
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Optional helper to add/override resources after init.
|
|
37
|
-
*/
|
|
38
|
-
export function addLibraryResources(resources) {
|
|
39
|
-
for (const [lng, namespaces] of Object.entries(resources)) {
|
|
40
|
-
for (const [ns, bundle] of Object.entries(namespaces)) {
|
|
41
|
-
i18n.addResourceBundle(lng, ns, bundle, true, true)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
function isPlainObject(v) {
|
|
2
|
-
return v && typeof v === "object" && !Array.isArray(v)
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export function deepMerge(base, override) {
|
|
6
|
-
if (!isPlainObject(base) || !isPlainObject(override)) return override ?? base
|
|
7
|
-
|
|
8
|
-
const out = { ...base }
|
|
9
|
-
for (const key of Object.keys(override)) {
|
|
10
|
-
out[key] =
|
|
11
|
-
key in out ? deepMerge(out[key], override[key]) : override[key]
|
|
12
|
-
}
|
|
13
|
-
return out
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function mergeResources(
|
|
17
|
-
defaults,
|
|
18
|
-
user
|
|
19
|
-
) {
|
|
20
|
-
if (!user) return defaults
|
|
21
|
-
return deepMerge(defaults, user)
|
|
22
|
-
}
|
package/src/index.css
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
/* This works for build, not dev. */
|
|
2
|
-
/* .cc {
|
|
3
|
-
@import "tailwindcss";
|
|
4
|
-
@import "react-phone-number-input/style.css";
|
|
5
|
-
} */
|
|
6
|
-
|
|
7
|
-
/* This can't be part of build as it leaks to the global scope. */
|
|
8
|
-
@import "tailwindcss";
|
|
9
|
-
@import "react-phone-number-input/style.css";
|
|
10
|
-
/* end */
|
|
11
|
-
|
|
12
|
-
@import "tw-animate-css";
|
|
13
|
-
|
|
14
|
-
/* Keep this if you want to continue using `dark:` variants on descendants */
|
|
15
|
-
@custom-variant dark (&:is(.cc.dark *));
|
|
16
|
-
|
|
17
|
-
.cc {
|
|
18
|
-
|
|
19
|
-
--background: #fdfdfd;
|
|
20
|
-
--foreground: #000000;
|
|
21
|
-
--color-black: #000000;
|
|
22
|
-
--color-white: #ffffff;
|
|
23
|
-
--card: #fdfdfd;
|
|
24
|
-
--card-foreground: #000000;
|
|
25
|
-
--popover: #fcfcfc;
|
|
26
|
-
--popover-foreground: #000000;
|
|
27
|
-
--primary: #7033ff;
|
|
28
|
-
--primary-foreground: #ffffff;
|
|
29
|
-
--secondary: #edf0f4;
|
|
30
|
-
--secondary-foreground: #080808;
|
|
31
|
-
--muted: #f5f5f5;
|
|
32
|
-
--muted-foreground: #525252;
|
|
33
|
-
--accent: #e2ebff;
|
|
34
|
-
--accent-foreground: #1e69dc;
|
|
35
|
-
--destructive: #e54b4f;
|
|
36
|
-
--destructive-foreground: #ffffff;
|
|
37
|
-
--border: #e7e7ee;
|
|
38
|
-
--input: #ebebeb;
|
|
39
|
-
--ring: #000000;
|
|
40
|
-
--chart-1: #4ac885;
|
|
41
|
-
--chart-2: #7033ff;
|
|
42
|
-
--chart-3: #fd822b;
|
|
43
|
-
--chart-4: #3276e4;
|
|
44
|
-
--chart-5: #747474;
|
|
45
|
-
--radius: 1.4rem;
|
|
46
|
-
--sidebar: #f5f8fb;
|
|
47
|
-
--sidebar-foreground: #000000;
|
|
48
|
-
--sidebar-primary: #000000;
|
|
49
|
-
--sidebar-primary-foreground: #ffffff;
|
|
50
|
-
--sidebar-accent: #ebebeb;
|
|
51
|
-
--sidebar-accent-foreground: #000000;
|
|
52
|
-
--sidebar-border: #ebebeb;
|
|
53
|
-
--sidebar-ring: #000000;
|
|
54
|
-
--shadow-x: 0px;
|
|
55
|
-
--shadow-y: 2px;
|
|
56
|
-
--shadow-blur: 3px;
|
|
57
|
-
--shadow-spread: 0px;
|
|
58
|
-
--shadow-opacity: 0.2;
|
|
59
|
-
--shadow-color: #000000;
|
|
60
|
-
|
|
61
|
-
/* ===== Scrollbar CSS ===== */
|
|
62
|
-
/* Firefox */
|
|
63
|
-
* {
|
|
64
|
-
scrollbar-width: thin;
|
|
65
|
-
scrollbar-color: #858585 #ffffff;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/* Chrome, Edge, and Safari */
|
|
69
|
-
*::-webkit-scrollbar {
|
|
70
|
-
width: 8px;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
*::-webkit-scrollbar-track {
|
|
74
|
-
background: #ffffff;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
*::-webkit-scrollbar-thumb {
|
|
78
|
-
background-color: #858585;
|
|
79
|
-
border-radius: 4px;
|
|
80
|
-
border: 3px solid #ffffff;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/* Phone Input Override */
|
|
84
|
-
.PhoneInputCountry {
|
|
85
|
-
@apply absolute left-8 top-0 h-12 flex items-center gap-2 px-3 bg-transparent;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
.cc.dark {
|
|
90
|
-
--background: #1a1b1e;
|
|
91
|
-
--foreground: #f0f0f0;
|
|
92
|
-
--card: #222327;
|
|
93
|
-
--card-foreground: #f0f0f0;
|
|
94
|
-
--popover: #222327;
|
|
95
|
-
--popover-foreground: #f0f0f0;
|
|
96
|
-
--primary: #8c5cff;
|
|
97
|
-
--primary-foreground: #ffffff;
|
|
98
|
-
--secondary: #2a2c33;
|
|
99
|
-
--secondary-foreground: #f0f0f0;
|
|
100
|
-
--muted: #2a2c33;
|
|
101
|
-
--muted-foreground: #a0a0a0;
|
|
102
|
-
--accent: #1e293b;
|
|
103
|
-
--accent-foreground: #79c0ff;
|
|
104
|
-
--destructive: #f87171;
|
|
105
|
-
--destructive-foreground: #ffffff;
|
|
106
|
-
--border: #33353a;
|
|
107
|
-
--input: #33353a;
|
|
108
|
-
--ring: #8c5cff;
|
|
109
|
-
--chart-1: #4ade80;
|
|
110
|
-
--chart-2: #8c5cff;
|
|
111
|
-
--chart-3: #fca5a5;
|
|
112
|
-
--chart-4: #5993f4;
|
|
113
|
-
--chart-5: #a0a0a0;
|
|
114
|
-
--sidebar: #161618;
|
|
115
|
-
--sidebar-foreground: #f0f0f0;
|
|
116
|
-
--sidebar-primary: #8c5cff;
|
|
117
|
-
--sidebar-primary-foreground: #ffffff;
|
|
118
|
-
--sidebar-accent: #2a2c33;
|
|
119
|
-
--sidebar-accent-foreground: #8c5cff;
|
|
120
|
-
--sidebar-border: #33353a;
|
|
121
|
-
--sidebar-ring: #8c5cff;
|
|
122
|
-
--shadow-x: 0px;
|
|
123
|
-
--shadow-y: 2px;
|
|
124
|
-
--shadow-blur: 3px;
|
|
125
|
-
--shadow-spread: 0px;
|
|
126
|
-
--shadow-opacity: 0.2;
|
|
127
|
-
--shadow-color: #000000;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.cc {
|
|
131
|
-
@theme inline {
|
|
132
|
-
/* core colors */
|
|
133
|
-
--color-background: var(--background);
|
|
134
|
-
--color-foreground: var(--foreground);
|
|
135
|
-
|
|
136
|
-
--color-card: var(--card);
|
|
137
|
-
--color-card-foreground: var(--card-foreground);
|
|
138
|
-
|
|
139
|
-
--color-popover: var(--popover);
|
|
140
|
-
--color-popover-foreground: var(--popover-foreground);
|
|
141
|
-
|
|
142
|
-
--color-primary: var(--primary);
|
|
143
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
144
|
-
|
|
145
|
-
--color-secondary: var(--secondary);
|
|
146
|
-
--color-secondary-foreground: var(--secondary-foreground);
|
|
147
|
-
|
|
148
|
-
--color-muted: var(--muted);
|
|
149
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
150
|
-
|
|
151
|
-
--color-accent: var(--accent);
|
|
152
|
-
--color-accent-foreground: var(--accent-foreground);
|
|
153
|
-
|
|
154
|
-
--color-destructive: var(--destructive);
|
|
155
|
-
--color-destructive-foreground: var(--destructive-foreground);
|
|
156
|
-
|
|
157
|
-
/* borders & inputs */
|
|
158
|
-
--color-border: var(--border);
|
|
159
|
-
--color-input: var(--input);
|
|
160
|
-
--color-ring: var(--ring);
|
|
161
|
-
|
|
162
|
-
/* charts */
|
|
163
|
-
--color-chart-1: var(--chart-1);
|
|
164
|
-
--color-chart-2: var(--chart-2);
|
|
165
|
-
--color-chart-3: var(--chart-3);
|
|
166
|
-
--color-chart-4: var(--chart-4);
|
|
167
|
-
--color-chart-5: var(--chart-5);
|
|
168
|
-
|
|
169
|
-
/* sidebar */
|
|
170
|
-
--color-sidebar: var(--sidebar);
|
|
171
|
-
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
172
|
-
--color-sidebar-primary: var(--sidebar-primary);
|
|
173
|
-
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
174
|
-
--color-sidebar-accent: var(--sidebar-accent);
|
|
175
|
-
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
176
|
-
--color-sidebar-border: var(--sidebar-border);
|
|
177
|
-
--color-sidebar-ring: var(--sidebar-ring);
|
|
178
|
-
|
|
179
|
-
/* radius */
|
|
180
|
-
--radius-sm: calc(var(--radius) - 4px);
|
|
181
|
-
--radius-md: calc(var(--radius) - 2px);
|
|
182
|
-
--radius-lg: var(--radius);
|
|
183
|
-
--radius-xl: calc(var(--radius) + 4px);
|
|
184
|
-
|
|
185
|
-
/* shadows (optional but nice) */
|
|
186
|
-
--shadow-2xs: var(--shadow-x) var(--shadow-y) color-mix(in srgb, var(--shadow-color) calc(var(--shadow-opacity) * 100%), transparent);
|
|
187
|
-
|
|
188
|
-
--shadow-xs: var(--shadow-x) var(--shadow-y) var(--shadow-blur) var(--shadow-spread) color-mix(in srgb, var(--shadow-color) calc(var(--shadow-opacity) * 100%), transparent);
|
|
189
|
-
|
|
190
|
-
--shadow-sm: var(--shadow-x) var(--shadow-y) calc(var(--shadow-blur) * 1.5) var(--shadow-spread) color-mix(in srgb, var(--shadow-color) calc(var(--shadow-opacity) * 100%), transparent);
|
|
191
|
-
|
|
192
|
-
--shadow-md: calc(var(--shadow-x) * 2) calc(var(--shadow-y) * 2) calc(var(--shadow-blur) * 3) calc(var(--shadow-spread) - 1px) color-mix(in srgb, var(--shadow-color) calc(var(--shadow-opacity) * 100%), transparent);
|
|
193
|
-
|
|
194
|
-
--shadow-lg: calc(var(--shadow-x) * 4) calc(var(--shadow-y) * 4) calc(var(--shadow-blur) * 5) calc(var(--shadow-spread) - 3px) color-mix(in srgb, var(--shadow-color) calc(var(--shadow-opacity) * 100%), transparent);
|
|
195
|
-
|
|
196
|
-
--shadow-xl: calc(var(--shadow-x) * 6) calc(var(--shadow-y) * 6) calc(var(--shadow-blur) * 7) calc(var(--shadow-spread) - 5px) color-mix(in srgb, var(--shadow-color) calc(var(--shadow-opacity) * 100%), transparent);
|
|
197
|
-
|
|
198
|
-
--shadow-2xl: calc(var(--shadow-x) * 8) calc(var(--shadow-y) * 8) calc(max(0px, var(--shadow-blur) * 9 - 8px)) calc(var(--shadow-spread) - 12px) color-mix(in srgb, var(--shadow-color) calc(var(--shadow-opacity) * 250%), transparent);
|
|
199
|
-
|
|
200
|
-
--shadow: var(--shadow-md);
|
|
201
|
-
--color-shadow-color: var(--shadow-color);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/* Scope base styles to the wrapper (no global leakage) */
|
|
206
|
-
@layer base {
|
|
207
|
-
.cc {
|
|
208
|
-
@apply bg-background text-foreground;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
.cc * {
|
|
212
|
-
@apply border-border outline-ring/50;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
function addressComponentsToAddress(components) {
|
|
2
|
-
// Helper to extract a component by type
|
|
3
|
-
const get = (type) =>
|
|
4
|
-
components.find((c) => c.types.includes(type))?.long_name || "";
|
|
5
|
-
|
|
6
|
-
// Line one: street number + route
|
|
7
|
-
const streetNumber = get("street_number");
|
|
8
|
-
const route = get("route");
|
|
9
|
-
const addressLine1 = [streetNumber, route].filter(Boolean).join(" ");
|
|
10
|
-
|
|
11
|
-
// Line two: subpremise, suite, unit, etc.
|
|
12
|
-
const subpremise = get("subpremise");
|
|
13
|
-
const addressLine2 = subpremise ? `Unit ${subpremise}` : "";
|
|
14
|
-
|
|
15
|
-
// City (Google may return locality or postal_town depending on country)
|
|
16
|
-
const city = get("locality") || get("postal_town") || get("sublocality") || "";
|
|
17
|
-
|
|
18
|
-
// Province/state/region
|
|
19
|
-
const province = get("administrative_area_level_1");
|
|
20
|
-
|
|
21
|
-
// Postal/ZIP code
|
|
22
|
-
const postalCode = get("postal_code");
|
|
23
|
-
|
|
24
|
-
// Country
|
|
25
|
-
const country = get("country");
|
|
26
|
-
|
|
27
|
-
// Update the country to be the short name (ISO code) if available
|
|
28
|
-
const countryShort = components.find((c) => c.types.includes("country"))?.short_name;
|
|
29
|
-
const finalCountry = countryShort || country;
|
|
30
|
-
|
|
31
|
-
// Return the structured address
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
addressLine1,
|
|
35
|
-
addressLine2,
|
|
36
|
-
city,
|
|
37
|
-
province,
|
|
38
|
-
postalCode,
|
|
39
|
-
country: finalCountry,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export default addressComponentsToAddress;
|
package/src/lib/productSchema.js
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) 2025 Colab Commerce <https://colabcommerce.com>
|
|
3
|
-
* All rights reserved.
|
|
4
|
-
*
|
|
5
|
-
* This hook provides geolocation using the browser's Geolocation API. As well
|
|
6
|
-
* as an IP based fallback based on ipapi.co lookups
|
|
7
|
-
* Usage:
|
|
8
|
-
* const { location, source, isLoading, error, refresh, isGeolocationAvailable, isGeolocationEnabled } = useGeolocation();
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
12
|
-
import { useGeolocated } from 'react-geolocated'
|
|
13
|
-
import { setKey, fromLatLng } from 'react-geocode'
|
|
14
|
-
import addressComponentsToAddress from '@/lib/addressComponentsToAddress'
|
|
15
|
-
|
|
16
|
-
const IPAPI_URL = 'https://ipapi.co/json?key=kGu4vlFKun6lmweTaFlSP54Ypbq1yWmJlPIy0qCxC7shPBO1qQ'
|
|
17
|
-
|
|
18
|
-
function toError(value) {
|
|
19
|
-
if (value instanceof Error) return value
|
|
20
|
-
if (typeof value === 'string') return new Error(value)
|
|
21
|
-
try {
|
|
22
|
-
return new Error(JSON.stringify(value))
|
|
23
|
-
} catch {
|
|
24
|
-
return new Error('Unknown error')
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function coordsToLocation(coords) {
|
|
29
|
-
if (!coords) return null
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
latitude: coords.latitude,
|
|
33
|
-
longitude: coords.longitude,
|
|
34
|
-
accuracy: coords.accuracy,
|
|
35
|
-
altitude: coords.altitude ?? null,
|
|
36
|
-
altitudeAccuracy: coords.altitudeAccuracy ?? null,
|
|
37
|
-
heading: coords.heading ?? null,
|
|
38
|
-
speed: coords.speed ?? null,
|
|
39
|
-
city: null,
|
|
40
|
-
region: null,
|
|
41
|
-
country: null,
|
|
42
|
-
postal: null,
|
|
43
|
-
timezone: null,
|
|
44
|
-
ip: null,
|
|
45
|
-
raw: coords,
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function ipapiToLocation(payload) {
|
|
50
|
-
if (!payload || typeof payload !== 'object') return null
|
|
51
|
-
|
|
52
|
-
const latitude = Number(payload.latitude)
|
|
53
|
-
const longitude = Number(payload.longitude)
|
|
54
|
-
|
|
55
|
-
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
latitude,
|
|
59
|
-
longitude,
|
|
60
|
-
city: payload.city ?? null,
|
|
61
|
-
region: payload.region ?? null,
|
|
62
|
-
country: payload.country_name ?? payload.country ?? null,
|
|
63
|
-
postal: payload.postal ?? null,
|
|
64
|
-
timezone: payload.timezone ?? null,
|
|
65
|
-
ip: payload.ip ?? null,
|
|
66
|
-
raw: payload,
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export default function useGeolocation(options = {}) {
|
|
71
|
-
const {
|
|
72
|
-
userDecisionTimeout = 10_000,
|
|
73
|
-
autoGeoLocate = true,
|
|
74
|
-
positionOptions = { enableHighAccuracy: false, maximumAge: 0, timeout: 10_000 },
|
|
75
|
-
} = options
|
|
76
|
-
|
|
77
|
-
const {
|
|
78
|
-
coords,
|
|
79
|
-
isGeolocationAvailable,
|
|
80
|
-
isGeolocationEnabled,
|
|
81
|
-
positionError,
|
|
82
|
-
getPosition,
|
|
83
|
-
} = useGeolocated({
|
|
84
|
-
positionOptions,
|
|
85
|
-
userDecisionTimeout,
|
|
86
|
-
suppressLocationOnMount: true,
|
|
87
|
-
watchLocationPermissionChange: true,
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
const [location, setLocation] = useState(null)
|
|
91
|
-
const [source, setSource] = useState(null) // 'geolocation' | 'ipapi' | null
|
|
92
|
-
const [isLoading, setIsLoading] = useState(true)
|
|
93
|
-
const [error, setError] = useState(null)
|
|
94
|
-
|
|
95
|
-
const requestIdRef = useRef(0)
|
|
96
|
-
const ipapiAttemptedRequestIdRef = useRef(null)
|
|
97
|
-
const ipapiAbortRef = useRef(null)
|
|
98
|
-
const autoStartedRef = useRef(false)
|
|
99
|
-
|
|
100
|
-
const stopIpapi = useCallback(() => {
|
|
101
|
-
if (ipapiAbortRef.current) {
|
|
102
|
-
console.log('stopIpapi: aborting inflight ipapi request')
|
|
103
|
-
ipapiAbortRef.current.abort()
|
|
104
|
-
ipapiAbortRef.current = null
|
|
105
|
-
}
|
|
106
|
-
}, [])
|
|
107
|
-
|
|
108
|
-
const fetchIpapi = useCallback(async (requestId, reasonError = null) => {
|
|
109
|
-
// Important: only attempt the IP fallback once per refresh cycle.
|
|
110
|
-
// Otherwise, if `positionError` stays set and IP lookup fails,
|
|
111
|
-
// the fallback effect can re-trigger infinitely.
|
|
112
|
-
console.log('fetchIpapi called', { requestId, currentRequestId: requestIdRef.current, attemptedRequestId: ipapiAttemptedRequestIdRef.current })
|
|
113
|
-
if (ipapiAttemptedRequestIdRef.current === requestId) return
|
|
114
|
-
ipapiAttemptedRequestIdRef.current = requestId
|
|
115
|
-
|
|
116
|
-
//stopIpapi()
|
|
117
|
-
const controller = new AbortController()
|
|
118
|
-
ipapiAbortRef.current = controller
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
const res = await fetch(IPAPI_URL, {
|
|
122
|
-
method: 'GET',
|
|
123
|
-
headers: { Accept: 'application/json' },
|
|
124
|
-
cache: 'default',
|
|
125
|
-
signal: controller.signal,
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
if (!res.ok) {
|
|
129
|
-
throw new Error(`ipapi request failed: ${res.status} ${res.statusText}`)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const payload = await res.json()
|
|
133
|
-
const ipLocation = ipapiToLocation(payload)
|
|
134
|
-
if (!ipLocation) {
|
|
135
|
-
throw new Error('ipapi response did not include a valid latitude/longitude')
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
//if (requestIdRef.current !== requestId) return
|
|
139
|
-
setLocation(ipLocation)
|
|
140
|
-
setSource('ipapi')
|
|
141
|
-
setIsLoading(false)
|
|
142
|
-
setError(null)
|
|
143
|
-
} catch (err) {
|
|
144
|
-
if (controller.signal.aborted) return
|
|
145
|
-
if (requestIdRef.current !== requestId) return
|
|
146
|
-
|
|
147
|
-
const normalized = toError(err)
|
|
148
|
-
setIsLoading(false)
|
|
149
|
-
//setSource(null)
|
|
150
|
-
//setLocation(null)
|
|
151
|
-
setError(reasonError ? toError(reasonError) : normalized)
|
|
152
|
-
} finally {
|
|
153
|
-
if (ipapiAbortRef.current === controller) {
|
|
154
|
-
ipapiAbortRef.current = null
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}, [stopIpapi])
|
|
158
|
-
|
|
159
|
-
const refresh = useCallback(() => {
|
|
160
|
-
requestIdRef.current += 1
|
|
161
|
-
const requestId = requestIdRef.current
|
|
162
|
-
|
|
163
|
-
//stopIpapi()
|
|
164
|
-
setIsLoading(true)
|
|
165
|
-
setError(null)
|
|
166
|
-
setSource(null)
|
|
167
|
-
setLocation(null)
|
|
168
|
-
|
|
169
|
-
if (!isGeolocationAvailable) {
|
|
170
|
-
fetchIpapi(requestId)
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// If browser-level location services are disabled, skip straight to IP fallback.
|
|
175
|
-
if (isGeolocationEnabled === false) {
|
|
176
|
-
fetchIpapi(requestId)
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// If auto geolocation is disabled, skip straight to IP fallback.
|
|
181
|
-
if (!autoGeoLocate) {
|
|
182
|
-
fetchIpapi(requestId)
|
|
183
|
-
return
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
getPosition()
|
|
187
|
-
}, [fetchIpapi, getPosition, isGeolocationAvailable, isGeolocationEnabled, stopIpapi, autoGeoLocate])
|
|
188
|
-
|
|
189
|
-
// Cleanup inflight IP lookup on unmount.
|
|
190
|
-
useEffect(() => {
|
|
191
|
-
return () => stopIpapi()
|
|
192
|
-
}, [stopIpapi])
|
|
193
|
-
|
|
194
|
-
// Kick off the initial request once, when availability is known.
|
|
195
|
-
// (react-geolocated updates its booleans after mount; don't auto-refresh on every update)
|
|
196
|
-
useEffect(() => {
|
|
197
|
-
if (autoStartedRef.current) return
|
|
198
|
-
if (typeof isGeolocationAvailable !== 'boolean') return
|
|
199
|
-
autoStartedRef.current = true
|
|
200
|
-
refresh()
|
|
201
|
-
}, [refresh])
|
|
202
|
-
|
|
203
|
-
// If we get browser coords, prefer them (even if IP fallback already ran).
|
|
204
|
-
const browserLocation = useMemo(() => coordsToLocation(coords), [coords])
|
|
205
|
-
|
|
206
|
-
useEffect(() => {
|
|
207
|
-
if (!browserLocation) return
|
|
208
|
-
setLocation(browserLocation)
|
|
209
|
-
setSource('geolocation')
|
|
210
|
-
setIsLoading(false)
|
|
211
|
-
setError(null)
|
|
212
|
-
}, [browserLocation])
|
|
213
|
-
|
|
214
|
-
// If we get browserLocation and the city is null, we try to augment it with geocoding.
|
|
215
|
-
useEffect(() => {
|
|
216
|
-
async function augmentWithGeocode() {
|
|
217
|
-
if (!browserLocation) return
|
|
218
|
-
if (browserLocation.city) return
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
|
|
222
|
-
setKey('AIzaSyAnJmWEU1r63DiRWHkjczxzHyIEq3dhj4M')
|
|
223
|
-
const geocodeResults = await fromLatLng(browserLocation.latitude, browserLocation.longitude)
|
|
224
|
-
if (!geocodeResults.results || geocodeResults.results.length === 0) return
|
|
225
|
-
|
|
226
|
-
const firstResult = addressComponentsToAddress(geocodeResults?.results[0]?.address_components)
|
|
227
|
-
const augmented = {
|
|
228
|
-
...browserLocation,
|
|
229
|
-
city: firstResult?.city ?? null,
|
|
230
|
-
region: firstResult.province ?? null,
|
|
231
|
-
country: firstResult.country ?? null,
|
|
232
|
-
postal: firstResult.postalCode ?? null,
|
|
233
|
-
}
|
|
234
|
-
setLocation(augmented)
|
|
235
|
-
} catch {
|
|
236
|
-
// Ignore geocoding errors.
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
augmentWithGeocode()
|
|
241
|
-
}, [browserLocation])
|
|
242
|
-
|
|
243
|
-
const requestId = requestIdRef.current
|
|
244
|
-
fetchIpapi(requestId)
|
|
245
|
-
|
|
246
|
-
// If browser geolocation errors (blocked/denied/unavailable/timeout), fall back to IP.
|
|
247
|
-
useEffect(() => {
|
|
248
|
-
if (!positionError) return
|
|
249
|
-
if (source === 'geolocation') return
|
|
250
|
-
if (location) return
|
|
251
|
-
|
|
252
|
-
const requestId = requestIdRef.current
|
|
253
|
-
fetchIpapi(requestId, positionError)
|
|
254
|
-
}, [fetchIpapi, location, positionError, source])
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
location,
|
|
258
|
-
source,
|
|
259
|
-
isLoading,
|
|
260
|
-
error,
|
|
261
|
-
refresh,
|
|
262
|
-
isGeolocationAvailable,
|
|
263
|
-
isGeolocationEnabled,
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|