@nuskin/react-search-sdk 1.0.0 → 1.0.1-code-cleanup.2
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/README.md +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +558 -363
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +558 -363
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -7
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useMemo, useCallback, Component } from 'react';
|
|
2
2
|
import configurationSdk from '@nuskin/configuration-sdk';
|
|
3
3
|
import { SearchPanel } from '@nuskin/react-search-components';
|
|
4
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
5
|
import { SalesAIChatWidget } from '@nuskin/react-nui-chat-sdk';
|
|
5
6
|
import '@nuskin/react-nui-chat-sdk/styles.css';
|
|
6
7
|
import { createProductClient, ProductProvider, useProductById, ProductCard } from '@nuskin/react-product-sdk';
|
|
7
8
|
import styled from '@emotion/styled';
|
|
8
|
-
import { jsx } from 'react/jsx-runtime';
|
|
9
9
|
|
|
10
10
|
// src/components/CommerceSearch/CommerceSearch.tsx
|
|
11
11
|
var { getConfiguration, getEnvironmentCode } = configurationSdk;
|
|
@@ -120,21 +120,41 @@ async function fetchSearchPanelCopy(_options = {}) {
|
|
|
120
120
|
|
|
121
121
|
// src/copy/loadSearchPanelCopy.ts
|
|
122
122
|
var copyByLanguage = /* @__PURE__ */ new Map();
|
|
123
|
+
var loadedByLanguage = /* @__PURE__ */ new Map();
|
|
124
|
+
function cacheKey(language) {
|
|
125
|
+
return (language ?? "").toLowerCase();
|
|
126
|
+
}
|
|
123
127
|
function loadSearchPanelCopy(language) {
|
|
124
|
-
const key = (language
|
|
128
|
+
const key = cacheKey(language);
|
|
125
129
|
const cached = copyByLanguage.get(key);
|
|
126
130
|
if (cached) return cached;
|
|
127
131
|
const pending = fetchSearchPanelCopy(language ? { } : {});
|
|
128
132
|
copyByLanguage.set(key, pending);
|
|
129
|
-
pending.
|
|
133
|
+
pending.then(
|
|
134
|
+
(copy) => {
|
|
135
|
+
if (copyByLanguage.get(key) === pending) {
|
|
136
|
+
loadedByLanguage.set(key, copy);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
() => {
|
|
140
|
+
if (copyByLanguage.get(key) === pending) {
|
|
141
|
+
copyByLanguage.delete(key);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
);
|
|
130
145
|
return pending;
|
|
131
146
|
}
|
|
147
|
+
function peekSearchPanelCopy(language) {
|
|
148
|
+
return loadedByLanguage.get(cacheKey(language));
|
|
149
|
+
}
|
|
132
150
|
|
|
133
151
|
// src/hooks/useSearchPanelCopy.ts
|
|
134
152
|
function useSearchPanelCopy({
|
|
135
153
|
language
|
|
136
154
|
}) {
|
|
137
|
-
const [copy, setCopy] = useState(
|
|
155
|
+
const [copy, setCopy] = useState(
|
|
156
|
+
() => peekSearchPanelCopy(language)
|
|
157
|
+
);
|
|
138
158
|
useEffect(() => {
|
|
139
159
|
let isStale = false;
|
|
140
160
|
loadSearchPanelCopy(language).then((next) => {
|
|
@@ -152,7 +172,125 @@ function useSearchPanelCopy({
|
|
|
152
172
|
return copy;
|
|
153
173
|
}
|
|
154
174
|
|
|
155
|
-
// src/services/
|
|
175
|
+
// src/services/localeParts.ts
|
|
176
|
+
var TWO_LETTERS = /^[A-Za-z]{2}$/;
|
|
177
|
+
function marketProblem(country, language) {
|
|
178
|
+
const countryText = String(country ?? "").trim();
|
|
179
|
+
const languageText = String(language ?? "").trim();
|
|
180
|
+
if (TWO_LETTERS.test(countryText) && TWO_LETTERS.test(languageText)) {
|
|
181
|
+
return void 0;
|
|
182
|
+
}
|
|
183
|
+
return `the market is not valid (country ${JSON.stringify(countryText)}, language ${JSON.stringify(languageText)}): each must be two letters, for example US and en`;
|
|
184
|
+
}
|
|
185
|
+
function isValidMarket(country, language) {
|
|
186
|
+
return marketProblem(country, language) === void 0;
|
|
187
|
+
}
|
|
188
|
+
function createLocaleParts({
|
|
189
|
+
country,
|
|
190
|
+
language
|
|
191
|
+
}) {
|
|
192
|
+
const problem = marketProblem(country, language);
|
|
193
|
+
if (problem) throw new Error(problem);
|
|
194
|
+
const normalizedCountry = country.trim().toUpperCase();
|
|
195
|
+
const normalizedLanguage = language.trim().toLowerCase();
|
|
196
|
+
return {
|
|
197
|
+
country: normalizedCountry,
|
|
198
|
+
language: normalizedLanguage,
|
|
199
|
+
locale: `${normalizedCountry}-${normalizedLanguage}`
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function toCatalogLocale({ country, language }) {
|
|
203
|
+
return `${language}_${country}`;
|
|
204
|
+
}
|
|
205
|
+
function toPathPrefix({ country, language }) {
|
|
206
|
+
return `/${country.toLowerCase()}/${language}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/services/catalogRequest.ts
|
|
210
|
+
function resolveApiGatewayPath(apiGatewayPath, apiName = STOREFRONT_SHOPPING_CONTEXT) {
|
|
211
|
+
const apiGateway = String(apiGatewayPath ?? "").trim();
|
|
212
|
+
if (!apiGateway) return "";
|
|
213
|
+
return apiGateway.replace(/\{api_name\}/gi, apiName).replace(/\/$/, "");
|
|
214
|
+
}
|
|
215
|
+
function resolveCatalogApiBaseUrl(marketConfig) {
|
|
216
|
+
return resolveApiGatewayPath(marketConfig?.api_gateway_path);
|
|
217
|
+
}
|
|
218
|
+
function resolveCatalogStoreId(marketConfig) {
|
|
219
|
+
return String(marketConfig?.store_id ?? "").trim();
|
|
220
|
+
}
|
|
221
|
+
function buildCatalogSuggestionsUrl(apiBaseUrl) {
|
|
222
|
+
const normalizedBase = String(apiBaseUrl ?? "").trim().replace(/\/$/, "");
|
|
223
|
+
if (!normalizedBase) return "";
|
|
224
|
+
const suggestionsPath = normalizedBase.endsWith(
|
|
225
|
+
`/${STOREFRONT_SHOPPING_CONTEXT}`
|
|
226
|
+
) ? "/catalogs/search/suggestions" : CATALOG_SUGGESTIONS_PATH;
|
|
227
|
+
return `${normalizedBase}${suggestionsPath}`;
|
|
228
|
+
}
|
|
229
|
+
function buildSuggestionsRequestUrl({
|
|
230
|
+
suggestionsUrl,
|
|
231
|
+
searchTerm,
|
|
232
|
+
storeId,
|
|
233
|
+
localeParts
|
|
234
|
+
}) {
|
|
235
|
+
const queryParams = new URLSearchParams({
|
|
236
|
+
searchTerm,
|
|
237
|
+
page: String(DEFAULT_SUGGESTION_PAGE),
|
|
238
|
+
size: String(DEFAULT_SUGGESTION_SIZE),
|
|
239
|
+
locale: toCatalogLocale(localeParts),
|
|
240
|
+
storeId
|
|
241
|
+
});
|
|
242
|
+
return `${suggestionsUrl}?${queryParams.toString()}`;
|
|
243
|
+
}
|
|
244
|
+
async function resolveCatalogRequestConfig({
|
|
245
|
+
locationLike,
|
|
246
|
+
country,
|
|
247
|
+
getMarketConfig
|
|
248
|
+
}) {
|
|
249
|
+
const marketConfig = getMarketConfig ? await getMarketConfig({ locationLike, country }) ?? null : null;
|
|
250
|
+
const suggestionsUrl = buildCatalogSuggestionsUrl(
|
|
251
|
+
resolveCatalogApiBaseUrl(marketConfig)
|
|
252
|
+
);
|
|
253
|
+
if (!suggestionsUrl) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`no suggestions API for market ${country}: the market configuration has no gateway`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const storeId = resolveCatalogStoreId(marketConfig);
|
|
259
|
+
if (!storeId) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
`no suggestions API for market ${country}: the market configuration has no store`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return { suggestionsUrl, storeId };
|
|
265
|
+
}
|
|
266
|
+
function resolveCustomerTypeHeaders(getCustomerType) {
|
|
267
|
+
const customerType = getCustomerType?.()?.trim();
|
|
268
|
+
return customerType ? { "X-Customer-Type": customerType } : void 0;
|
|
269
|
+
}
|
|
270
|
+
var NO_RESULTS_ERROR_CODE = "SFOSER002";
|
|
271
|
+
async function isNoResultsResponse(response) {
|
|
272
|
+
if (response.status !== 404) return false;
|
|
273
|
+
try {
|
|
274
|
+
const body = await response.json();
|
|
275
|
+
return body?.code === NO_RESULTS_ERROR_CODE;
|
|
276
|
+
} catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function fetchJson(fetchImpl, requestUrl, requestHeaders = {}) {
|
|
281
|
+
const response = await fetchImpl(requestUrl, {
|
|
282
|
+
method: "GET",
|
|
283
|
+
credentials: "include",
|
|
284
|
+
headers: { Accept: "application/json", ...requestHeaders }
|
|
285
|
+
});
|
|
286
|
+
if (!response.ok) {
|
|
287
|
+
if (await isNoResultsResponse(response)) return null;
|
|
288
|
+
throw new Error(`Request failed with status ${response.status}`);
|
|
289
|
+
}
|
|
290
|
+
return response.json();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/services/recentSearches.ts
|
|
156
294
|
function canUseStorage(storage) {
|
|
157
295
|
return Boolean(storage) && typeof storage.getItem === "function";
|
|
158
296
|
}
|
|
@@ -167,22 +305,8 @@ function getSessionStorage(providedStorage) {
|
|
|
167
305
|
}
|
|
168
306
|
return null;
|
|
169
307
|
}
|
|
170
|
-
function normalizeLocaleParts({
|
|
171
|
-
country,
|
|
172
|
-
language,
|
|
173
|
-
locale
|
|
174
|
-
}) {
|
|
175
|
-
const normalizedCountry = String(country ?? "").trim().toUpperCase();
|
|
176
|
-
const normalizedLanguage = String(language ?? "").trim().toLowerCase();
|
|
177
|
-
const normalizedLocale = String(locale ?? "").trim() || "UNKNOWN";
|
|
178
|
-
return {
|
|
179
|
-
country: normalizedCountry || "UNKNOWN",
|
|
180
|
-
language: normalizedLanguage || "unknown",
|
|
181
|
-
locale: normalizedLocale
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
308
|
function getRecentHeaderSearchesStorageKey(localeParts) {
|
|
185
|
-
const { country, language, locale } =
|
|
309
|
+
const { country, language, locale } = localeParts;
|
|
186
310
|
return `${RECENT_SEARCHES_PREFIX}:${locale}:${country}:${language}`;
|
|
187
311
|
}
|
|
188
312
|
function getRecentHeaderSearches(localeParts, storage) {
|
|
@@ -236,63 +360,8 @@ function clearRecentHeaderSearches(localeParts, storage) {
|
|
|
236
360
|
} catch {
|
|
237
361
|
}
|
|
238
362
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (typeof window !== "undefined" && window.location)
|
|
242
|
-
return window.location;
|
|
243
|
-
return null;
|
|
244
|
-
}
|
|
245
|
-
function resolveApiGatewayPath(apiGatewayPath, apiName = STOREFRONT_SHOPPING_CONTEXT) {
|
|
246
|
-
const apiGateway = String(apiGatewayPath ?? "").trim();
|
|
247
|
-
if (!apiGateway) return "";
|
|
248
|
-
return apiGateway.replace(/\{api_name\}/gi, apiName).replace(/\/$/, "");
|
|
249
|
-
}
|
|
250
|
-
function resolveCatalogApiBaseUrl(marketConfig) {
|
|
251
|
-
return resolveApiGatewayPath(marketConfig?.api_gateway_path);
|
|
252
|
-
}
|
|
253
|
-
function resolveCatalogStoreId(marketConfig) {
|
|
254
|
-
return String(marketConfig?.store_id ?? "").trim();
|
|
255
|
-
}
|
|
256
|
-
function buildCatalogSuggestionsUrl(apiBaseUrl) {
|
|
257
|
-
const normalizedBase = String(apiBaseUrl ?? "").trim().replace(/\/$/, "");
|
|
258
|
-
if (!normalizedBase) return "";
|
|
259
|
-
const suggestionsPath = normalizedBase.endsWith(
|
|
260
|
-
`/${STOREFRONT_SHOPPING_CONTEXT}`
|
|
261
|
-
) ? "/catalogs/search/suggestions" : CATALOG_SUGGESTIONS_PATH;
|
|
262
|
-
return `${normalizedBase}${suggestionsPath}`;
|
|
263
|
-
}
|
|
264
|
-
function resolveSuggestionsUrl(suggestionsApi, locationLike) {
|
|
265
|
-
const pathOrUrl = String(suggestionsApi ?? "").trim();
|
|
266
|
-
if (!pathOrUrl) return "";
|
|
267
|
-
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
268
|
-
return pathOrUrl;
|
|
269
|
-
}
|
|
270
|
-
const normalizedPath = pathOrUrl.startsWith("/") ? pathOrUrl : `/${pathOrUrl}`;
|
|
271
|
-
const origin = String(locationLike?.origin ?? "").trim();
|
|
272
|
-
return origin ? `${origin}${normalizedPath}` : normalizedPath;
|
|
273
|
-
}
|
|
274
|
-
async function resolveCatalogRequestConfig({
|
|
275
|
-
suggestionsApi,
|
|
276
|
-
storeIdOption,
|
|
277
|
-
locationLike,
|
|
278
|
-
country,
|
|
279
|
-
getMarketConfig
|
|
280
|
-
}) {
|
|
281
|
-
const explicitStoreId = storeIdOption !== void 0 && storeIdOption !== null && String(storeIdOption).trim() !== "" ? String(storeIdOption).trim() : "";
|
|
282
|
-
const marketConfig = getMarketConfig ? await getMarketConfig({ locationLike, country }) ?? null : null;
|
|
283
|
-
const configuredApiBase = resolveCatalogApiBaseUrl(marketConfig);
|
|
284
|
-
const configuredStoreId = resolveCatalogStoreId(marketConfig);
|
|
285
|
-
const explicitSuggestionsApi = String(suggestionsApi ?? "").trim();
|
|
286
|
-
return {
|
|
287
|
-
suggestionsUrl: explicitSuggestionsApi ? resolveSuggestionsUrl(explicitSuggestionsApi, locationLike) : buildCatalogSuggestionsUrl(configuredApiBase),
|
|
288
|
-
storeId: explicitStoreId || configuredStoreId
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
function toCatalogLocale(localeParts) {
|
|
292
|
-
const language = String(localeParts.language ?? "en").toLowerCase();
|
|
293
|
-
const country = String(localeParts.country ?? "US").toUpperCase();
|
|
294
|
-
return `${language}_${country}`;
|
|
295
|
-
}
|
|
363
|
+
|
|
364
|
+
// src/services/searchPaths.ts
|
|
296
365
|
function buildSearchResultsPath(query, localeParts) {
|
|
297
366
|
const trimmedQuery = String(query ?? "").trim();
|
|
298
367
|
const hyphenatedQuery = trimmedQuery.replace(/\s+/g, "-");
|
|
@@ -300,13 +369,14 @@ function buildSearchResultsPath(query, localeParts) {
|
|
|
300
369
|
/%2D/gi,
|
|
301
370
|
"-"
|
|
302
371
|
);
|
|
303
|
-
|
|
304
|
-
const language = localeParts.language.toLowerCase();
|
|
305
|
-
return `/${country}/${language}${DEFAULT_SEARCH_PATH}/${encodedQuery}`;
|
|
372
|
+
return `${toPathPrefix(localeParts)}${DEFAULT_SEARCH_PATH}/${encodedQuery}`;
|
|
306
373
|
}
|
|
374
|
+
var SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
307
375
|
function normalizeOptionUrl(rawValue) {
|
|
308
376
|
if (typeof rawValue !== "string") return "";
|
|
309
|
-
|
|
377
|
+
const value = rawValue.trim();
|
|
378
|
+
if (!value || value.startsWith("/") || SCHEME.test(value)) return value;
|
|
379
|
+
return `/${value}`;
|
|
310
380
|
}
|
|
311
381
|
function safeDecodeURIComponent(value) {
|
|
312
382
|
try {
|
|
@@ -336,16 +406,14 @@ function slugifyPathSegment(value) {
|
|
|
336
406
|
function buildProductSlugPath(slugValue, localeParts) {
|
|
337
407
|
const slug = slugifyPathSegment(slugValue);
|
|
338
408
|
if (!slug) return "";
|
|
339
|
-
|
|
340
|
-
const language = String(localeParts.language ?? "en").toLowerCase();
|
|
341
|
-
return `/${country}/${language}/product/${slug}`;
|
|
409
|
+
return `${toPathPrefix(localeParts)}/product/${slug}`;
|
|
342
410
|
}
|
|
343
411
|
function isNavigableUrl(value) {
|
|
344
412
|
if (!value) return false;
|
|
345
413
|
return value.startsWith("/") || /^https?:\/\//i.test(value);
|
|
346
414
|
}
|
|
347
415
|
function stripLocalePrefix(path, localeParts) {
|
|
348
|
-
const prefix =
|
|
416
|
+
const prefix = toPathPrefix(localeParts);
|
|
349
417
|
return path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
|
|
350
418
|
}
|
|
351
419
|
function resolveOptionHref(option, localeParts) {
|
|
@@ -362,67 +430,61 @@ function resolveOptionHref(option, localeParts) {
|
|
|
362
430
|
}
|
|
363
431
|
return "";
|
|
364
432
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
433
|
+
|
|
434
|
+
// src/services/suggestionsPayload.ts
|
|
435
|
+
function emptySuggestions() {
|
|
436
|
+
return { suggestions: [], didYouMean: [], totalResultsCount: 0 };
|
|
368
437
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
try {
|
|
373
|
-
const body = await response.json();
|
|
374
|
-
return body?.code === NO_RESULTS_ERROR_CODE;
|
|
375
|
-
} catch {
|
|
376
|
-
return false;
|
|
438
|
+
function resolveTotalResultsCount(payload, fallback) {
|
|
439
|
+
if (typeof payload?.totalResultsCount === "number") {
|
|
440
|
+
return payload.totalResultsCount;
|
|
377
441
|
}
|
|
442
|
+
const pageableTotal = Number(
|
|
443
|
+
payload?.pageableInfo?.totalCount ?? Number.NaN
|
|
444
|
+
);
|
|
445
|
+
return Number.isNaN(pageableTotal) ? fallback : pageableTotal;
|
|
378
446
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
return await response.json();
|
|
396
|
-
} catch (error) {
|
|
397
|
-
lastError = error;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
throw lastError ?? new Error("Unable to fetch header search data");
|
|
447
|
+
function normalizeSuggestionsPayload(payload) {
|
|
448
|
+
const suggestions = Array.isArray(payload?.suggestions) ? payload.suggestions : [];
|
|
449
|
+
const products = Array.isArray(payload?.product) ? payload.product : null;
|
|
450
|
+
const rawDidYouMean = payload?.didYouMean ?? payload?.did_you_mean;
|
|
451
|
+
const didYouMean = Array.isArray(rawDidYouMean) ? rawDidYouMean : [];
|
|
452
|
+
return {
|
|
453
|
+
...payload ?? {},
|
|
454
|
+
suggestions,
|
|
455
|
+
didYouMean,
|
|
456
|
+
totalResultsCount: resolveTotalResultsCount(
|
|
457
|
+
payload,
|
|
458
|
+
products?.length ?? suggestions.length
|
|
459
|
+
)
|
|
460
|
+
};
|
|
401
461
|
}
|
|
402
|
-
|
|
462
|
+
|
|
463
|
+
// src/services/SearchApiService.ts
|
|
464
|
+
function getBrowserLocation(providedLocation) {
|
|
465
|
+
if (providedLocation) return providedLocation;
|
|
466
|
+
if (typeof window !== "undefined" && window.location)
|
|
467
|
+
return window.location;
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
function createSearchService(options) {
|
|
403
471
|
const {
|
|
404
472
|
fetchImpl: fetchImplOption,
|
|
405
473
|
location: providedLocation,
|
|
406
474
|
storage: providedStorage,
|
|
407
|
-
suggestionsApi,
|
|
408
|
-
storeId: storeIdOption,
|
|
409
475
|
country,
|
|
410
476
|
language,
|
|
411
|
-
locale,
|
|
412
477
|
getMarketConfig,
|
|
413
478
|
getCustomerType,
|
|
414
479
|
minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH
|
|
415
480
|
} = options;
|
|
416
|
-
const fetchImpl = fetchImplOption !== void 0 ? fetchImplOption : typeof fetch === "function" ? fetch.bind(globalThis) : null;
|
|
417
481
|
const locationLike = getBrowserLocation(providedLocation);
|
|
418
482
|
const storage = getSessionStorage(providedStorage);
|
|
419
|
-
const localeParts =
|
|
483
|
+
const localeParts = createLocaleParts({ country, language });
|
|
420
484
|
let catalogRequestConfigPromise = null;
|
|
421
485
|
function loadCatalogRequestConfig() {
|
|
422
486
|
if (!catalogRequestConfigPromise) {
|
|
423
487
|
catalogRequestConfigPromise = resolveCatalogRequestConfig({
|
|
424
|
-
suggestionsApi,
|
|
425
|
-
storeIdOption,
|
|
426
488
|
locationLike,
|
|
427
489
|
country: localeParts.country,
|
|
428
490
|
getMarketConfig
|
|
@@ -440,48 +502,21 @@ function createSearchService(options = {}) {
|
|
|
440
502
|
const service = {
|
|
441
503
|
async fetchSuggestions(searchTerm) {
|
|
442
504
|
const trimmedSearchTerm = String(searchTerm ?? "").trim();
|
|
443
|
-
if (!trimmedSearchTerm || trimmedSearchTerm.length < minimumSearchLength
|
|
444
|
-
return
|
|
445
|
-
suggestions: [],
|
|
446
|
-
didYouMean: [],
|
|
447
|
-
totalResultsCount: 0
|
|
448
|
-
};
|
|
505
|
+
if (!trimmedSearchTerm || trimmedSearchTerm.length < minimumSearchLength) {
|
|
506
|
+
return emptySuggestions();
|
|
449
507
|
}
|
|
450
508
|
const { suggestionsUrl, storeId } = await loadCatalogRequestConfig();
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
searchTerm: trimmedSearchTerm,
|
|
460
|
-
page: String(DEFAULT_SUGGESTION_PAGE),
|
|
461
|
-
size: String(DEFAULT_SUGGESTION_SIZE),
|
|
462
|
-
locale: toCatalogLocale(localeParts),
|
|
463
|
-
storeId: String(storeId)
|
|
464
|
-
});
|
|
465
|
-
const requestUrls = [`${suggestionsUrl}?${queryParams.toString()}`];
|
|
466
|
-
const responsePayload = await fetchJsonFromCandidates(
|
|
467
|
-
fetchImpl,
|
|
468
|
-
requestUrls,
|
|
509
|
+
const responsePayload = await fetchJson(
|
|
510
|
+
fetchImplOption ?? fetch.bind(globalThis),
|
|
511
|
+
buildSuggestionsRequestUrl({
|
|
512
|
+
suggestionsUrl,
|
|
513
|
+
searchTerm: trimmedSearchTerm,
|
|
514
|
+
storeId,
|
|
515
|
+
localeParts
|
|
516
|
+
}),
|
|
469
517
|
resolveCustomerTypeHeaders(getCustomerType)
|
|
470
518
|
);
|
|
471
|
-
|
|
472
|
-
const productField = Array.isArray(responsePayload?.product) ? responsePayload?.product : null;
|
|
473
|
-
const rawDidYouMean = responsePayload?.didYouMean ?? responsePayload?.did_you_mean;
|
|
474
|
-
const didYouMeanField = Array.isArray(rawDidYouMean) ? rawDidYouMean : [];
|
|
475
|
-
const pageableTotal = Number(
|
|
476
|
-
responsePayload?.pageableInfo?.totalCount ?? Number.NaN
|
|
477
|
-
);
|
|
478
|
-
const totalResultsCount = typeof responsePayload?.totalResultsCount === "number" ? responsePayload.totalResultsCount : !Number.isNaN(pageableTotal) ? pageableTotal : productField?.length ?? suggestionsField.length;
|
|
479
|
-
return {
|
|
480
|
-
...responsePayload ?? {},
|
|
481
|
-
suggestions: suggestionsField,
|
|
482
|
-
didYouMean: didYouMeanField,
|
|
483
|
-
totalResultsCount
|
|
484
|
-
};
|
|
519
|
+
return normalizeSuggestionsPayload(responsePayload);
|
|
485
520
|
},
|
|
486
521
|
submitProductSearch(query, option) {
|
|
487
522
|
const trimmedQuery = String(query ?? "").trim();
|
|
@@ -526,7 +561,7 @@ function createSearchService(options = {}) {
|
|
|
526
561
|
|
|
527
562
|
// src/services/marketConfig.ts
|
|
528
563
|
var fetchMarketConfig = async ({ country }) => {
|
|
529
|
-
const countryCode =
|
|
564
|
+
const countryCode = country.trim().toUpperCase();
|
|
530
565
|
const configuration = await getConfiguration([EQUINOX_MARKETS_MAP], {
|
|
531
566
|
country: countryCode,
|
|
532
567
|
...getEnvironmentCode() ? { environment: getEnvironmentCode() } : {},
|
|
@@ -549,14 +584,25 @@ var SEARCH_GTM_USER_STATUS = {
|
|
|
549
584
|
var GTM_FALLBACK_NA = "NA";
|
|
550
585
|
|
|
551
586
|
// src/utils/navigationTarget.ts
|
|
587
|
+
var EXTERNAL_URL = /^https?:\/\//i;
|
|
588
|
+
var INTERNAL_PATH = /^\/(?![/\\])[^\t\n\r]*$/;
|
|
552
589
|
function toNavigationTarget(path) {
|
|
553
|
-
|
|
590
|
+
if (EXTERNAL_URL.test(path)) return { type: "external", url: path };
|
|
591
|
+
if (INTERNAL_PATH.test(path)) return { type: "internal", path };
|
|
592
|
+
return void 0;
|
|
554
593
|
}
|
|
555
594
|
|
|
556
595
|
// src/utils/productEnvironment.ts
|
|
557
|
-
var PRODUCT_ENVIRONMENTS =
|
|
596
|
+
var PRODUCT_ENVIRONMENTS = {
|
|
597
|
+
dev: true,
|
|
598
|
+
test: true,
|
|
599
|
+
prod: true
|
|
600
|
+
};
|
|
601
|
+
function isProductEnvironment(code) {
|
|
602
|
+
return Object.keys(PRODUCT_ENVIRONMENTS).includes(code);
|
|
603
|
+
}
|
|
558
604
|
function toProductEnvironment(environmentCode) {
|
|
559
|
-
return environmentCode &&
|
|
605
|
+
return environmentCode !== void 0 && isProductEnvironment(environmentCode) ? environmentCode : void 0;
|
|
560
606
|
}
|
|
561
607
|
function getProductEnvironment() {
|
|
562
608
|
try {
|
|
@@ -565,8 +611,132 @@ function getProductEnvironment() {
|
|
|
565
611
|
return void 0;
|
|
566
612
|
}
|
|
567
613
|
}
|
|
614
|
+
var NOTHING = () => void 0;
|
|
615
|
+
function CommerceSearchUnavailable({
|
|
616
|
+
isOpen,
|
|
617
|
+
onClose,
|
|
618
|
+
country,
|
|
619
|
+
language,
|
|
620
|
+
logo
|
|
621
|
+
}) {
|
|
622
|
+
useEffect(() => {
|
|
623
|
+
console.error(
|
|
624
|
+
`[search] Search is unavailable: ${marketProblem(country, language)}`
|
|
625
|
+
);
|
|
626
|
+
}, [country, language]);
|
|
627
|
+
return /* @__PURE__ */ jsx(
|
|
628
|
+
SearchPanel,
|
|
629
|
+
{
|
|
630
|
+
isOpen,
|
|
631
|
+
onClose,
|
|
632
|
+
copy: SEARCH_PANEL_COPY_EN,
|
|
633
|
+
logo,
|
|
634
|
+
input: { value: "", onChange: NOTHING, onSubmit: NOTHING },
|
|
635
|
+
suggestions: {
|
|
636
|
+
items: [],
|
|
637
|
+
loading: false,
|
|
638
|
+
showNoResults: false,
|
|
639
|
+
showDidYouMean: false,
|
|
640
|
+
hasMore: false,
|
|
641
|
+
query: "",
|
|
642
|
+
onSelect: NOTHING,
|
|
643
|
+
onRunAction: NOTHING,
|
|
644
|
+
onSeeMore: NOTHING
|
|
645
|
+
},
|
|
646
|
+
recentSearches: {
|
|
647
|
+
items: [],
|
|
648
|
+
onSelect: NOTHING,
|
|
649
|
+
onClear: NOTHING
|
|
650
|
+
},
|
|
651
|
+
agentPanel: /* @__PURE__ */ jsx("p", { role: "alert", children: "Search is unavailable right now." })
|
|
652
|
+
}
|
|
653
|
+
);
|
|
654
|
+
}
|
|
568
655
|
|
|
569
|
-
// src/utils/
|
|
656
|
+
// src/utils/nuiSearchGate.ts
|
|
657
|
+
var NUI_SEARCH_FLAG = "enableNuiSearch";
|
|
658
|
+
function parseFlagValue(raw) {
|
|
659
|
+
const value = raw?.trim().toLowerCase();
|
|
660
|
+
if (value === "true" || value === "1") return true;
|
|
661
|
+
if (value === "false" || value === "0") return false;
|
|
662
|
+
return void 0;
|
|
663
|
+
}
|
|
664
|
+
function readCookieFlag(cookie) {
|
|
665
|
+
for (const part of cookie.split(";")) {
|
|
666
|
+
const separator = part.indexOf("=");
|
|
667
|
+
if (separator < 0) continue;
|
|
668
|
+
if (part.slice(0, separator).trim() !== NUI_SEARCH_FLAG) continue;
|
|
669
|
+
try {
|
|
670
|
+
return parseFlagValue(
|
|
671
|
+
decodeURIComponent(part.slice(separator + 1))
|
|
672
|
+
);
|
|
673
|
+
} catch {
|
|
674
|
+
return void 0;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return void 0;
|
|
678
|
+
}
|
|
679
|
+
function resolveNuiSearchFlag(env = {}) {
|
|
680
|
+
const inBrowser = typeof window !== "undefined" && typeof document !== "undefined";
|
|
681
|
+
const search = env.search ?? (inBrowser ? window.location.search : "");
|
|
682
|
+
const cookie = env.cookie ?? (inBrowser ? document.cookie : "");
|
|
683
|
+
return parseFlagValue(new URLSearchParams(search).get(NUI_SEARCH_FLAG)) ?? readCookieFlag(cookie);
|
|
684
|
+
}
|
|
685
|
+
function isNuiSearchEnabled({
|
|
686
|
+
isAuthenticated,
|
|
687
|
+
flag,
|
|
688
|
+
allowGuests = false
|
|
689
|
+
}) {
|
|
690
|
+
return flag === true && (isAuthenticated === true || allowGuests);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/hooks/useNuiAgentGate.ts
|
|
694
|
+
function useNuiAgentGate({
|
|
695
|
+
hasAgent,
|
|
696
|
+
isOpen,
|
|
697
|
+
isAuthenticated,
|
|
698
|
+
allowGuestAgent
|
|
699
|
+
}) {
|
|
700
|
+
const [flag, setFlag] = useState(void 0);
|
|
701
|
+
useEffect(() => {
|
|
702
|
+
if (isOpen) setFlag(resolveNuiSearchFlag());
|
|
703
|
+
}, [isOpen]);
|
|
704
|
+
return hasAgent && isNuiSearchEnabled({
|
|
705
|
+
isAuthenticated,
|
|
706
|
+
flag,
|
|
707
|
+
allowGuests: allowGuestAgent
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
function uniqueRecentSearches(queries) {
|
|
711
|
+
const seen = /* @__PURE__ */ new Set();
|
|
712
|
+
return queries.filter((query) => {
|
|
713
|
+
const key = query.trim().toLowerCase();
|
|
714
|
+
if (!key || seen.has(key)) return false;
|
|
715
|
+
seen.add(key);
|
|
716
|
+
return true;
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
function useRecentSearches({
|
|
720
|
+
service,
|
|
721
|
+
isOpen,
|
|
722
|
+
refreshKey
|
|
723
|
+
}) {
|
|
724
|
+
const [recentSearches, setRecentSearches] = useState([]);
|
|
725
|
+
useEffect(() => {
|
|
726
|
+
if (isOpen) {
|
|
727
|
+
setRecentSearches(
|
|
728
|
+
uniqueRecentSearches(service.getRecentSearches())
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
}, [isOpen, service, refreshKey]);
|
|
732
|
+
const clearRecentSearches = useCallback(() => {
|
|
733
|
+
service.clearRecentSearches();
|
|
734
|
+
setRecentSearches([]);
|
|
735
|
+
}, [service]);
|
|
736
|
+
return { recentSearches, clearRecentSearches };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/utils/catalogStatus.ts
|
|
570
740
|
var CART_DISABLED_PRODUCT_STATUSES = /* @__PURE__ */ new Set([
|
|
571
741
|
"discontinued",
|
|
572
742
|
"preview",
|
|
@@ -614,6 +784,8 @@ function isSuggestionDisabled(suggestionOption) {
|
|
|
614
784
|
!suggestionOption || suggestionOption["out_of_stock"] || suggestionOption["outOfStock"]
|
|
615
785
|
);
|
|
616
786
|
}
|
|
787
|
+
|
|
788
|
+
// src/utils/catalogHelpers.ts
|
|
617
789
|
function getImageFromProperty(properties = {}) {
|
|
618
790
|
let image = String(properties["primaryimage"] ?? "").trim() || String(properties["imageURL"] ?? "").trim();
|
|
619
791
|
if (!image) {
|
|
@@ -851,6 +1023,46 @@ function normalizeSuggestionsResponse(response) {
|
|
|
851
1023
|
didYouMean: normalizeDidYouMean(r.didYouMean)
|
|
852
1024
|
};
|
|
853
1025
|
}
|
|
1026
|
+
function resolveTotalResultsCount2(response, fallback) {
|
|
1027
|
+
if (typeof response.totalResultsCount === "number") {
|
|
1028
|
+
return response.totalResultsCount;
|
|
1029
|
+
}
|
|
1030
|
+
if (typeof response.pageableInfo?.totalCount === "number") {
|
|
1031
|
+
return response.pageableInfo.totalCount;
|
|
1032
|
+
}
|
|
1033
|
+
return fallback;
|
|
1034
|
+
}
|
|
1035
|
+
var FETCH_FAILED = {
|
|
1036
|
+
suggestions: [],
|
|
1037
|
+
didYouMean: [],
|
|
1038
|
+
hasMoreResults: false,
|
|
1039
|
+
emptyFetched: true,
|
|
1040
|
+
failed: true
|
|
1041
|
+
};
|
|
1042
|
+
function toSettledResults(response) {
|
|
1043
|
+
const { suggestions: allSuggestions, didYouMean } = normalizeSuggestionsResponse(response);
|
|
1044
|
+
if (didYouMean.length > 0) {
|
|
1045
|
+
return {
|
|
1046
|
+
suggestions: [],
|
|
1047
|
+
didYouMean,
|
|
1048
|
+
hasMoreResults: false,
|
|
1049
|
+
emptyFetched: false,
|
|
1050
|
+
failed: false
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
const total = Math.max(
|
|
1054
|
+
resolveTotalResultsCount2(response, allSuggestions.length),
|
|
1055
|
+
allSuggestions.length
|
|
1056
|
+
);
|
|
1057
|
+
const suggestions = allSuggestions.slice(0, MAX_VISIBLE_SUGGESTIONS);
|
|
1058
|
+
return {
|
|
1059
|
+
suggestions,
|
|
1060
|
+
didYouMean,
|
|
1061
|
+
hasMoreResults: total > MAX_VISIBLE_SUGGESTIONS,
|
|
1062
|
+
emptyFetched: suggestions.length === 0,
|
|
1063
|
+
failed: false
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
854
1066
|
function useSearchSuggestions({
|
|
855
1067
|
service,
|
|
856
1068
|
enabled = true
|
|
@@ -862,7 +1074,6 @@ function useSearchSuggestions({
|
|
|
862
1074
|
const [emptyFetched, setEmptyFetched] = useState(false);
|
|
863
1075
|
const [suggestionFetchFailed, setSuggestionFetchFailed] = useState(false);
|
|
864
1076
|
const [hasMoreResults, setHasMoreResults] = useState(false);
|
|
865
|
-
const [totalResultsCount, setTotalResultsCount] = useState(0);
|
|
866
1077
|
const [settledQuery, setSettledQuery] = useState("");
|
|
867
1078
|
const fetchDebounced = useRef(null);
|
|
868
1079
|
const latestRequestId = useRef(0);
|
|
@@ -872,7 +1083,6 @@ function useSearchSuggestions({
|
|
|
872
1083
|
setEmptyFetched(false);
|
|
873
1084
|
setSuggestionFetchFailed(false);
|
|
874
1085
|
setHasMoreResults(false);
|
|
875
|
-
setTotalResultsCount(0);
|
|
876
1086
|
setSettledQuery("");
|
|
877
1087
|
setLoading(false);
|
|
878
1088
|
}, []);
|
|
@@ -890,6 +1100,14 @@ function useSearchSuggestions({
|
|
|
890
1100
|
resetSuggestionsState();
|
|
891
1101
|
setQuery("");
|
|
892
1102
|
}, [resetSuggestionsState]);
|
|
1103
|
+
const settle = useCallback((results, term) => {
|
|
1104
|
+
setDidYouMean(results.didYouMean);
|
|
1105
|
+
setSuggestions(results.suggestions);
|
|
1106
|
+
setHasMoreResults(results.hasMoreResults);
|
|
1107
|
+
setSuggestionFetchFailed(results.failed);
|
|
1108
|
+
setSettledQuery(term);
|
|
1109
|
+
setEmptyFetched(results.emptyFetched);
|
|
1110
|
+
}, []);
|
|
893
1111
|
const runFetch = useCallback(
|
|
894
1112
|
async (nextQuery) => {
|
|
895
1113
|
const trimmedQuery = nextQuery.trim();
|
|
@@ -901,53 +1119,23 @@ function useSearchSuggestions({
|
|
|
901
1119
|
setLoading(true);
|
|
902
1120
|
setEmptyFetched(false);
|
|
903
1121
|
setSuggestionFetchFailed(false);
|
|
1122
|
+
let results;
|
|
904
1123
|
try {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
const {
|
|
910
|
-
suggestions: allSuggestions,
|
|
911
|
-
didYouMean: suggestedTerms
|
|
912
|
-
} = normalizeSuggestionsResponse(fetchedSuggestions);
|
|
913
|
-
const visibleSuggestions = allSuggestions.slice(
|
|
914
|
-
0,
|
|
915
|
-
MAX_VISIBLE_SUGGESTIONS
|
|
916
|
-
);
|
|
917
|
-
const hasDidYouMean = suggestedTerms.length > 0;
|
|
918
|
-
const normalizedTotal = typeof fetchedSuggestions.totalResultsCount === "number" ? fetchedSuggestions.totalResultsCount : typeof fetchedSuggestions.pageableInfo?.totalCount === "number" ? fetchedSuggestions.pageableInfo.totalCount : allSuggestions.length;
|
|
919
|
-
const effectiveTotal = hasDidYouMean ? suggestedTerms.length : Math.max(normalizedTotal, allSuggestions.length);
|
|
920
|
-
setDidYouMean(suggestedTerms);
|
|
921
|
-
setSuggestions(hasDidYouMean ? [] : visibleSuggestions);
|
|
922
|
-
setHasMoreResults(
|
|
923
|
-
!hasDidYouMean && effectiveTotal > MAX_VISIBLE_SUGGESTIONS
|
|
924
|
-
);
|
|
925
|
-
setTotalResultsCount(effectiveTotal);
|
|
926
|
-
setSuggestionFetchFailed(false);
|
|
927
|
-
setSettledQuery(trimmedQuery);
|
|
928
|
-
setEmptyFetched(
|
|
929
|
-
hasDidYouMean ? false : visibleSuggestions.length === 0 && suggestedTerms.length === 0
|
|
1124
|
+
results = toSettledResults(
|
|
1125
|
+
await service.fetchSuggestions(trimmedQuery)
|
|
930
1126
|
);
|
|
931
|
-
} catch {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
}
|
|
935
|
-
setSuggestions([]);
|
|
936
|
-
setDidYouMean([]);
|
|
937
|
-
setEmptyFetched(true);
|
|
938
|
-
setSuggestionFetchFailed(true);
|
|
939
|
-
setHasMoreResults(false);
|
|
940
|
-
setTotalResultsCount(0);
|
|
941
|
-
setSettledQuery(trimmedQuery);
|
|
942
|
-
} finally {
|
|
943
|
-
if (requestId === latestRequestId.current) {
|
|
944
|
-
setLoading(false);
|
|
945
|
-
}
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
console.warn("[search] Could not load suggestions:", error);
|
|
1129
|
+
results = FETCH_FAILED;
|
|
946
1130
|
}
|
|
1131
|
+
if (requestId !== latestRequestId.current) return;
|
|
1132
|
+
settle(results, trimmedQuery);
|
|
1133
|
+
setLoading(false);
|
|
947
1134
|
},
|
|
948
|
-
[enabled, service, resetSuggestionsState]
|
|
1135
|
+
[enabled, service, resetSuggestionsState, settle]
|
|
949
1136
|
);
|
|
950
1137
|
useEffect(() => {
|
|
1138
|
+
latestRequestId.current += 1;
|
|
951
1139
|
if (!enabled) {
|
|
952
1140
|
resetSuggestionsState();
|
|
953
1141
|
return void 0;
|
|
@@ -955,7 +1143,6 @@ function useSearchSuggestions({
|
|
|
955
1143
|
if (fetchDebounced.current) {
|
|
956
1144
|
clearTimeout(fetchDebounced.current);
|
|
957
1145
|
}
|
|
958
|
-
latestRequestId.current += 1;
|
|
959
1146
|
fetchDebounced.current = setTimeout(() => {
|
|
960
1147
|
void runFetch(query);
|
|
961
1148
|
}, DEBOUNCE_MS);
|
|
@@ -969,15 +1156,12 @@ function useSearchSuggestions({
|
|
|
969
1156
|
clearQuery,
|
|
970
1157
|
emptyFetched,
|
|
971
1158
|
hasMoreResults,
|
|
972
|
-
hasQuery: Boolean(query.trim()),
|
|
973
1159
|
didYouMean,
|
|
974
1160
|
loading,
|
|
975
1161
|
query,
|
|
976
1162
|
settledQuery,
|
|
977
|
-
setQuery,
|
|
978
1163
|
suggestionFetchFailed,
|
|
979
1164
|
suggestions,
|
|
980
|
-
totalResultsCount,
|
|
981
1165
|
updateQuery
|
|
982
1166
|
};
|
|
983
1167
|
}
|
|
@@ -1052,43 +1236,6 @@ function trackProductSearch(input) {
|
|
|
1052
1236
|
});
|
|
1053
1237
|
}
|
|
1054
1238
|
|
|
1055
|
-
// src/utils/nuiSearchGate.ts
|
|
1056
|
-
var NUI_SEARCH_FLAG = "enableNuiSearch";
|
|
1057
|
-
function parseFlagValue(raw) {
|
|
1058
|
-
const value = raw?.trim().toLowerCase();
|
|
1059
|
-
if (value === "true" || value === "1") return true;
|
|
1060
|
-
if (value === "false" || value === "0") return false;
|
|
1061
|
-
return void 0;
|
|
1062
|
-
}
|
|
1063
|
-
function readCookieFlag(cookie) {
|
|
1064
|
-
for (const part of cookie.split(";")) {
|
|
1065
|
-
const separator = part.indexOf("=");
|
|
1066
|
-
if (separator < 0) continue;
|
|
1067
|
-
if (part.slice(0, separator).trim() !== NUI_SEARCH_FLAG) continue;
|
|
1068
|
-
try {
|
|
1069
|
-
return parseFlagValue(
|
|
1070
|
-
decodeURIComponent(part.slice(separator + 1))
|
|
1071
|
-
);
|
|
1072
|
-
} catch {
|
|
1073
|
-
return void 0;
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
return void 0;
|
|
1077
|
-
}
|
|
1078
|
-
function resolveNuiSearchFlag(env = {}) {
|
|
1079
|
-
const inBrowser = typeof window !== "undefined" && typeof document !== "undefined";
|
|
1080
|
-
const search = env.search ?? (inBrowser ? window.location.search : "");
|
|
1081
|
-
const cookie = env.cookie ?? (inBrowser ? document.cookie : "");
|
|
1082
|
-
return parseFlagValue(new URLSearchParams(search).get(NUI_SEARCH_FLAG)) ?? readCookieFlag(cookie);
|
|
1083
|
-
}
|
|
1084
|
-
function isNuiSearchEnabled({
|
|
1085
|
-
isAuthenticated,
|
|
1086
|
-
flag,
|
|
1087
|
-
allowGuests = false
|
|
1088
|
-
}) {
|
|
1089
|
-
return flag === true && (isAuthenticated === true || allowGuests);
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
1239
|
// src/utils/searchRouting.ts
|
|
1093
1240
|
function decideSearchRoute({
|
|
1094
1241
|
enabled,
|
|
@@ -1101,6 +1248,96 @@ function decideSearchRoute({
|
|
|
1101
1248
|
return snapshot.suggestions.length > 0 ? "search" : "handoff";
|
|
1102
1249
|
}
|
|
1103
1250
|
|
|
1251
|
+
// src/hooks/useSubmitRouting.ts
|
|
1252
|
+
var SUBMIT_WAIT_TIMEOUT_MS = 2e3;
|
|
1253
|
+
function useSubmitRouting({
|
|
1254
|
+
enabled,
|
|
1255
|
+
minimumSearchLength,
|
|
1256
|
+
search,
|
|
1257
|
+
gtmContext,
|
|
1258
|
+
pendingTerm,
|
|
1259
|
+
setPendingTerm,
|
|
1260
|
+
onSearch,
|
|
1261
|
+
onHandoff
|
|
1262
|
+
}) {
|
|
1263
|
+
const {
|
|
1264
|
+
didYouMean,
|
|
1265
|
+
loading,
|
|
1266
|
+
settledQuery,
|
|
1267
|
+
suggestionFetchFailed,
|
|
1268
|
+
suggestions
|
|
1269
|
+
} = search;
|
|
1270
|
+
const decide = useCallback(
|
|
1271
|
+
(term) => decideSearchRoute({
|
|
1272
|
+
enabled,
|
|
1273
|
+
snapshot: {
|
|
1274
|
+
query: term,
|
|
1275
|
+
loading,
|
|
1276
|
+
settledQuery,
|
|
1277
|
+
suggestions,
|
|
1278
|
+
suggestionFetchFailed
|
|
1279
|
+
}
|
|
1280
|
+
}),
|
|
1281
|
+
[enabled, loading, settledQuery, suggestions, suggestionFetchFailed]
|
|
1282
|
+
);
|
|
1283
|
+
const act = useCallback(
|
|
1284
|
+
(term, route) => {
|
|
1285
|
+
setPendingTerm(null);
|
|
1286
|
+
if (route === "handoff") {
|
|
1287
|
+
trackChatEntryPoint(gtmContext ?? {});
|
|
1288
|
+
onHandoff({
|
|
1289
|
+
query: term,
|
|
1290
|
+
didYouMean: didYouMean.map((item) => item.value)
|
|
1291
|
+
});
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
onSearch(term);
|
|
1295
|
+
},
|
|
1296
|
+
[didYouMean, gtmContext, onHandoff, onSearch, setPendingTerm]
|
|
1297
|
+
);
|
|
1298
|
+
const submit = useCallback(
|
|
1299
|
+
(value) => {
|
|
1300
|
+
const term = value.trim();
|
|
1301
|
+
if (!term) return;
|
|
1302
|
+
if (term.length < minimumSearchLength) {
|
|
1303
|
+
onSearch(term);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
const route = decide(term);
|
|
1307
|
+
if (route === "wait") {
|
|
1308
|
+
setPendingTerm(term);
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
act(term, route);
|
|
1312
|
+
},
|
|
1313
|
+
[act, decide, minimumSearchLength, onSearch, setPendingTerm]
|
|
1314
|
+
);
|
|
1315
|
+
const latest = useRef({ act, decide });
|
|
1316
|
+
useEffect(() => {
|
|
1317
|
+
latest.current = { act, decide };
|
|
1318
|
+
});
|
|
1319
|
+
useEffect(() => {
|
|
1320
|
+
if (pendingTerm === null) return void 0;
|
|
1321
|
+
const route = latest.current.decide(pendingTerm);
|
|
1322
|
+
if (route !== "wait") {
|
|
1323
|
+
latest.current.act(pendingTerm, route);
|
|
1324
|
+
return void 0;
|
|
1325
|
+
}
|
|
1326
|
+
const timer = setTimeout(
|
|
1327
|
+
() => latest.current.act(pendingTerm, "search"),
|
|
1328
|
+
SUBMIT_WAIT_TIMEOUT_MS
|
|
1329
|
+
);
|
|
1330
|
+
return () => clearTimeout(timer);
|
|
1331
|
+
}, [
|
|
1332
|
+
pendingTerm,
|
|
1333
|
+
loading,
|
|
1334
|
+
settledQuery,
|
|
1335
|
+
suggestions,
|
|
1336
|
+
suggestionFetchFailed
|
|
1337
|
+
]);
|
|
1338
|
+
return submit;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1104
1341
|
// src/utils/cachedProductClient.ts
|
|
1105
1342
|
function withProductCache(client) {
|
|
1106
1343
|
const lookups = /* @__PURE__ */ new Map();
|
|
@@ -1168,7 +1405,8 @@ var AgentErrorBoundary = class extends Component {
|
|
|
1168
1405
|
static getDerivedStateFromError() {
|
|
1169
1406
|
return { failed: true };
|
|
1170
1407
|
}
|
|
1171
|
-
componentDidCatch(
|
|
1408
|
+
componentDidCatch(error) {
|
|
1409
|
+
console.warn("[search] The agent could not be shown:", error);
|
|
1172
1410
|
this.props.onError();
|
|
1173
1411
|
}
|
|
1174
1412
|
render() {
|
|
@@ -1181,12 +1419,16 @@ function useHandoff({
|
|
|
1181
1419
|
agentContext,
|
|
1182
1420
|
onUnavailable
|
|
1183
1421
|
}) {
|
|
1422
|
+
const onUnavailableRef = useRef(onUnavailable);
|
|
1423
|
+
useEffect(() => {
|
|
1424
|
+
onUnavailableRef.current = onUnavailable;
|
|
1425
|
+
});
|
|
1184
1426
|
useEffect(() => {
|
|
1185
|
-
let
|
|
1427
|
+
let canceled = false;
|
|
1186
1428
|
let timer;
|
|
1187
1429
|
const startedAt = Date.now();
|
|
1188
1430
|
const tryOpen = () => {
|
|
1189
|
-
if (
|
|
1431
|
+
if (canceled) return;
|
|
1190
1432
|
const handle = getSalesAIHandle();
|
|
1191
1433
|
if (handle) {
|
|
1192
1434
|
handle.open({
|
|
@@ -1196,14 +1438,17 @@ function useHandoff({
|
|
|
1196
1438
|
return;
|
|
1197
1439
|
}
|
|
1198
1440
|
if (Date.now() - startedAt >= HANDLE_TIMEOUT_MS) {
|
|
1199
|
-
|
|
1441
|
+
console.warn(
|
|
1442
|
+
`[search] The agent did not start within ${HANDLE_TIMEOUT_MS / 1e3} seconds.`
|
|
1443
|
+
);
|
|
1444
|
+
onUnavailableRef.current();
|
|
1200
1445
|
return;
|
|
1201
1446
|
}
|
|
1202
1447
|
timer = setTimeout(tryOpen, HANDLE_POLL_MS);
|
|
1203
1448
|
};
|
|
1204
1449
|
tryOpen();
|
|
1205
1450
|
return () => {
|
|
1206
|
-
|
|
1451
|
+
canceled = true;
|
|
1207
1452
|
if (timer !== void 0) clearTimeout(timer);
|
|
1208
1453
|
};
|
|
1209
1454
|
}, [query]);
|
|
@@ -1238,16 +1483,6 @@ function NuiAgentPanel({
|
|
|
1238
1483
|
}
|
|
1239
1484
|
) });
|
|
1240
1485
|
}
|
|
1241
|
-
var SUBMIT_WAIT_TIMEOUT_MS = 2e3;
|
|
1242
|
-
function uniqueRecentSearches(queries) {
|
|
1243
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1244
|
-
return queries.filter((query) => {
|
|
1245
|
-
const key = query.trim().toLowerCase();
|
|
1246
|
-
if (!key || seen.has(key)) return false;
|
|
1247
|
-
seen.add(key);
|
|
1248
|
-
return true;
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
1486
|
function CommerceSearchView({
|
|
1252
1487
|
copy,
|
|
1253
1488
|
isOpen,
|
|
@@ -1262,11 +1497,9 @@ function CommerceSearchView({
|
|
|
1262
1497
|
minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH,
|
|
1263
1498
|
gtmContext
|
|
1264
1499
|
}) {
|
|
1265
|
-
const [flag, setFlag] = useState(void 0);
|
|
1266
1500
|
const [handoff, setHandoff] = useState(null);
|
|
1267
1501
|
const agentQuery = handoff?.query ?? null;
|
|
1268
1502
|
const [pendingTerm, setPendingTerm] = useState(null);
|
|
1269
|
-
const [recentSearches, setRecentSearches] = useState([]);
|
|
1270
1503
|
const search = useSearchSuggestions({
|
|
1271
1504
|
service,
|
|
1272
1505
|
enabled: isOpen && agentQuery === null
|
|
@@ -1278,31 +1511,25 @@ function CommerceSearchView({
|
|
|
1278
1511
|
hasMoreResults,
|
|
1279
1512
|
loading,
|
|
1280
1513
|
query,
|
|
1281
|
-
settledQuery,
|
|
1282
|
-
suggestionFetchFailed,
|
|
1283
1514
|
suggestions,
|
|
1284
1515
|
updateQuery
|
|
1285
1516
|
} = search;
|
|
1286
|
-
const
|
|
1517
|
+
const agentEnabled = useNuiAgentGate({
|
|
1518
|
+
hasAgent: agent !== void 0,
|
|
1519
|
+
isOpen,
|
|
1287
1520
|
isAuthenticated,
|
|
1288
|
-
|
|
1289
|
-
|
|
1521
|
+
allowGuestAgent
|
|
1522
|
+
});
|
|
1523
|
+
const { recentSearches, clearRecentSearches } = useRecentSearches({
|
|
1524
|
+
service,
|
|
1525
|
+
isOpen,
|
|
1526
|
+
refreshKey: agentQuery
|
|
1290
1527
|
});
|
|
1291
1528
|
const items = getVisibleSuggestions({
|
|
1292
1529
|
didYouMean,
|
|
1293
1530
|
suggestions
|
|
1294
1531
|
});
|
|
1295
1532
|
const showDidYouMean = didYouMean.length > 0;
|
|
1296
|
-
useEffect(() => {
|
|
1297
|
-
if (isOpen) setFlag(resolveNuiSearchFlag());
|
|
1298
|
-
}, [isOpen]);
|
|
1299
|
-
useEffect(() => {
|
|
1300
|
-
if (isOpen) {
|
|
1301
|
-
setRecentSearches(
|
|
1302
|
-
uniqueRecentSearches(service.getRecentSearches())
|
|
1303
|
-
);
|
|
1304
|
-
}
|
|
1305
|
-
}, [isOpen, service, agentQuery]);
|
|
1306
1533
|
const close = useCallback(() => {
|
|
1307
1534
|
setHandoff(null);
|
|
1308
1535
|
setPendingTerm(null);
|
|
@@ -1341,72 +1568,16 @@ function CommerceSearchView({
|
|
|
1341
1568
|
},
|
|
1342
1569
|
[items, navigateTo, service, trackSemantic]
|
|
1343
1570
|
);
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
loading,
|
|
1350
|
-
settledQuery,
|
|
1351
|
-
suggestions,
|
|
1352
|
-
suggestionFetchFailed
|
|
1353
|
-
}
|
|
1354
|
-
}),
|
|
1355
|
-
[enabled, loading, settledQuery, suggestions, suggestionFetchFailed]
|
|
1356
|
-
);
|
|
1357
|
-
const act = useCallback(
|
|
1358
|
-
(term, route) => {
|
|
1359
|
-
setPendingTerm(null);
|
|
1360
|
-
if (route === "handoff") {
|
|
1361
|
-
trackChatEntryPoint(gtmContext ?? {});
|
|
1362
|
-
setHandoff({
|
|
1363
|
-
query: term,
|
|
1364
|
-
didYouMean: didYouMean.map((item) => item.value)
|
|
1365
|
-
});
|
|
1366
|
-
return;
|
|
1367
|
-
}
|
|
1368
|
-
runSearch(term);
|
|
1369
|
-
},
|
|
1370
|
-
[didYouMean, gtmContext, runSearch]
|
|
1371
|
-
);
|
|
1372
|
-
const handleSubmit = useCallback(
|
|
1373
|
-
(value) => {
|
|
1374
|
-
const term = value.trim();
|
|
1375
|
-
if (!term) return;
|
|
1376
|
-
if (term.length < minimumSearchLength) {
|
|
1377
|
-
runSearch(term);
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
const route = decide(term);
|
|
1381
|
-
if (route === "wait") {
|
|
1382
|
-
setPendingTerm(term);
|
|
1383
|
-
return;
|
|
1384
|
-
}
|
|
1385
|
-
act(term, route);
|
|
1386
|
-
},
|
|
1387
|
-
[act, decide, minimumSearchLength, runSearch]
|
|
1388
|
-
);
|
|
1389
|
-
const latest = useRef({ act, decide });
|
|
1390
|
-
latest.current = { act, decide };
|
|
1391
|
-
useEffect(() => {
|
|
1392
|
-
if (pendingTerm === null) return void 0;
|
|
1393
|
-
const route = latest.current.decide(pendingTerm);
|
|
1394
|
-
if (route !== "wait") {
|
|
1395
|
-
latest.current.act(pendingTerm, route);
|
|
1396
|
-
return void 0;
|
|
1397
|
-
}
|
|
1398
|
-
const timer = setTimeout(
|
|
1399
|
-
() => latest.current.act(pendingTerm, "search"),
|
|
1400
|
-
SUBMIT_WAIT_TIMEOUT_MS
|
|
1401
|
-
);
|
|
1402
|
-
return () => clearTimeout(timer);
|
|
1403
|
-
}, [
|
|
1571
|
+
const handleSubmit = useSubmitRouting({
|
|
1572
|
+
enabled: agentEnabled,
|
|
1573
|
+
minimumSearchLength,
|
|
1574
|
+
search,
|
|
1575
|
+
...gtmContext ? { gtmContext } : {},
|
|
1404
1576
|
pendingTerm,
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
]);
|
|
1577
|
+
setPendingTerm,
|
|
1578
|
+
onSearch: runSearch,
|
|
1579
|
+
onHandoff: setHandoff
|
|
1580
|
+
});
|
|
1410
1581
|
const handleSelect = useCallback(
|
|
1411
1582
|
(option) => {
|
|
1412
1583
|
if (isSuggestionDisabled(
|
|
@@ -1431,10 +1602,11 @@ function CommerceSearchView({
|
|
|
1431
1602
|
)) {
|
|
1432
1603
|
return;
|
|
1433
1604
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1605
|
+
navigateTo(
|
|
1606
|
+
service.activateSuggestionAction(
|
|
1607
|
+
option
|
|
1608
|
+
)
|
|
1436
1609
|
);
|
|
1437
|
-
void Promise.resolve(result).then(navigateTo).catch(() => void 0);
|
|
1438
1610
|
},
|
|
1439
1611
|
[navigateTo, service]
|
|
1440
1612
|
);
|
|
@@ -1449,10 +1621,6 @@ function CommerceSearchView({
|
|
|
1449
1621
|
},
|
|
1450
1622
|
[navigateTo, service, trackSemantic]
|
|
1451
1623
|
);
|
|
1452
|
-
const handleClearRecent = useCallback(() => {
|
|
1453
|
-
service.clearRecentSearches();
|
|
1454
|
-
setRecentSearches([]);
|
|
1455
|
-
}, [service]);
|
|
1456
1624
|
const handleAgentUnavailable = useCallback(() => {
|
|
1457
1625
|
if (agentQuery !== null)
|
|
1458
1626
|
navigateTo(service.submitProductSearch(agentQuery));
|
|
@@ -1501,14 +1669,29 @@ function CommerceSearchView({
|
|
|
1501
1669
|
recentSearches: {
|
|
1502
1670
|
items: recentSearches,
|
|
1503
1671
|
onSelect: handleRecentSelect,
|
|
1504
|
-
onClear:
|
|
1672
|
+
onClear: clearRecentSearches
|
|
1505
1673
|
},
|
|
1506
1674
|
agentPanel
|
|
1507
1675
|
}
|
|
1508
1676
|
);
|
|
1509
1677
|
}
|
|
1510
1678
|
var GUEST = "guest";
|
|
1511
|
-
function CommerceSearch({
|
|
1679
|
+
function CommerceSearch(props) {
|
|
1680
|
+
if (!isValidMarket(props.country, props.language)) {
|
|
1681
|
+
return /* @__PURE__ */ jsx(
|
|
1682
|
+
CommerceSearchUnavailable,
|
|
1683
|
+
{
|
|
1684
|
+
isOpen: props.isOpen,
|
|
1685
|
+
onClose: props.onClose,
|
|
1686
|
+
country: props.country,
|
|
1687
|
+
language: props.language,
|
|
1688
|
+
logo: props.logo
|
|
1689
|
+
}
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
return /* @__PURE__ */ jsx(ValidMarketSearch, { ...props });
|
|
1693
|
+
}
|
|
1694
|
+
function ValidMarketSearch({
|
|
1512
1695
|
isOpen,
|
|
1513
1696
|
onClose,
|
|
1514
1697
|
navigate,
|
|
@@ -1527,13 +1710,15 @@ function CommerceSearch({
|
|
|
1527
1710
|
language,
|
|
1528
1711
|
enabled: hasOpened
|
|
1529
1712
|
});
|
|
1530
|
-
const
|
|
1531
|
-
customerTypeRef
|
|
1713
|
+
const customerType = customer.isAuthenticated ? customer.accountType ?? GUEST : GUEST;
|
|
1714
|
+
const customerTypeRef = useRef(customerType);
|
|
1715
|
+
useEffect(() => {
|
|
1716
|
+
customerTypeRef.current = customerType;
|
|
1717
|
+
}, [customerType]);
|
|
1532
1718
|
const service = useMemo(
|
|
1533
1719
|
() => createSearchService({
|
|
1534
1720
|
country,
|
|
1535
1721
|
language,
|
|
1536
|
-
locale: `${country.toUpperCase()}-${language.toLowerCase()}`,
|
|
1537
1722
|
getMarketConfig: fetchMarketConfig,
|
|
1538
1723
|
minimumSearchLength,
|
|
1539
1724
|
getCustomerType: () => customerTypeRef.current
|
|
@@ -1541,7 +1726,17 @@ function CommerceSearch({
|
|
|
1541
1726
|
[country, language, minimumSearchLength]
|
|
1542
1727
|
);
|
|
1543
1728
|
const navigateToPath = useCallback(
|
|
1544
|
-
(path) =>
|
|
1729
|
+
(path) => {
|
|
1730
|
+
const target = toNavigationTarget(path);
|
|
1731
|
+
if (target) {
|
|
1732
|
+
navigate(target);
|
|
1733
|
+
} else {
|
|
1734
|
+
console.warn(
|
|
1735
|
+
"[search] Not navigating to a path the SDK does not recognize:",
|
|
1736
|
+
path
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
},
|
|
1545
1740
|
[navigate]
|
|
1546
1741
|
);
|
|
1547
1742
|
const { isAuthenticated, accountType, userId } = customer;
|