@cartbase/storefront 0.17.0 → 0.18.0
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/LICENSE +21 -21
- package/package.json +258 -256
- package/src/api/auth.ts +108 -108
- package/src/api/carts.ts +523 -523
- package/src/api/categories.ts +184 -184
- package/src/api/checkout.ts +526 -526
- package/src/api/collections.ts +130 -130
- package/src/api/consent.ts +75 -75
- package/src/api/content.ts +125 -125
- package/src/api/customers.ts +303 -303
- package/src/api/gift-cards.ts +112 -112
- package/src/api/http.ts +180 -180
- package/src/api/index.ts +30 -30
- package/src/api/menus.ts +77 -77
- package/src/api/metaobjects.ts +136 -136
- package/src/api/orders.ts +290 -290
- package/src/api/products.ts +43 -1
- package/src/api/redirects.ts +37 -37
- package/src/api/regions.ts +200 -200
- package/src/api/reviews.ts +259 -259
- package/src/api/search.ts +163 -163
- package/src/api/store.ts +35 -35
- package/src/api/types.ts +91 -91
- package/src/cart-drawer/context.tsx +277 -164
- package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
- package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
- package/src/cart-drawer/gift-wrap.tsx +82 -82
- package/src/cart-drawer/index.ts +71 -68
- package/src/cart-drawer/item/index.tsx +162 -162
- package/src/cart-drawer/item/quantity.tsx +8 -14
- package/src/cart-drawer/item/upsell.tsx +110 -110
- package/src/cart-drawer/labels.ts +123 -123
- package/src/cart-drawer/mutation-queue.ts +78 -0
- package/src/cart-drawer/notes.tsx +131 -131
- package/src/cart-drawer/sticky-footer.tsx +73 -73
- package/src/cart-drawer/summary-breakdown.tsx +197 -197
- package/src/checkout/address-error-copy.ts +117 -117
- package/src/checkout/boxnow-locker-selector.tsx +410 -410
- package/src/checkout/compare-addresses.ts +40 -40
- package/src/checkout/discount-section.tsx +218 -218
- package/src/checkout/error-copy-codes.ts +63 -63
- package/src/checkout/geocode.ts +154 -154
- package/src/checkout/gift-card-section.tsx +224 -224
- package/src/checkout/index.ts +72 -72
- package/src/checkout/labels.ts +495 -495
- package/src/checkout/payment-button.tsx +372 -372
- package/src/checkout/payment-error-copy.ts +154 -154
- package/src/checkout/promotion-error-copy.ts +91 -91
- package/src/checkout/use-checkout-orchestration.ts +1592 -1592
- package/src/common/country-flag.tsx +52 -52
- package/src/common/country-select.tsx +11 -11
- package/src/common/index.ts +20 -20
- package/src/common/market-select.tsx +57 -57
- package/src/index.ts +12 -12
- package/src/lib/country-name.ts +59 -59
- package/src/lib/get-product-price.ts +133 -133
- package/src/lib/media-image.tsx +39 -39
- package/src/lib/payment-constants.ts +53 -53
- package/src/lib/platform.ts +13 -13
- package/src/lib/price.tsx +39 -39
- package/src/lib/store-api-error.ts +36 -36
- package/src/lib/variant-caption.ts +32 -32
- package/src/locales/bg.ts +467 -467
- package/src/locales/context.ts +37 -37
- package/src/locales/en.ts +26 -26
- package/src/locales/es.ts +466 -466
- package/src/locales/index.ts +19 -19
- package/src/locales/provider.tsx +59 -59
- package/src/locales/types.ts +77 -77
- package/src/order/index.ts +62 -62
- package/src/order/labels.ts +79 -79
- package/src/order/order-totals.tsx +250 -250
- package/src/primitives/select-field.tsx +93 -93
- package/src/products/image-gallery.tsx +43 -43
- package/src/products/index.ts +68 -59
- package/src/products/option-select.tsx +67 -67
- package/src/products/product-actions-wrapper.tsx +63 -62
- package/src/products/product-actions.tsx +151 -150
- package/src/products/product-price.tsx +69 -69
- package/src/products/product-promises.tsx +61 -61
- package/src/products/product-specs.tsx +65 -65
- package/src/products/product-tabs.tsx +123 -123
- package/src/products/product-template.tsx +163 -162
- package/src/products/purchase-options.tsx +130 -130
- package/src/products/sets.ts +110 -0
- package/src/products/use-product-actions.ts +199 -164
- package/src/products/variant-matching.ts +71 -71
- package/src/products/variant-url.ts +74 -74
- package/src/reviews-ui/index.ts +73 -73
- package/src/store/category-template.tsx +136 -136
- package/src/store/index.ts +40 -40
- package/src/tracking/chatgpt-pixel.tsx +99 -99
- package/src/tracking/consent-init.tsx +62 -62
- package/src/tracking/events.ts +348 -348
- package/src/tracking/ga4.tsx +93 -93
- package/src/tracking/google-ads.ts +84 -84
- package/src/tracking/gtm.tsx +60 -60
- package/src/tracking/inline-script.ts +49 -49
- package/src/tracking/oaiq.ts +206 -206
- package/src/tracking/tiktok-pixel.tsx +91 -91
- package/src/tracking/track-init.tsx +56 -56
- package/src/tracking/track-order-purchase.tsx +122 -122
- package/src/tracking/ttq.ts +180 -180
- package/src/tracking/use-tracking-config.ts +54 -54
- package/theme/index.css +25 -25
- package/theme/theme.css +150 -150
- package/theme/tokens.css +106 -106
package/src/checkout/geocode.ts
CHANGED
|
@@ -1,154 +1,154 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Geo + Bulgarian place-name helpers for the carrier pickers (Econt office,
|
|
3
|
-
* BoxNow locker).
|
|
4
|
-
*
|
|
5
|
-
* Ported from `@1click/ui/src/lib/geocode.ts` (v2.3.1) verbatim — the lib
|
|
6
|
-
* family of `@cartbase/storefront` does not ship a geocode module, so the
|
|
7
|
-
* checkout family (its only consumer) carries it. Extra file beyond the
|
|
8
|
-
* pre-declared export list → exported from the family barrel only.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* transliterateBgToLatin — Bulgarian Cyrillic → Latin transliteration
|
|
13
|
-
* using the streamlined system (official BG government standard, used
|
|
14
|
-
* on passports and road signs).
|
|
15
|
-
*
|
|
16
|
-
* Purpose: when a Bulgarian user types "Sofia" but the data source has
|
|
17
|
-
* "София", substring matching fails. Normalize both sides through this
|
|
18
|
-
* function before comparing.
|
|
19
|
-
*
|
|
20
|
-
* Idempotent — Latin input passes through unchanged (just lowercased).
|
|
21
|
-
*/
|
|
22
|
-
const BG_CYRILLIC_TO_LATIN: Record<string, string> = {
|
|
23
|
-
а: "a", б: "b", в: "v", г: "g", д: "d", е: "e", ж: "zh",
|
|
24
|
-
з: "z", и: "i", й: "y", к: "k", л: "l", м: "m", н: "n",
|
|
25
|
-
о: "o", п: "p", р: "r", с: "s", т: "t", у: "u", ф: "f",
|
|
26
|
-
х: "h", ц: "ts", ч: "ch", ш: "sh", щ: "sht",
|
|
27
|
-
ъ: "a", ь: "y", ю: "yu", я: "ya",
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function transliterateBgToLatin(s: string): string {
|
|
31
|
-
return s
|
|
32
|
-
.toLowerCase()
|
|
33
|
-
.split("")
|
|
34
|
-
.map((c) => BG_CYRILLIC_TO_LATIN[c] ?? c)
|
|
35
|
-
.join("")
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* normalizeForMatch — produce a canonical key for fuzzy place-name matching.
|
|
40
|
-
*
|
|
41
|
-
* Built on `transliterateBgToLatin` but additionally collapses common
|
|
42
|
-
* Bulgarian / South Slavic Latinization variants:
|
|
43
|
-
*
|
|
44
|
-
* - "София" → "sofiya" → "sofia" (strict transliteration → common)
|
|
45
|
-
* - "Sofia" → "sofia" → "sofia"
|
|
46
|
-
* - "Sofiya" → "sofiya" → "sofia"
|
|
47
|
-
* - "Sofija" → "sofija" → "sofia"
|
|
48
|
-
* - "Sofiia" → "sofiia" → "sofia"
|
|
49
|
-
*
|
|
50
|
-
* Rule: any "iy" / "ij" / "ii" before a vowel collapses to "i". This is
|
|
51
|
-
* the ONE pattern that breaks substring matching for Bulgarian cities.
|
|
52
|
-
* Strict transliteration emits "ya" for "я" (correct on passports), but
|
|
53
|
-
* BoxNow / Econt / OpenStreetMap commonly use the more colloquial "ia"
|
|
54
|
-
* spelling — so users typing "Sofia" must match data containing "София".
|
|
55
|
-
*
|
|
56
|
-
* Use this — not `transliterateBgToLatin` directly — wherever you compare
|
|
57
|
-
* a user-entered place name against an external data source's name.
|
|
58
|
-
* Reserve `transliterateBgToLatin` for outbound contexts (API queries,
|
|
59
|
-
* passport-shaped output) where the strict form is required.
|
|
60
|
-
*/
|
|
61
|
-
export function normalizeForMatch(s: string): string {
|
|
62
|
-
return transliterateBgToLatin(s)
|
|
63
|
-
.replace(/iy(?=[aeiouy])/g, "i")
|
|
64
|
-
.replace(/ij(?=[aeiouy])/g, "i")
|
|
65
|
-
.replace(/ii(?=[aeiouy])/g, "i")
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Shared geo helpers used by Bulgarian fulfillment selectors (Econt, BoxNow).
|
|
70
|
-
*
|
|
71
|
-
* Nominatim usage is rate-limited (1 req/s/IP per their ToS); for a
|
|
72
|
-
* typical checkout we only call once per address change, well under the
|
|
73
|
-
* limit.
|
|
74
|
-
*/
|
|
75
|
-
|
|
76
|
-
export function distanceMeters(
|
|
77
|
-
lat1: number,
|
|
78
|
-
lon1: number,
|
|
79
|
-
lat2: number,
|
|
80
|
-
lon2: number
|
|
81
|
-
): number {
|
|
82
|
-
const R = 6371000
|
|
83
|
-
const dLat = ((lat2 - lat1) * Math.PI) / 180
|
|
84
|
-
const dLon = ((lon2 - lon1) * Math.PI) / 180
|
|
85
|
-
const a =
|
|
86
|
-
Math.sin(dLat / 2) ** 2 +
|
|
87
|
-
Math.cos((lat1 * Math.PI) / 180) *
|
|
88
|
-
Math.cos((lat2 * Math.PI) / 180) *
|
|
89
|
-
Math.sin(dLon / 2) ** 2
|
|
90
|
-
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function formatDistance(meters: number): string {
|
|
94
|
-
if (meters < 1000) return `${Math.round(meters)} m`
|
|
95
|
-
return `${(meters / 1000).toFixed(1)} km`
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* cleanAddress — strip Bulgarian address prefixes/suffixes (ул., бул.,
|
|
100
|
-
* вх., ет., ап., etc.) to give Nominatim a better chance at a hit.
|
|
101
|
-
*/
|
|
102
|
-
export function cleanAddress(raw: string): string {
|
|
103
|
-
return raw
|
|
104
|
-
.replace(/^(ул|ul)\.?\s*/i, "")
|
|
105
|
-
.replace(/^(бул|bul)\.?\s*/i, "")
|
|
106
|
-
.replace(/^(пл|pl)\.?\s*/i, "")
|
|
107
|
-
.replace(/^(жк|zhk|jk)\.?\s*/i, "")
|
|
108
|
-
.replace(/^(ж\.к|zh\.k)\.?\s*/i, "")
|
|
109
|
-
.replace(/^(кв|kv)\.?\s*/i, "")
|
|
110
|
-
.replace(/^(с|s)\.?\s+/i, "")
|
|
111
|
-
.replace(/^(гр|gr)\.?\s*/i, "")
|
|
112
|
-
.replace(/[„""()]/g, "")
|
|
113
|
-
.replace(
|
|
114
|
-
/\s*[,;]?\s*(вх|vh|ет|et|fl|ап|ap|apt|офис|office|стая|room|секция|корпус|каб|entr|entrance)\.?\s*.*/i,
|
|
115
|
-
""
|
|
116
|
-
)
|
|
117
|
-
.replace(/\s*(бл|bl|block)\.?\s*(\d+)\s*.*/i, " $2")
|
|
118
|
-
.replace(/(\d+)\s*[А-Яа-яA-Za-z]$/, "$1")
|
|
119
|
-
.replace(/[,.\s]+$/, "")
|
|
120
|
-
.trim()
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* geocodeAddress — hit OpenStreetMap Nominatim for a Bulgarian address.
|
|
125
|
-
* Returns { lat, lng } or null when no hit / network error.
|
|
126
|
-
*/
|
|
127
|
-
export async function geocodeAddress(
|
|
128
|
-
city: string,
|
|
129
|
-
address: string
|
|
130
|
-
): Promise<{ lat: number; lng: number } | null> {
|
|
131
|
-
try {
|
|
132
|
-
const cleaned = cleanAddress(address)
|
|
133
|
-
const numMatch = cleaned.match(/^(.+?)\s+(\d+\S*)$/)
|
|
134
|
-
const street = numMatch ? `${numMatch[2]} ${numMatch[1]}` : cleaned
|
|
135
|
-
|
|
136
|
-
const url = new URL("https://nominatim.openstreetmap.org/search")
|
|
137
|
-
url.searchParams.set("city", city)
|
|
138
|
-
url.searchParams.set("street", street)
|
|
139
|
-
url.searchParams.set("country", "Bulgaria")
|
|
140
|
-
url.searchParams.set("format", "json")
|
|
141
|
-
url.searchParams.set("limit", "1")
|
|
142
|
-
|
|
143
|
-
const res = await fetch(url.toString(), {
|
|
144
|
-
headers: { "User-Agent": "cartbase-storefront/1.0" },
|
|
145
|
-
})
|
|
146
|
-
const data = await res.json()
|
|
147
|
-
if (data?.[0]) {
|
|
148
|
-
return { lat: parseFloat(data[0].lat), lng: parseFloat(data[0].lon) }
|
|
149
|
-
}
|
|
150
|
-
return null
|
|
151
|
-
} catch {
|
|
152
|
-
return null
|
|
153
|
-
}
|
|
154
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Geo + Bulgarian place-name helpers for the carrier pickers (Econt office,
|
|
3
|
+
* BoxNow locker).
|
|
4
|
+
*
|
|
5
|
+
* Ported from `@1click/ui/src/lib/geocode.ts` (v2.3.1) verbatim — the lib
|
|
6
|
+
* family of `@cartbase/storefront` does not ship a geocode module, so the
|
|
7
|
+
* checkout family (its only consumer) carries it. Extra file beyond the
|
|
8
|
+
* pre-declared export list → exported from the family barrel only.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* transliterateBgToLatin — Bulgarian Cyrillic → Latin transliteration
|
|
13
|
+
* using the streamlined system (official BG government standard, used
|
|
14
|
+
* on passports and road signs).
|
|
15
|
+
*
|
|
16
|
+
* Purpose: when a Bulgarian user types "Sofia" but the data source has
|
|
17
|
+
* "София", substring matching fails. Normalize both sides through this
|
|
18
|
+
* function before comparing.
|
|
19
|
+
*
|
|
20
|
+
* Idempotent — Latin input passes through unchanged (just lowercased).
|
|
21
|
+
*/
|
|
22
|
+
const BG_CYRILLIC_TO_LATIN: Record<string, string> = {
|
|
23
|
+
а: "a", б: "b", в: "v", г: "g", д: "d", е: "e", ж: "zh",
|
|
24
|
+
з: "z", и: "i", й: "y", к: "k", л: "l", м: "m", н: "n",
|
|
25
|
+
о: "o", п: "p", р: "r", с: "s", т: "t", у: "u", ф: "f",
|
|
26
|
+
х: "h", ц: "ts", ч: "ch", ш: "sh", щ: "sht",
|
|
27
|
+
ъ: "a", ь: "y", ю: "yu", я: "ya",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function transliterateBgToLatin(s: string): string {
|
|
31
|
+
return s
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.split("")
|
|
34
|
+
.map((c) => BG_CYRILLIC_TO_LATIN[c] ?? c)
|
|
35
|
+
.join("")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* normalizeForMatch — produce a canonical key for fuzzy place-name matching.
|
|
40
|
+
*
|
|
41
|
+
* Built on `transliterateBgToLatin` but additionally collapses common
|
|
42
|
+
* Bulgarian / South Slavic Latinization variants:
|
|
43
|
+
*
|
|
44
|
+
* - "София" → "sofiya" → "sofia" (strict transliteration → common)
|
|
45
|
+
* - "Sofia" → "sofia" → "sofia"
|
|
46
|
+
* - "Sofiya" → "sofiya" → "sofia"
|
|
47
|
+
* - "Sofija" → "sofija" → "sofia"
|
|
48
|
+
* - "Sofiia" → "sofiia" → "sofia"
|
|
49
|
+
*
|
|
50
|
+
* Rule: any "iy" / "ij" / "ii" before a vowel collapses to "i". This is
|
|
51
|
+
* the ONE pattern that breaks substring matching for Bulgarian cities.
|
|
52
|
+
* Strict transliteration emits "ya" for "я" (correct on passports), but
|
|
53
|
+
* BoxNow / Econt / OpenStreetMap commonly use the more colloquial "ia"
|
|
54
|
+
* spelling — so users typing "Sofia" must match data containing "София".
|
|
55
|
+
*
|
|
56
|
+
* Use this — not `transliterateBgToLatin` directly — wherever you compare
|
|
57
|
+
* a user-entered place name against an external data source's name.
|
|
58
|
+
* Reserve `transliterateBgToLatin` for outbound contexts (API queries,
|
|
59
|
+
* passport-shaped output) where the strict form is required.
|
|
60
|
+
*/
|
|
61
|
+
export function normalizeForMatch(s: string): string {
|
|
62
|
+
return transliterateBgToLatin(s)
|
|
63
|
+
.replace(/iy(?=[aeiouy])/g, "i")
|
|
64
|
+
.replace(/ij(?=[aeiouy])/g, "i")
|
|
65
|
+
.replace(/ii(?=[aeiouy])/g, "i")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Shared geo helpers used by Bulgarian fulfillment selectors (Econt, BoxNow).
|
|
70
|
+
*
|
|
71
|
+
* Nominatim usage is rate-limited (1 req/s/IP per their ToS); for a
|
|
72
|
+
* typical checkout we only call once per address change, well under the
|
|
73
|
+
* limit.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
export function distanceMeters(
|
|
77
|
+
lat1: number,
|
|
78
|
+
lon1: number,
|
|
79
|
+
lat2: number,
|
|
80
|
+
lon2: number
|
|
81
|
+
): number {
|
|
82
|
+
const R = 6371000
|
|
83
|
+
const dLat = ((lat2 - lat1) * Math.PI) / 180
|
|
84
|
+
const dLon = ((lon2 - lon1) * Math.PI) / 180
|
|
85
|
+
const a =
|
|
86
|
+
Math.sin(dLat / 2) ** 2 +
|
|
87
|
+
Math.cos((lat1 * Math.PI) / 180) *
|
|
88
|
+
Math.cos((lat2 * Math.PI) / 180) *
|
|
89
|
+
Math.sin(dLon / 2) ** 2
|
|
90
|
+
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function formatDistance(meters: number): string {
|
|
94
|
+
if (meters < 1000) return `${Math.round(meters)} m`
|
|
95
|
+
return `${(meters / 1000).toFixed(1)} km`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* cleanAddress — strip Bulgarian address prefixes/suffixes (ул., бул.,
|
|
100
|
+
* вх., ет., ап., etc.) to give Nominatim a better chance at a hit.
|
|
101
|
+
*/
|
|
102
|
+
export function cleanAddress(raw: string): string {
|
|
103
|
+
return raw
|
|
104
|
+
.replace(/^(ул|ul)\.?\s*/i, "")
|
|
105
|
+
.replace(/^(бул|bul)\.?\s*/i, "")
|
|
106
|
+
.replace(/^(пл|pl)\.?\s*/i, "")
|
|
107
|
+
.replace(/^(жк|zhk|jk)\.?\s*/i, "")
|
|
108
|
+
.replace(/^(ж\.к|zh\.k)\.?\s*/i, "")
|
|
109
|
+
.replace(/^(кв|kv)\.?\s*/i, "")
|
|
110
|
+
.replace(/^(с|s)\.?\s+/i, "")
|
|
111
|
+
.replace(/^(гр|gr)\.?\s*/i, "")
|
|
112
|
+
.replace(/[„""()]/g, "")
|
|
113
|
+
.replace(
|
|
114
|
+
/\s*[,;]?\s*(вх|vh|ет|et|fl|ап|ap|apt|офис|office|стая|room|секция|корпус|каб|entr|entrance)\.?\s*.*/i,
|
|
115
|
+
""
|
|
116
|
+
)
|
|
117
|
+
.replace(/\s*(бл|bl|block)\.?\s*(\d+)\s*.*/i, " $2")
|
|
118
|
+
.replace(/(\d+)\s*[А-Яа-яA-Za-z]$/, "$1")
|
|
119
|
+
.replace(/[,.\s]+$/, "")
|
|
120
|
+
.trim()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* geocodeAddress — hit OpenStreetMap Nominatim for a Bulgarian address.
|
|
125
|
+
* Returns { lat, lng } or null when no hit / network error.
|
|
126
|
+
*/
|
|
127
|
+
export async function geocodeAddress(
|
|
128
|
+
city: string,
|
|
129
|
+
address: string
|
|
130
|
+
): Promise<{ lat: number; lng: number } | null> {
|
|
131
|
+
try {
|
|
132
|
+
const cleaned = cleanAddress(address)
|
|
133
|
+
const numMatch = cleaned.match(/^(.+?)\s+(\d+\S*)$/)
|
|
134
|
+
const street = numMatch ? `${numMatch[2]} ${numMatch[1]}` : cleaned
|
|
135
|
+
|
|
136
|
+
const url = new URL("https://nominatim.openstreetmap.org/search")
|
|
137
|
+
url.searchParams.set("city", city)
|
|
138
|
+
url.searchParams.set("street", street)
|
|
139
|
+
url.searchParams.set("country", "Bulgaria")
|
|
140
|
+
url.searchParams.set("format", "json")
|
|
141
|
+
url.searchParams.set("limit", "1")
|
|
142
|
+
|
|
143
|
+
const res = await fetch(url.toString(), {
|
|
144
|
+
headers: { "User-Agent": "cartbase-storefront/1.0" },
|
|
145
|
+
})
|
|
146
|
+
const data = await res.json()
|
|
147
|
+
if (data?.[0]) {
|
|
148
|
+
return { lat: parseFloat(data[0].lat), lng: parseFloat(data[0].lon) }
|
|
149
|
+
}
|
|
150
|
+
return null
|
|
151
|
+
} catch {
|
|
152
|
+
return null
|
|
153
|
+
}
|
|
154
|
+
}
|