@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
package/src/lib/useHours.js
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import * as Collapsible from "@radix-ui/react-collapsible";
|
|
3
|
-
import { ChevronDown } from "lucide-react";
|
|
4
|
-
import { useTranslation } from "react-i18next";
|
|
5
|
-
|
|
6
|
-
const DAY_LABELS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
7
|
-
|
|
8
|
-
export default function useHours({
|
|
9
|
-
hours,
|
|
10
|
-
openingSoonMinutes = 60,
|
|
11
|
-
closingSoonMinutes = 60,
|
|
12
|
-
t,
|
|
13
|
-
}) {
|
|
14
|
-
|
|
15
|
-
const computed = React.useMemo(() => {
|
|
16
|
-
const safeHours = Array.isArray(hours) ? hours : [];
|
|
17
|
-
const storeTz =
|
|
18
|
-
safeHours.find((h) => h?.timezone)?.timezone ||
|
|
19
|
-
Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
20
|
-
|
|
21
|
-
const now = new Date();
|
|
22
|
-
const storeWeekdayIndex = getWeekdayIndexInTimeZone(now, storeTz); // 0..6
|
|
23
|
-
const today = safeHours.find((h) => h?.day === storeWeekdayIndex);
|
|
24
|
-
|
|
25
|
-
const status = computeStatus({
|
|
26
|
-
now,
|
|
27
|
-
today,
|
|
28
|
-
openingSoonMinutes,
|
|
29
|
-
closingSoonMinutes,
|
|
30
|
-
t,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// Display Monday..Sunday
|
|
34
|
-
const displayOrder = [1, 2, 3, 4, 5, 6, 0];
|
|
35
|
-
const byDay = new Map(safeHours.map((h) => [h.day, h]));
|
|
36
|
-
const rows = displayOrder.map((dayIdx) => {
|
|
37
|
-
const h = byDay.get(dayIdx);
|
|
38
|
-
const label = t(`default:${DAY_LABELS[dayIdx].toLowerCase()}`);
|
|
39
|
-
const isToday = dayIdx === storeWeekdayIndex;
|
|
40
|
-
|
|
41
|
-
let text = t("default:closed");
|
|
42
|
-
if (h?.open) {
|
|
43
|
-
const openDate = buildOpenDateForDisplay(h);
|
|
44
|
-
const closeDate = buildCloseDateForDisplay(h, openDate);
|
|
45
|
-
if (openDate && closeDate) {
|
|
46
|
-
text = `${formatTimeLocal(openDate)} - ${formatTimeLocal(closeDate)}`;
|
|
47
|
-
} else {
|
|
48
|
-
text = t("default:open");
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return { dayIdx, label, text, isToday };
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
statusLabel: t(`default:${status.label.toLowerCase().replace(" ", "_")}`), // "Closed" | "Opening Soon" | "Open" | "Closing Soon"
|
|
57
|
-
statusDetail: status.detail, // "Opens at 9am" | "Closes at 4:30pm" | ""
|
|
58
|
-
rows,
|
|
59
|
-
};
|
|
60
|
-
}, [hours, openingSoonMinutes, closingSoonMinutes, t]);
|
|
61
|
-
|
|
62
|
-
return { computed }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Toggle label logic based on today's schedule (store timezone for "today",
|
|
67
|
-
* comparisons against the user's current instant).
|
|
68
|
-
*
|
|
69
|
-
* Adds a detail line:
|
|
70
|
-
* - Closed / Opening Soon -> "Opens at X"
|
|
71
|
-
* - Open / Closing Soon -> "Closes at Y"
|
|
72
|
-
*/
|
|
73
|
-
function computeStatus({ now, today, openingSoonMinutes, closingSoonMinutes, t }) {
|
|
74
|
-
if (!today || !today.open) return { label: t('default:closed'), detail: "" };
|
|
75
|
-
|
|
76
|
-
const openDate = buildOpenDateForDisplay(today);
|
|
77
|
-
const closeDate = buildCloseDateForDisplay(today, openDate);
|
|
78
|
-
|
|
79
|
-
// If schedule incomplete, still show basic label
|
|
80
|
-
if (!openDate || !closeDate) return { label: t('default:open'), detail: "" };
|
|
81
|
-
|
|
82
|
-
const nowMs = now.getTime();
|
|
83
|
-
const openMs = openDate.getTime();
|
|
84
|
-
const closeMs = closeDate.getTime();
|
|
85
|
-
|
|
86
|
-
const openingSoonStart = openMs - openingSoonMinutes * 60 * 1000;
|
|
87
|
-
const closingSoonStart = closeMs - closingSoonMinutes * 60 * 1000;
|
|
88
|
-
|
|
89
|
-
const opensDetail = t('default:opens_at', { time: formatTimeLocal(openDate) });
|
|
90
|
-
const closesDetail = t('default:closes_at', { time: formatTimeLocal(closeDate) });
|
|
91
|
-
|
|
92
|
-
if (nowMs < openMs) {
|
|
93
|
-
if (nowMs >= openingSoonStart) return { label: t('default:opening_soon'), detail: opensDetail };
|
|
94
|
-
return { label: t('default:closed'), detail: opensDetail };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (nowMs >= openMs && nowMs < closeMs) {
|
|
98
|
-
if (nowMs >= closingSoonStart) return { label: t('default:closing_soon'), detail: closesDetail };
|
|
99
|
-
return { label: t('default:open'), detail: closesDetail };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return { label: t('default:closed'), detail: opensDetail };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Returns weekday index (0=Sun..6=Sat) for a Date, as seen in `timeZone`. */
|
|
106
|
-
function getWeekdayIndexInTimeZone(date, timeZone) {
|
|
107
|
-
const weekday = new Intl.DateTimeFormat("en-US", { weekday: "short", timeZone }).format(date);
|
|
108
|
-
switch (weekday) {
|
|
109
|
-
case "Sun":
|
|
110
|
-
return 0;
|
|
111
|
-
case "Mon":
|
|
112
|
-
return 1;
|
|
113
|
-
case "Tue":
|
|
114
|
-
return 2;
|
|
115
|
-
case "Wed":
|
|
116
|
-
return 3;
|
|
117
|
-
case "Thu":
|
|
118
|
-
return 4;
|
|
119
|
-
case "Fri":
|
|
120
|
-
return 5;
|
|
121
|
-
case "Sat":
|
|
122
|
-
return 6;
|
|
123
|
-
default:
|
|
124
|
-
return new Date(date).getDay();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Build "open" Date (instant) for display in user's local time.
|
|
130
|
-
* Prefer `open_at` because it encodes offset and is DST-correct for that date.
|
|
131
|
-
*/
|
|
132
|
-
function buildOpenDateForDisplay(h) {
|
|
133
|
-
if (!h) return null;
|
|
134
|
-
|
|
135
|
-
if (h.open_at) {
|
|
136
|
-
const d = new Date(h.open_at);
|
|
137
|
-
return isValidDate(d) ? d : null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (typeof h.open_at_hour === "number" && typeof h.open_at_minute === "number") {
|
|
141
|
-
return buildDateFromLocalStoreTime({
|
|
142
|
-
storeHour: h.open_at_hour,
|
|
143
|
-
storeMinute: h.open_at_minute,
|
|
144
|
-
utcOffsetMinute: h.utc_offset_minute,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Build "close" Date (instant). If close <= open, assume next day.
|
|
153
|
-
*/
|
|
154
|
-
function buildCloseDateForDisplay(h, openDate) {
|
|
155
|
-
if (!h) return null;
|
|
156
|
-
|
|
157
|
-
if (typeof h.close_at_hour !== "number" || typeof h.close_at_minute !== "number") return null;
|
|
158
|
-
|
|
159
|
-
let closeDate = buildDateFromLocalStoreTime({
|
|
160
|
-
storeHour: h.close_at_hour,
|
|
161
|
-
storeMinute: h.close_at_minute,
|
|
162
|
-
utcOffsetMinute: h.utc_offset_minute,
|
|
163
|
-
anchorDate: openDate || undefined,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
if (!closeDate || !openDate) return closeDate;
|
|
167
|
-
|
|
168
|
-
if (closeDate.getTime() <= openDate.getTime()) {
|
|
169
|
-
closeDate = new Date(closeDate.getTime() + 24 * 60 * 60 * 1000);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return closeDate;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Build an instant from a store-local time-of-day plus a fixed UTC offset.
|
|
177
|
-
* storeLocal = UTC + offset => UTC = storeLocal - offset
|
|
178
|
-
*/
|
|
179
|
-
function buildDateFromLocalStoreTime({ storeHour, storeMinute, utcOffsetMinute, anchorDate }) {
|
|
180
|
-
const base = anchorDate ? new Date(anchorDate) : new Date();
|
|
181
|
-
const y = base.getFullYear();
|
|
182
|
-
const m = base.getMonth();
|
|
183
|
-
const d = base.getDate();
|
|
184
|
-
|
|
185
|
-
const utcMillis = Date.UTC(y, m, d, storeHour, storeMinute, 0) - utcOffsetMinute * 60 * 1000;
|
|
186
|
-
const out = new Date(utcMillis);
|
|
187
|
-
|
|
188
|
-
return isValidDate(out) ? out : null;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function isValidDate(d) {
|
|
192
|
-
return d instanceof Date && !Number.isNaN(d.getTime());
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** Format in user's local timezone as "9am" or "4:30pm". */
|
|
196
|
-
function formatTimeLocal(date) {
|
|
197
|
-
return new Intl.DateTimeFormat("en-US", {
|
|
198
|
-
hour: "numeric",
|
|
199
|
-
minute: "2-digit",
|
|
200
|
-
})
|
|
201
|
-
.format(date)
|
|
202
|
-
.replace(":00", "")
|
|
203
|
-
.replace(" AM", "am")
|
|
204
|
-
.replace(" PM", "pm");
|
|
205
|
-
}
|
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @typedef {{ lat: number, lng: number }} LatLng
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @typedef {{
|
|
9
|
-
* placeId: string,
|
|
10
|
-
* description: string,
|
|
11
|
-
* mainText?: string,
|
|
12
|
-
* secondaryText?: string,
|
|
13
|
-
* types?: string[],
|
|
14
|
-
* distanceMeters?: number,
|
|
15
|
-
* location?: { lat: number, lng: number } | null,
|
|
16
|
-
* city?: string | null,
|
|
17
|
-
* region?: string | null, // State / Province
|
|
18
|
-
* country?: string | null, // e.g. "US" (shortText) or "United States" (longText) depending on preference
|
|
19
|
-
* postalCode?: string | null
|
|
20
|
-
* }} AutocompleteResult
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
function pickComponent(components, wantedTypes) {
|
|
24
|
-
// returns the first component that matches ANY of the wanted types
|
|
25
|
-
if (!Array.isArray(components)) return null;
|
|
26
|
-
for (const c of components) {
|
|
27
|
-
const t = Array.isArray(c?.types) ? c.types : [];
|
|
28
|
-
if (wantedTypes.some((wt) => t.includes(wt))) return c;
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function parseAddressComponents(addressComponents) {
|
|
34
|
-
// Places (New) AddressComponent has: longText, shortText, types :contentReference[oaicite:2]{index=2}
|
|
35
|
-
const cityComp =
|
|
36
|
-
pickComponent(addressComponents, ["locality"]) ||
|
|
37
|
-
pickComponent(addressComponents, ["postal_town"]) ||
|
|
38
|
-
pickComponent(addressComponents, ["administrative_area_level_2"]); // fallback
|
|
39
|
-
|
|
40
|
-
const regionComp = pickComponent(addressComponents, [
|
|
41
|
-
"administrative_area_level_1",
|
|
42
|
-
]);
|
|
43
|
-
|
|
44
|
-
const countryComp = pickComponent(addressComponents, ["country"]);
|
|
45
|
-
|
|
46
|
-
const postalComp = pickComponent(addressComponents, ["postal_code"]);
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
city: cityComp?.longText ?? null,
|
|
50
|
-
// region: use shortText for "CA", "BC" etc; fall back to longText if missing
|
|
51
|
-
region: regionComp?.shortText ?? regionComp?.longText ?? null,
|
|
52
|
-
// country: use shortText ("US") if available; otherwise longText
|
|
53
|
-
country: countryComp?.shortText ?? countryComp?.longText ?? null,
|
|
54
|
-
postalCode: postalComp?.longText ?? postalComp?.shortText ?? null,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Google Places Autocomplete (v1) + Place Details enrichment (location + addressComponents)
|
|
60
|
-
*
|
|
61
|
-
* @param {Object} params
|
|
62
|
-
* @param {string} params.query
|
|
63
|
-
* @param {string} params.apiKey
|
|
64
|
-
* @param {LatLng|null} [params.center=null]
|
|
65
|
-
* @param {number} [params.radiusMeters=500]
|
|
66
|
-
* @param {number} [params.debounceTime=300]
|
|
67
|
-
* @param {string} [params.type] - single primary type filter (e.g. "(cities)" or "postal_code")
|
|
68
|
-
* @param {string[]} [params.types] - if provided, only the first entry is used
|
|
69
|
-
* @param {number} [params.maxResults=8] - number of suggestions to enrich (details calls)
|
|
70
|
-
* @param {number} [params.minLength=2]
|
|
71
|
-
*
|
|
72
|
-
* @returns {{ results: AutocompleteResult[], isLoading: boolean, error: Error | null }}
|
|
73
|
-
*/
|
|
74
|
-
export function usePlacesAutocomplete({
|
|
75
|
-
query,
|
|
76
|
-
apiKey,
|
|
77
|
-
center = null,
|
|
78
|
-
radiusMeters = 500,
|
|
79
|
-
debounceTime = 300,
|
|
80
|
-
type,
|
|
81
|
-
types,
|
|
82
|
-
maxResults = 8,
|
|
83
|
-
minLength = 2,
|
|
84
|
-
} = {}) {
|
|
85
|
-
const [results, setResults] = useState([]);
|
|
86
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
87
|
-
const [error, setError] = useState(null);
|
|
88
|
-
|
|
89
|
-
const abortRef = useRef({ autocomplete: null, details: null });
|
|
90
|
-
const debounceTimerRef = useRef(null);
|
|
91
|
-
const requestSeqRef = useRef(0);
|
|
92
|
-
|
|
93
|
-
// Cache placeId -> enriched payload to reduce repeated detail calls
|
|
94
|
-
const cacheRef = useRef(new Map());
|
|
95
|
-
|
|
96
|
-
const input = useMemo(() => (query ?? "").trim(), [query]);
|
|
97
|
-
|
|
98
|
-
useEffect(() => {
|
|
99
|
-
if (!input || input.length < minLength) {
|
|
100
|
-
abortRef.current.autocomplete?.abort?.();
|
|
101
|
-
abortRef.current.details?.abort?.();
|
|
102
|
-
setResults([]);
|
|
103
|
-
setIsLoading(false);
|
|
104
|
-
setError(null);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (!apiKey) {
|
|
109
|
-
setError(new Error("Google Places API key is required"));
|
|
110
|
-
setResults([]);
|
|
111
|
-
setIsLoading(false);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
setError(null);
|
|
116
|
-
clearTimeout(debounceTimerRef.current);
|
|
117
|
-
|
|
118
|
-
const mySeq = ++requestSeqRef.current;
|
|
119
|
-
|
|
120
|
-
debounceTimerRef.current = setTimeout(async () => {
|
|
121
|
-
abortRef.current.autocomplete?.abort?.();
|
|
122
|
-
abortRef.current.details?.abort?.();
|
|
123
|
-
|
|
124
|
-
const acController = new AbortController();
|
|
125
|
-
const detailsController = new AbortController();
|
|
126
|
-
abortRef.current.autocomplete = acController;
|
|
127
|
-
abortRef.current.details = detailsController;
|
|
128
|
-
|
|
129
|
-
setIsLoading(true);
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
// 1) Autocomplete (New) POST
|
|
133
|
-
const body = { input };
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
center &&
|
|
137
|
-
Number.isFinite(center.lat) &&
|
|
138
|
-
Number.isFinite(center.lng)
|
|
139
|
-
) {
|
|
140
|
-
body.locationBias = {
|
|
141
|
-
circle: {
|
|
142
|
-
center: { latitude: center.lat, longitude: center.lng },
|
|
143
|
-
radius: radiusMeters,
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Include up to 5 types if provided; otherwise use single type if provided
|
|
149
|
-
if (Array.isArray(types) && types.length) {
|
|
150
|
-
// Places API (New) uses `includedPrimaryTypes` (max 5)
|
|
151
|
-
body.includedPrimaryTypes = types.slice(0, 5);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const acRes = await fetch(
|
|
155
|
-
"https://places.googleapis.com/v1/places:autocomplete",
|
|
156
|
-
{
|
|
157
|
-
method: "POST",
|
|
158
|
-
signal: acController.signal,
|
|
159
|
-
headers: {
|
|
160
|
-
"Content-Type": "application/json",
|
|
161
|
-
"X-Goog-Api-Key": apiKey,
|
|
162
|
-
},
|
|
163
|
-
body: JSON.stringify(body),
|
|
164
|
-
}
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
if (!acRes.ok) {
|
|
168
|
-
const text = await acRes.text();
|
|
169
|
-
throw new Error(`Autocomplete error ${acRes.status}: ${text}`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const acJson = await acRes.json();
|
|
173
|
-
const suggestions = Array.isArray(acJson?.suggestions)
|
|
174
|
-
? acJson.suggestions
|
|
175
|
-
: [];
|
|
176
|
-
|
|
177
|
-
const placePredictions = suggestions
|
|
178
|
-
.map((s) => s?.placePrediction)
|
|
179
|
-
.filter(Boolean);
|
|
180
|
-
|
|
181
|
-
const base = placePredictions.map((p) => {
|
|
182
|
-
const mainText = p?.structuredFormat?.mainText?.text;
|
|
183
|
-
const secondaryText = p?.structuredFormat?.secondaryText?.text;
|
|
184
|
-
const description =
|
|
185
|
-
p?.text?.text || [mainText, secondaryText].filter(Boolean).join(", ");
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
placeId: p.placeId,
|
|
189
|
-
description,
|
|
190
|
-
mainText,
|
|
191
|
-
secondaryText,
|
|
192
|
-
types: p.types,
|
|
193
|
-
distanceMeters: p.distanceMeters,
|
|
194
|
-
location: null,
|
|
195
|
-
city: null,
|
|
196
|
-
region: null,
|
|
197
|
-
country: null,
|
|
198
|
-
postalCode: null,
|
|
199
|
-
};
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
if (requestSeqRef.current !== mySeq) return;
|
|
203
|
-
|
|
204
|
-
// 2) Enrich each item with Place Details: location + addressComponents
|
|
205
|
-
// Field masks are required/best practice and supported via `fields` param or X-Goog-FieldMask header. :contentReference[oaicite:3]{index=3}
|
|
206
|
-
const toEnrich = base.slice(0, Math.max(0, maxResults));
|
|
207
|
-
|
|
208
|
-
const enriched = await Promise.all(
|
|
209
|
-
toEnrich.map(async (r) => {
|
|
210
|
-
const cached = cacheRef.current.get(r.placeId);
|
|
211
|
-
if (cached) return { ...r, ...cached };
|
|
212
|
-
|
|
213
|
-
const url = `https://places.googleapis.com/v1/places/${encodeURIComponent(
|
|
214
|
-
r.placeId
|
|
215
|
-
)}?fields=location,addressComponents`;
|
|
216
|
-
|
|
217
|
-
const detRes = await fetch(url, {
|
|
218
|
-
method: "GET",
|
|
219
|
-
signal: detailsController.signal,
|
|
220
|
-
headers: {
|
|
221
|
-
"X-Goog-Api-Key": apiKey,
|
|
222
|
-
"X-Goog-FieldMask": "location,addressComponents",
|
|
223
|
-
},
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
if (!detRes.ok) return r;
|
|
227
|
-
|
|
228
|
-
const detJson = await detRes.json();
|
|
229
|
-
|
|
230
|
-
const loc = detJson?.location;
|
|
231
|
-
const location =
|
|
232
|
-
loc &&
|
|
233
|
-
Number.isFinite(loc.latitude) &&
|
|
234
|
-
Number.isFinite(loc.longitude)
|
|
235
|
-
? { lat: loc.latitude, lng: loc.longitude }
|
|
236
|
-
: null;
|
|
237
|
-
|
|
238
|
-
const parsed = parseAddressComponents(detJson?.addressComponents);
|
|
239
|
-
|
|
240
|
-
const patch = {
|
|
241
|
-
location,
|
|
242
|
-
city: parsed.city,
|
|
243
|
-
region: parsed.region,
|
|
244
|
-
country: parsed.country,
|
|
245
|
-
postalCode: parsed.postalCode,
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
cacheRef.current.set(r.placeId, patch);
|
|
249
|
-
return { ...r, ...patch };
|
|
250
|
-
})
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
const enrichedById = new Map(enriched.map((e) => [e.placeId, e]));
|
|
254
|
-
const finalResults = base.map((r) => enrichedById.get(r.placeId) || r);
|
|
255
|
-
|
|
256
|
-
if (requestSeqRef.current !== mySeq) return;
|
|
257
|
-
setResults(finalResults);
|
|
258
|
-
} catch (e) {
|
|
259
|
-
if (e?.name === "AbortError") return;
|
|
260
|
-
if (requestSeqRef.current !== mySeq) return;
|
|
261
|
-
|
|
262
|
-
setError(e instanceof Error ? e : new Error("Unknown error"));
|
|
263
|
-
setResults([]);
|
|
264
|
-
} finally {
|
|
265
|
-
if (requestSeqRef.current === mySeq) setIsLoading(false);
|
|
266
|
-
}
|
|
267
|
-
}, debounceTime);
|
|
268
|
-
|
|
269
|
-
return () => {
|
|
270
|
-
clearTimeout(debounceTimerRef.current);
|
|
271
|
-
abortRef.current.autocomplete?.abort?.();
|
|
272
|
-
abortRef.current.details?.abort?.();
|
|
273
|
-
};
|
|
274
|
-
}, [
|
|
275
|
-
input,
|
|
276
|
-
apiKey,
|
|
277
|
-
center?.lat,
|
|
278
|
-
center?.lng,
|
|
279
|
-
radiusMeters,
|
|
280
|
-
debounceTime,
|
|
281
|
-
minLength,
|
|
282
|
-
maxResults,
|
|
283
|
-
]);
|
|
284
|
-
|
|
285
|
-
return { results, isLoading, error };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
export default usePlacesAutocomplete;
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
const useProductAvailability = (storeProducts, requestedProducts, filterBy = ['sku']) => {
|
|
2
|
-
const availability = {}
|
|
3
|
-
|
|
4
|
-
requestedProducts.map((reqProd) => {
|
|
5
|
-
const storeProd = storeProducts.find(sp => filterBy.some(key => sp[key] === reqProd[key]))
|
|
6
|
-
if (storeProd) {
|
|
7
|
-
availability[reqProd.sku] = {
|
|
8
|
-
stocked: storeProd.stocked,
|
|
9
|
-
available: storeProd.available,
|
|
10
|
-
}
|
|
11
|
-
} else {
|
|
12
|
-
availability[reqProd.sku] = {
|
|
13
|
-
stocked: false,
|
|
14
|
-
available: false,
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
const allStocked = Object.values(availability).every(avail => avail.stocked)
|
|
20
|
-
const allAvailable = Object.values(availability).every(avail => avail.available)
|
|
21
|
-
const noneStocked = Object.values(availability).every(avail => !avail.stocked)
|
|
22
|
-
const noneAvailable = Object.values(availability).every(avail => !avail.available)
|
|
23
|
-
|
|
24
|
-
// Availability summaries: allStocked, allAvailable, mixed, none
|
|
25
|
-
// Return one availabilitySummary key along with availability details
|
|
26
|
-
let availabilitySummary = 'mixed'
|
|
27
|
-
if (allStocked) {
|
|
28
|
-
availabilitySummary = 'allStocked'
|
|
29
|
-
} else if (allAvailable) {
|
|
30
|
-
availabilitySummary = 'allAvailable'
|
|
31
|
-
} else if (noneStocked && noneAvailable) {
|
|
32
|
-
availabilitySummary = 'none'
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return { availability, allAvailable, allStocked, noneStocked, noneAvailable, availabilitySummary }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export default useProductAvailability
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) 2025 Colab Commerce <https://colabcommerce.com>
|
|
3
|
-
* All rights reserved.
|
|
4
|
-
*
|
|
5
|
-
* This hook provides a simple interface for Rudderstack analytics tracking
|
|
6
|
-
* exposing the browser APIs for tracking events, page views, and identifying users.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* const { track, page, identify } = useRudderStackAnalytics();
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { useEffect, useState } from 'react';
|
|
13
|
-
import { RudderAnalytics } from '@rudderstack/analytics-js';
|
|
14
|
-
|
|
15
|
-
const useRudderStackAnalytics = () => {
|
|
16
|
-
const [analytics, setAnalytics] = useState()
|
|
17
|
-
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
if (!analytics) {
|
|
20
|
-
const analyticsInstance = new RudderAnalytics();
|
|
21
|
-
analyticsInstance.load(import.meta.env.RS_WRITE_KEY, import.meta.env.RS_DATA_PLANE_URL);
|
|
22
|
-
|
|
23
|
-
analyticsInstance.ready(() => {
|
|
24
|
-
console.log('We are all set!!!');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
setAnalytics(analyticsInstance);
|
|
28
|
-
}
|
|
29
|
-
}, [analytics]);
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
track: (eventName, properties) => {
|
|
33
|
-
if (analytics) {
|
|
34
|
-
analytics.track(eventName, properties);
|
|
35
|
-
};
|
|
36
|
-
},
|
|
37
|
-
page: (category, name, properties) => {
|
|
38
|
-
if (analytics) {
|
|
39
|
-
analytics.page(category, name, properties);
|
|
40
|
-
};
|
|
41
|
-
},
|
|
42
|
-
identify: (userId, traits) => {
|
|
43
|
-
if (analytics) {
|
|
44
|
-
analytics.identify(userId, traits);
|
|
45
|
-
};
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export default useRudderStackAnalytics
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) 2025 Colab Commerce <https://colabcommerce.com>
|
|
3
|
-
* All rights reserved.
|
|
4
|
-
*
|
|
5
|
-
* This hook provides store location results based upon a lat/lng location, page, pageSize, as well
|
|
6
|
-
* as an arrays of
|
|
7
|
-
* availableCollectionIds
|
|
8
|
-
* stockedCollectionIds
|
|
9
|
-
* availableProductSkus
|
|
10
|
-
* stockedProductSkus
|
|
11
|
-
* availableExternalProductIds
|
|
12
|
-
* stockedExternalProductIds
|
|
13
|
-
* Usage:
|
|
14
|
-
* const { stores, meta } = useSearchResults({
|
|
15
|
-
* location, stockedCollectionIds, availableCollectionIds, stockedProductSkus, availableProductSkus, stockedExternalProductIds, availableExternalProductIds, page, pageSize });
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { useEffect, useState } from 'react'
|
|
19
|
-
|
|
20
|
-
export default function useSearchResults({
|
|
21
|
-
organizationId,
|
|
22
|
-
location,
|
|
23
|
-
availableCollectionIds = [],
|
|
24
|
-
stockedCollectionIds = [],
|
|
25
|
-
availableProductSkus = [],
|
|
26
|
-
stockedProductSkus = [],
|
|
27
|
-
availableExternalProductIds = [],
|
|
28
|
-
stockedExternalProductIds = [],
|
|
29
|
-
radius = '25mi',
|
|
30
|
-
page = 1,
|
|
31
|
-
pageSize = 10,
|
|
32
|
-
}) {
|
|
33
|
-
|
|
34
|
-
const [stores, setStores] = useState([])
|
|
35
|
-
const [meta, setMeta] = useState({ total: 0, page: 1, pageSize: 5 })
|
|
36
|
-
const [isLoading, setIsLoading] = useState(true)
|
|
37
|
-
const [error, setError] = useState(null)
|
|
38
|
-
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
async function fetchStores() {
|
|
41
|
-
if (!location) {
|
|
42
|
-
setStores([])
|
|
43
|
-
setMeta({ total: 0, page: 1, pageSize: 5 })
|
|
44
|
-
setIsLoading(false)
|
|
45
|
-
setError(null)
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
setIsLoading(true)
|
|
50
|
-
setError(null)
|
|
51
|
-
|
|
52
|
-
const params = new URLSearchParams()
|
|
53
|
-
params.append('latitude', location.latitude)
|
|
54
|
-
params.append('longitude', location.longitude)
|
|
55
|
-
params.append('per_page', pageSize)
|
|
56
|
-
params.append('radius', radius)
|
|
57
|
-
|
|
58
|
-
availableCollectionIds.forEach(id => params.append('availableCollectionIds', id))
|
|
59
|
-
stockedCollectionIds.forEach(id => params.append('stockedCollectionIds', id))
|
|
60
|
-
availableProductSkus.forEach(sku => params.append('availableProductSkus', sku))
|
|
61
|
-
stockedProductSkus.forEach(sku => params.append('stockedProductSkus', sku))
|
|
62
|
-
availableExternalProductIds.forEach(id => params.append('availableExternalProductIds', id))
|
|
63
|
-
stockedExternalProductIds.forEach(id => params.append('stockedExternalProductIds', id))
|
|
64
|
-
|
|
65
|
-
const response = await fetch(`https://api.colabcommerce.com/widget_api/store_locator?${params.toString()}`, {
|
|
66
|
-
method: 'GET',
|
|
67
|
-
headers: {
|
|
68
|
-
'Content-Type': 'application/json',
|
|
69
|
-
'X-Cc-Id': organizationId
|
|
70
|
-
},
|
|
71
|
-
})
|
|
72
|
-
const data = await response.json()
|
|
73
|
-
|
|
74
|
-
if (!response.ok) {
|
|
75
|
-
setStores([])
|
|
76
|
-
setMeta({ total: 0, page: 1, pageSize: 5 })
|
|
77
|
-
setIsLoading(false)
|
|
78
|
-
setError(new Error(data.message || 'Failed to fetch store locations'))
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
setStores(data.results || [])
|
|
82
|
-
setMeta(data.meta || { total: 0, page: 1, pageSize: 10 })
|
|
83
|
-
setIsLoading(false)
|
|
84
|
-
setError(null)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
fetchStores()
|
|
88
|
-
}, [
|
|
89
|
-
location,
|
|
90
|
-
availableCollectionIds,
|
|
91
|
-
stockedCollectionIds,
|
|
92
|
-
availableProductSkus,
|
|
93
|
-
stockedProductSkus,
|
|
94
|
-
availableExternalProductIds,
|
|
95
|
-
stockedExternalProductIds,
|
|
96
|
-
radius,
|
|
97
|
-
page,
|
|
98
|
-
pageSize,
|
|
99
|
-
])
|
|
100
|
-
|
|
101
|
-
return { stores, meta, isLoading, error }
|
|
102
|
-
}
|