@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.
Files changed (97) hide show
  1. package/dist/QuoteForm.js +24 -0
  2. package/dist/Store.js +37 -0
  3. package/dist/StoreLocator.js +34 -0
  4. package/dist/StoreLocatorProvider.js +1 -0
  5. package/dist/elements.css +1 -0
  6. package/dist/index-DvX0QvFh.js +31 -0
  7. package/dist/navigation-DpGLbcKb.js +1 -0
  8. package/dist/store-locator-Bto20jHS.js +1 -0
  9. package/dist/styles.js +1 -0
  10. package/dist/translations-6mspyPRw.js +1 -0
  11. package/dist/useStoreLocator.js +1 -0
  12. package/dist/useStoreLocatorConfig-r4Y_2t-M.js +1 -0
  13. package/package.json +5 -1
  14. package/.pnp.cjs +0 -16484
  15. package/.pnp.loader.mjs +0 -2126
  16. package/.yarn/install-state.gz +0 -0
  17. package/.yarn/releases/yarn-4.12.0.cjs +0 -942
  18. package/.yarnrc.yml +0 -1
  19. package/cypress/fixtures/example.json +0 -5
  20. package/cypress/support/commands.js +0 -25
  21. package/cypress/support/component-index.html +0 -15
  22. package/cypress/support/component.js +0 -26
  23. package/eslint.config.js +0 -32
  24. package/index.html +0 -13
  25. package/playground/index.html +0 -14
  26. package/playground/main.jsx +0 -36
  27. package/src/App.css +0 -0
  28. package/src/App.jsx +0 -65
  29. package/src/components/CollapsibleStoreHours/index.jsx +0 -269
  30. package/src/components/HoursList/index.jsx +0 -225
  31. package/src/components/LeadForm/index.jsx +0 -241
  32. package/src/components/MessageDialog/index.jsx +0 -169
  33. package/src/components/QuoteForm/index.jsx +0 -82
  34. package/src/components/QuoteFormSearch/index.jsx +0 -276
  35. package/src/components/QuoteFormStoreList/index.jsx +0 -65
  36. package/src/components/QuoteFormStoreListItem/index.jsx +0 -134
  37. package/src/components/QuoteLeadForm/index.jsx +0 -16
  38. package/src/components/QuoteMap/index.jsx +0 -96
  39. package/src/components/QuoteMapMarker/index.jsx +0 -56
  40. package/src/components/StaticMap/index.jsx +0 -24
  41. package/src/components/Store/index.jsx +0 -44
  42. package/src/components/StoreContact/index.jsx +0 -96
  43. package/src/components/StoreInfo/index.jsx +0 -50
  44. package/src/components/StoreList/index.jsx +0 -59
  45. package/src/components/StoreListItem/index.jsx +0 -99
  46. package/src/components/StoreListItem/indexStoreListItem.cy.jsx +0 -30
  47. package/src/components/StoreListNoneFound/index.jsx +0 -16
  48. package/src/components/StoreLocator/index.jsx +0 -43
  49. package/src/components/StoreLocatorMap/index.jsx +0 -93
  50. package/src/components/StoreLocatorMapMarker/index.jsx +0 -55
  51. package/src/components/StoreLocatorMessageDialog/index.jsx +0 -20
  52. package/src/components/StoreLocatorSearch/index.jsx +0 -316
  53. package/src/components/StoreMap/index.jsx +0 -30
  54. package/src/components/StoreMeta/index.jsx +0 -7
  55. package/src/components/StoreProducts/index.jsx +0 -112
  56. package/src/components/ui/Badge/index.jsx +0 -46
  57. package/src/components/ui/Button/index.jsx +0 -56
  58. package/src/components/ui/Button/indexButton.cy.jsx +0 -9
  59. package/src/components/ui/Card/index.jsx +0 -90
  60. package/src/components/ui/Input/index.jsx +0 -19
  61. package/src/components/ui/Input/indexInput.cy.jsx +0 -9
  62. package/src/components/ui/LoadingPuff/index.jsx +0 -10
  63. package/src/components/ui/Panel/index.jsx +0 -23
  64. package/src/components/ui/PhoneNumberInput/index.jsx +0 -17
  65. package/src/contexts/quote-form.jsx +0 -94
  66. package/src/contexts/store-locator.jsx +0 -83
  67. package/src/contexts/store.jsx +0 -59
  68. package/src/contexts/translations.jsx +0 -11
  69. package/src/dist.css +0 -229
  70. package/src/entries/QuoteForm.js +0 -2
  71. package/src/entries/Store.js +0 -2
  72. package/src/entries/StoreLocator.js +0 -2
  73. package/src/entries/StoreLocatorProvider.js +0 -2
  74. package/src/entries/styles.js +0 -2
  75. package/src/entries/useStoreLocator.js +0 -2
  76. package/src/i18n/defaultResources.js +0 -19
  77. package/src/i18n/index.js +0 -44
  78. package/src/i18n/mergeResources.js +0 -22
  79. package/src/index.css +0 -214
  80. package/src/lib/addressComponentsToAddress.js +0 -43
  81. package/src/lib/productSchema.js +0 -6
  82. package/src/lib/useGeolocation.js +0 -266
  83. package/src/lib/useHours.js +0 -205
  84. package/src/lib/usePlacesAutocomplete.js +0 -288
  85. package/src/lib/useProductAvailability.js +0 -38
  86. package/src/lib/useRudderAnalytics.js +0 -50
  87. package/src/lib/useSearchResults.js +0 -102
  88. package/src/lib/useStoreLocatorConfig.js +0 -50
  89. package/src/lib/utils/cn.js +0 -6
  90. package/src/lib/utils/measure.js +0 -31
  91. package/src/locales/en/default.json +0 -58
  92. package/src/locales/es/default.json +0 -58
  93. package/src/locales/fr/default.json +0 -58
  94. package/src/locales/it/default.json +0 -58
  95. package/src/main.jsx +0 -10
  96. package/vite.config.js +0 -67
  97. /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;
@@ -1,6 +0,0 @@
1
- const productSchema = {
2
- type: 'object',
3
- properties: {
4
- sku: { type: 'string' },
5
- },
6
- }
@@ -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
-