@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.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
var react = require('react');
|
|
4
4
|
var configurationSdk = require('@nuskin/configuration-sdk');
|
|
5
5
|
var reactSearchComponents = require('@nuskin/react-search-components');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
7
|
var reactNuiChatSdk = require('@nuskin/react-nui-chat-sdk');
|
|
7
8
|
require('@nuskin/react-nui-chat-sdk/styles.css');
|
|
8
9
|
var reactProductSdk = require('@nuskin/react-product-sdk');
|
|
9
10
|
var styled = require('@emotion/styled');
|
|
10
|
-
var jsxRuntime = require('react/jsx-runtime');
|
|
11
11
|
|
|
12
12
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
13
|
|
|
@@ -127,21 +127,41 @@ async function fetchSearchPanelCopy(_options = {}) {
|
|
|
127
127
|
|
|
128
128
|
// src/copy/loadSearchPanelCopy.ts
|
|
129
129
|
var copyByLanguage = /* @__PURE__ */ new Map();
|
|
130
|
+
var loadedByLanguage = /* @__PURE__ */ new Map();
|
|
131
|
+
function cacheKey(language) {
|
|
132
|
+
return (language ?? "").toLowerCase();
|
|
133
|
+
}
|
|
130
134
|
function loadSearchPanelCopy(language) {
|
|
131
|
-
const key = (language
|
|
135
|
+
const key = cacheKey(language);
|
|
132
136
|
const cached = copyByLanguage.get(key);
|
|
133
137
|
if (cached) return cached;
|
|
134
138
|
const pending = fetchSearchPanelCopy(language ? { } : {});
|
|
135
139
|
copyByLanguage.set(key, pending);
|
|
136
|
-
pending.
|
|
140
|
+
pending.then(
|
|
141
|
+
(copy) => {
|
|
142
|
+
if (copyByLanguage.get(key) === pending) {
|
|
143
|
+
loadedByLanguage.set(key, copy);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
() => {
|
|
147
|
+
if (copyByLanguage.get(key) === pending) {
|
|
148
|
+
copyByLanguage.delete(key);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
);
|
|
137
152
|
return pending;
|
|
138
153
|
}
|
|
154
|
+
function peekSearchPanelCopy(language) {
|
|
155
|
+
return loadedByLanguage.get(cacheKey(language));
|
|
156
|
+
}
|
|
139
157
|
|
|
140
158
|
// src/hooks/useSearchPanelCopy.ts
|
|
141
159
|
function useSearchPanelCopy({
|
|
142
160
|
language
|
|
143
161
|
}) {
|
|
144
|
-
const [copy, setCopy] = react.useState(
|
|
162
|
+
const [copy, setCopy] = react.useState(
|
|
163
|
+
() => peekSearchPanelCopy(language)
|
|
164
|
+
);
|
|
145
165
|
react.useEffect(() => {
|
|
146
166
|
let isStale = false;
|
|
147
167
|
loadSearchPanelCopy(language).then((next) => {
|
|
@@ -159,7 +179,125 @@ function useSearchPanelCopy({
|
|
|
159
179
|
return copy;
|
|
160
180
|
}
|
|
161
181
|
|
|
162
|
-
// src/services/
|
|
182
|
+
// src/services/localeParts.ts
|
|
183
|
+
var TWO_LETTERS = /^[A-Za-z]{2}$/;
|
|
184
|
+
function marketProblem(country, language) {
|
|
185
|
+
const countryText = String(country ?? "").trim();
|
|
186
|
+
const languageText = String(language ?? "").trim();
|
|
187
|
+
if (TWO_LETTERS.test(countryText) && TWO_LETTERS.test(languageText)) {
|
|
188
|
+
return void 0;
|
|
189
|
+
}
|
|
190
|
+
return `the market is not valid (country ${JSON.stringify(countryText)}, language ${JSON.stringify(languageText)}): each must be two letters, for example US and en`;
|
|
191
|
+
}
|
|
192
|
+
function isValidMarket(country, language) {
|
|
193
|
+
return marketProblem(country, language) === void 0;
|
|
194
|
+
}
|
|
195
|
+
function createLocaleParts({
|
|
196
|
+
country,
|
|
197
|
+
language
|
|
198
|
+
}) {
|
|
199
|
+
const problem = marketProblem(country, language);
|
|
200
|
+
if (problem) throw new Error(problem);
|
|
201
|
+
const normalizedCountry = country.trim().toUpperCase();
|
|
202
|
+
const normalizedLanguage = language.trim().toLowerCase();
|
|
203
|
+
return {
|
|
204
|
+
country: normalizedCountry,
|
|
205
|
+
language: normalizedLanguage,
|
|
206
|
+
locale: `${normalizedCountry}-${normalizedLanguage}`
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function toCatalogLocale({ country, language }) {
|
|
210
|
+
return `${language}_${country}`;
|
|
211
|
+
}
|
|
212
|
+
function toPathPrefix({ country, language }) {
|
|
213
|
+
return `/${country.toLowerCase()}/${language}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/services/catalogRequest.ts
|
|
217
|
+
function resolveApiGatewayPath(apiGatewayPath, apiName = STOREFRONT_SHOPPING_CONTEXT) {
|
|
218
|
+
const apiGateway = String(apiGatewayPath ?? "").trim();
|
|
219
|
+
if (!apiGateway) return "";
|
|
220
|
+
return apiGateway.replace(/\{api_name\}/gi, apiName).replace(/\/$/, "");
|
|
221
|
+
}
|
|
222
|
+
function resolveCatalogApiBaseUrl(marketConfig) {
|
|
223
|
+
return resolveApiGatewayPath(marketConfig?.api_gateway_path);
|
|
224
|
+
}
|
|
225
|
+
function resolveCatalogStoreId(marketConfig) {
|
|
226
|
+
return String(marketConfig?.store_id ?? "").trim();
|
|
227
|
+
}
|
|
228
|
+
function buildCatalogSuggestionsUrl(apiBaseUrl) {
|
|
229
|
+
const normalizedBase = String(apiBaseUrl ?? "").trim().replace(/\/$/, "");
|
|
230
|
+
if (!normalizedBase) return "";
|
|
231
|
+
const suggestionsPath = normalizedBase.endsWith(
|
|
232
|
+
`/${STOREFRONT_SHOPPING_CONTEXT}`
|
|
233
|
+
) ? "/catalogs/search/suggestions" : CATALOG_SUGGESTIONS_PATH;
|
|
234
|
+
return `${normalizedBase}${suggestionsPath}`;
|
|
235
|
+
}
|
|
236
|
+
function buildSuggestionsRequestUrl({
|
|
237
|
+
suggestionsUrl,
|
|
238
|
+
searchTerm,
|
|
239
|
+
storeId,
|
|
240
|
+
localeParts
|
|
241
|
+
}) {
|
|
242
|
+
const queryParams = new URLSearchParams({
|
|
243
|
+
searchTerm,
|
|
244
|
+
page: String(DEFAULT_SUGGESTION_PAGE),
|
|
245
|
+
size: String(DEFAULT_SUGGESTION_SIZE),
|
|
246
|
+
locale: toCatalogLocale(localeParts),
|
|
247
|
+
storeId
|
|
248
|
+
});
|
|
249
|
+
return `${suggestionsUrl}?${queryParams.toString()}`;
|
|
250
|
+
}
|
|
251
|
+
async function resolveCatalogRequestConfig({
|
|
252
|
+
locationLike,
|
|
253
|
+
country,
|
|
254
|
+
getMarketConfig
|
|
255
|
+
}) {
|
|
256
|
+
const marketConfig = getMarketConfig ? await getMarketConfig({ locationLike, country }) ?? null : null;
|
|
257
|
+
const suggestionsUrl = buildCatalogSuggestionsUrl(
|
|
258
|
+
resolveCatalogApiBaseUrl(marketConfig)
|
|
259
|
+
);
|
|
260
|
+
if (!suggestionsUrl) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`no suggestions API for market ${country}: the market configuration has no gateway`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
const storeId = resolveCatalogStoreId(marketConfig);
|
|
266
|
+
if (!storeId) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`no suggestions API for market ${country}: the market configuration has no store`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return { suggestionsUrl, storeId };
|
|
272
|
+
}
|
|
273
|
+
function resolveCustomerTypeHeaders(getCustomerType) {
|
|
274
|
+
const customerType = getCustomerType?.()?.trim();
|
|
275
|
+
return customerType ? { "X-Customer-Type": customerType } : void 0;
|
|
276
|
+
}
|
|
277
|
+
var NO_RESULTS_ERROR_CODE = "SFOSER002";
|
|
278
|
+
async function isNoResultsResponse(response) {
|
|
279
|
+
if (response.status !== 404) return false;
|
|
280
|
+
try {
|
|
281
|
+
const body = await response.json();
|
|
282
|
+
return body?.code === NO_RESULTS_ERROR_CODE;
|
|
283
|
+
} catch {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function fetchJson(fetchImpl, requestUrl, requestHeaders = {}) {
|
|
288
|
+
const response = await fetchImpl(requestUrl, {
|
|
289
|
+
method: "GET",
|
|
290
|
+
credentials: "include",
|
|
291
|
+
headers: { Accept: "application/json", ...requestHeaders }
|
|
292
|
+
});
|
|
293
|
+
if (!response.ok) {
|
|
294
|
+
if (await isNoResultsResponse(response)) return null;
|
|
295
|
+
throw new Error(`Request failed with status ${response.status}`);
|
|
296
|
+
}
|
|
297
|
+
return response.json();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/services/recentSearches.ts
|
|
163
301
|
function canUseStorage(storage) {
|
|
164
302
|
return Boolean(storage) && typeof storage.getItem === "function";
|
|
165
303
|
}
|
|
@@ -174,22 +312,8 @@ function getSessionStorage(providedStorage) {
|
|
|
174
312
|
}
|
|
175
313
|
return null;
|
|
176
314
|
}
|
|
177
|
-
function normalizeLocaleParts({
|
|
178
|
-
country,
|
|
179
|
-
language,
|
|
180
|
-
locale
|
|
181
|
-
}) {
|
|
182
|
-
const normalizedCountry = String(country ?? "").trim().toUpperCase();
|
|
183
|
-
const normalizedLanguage = String(language ?? "").trim().toLowerCase();
|
|
184
|
-
const normalizedLocale = String(locale ?? "").trim() || "UNKNOWN";
|
|
185
|
-
return {
|
|
186
|
-
country: normalizedCountry || "UNKNOWN",
|
|
187
|
-
language: normalizedLanguage || "unknown",
|
|
188
|
-
locale: normalizedLocale
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
315
|
function getRecentHeaderSearchesStorageKey(localeParts) {
|
|
192
|
-
const { country, language, locale } =
|
|
316
|
+
const { country, language, locale } = localeParts;
|
|
193
317
|
return `${RECENT_SEARCHES_PREFIX}:${locale}:${country}:${language}`;
|
|
194
318
|
}
|
|
195
319
|
function getRecentHeaderSearches(localeParts, storage) {
|
|
@@ -243,63 +367,8 @@ function clearRecentHeaderSearches(localeParts, storage) {
|
|
|
243
367
|
} catch {
|
|
244
368
|
}
|
|
245
369
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (typeof window !== "undefined" && window.location)
|
|
249
|
-
return window.location;
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
function resolveApiGatewayPath(apiGatewayPath, apiName = STOREFRONT_SHOPPING_CONTEXT) {
|
|
253
|
-
const apiGateway = String(apiGatewayPath ?? "").trim();
|
|
254
|
-
if (!apiGateway) return "";
|
|
255
|
-
return apiGateway.replace(/\{api_name\}/gi, apiName).replace(/\/$/, "");
|
|
256
|
-
}
|
|
257
|
-
function resolveCatalogApiBaseUrl(marketConfig) {
|
|
258
|
-
return resolveApiGatewayPath(marketConfig?.api_gateway_path);
|
|
259
|
-
}
|
|
260
|
-
function resolveCatalogStoreId(marketConfig) {
|
|
261
|
-
return String(marketConfig?.store_id ?? "").trim();
|
|
262
|
-
}
|
|
263
|
-
function buildCatalogSuggestionsUrl(apiBaseUrl) {
|
|
264
|
-
const normalizedBase = String(apiBaseUrl ?? "").trim().replace(/\/$/, "");
|
|
265
|
-
if (!normalizedBase) return "";
|
|
266
|
-
const suggestionsPath = normalizedBase.endsWith(
|
|
267
|
-
`/${STOREFRONT_SHOPPING_CONTEXT}`
|
|
268
|
-
) ? "/catalogs/search/suggestions" : CATALOG_SUGGESTIONS_PATH;
|
|
269
|
-
return `${normalizedBase}${suggestionsPath}`;
|
|
270
|
-
}
|
|
271
|
-
function resolveSuggestionsUrl(suggestionsApi, locationLike) {
|
|
272
|
-
const pathOrUrl = String(suggestionsApi ?? "").trim();
|
|
273
|
-
if (!pathOrUrl) return "";
|
|
274
|
-
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
275
|
-
return pathOrUrl;
|
|
276
|
-
}
|
|
277
|
-
const normalizedPath = pathOrUrl.startsWith("/") ? pathOrUrl : `/${pathOrUrl}`;
|
|
278
|
-
const origin = String(locationLike?.origin ?? "").trim();
|
|
279
|
-
return origin ? `${origin}${normalizedPath}` : normalizedPath;
|
|
280
|
-
}
|
|
281
|
-
async function resolveCatalogRequestConfig({
|
|
282
|
-
suggestionsApi,
|
|
283
|
-
storeIdOption,
|
|
284
|
-
locationLike,
|
|
285
|
-
country,
|
|
286
|
-
getMarketConfig
|
|
287
|
-
}) {
|
|
288
|
-
const explicitStoreId = storeIdOption !== void 0 && storeIdOption !== null && String(storeIdOption).trim() !== "" ? String(storeIdOption).trim() : "";
|
|
289
|
-
const marketConfig = getMarketConfig ? await getMarketConfig({ locationLike, country }) ?? null : null;
|
|
290
|
-
const configuredApiBase = resolveCatalogApiBaseUrl(marketConfig);
|
|
291
|
-
const configuredStoreId = resolveCatalogStoreId(marketConfig);
|
|
292
|
-
const explicitSuggestionsApi = String(suggestionsApi ?? "").trim();
|
|
293
|
-
return {
|
|
294
|
-
suggestionsUrl: explicitSuggestionsApi ? resolveSuggestionsUrl(explicitSuggestionsApi, locationLike) : buildCatalogSuggestionsUrl(configuredApiBase),
|
|
295
|
-
storeId: explicitStoreId || configuredStoreId
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
function toCatalogLocale(localeParts) {
|
|
299
|
-
const language = String(localeParts.language ?? "en").toLowerCase();
|
|
300
|
-
const country = String(localeParts.country ?? "US").toUpperCase();
|
|
301
|
-
return `${language}_${country}`;
|
|
302
|
-
}
|
|
370
|
+
|
|
371
|
+
// src/services/searchPaths.ts
|
|
303
372
|
function buildSearchResultsPath(query, localeParts) {
|
|
304
373
|
const trimmedQuery = String(query ?? "").trim();
|
|
305
374
|
const hyphenatedQuery = trimmedQuery.replace(/\s+/g, "-");
|
|
@@ -307,13 +376,14 @@ function buildSearchResultsPath(query, localeParts) {
|
|
|
307
376
|
/%2D/gi,
|
|
308
377
|
"-"
|
|
309
378
|
);
|
|
310
|
-
|
|
311
|
-
const language = localeParts.language.toLowerCase();
|
|
312
|
-
return `/${country}/${language}${DEFAULT_SEARCH_PATH}/${encodedQuery}`;
|
|
379
|
+
return `${toPathPrefix(localeParts)}${DEFAULT_SEARCH_PATH}/${encodedQuery}`;
|
|
313
380
|
}
|
|
381
|
+
var SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
314
382
|
function normalizeOptionUrl(rawValue) {
|
|
315
383
|
if (typeof rawValue !== "string") return "";
|
|
316
|
-
|
|
384
|
+
const value = rawValue.trim();
|
|
385
|
+
if (!value || value.startsWith("/") || SCHEME.test(value)) return value;
|
|
386
|
+
return `/${value}`;
|
|
317
387
|
}
|
|
318
388
|
function safeDecodeURIComponent(value) {
|
|
319
389
|
try {
|
|
@@ -343,16 +413,14 @@ function slugifyPathSegment(value) {
|
|
|
343
413
|
function buildProductSlugPath(slugValue, localeParts) {
|
|
344
414
|
const slug = slugifyPathSegment(slugValue);
|
|
345
415
|
if (!slug) return "";
|
|
346
|
-
|
|
347
|
-
const language = String(localeParts.language ?? "en").toLowerCase();
|
|
348
|
-
return `/${country}/${language}/product/${slug}`;
|
|
416
|
+
return `${toPathPrefix(localeParts)}/product/${slug}`;
|
|
349
417
|
}
|
|
350
418
|
function isNavigableUrl(value) {
|
|
351
419
|
if (!value) return false;
|
|
352
420
|
return value.startsWith("/") || /^https?:\/\//i.test(value);
|
|
353
421
|
}
|
|
354
422
|
function stripLocalePrefix(path, localeParts) {
|
|
355
|
-
const prefix =
|
|
423
|
+
const prefix = toPathPrefix(localeParts);
|
|
356
424
|
return path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
|
|
357
425
|
}
|
|
358
426
|
function resolveOptionHref(option, localeParts) {
|
|
@@ -369,67 +437,61 @@ function resolveOptionHref(option, localeParts) {
|
|
|
369
437
|
}
|
|
370
438
|
return "";
|
|
371
439
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
440
|
+
|
|
441
|
+
// src/services/suggestionsPayload.ts
|
|
442
|
+
function emptySuggestions() {
|
|
443
|
+
return { suggestions: [], didYouMean: [], totalResultsCount: 0 };
|
|
375
444
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
try {
|
|
380
|
-
const body = await response.json();
|
|
381
|
-
return body?.code === NO_RESULTS_ERROR_CODE;
|
|
382
|
-
} catch {
|
|
383
|
-
return false;
|
|
445
|
+
function resolveTotalResultsCount(payload, fallback) {
|
|
446
|
+
if (typeof payload?.totalResultsCount === "number") {
|
|
447
|
+
return payload.totalResultsCount;
|
|
384
448
|
}
|
|
449
|
+
const pageableTotal = Number(
|
|
450
|
+
payload?.pageableInfo?.totalCount ?? Number.NaN
|
|
451
|
+
);
|
|
452
|
+
return Number.isNaN(pageableTotal) ? fallback : pageableTotal;
|
|
385
453
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
return await response.json();
|
|
403
|
-
} catch (error) {
|
|
404
|
-
lastError = error;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
throw lastError ?? new Error("Unable to fetch header search data");
|
|
454
|
+
function normalizeSuggestionsPayload(payload) {
|
|
455
|
+
const suggestions = Array.isArray(payload?.suggestions) ? payload.suggestions : [];
|
|
456
|
+
const products = Array.isArray(payload?.product) ? payload.product : null;
|
|
457
|
+
const rawDidYouMean = payload?.didYouMean ?? payload?.did_you_mean;
|
|
458
|
+
const didYouMean = Array.isArray(rawDidYouMean) ? rawDidYouMean : [];
|
|
459
|
+
return {
|
|
460
|
+
...payload ?? {},
|
|
461
|
+
suggestions,
|
|
462
|
+
didYouMean,
|
|
463
|
+
totalResultsCount: resolveTotalResultsCount(
|
|
464
|
+
payload,
|
|
465
|
+
products?.length ?? suggestions.length
|
|
466
|
+
)
|
|
467
|
+
};
|
|
408
468
|
}
|
|
409
|
-
|
|
469
|
+
|
|
470
|
+
// src/services/SearchApiService.ts
|
|
471
|
+
function getBrowserLocation(providedLocation) {
|
|
472
|
+
if (providedLocation) return providedLocation;
|
|
473
|
+
if (typeof window !== "undefined" && window.location)
|
|
474
|
+
return window.location;
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
function createSearchService(options) {
|
|
410
478
|
const {
|
|
411
479
|
fetchImpl: fetchImplOption,
|
|
412
480
|
location: providedLocation,
|
|
413
481
|
storage: providedStorage,
|
|
414
|
-
suggestionsApi,
|
|
415
|
-
storeId: storeIdOption,
|
|
416
482
|
country,
|
|
417
483
|
language,
|
|
418
|
-
locale,
|
|
419
484
|
getMarketConfig,
|
|
420
485
|
getCustomerType,
|
|
421
486
|
minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH
|
|
422
487
|
} = options;
|
|
423
|
-
const fetchImpl = fetchImplOption !== void 0 ? fetchImplOption : typeof fetch === "function" ? fetch.bind(globalThis) : null;
|
|
424
488
|
const locationLike = getBrowserLocation(providedLocation);
|
|
425
489
|
const storage = getSessionStorage(providedStorage);
|
|
426
|
-
const localeParts =
|
|
490
|
+
const localeParts = createLocaleParts({ country, language });
|
|
427
491
|
let catalogRequestConfigPromise = null;
|
|
428
492
|
function loadCatalogRequestConfig() {
|
|
429
493
|
if (!catalogRequestConfigPromise) {
|
|
430
494
|
catalogRequestConfigPromise = resolveCatalogRequestConfig({
|
|
431
|
-
suggestionsApi,
|
|
432
|
-
storeIdOption,
|
|
433
495
|
locationLike,
|
|
434
496
|
country: localeParts.country,
|
|
435
497
|
getMarketConfig
|
|
@@ -447,48 +509,21 @@ function createSearchService(options = {}) {
|
|
|
447
509
|
const service = {
|
|
448
510
|
async fetchSuggestions(searchTerm) {
|
|
449
511
|
const trimmedSearchTerm = String(searchTerm ?? "").trim();
|
|
450
|
-
if (!trimmedSearchTerm || trimmedSearchTerm.length < minimumSearchLength
|
|
451
|
-
return
|
|
452
|
-
suggestions: [],
|
|
453
|
-
didYouMean: [],
|
|
454
|
-
totalResultsCount: 0
|
|
455
|
-
};
|
|
512
|
+
if (!trimmedSearchTerm || trimmedSearchTerm.length < minimumSearchLength) {
|
|
513
|
+
return emptySuggestions();
|
|
456
514
|
}
|
|
457
515
|
const { suggestionsUrl, storeId } = await loadCatalogRequestConfig();
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
searchTerm: trimmedSearchTerm,
|
|
467
|
-
page: String(DEFAULT_SUGGESTION_PAGE),
|
|
468
|
-
size: String(DEFAULT_SUGGESTION_SIZE),
|
|
469
|
-
locale: toCatalogLocale(localeParts),
|
|
470
|
-
storeId: String(storeId)
|
|
471
|
-
});
|
|
472
|
-
const requestUrls = [`${suggestionsUrl}?${queryParams.toString()}`];
|
|
473
|
-
const responsePayload = await fetchJsonFromCandidates(
|
|
474
|
-
fetchImpl,
|
|
475
|
-
requestUrls,
|
|
516
|
+
const responsePayload = await fetchJson(
|
|
517
|
+
fetchImplOption ?? fetch.bind(globalThis),
|
|
518
|
+
buildSuggestionsRequestUrl({
|
|
519
|
+
suggestionsUrl,
|
|
520
|
+
searchTerm: trimmedSearchTerm,
|
|
521
|
+
storeId,
|
|
522
|
+
localeParts
|
|
523
|
+
}),
|
|
476
524
|
resolveCustomerTypeHeaders(getCustomerType)
|
|
477
525
|
);
|
|
478
|
-
|
|
479
|
-
const productField = Array.isArray(responsePayload?.product) ? responsePayload?.product : null;
|
|
480
|
-
const rawDidYouMean = responsePayload?.didYouMean ?? responsePayload?.did_you_mean;
|
|
481
|
-
const didYouMeanField = Array.isArray(rawDidYouMean) ? rawDidYouMean : [];
|
|
482
|
-
const pageableTotal = Number(
|
|
483
|
-
responsePayload?.pageableInfo?.totalCount ?? Number.NaN
|
|
484
|
-
);
|
|
485
|
-
const totalResultsCount = typeof responsePayload?.totalResultsCount === "number" ? responsePayload.totalResultsCount : !Number.isNaN(pageableTotal) ? pageableTotal : productField?.length ?? suggestionsField.length;
|
|
486
|
-
return {
|
|
487
|
-
...responsePayload ?? {},
|
|
488
|
-
suggestions: suggestionsField,
|
|
489
|
-
didYouMean: didYouMeanField,
|
|
490
|
-
totalResultsCount
|
|
491
|
-
};
|
|
526
|
+
return normalizeSuggestionsPayload(responsePayload);
|
|
492
527
|
},
|
|
493
528
|
submitProductSearch(query, option) {
|
|
494
529
|
const trimmedQuery = String(query ?? "").trim();
|
|
@@ -533,7 +568,7 @@ function createSearchService(options = {}) {
|
|
|
533
568
|
|
|
534
569
|
// src/services/marketConfig.ts
|
|
535
570
|
var fetchMarketConfig = async ({ country }) => {
|
|
536
|
-
const countryCode =
|
|
571
|
+
const countryCode = country.trim().toUpperCase();
|
|
537
572
|
const configuration = await getConfiguration([EQUINOX_MARKETS_MAP], {
|
|
538
573
|
country: countryCode,
|
|
539
574
|
...getEnvironmentCode() ? { environment: getEnvironmentCode() } : {},
|
|
@@ -556,14 +591,25 @@ var SEARCH_GTM_USER_STATUS = {
|
|
|
556
591
|
var GTM_FALLBACK_NA = "NA";
|
|
557
592
|
|
|
558
593
|
// src/utils/navigationTarget.ts
|
|
594
|
+
var EXTERNAL_URL = /^https?:\/\//i;
|
|
595
|
+
var INTERNAL_PATH = /^\/(?![/\\])[^\t\n\r]*$/;
|
|
559
596
|
function toNavigationTarget(path) {
|
|
560
|
-
|
|
597
|
+
if (EXTERNAL_URL.test(path)) return { type: "external", url: path };
|
|
598
|
+
if (INTERNAL_PATH.test(path)) return { type: "internal", path };
|
|
599
|
+
return void 0;
|
|
561
600
|
}
|
|
562
601
|
|
|
563
602
|
// src/utils/productEnvironment.ts
|
|
564
|
-
var PRODUCT_ENVIRONMENTS =
|
|
603
|
+
var PRODUCT_ENVIRONMENTS = {
|
|
604
|
+
dev: true,
|
|
605
|
+
test: true,
|
|
606
|
+
prod: true
|
|
607
|
+
};
|
|
608
|
+
function isProductEnvironment(code) {
|
|
609
|
+
return Object.keys(PRODUCT_ENVIRONMENTS).includes(code);
|
|
610
|
+
}
|
|
565
611
|
function toProductEnvironment(environmentCode) {
|
|
566
|
-
return environmentCode &&
|
|
612
|
+
return environmentCode !== void 0 && isProductEnvironment(environmentCode) ? environmentCode : void 0;
|
|
567
613
|
}
|
|
568
614
|
function getProductEnvironment() {
|
|
569
615
|
try {
|
|
@@ -572,8 +618,132 @@ function getProductEnvironment() {
|
|
|
572
618
|
return void 0;
|
|
573
619
|
}
|
|
574
620
|
}
|
|
621
|
+
var NOTHING = () => void 0;
|
|
622
|
+
function CommerceSearchUnavailable({
|
|
623
|
+
isOpen,
|
|
624
|
+
onClose,
|
|
625
|
+
country,
|
|
626
|
+
language,
|
|
627
|
+
logo
|
|
628
|
+
}) {
|
|
629
|
+
react.useEffect(() => {
|
|
630
|
+
console.error(
|
|
631
|
+
`[search] Search is unavailable: ${marketProblem(country, language)}`
|
|
632
|
+
);
|
|
633
|
+
}, [country, language]);
|
|
634
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
635
|
+
reactSearchComponents.SearchPanel,
|
|
636
|
+
{
|
|
637
|
+
isOpen,
|
|
638
|
+
onClose,
|
|
639
|
+
copy: SEARCH_PANEL_COPY_EN,
|
|
640
|
+
logo,
|
|
641
|
+
input: { value: "", onChange: NOTHING, onSubmit: NOTHING },
|
|
642
|
+
suggestions: {
|
|
643
|
+
items: [],
|
|
644
|
+
loading: false,
|
|
645
|
+
showNoResults: false,
|
|
646
|
+
showDidYouMean: false,
|
|
647
|
+
hasMore: false,
|
|
648
|
+
query: "",
|
|
649
|
+
onSelect: NOTHING,
|
|
650
|
+
onRunAction: NOTHING,
|
|
651
|
+
onSeeMore: NOTHING
|
|
652
|
+
},
|
|
653
|
+
recentSearches: {
|
|
654
|
+
items: [],
|
|
655
|
+
onSelect: NOTHING,
|
|
656
|
+
onClear: NOTHING
|
|
657
|
+
},
|
|
658
|
+
agentPanel: /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: "Search is unavailable right now." })
|
|
659
|
+
}
|
|
660
|
+
);
|
|
661
|
+
}
|
|
575
662
|
|
|
576
|
-
// src/utils/
|
|
663
|
+
// src/utils/nuiSearchGate.ts
|
|
664
|
+
var NUI_SEARCH_FLAG = "enableNuiSearch";
|
|
665
|
+
function parseFlagValue(raw) {
|
|
666
|
+
const value = raw?.trim().toLowerCase();
|
|
667
|
+
if (value === "true" || value === "1") return true;
|
|
668
|
+
if (value === "false" || value === "0") return false;
|
|
669
|
+
return void 0;
|
|
670
|
+
}
|
|
671
|
+
function readCookieFlag(cookie) {
|
|
672
|
+
for (const part of cookie.split(";")) {
|
|
673
|
+
const separator = part.indexOf("=");
|
|
674
|
+
if (separator < 0) continue;
|
|
675
|
+
if (part.slice(0, separator).trim() !== NUI_SEARCH_FLAG) continue;
|
|
676
|
+
try {
|
|
677
|
+
return parseFlagValue(
|
|
678
|
+
decodeURIComponent(part.slice(separator + 1))
|
|
679
|
+
);
|
|
680
|
+
} catch {
|
|
681
|
+
return void 0;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return void 0;
|
|
685
|
+
}
|
|
686
|
+
function resolveNuiSearchFlag(env = {}) {
|
|
687
|
+
const inBrowser = typeof window !== "undefined" && typeof document !== "undefined";
|
|
688
|
+
const search = env.search ?? (inBrowser ? window.location.search : "");
|
|
689
|
+
const cookie = env.cookie ?? (inBrowser ? document.cookie : "");
|
|
690
|
+
return parseFlagValue(new URLSearchParams(search).get(NUI_SEARCH_FLAG)) ?? readCookieFlag(cookie);
|
|
691
|
+
}
|
|
692
|
+
function isNuiSearchEnabled({
|
|
693
|
+
isAuthenticated,
|
|
694
|
+
flag,
|
|
695
|
+
allowGuests = false
|
|
696
|
+
}) {
|
|
697
|
+
return flag === true && (isAuthenticated === true || allowGuests);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/hooks/useNuiAgentGate.ts
|
|
701
|
+
function useNuiAgentGate({
|
|
702
|
+
hasAgent,
|
|
703
|
+
isOpen,
|
|
704
|
+
isAuthenticated,
|
|
705
|
+
allowGuestAgent
|
|
706
|
+
}) {
|
|
707
|
+
const [flag, setFlag] = react.useState(void 0);
|
|
708
|
+
react.useEffect(() => {
|
|
709
|
+
if (isOpen) setFlag(resolveNuiSearchFlag());
|
|
710
|
+
}, [isOpen]);
|
|
711
|
+
return hasAgent && isNuiSearchEnabled({
|
|
712
|
+
isAuthenticated,
|
|
713
|
+
flag,
|
|
714
|
+
allowGuests: allowGuestAgent
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
function uniqueRecentSearches(queries) {
|
|
718
|
+
const seen = /* @__PURE__ */ new Set();
|
|
719
|
+
return queries.filter((query) => {
|
|
720
|
+
const key = query.trim().toLowerCase();
|
|
721
|
+
if (!key || seen.has(key)) return false;
|
|
722
|
+
seen.add(key);
|
|
723
|
+
return true;
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
function useRecentSearches({
|
|
727
|
+
service,
|
|
728
|
+
isOpen,
|
|
729
|
+
refreshKey
|
|
730
|
+
}) {
|
|
731
|
+
const [recentSearches, setRecentSearches] = react.useState([]);
|
|
732
|
+
react.useEffect(() => {
|
|
733
|
+
if (isOpen) {
|
|
734
|
+
setRecentSearches(
|
|
735
|
+
uniqueRecentSearches(service.getRecentSearches())
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
}, [isOpen, service, refreshKey]);
|
|
739
|
+
const clearRecentSearches = react.useCallback(() => {
|
|
740
|
+
service.clearRecentSearches();
|
|
741
|
+
setRecentSearches([]);
|
|
742
|
+
}, [service]);
|
|
743
|
+
return { recentSearches, clearRecentSearches };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/utils/catalogStatus.ts
|
|
577
747
|
var CART_DISABLED_PRODUCT_STATUSES = /* @__PURE__ */ new Set([
|
|
578
748
|
"discontinued",
|
|
579
749
|
"preview",
|
|
@@ -621,6 +791,8 @@ function isSuggestionDisabled(suggestionOption) {
|
|
|
621
791
|
!suggestionOption || suggestionOption["out_of_stock"] || suggestionOption["outOfStock"]
|
|
622
792
|
);
|
|
623
793
|
}
|
|
794
|
+
|
|
795
|
+
// src/utils/catalogHelpers.ts
|
|
624
796
|
function getImageFromProperty(properties = {}) {
|
|
625
797
|
let image = String(properties["primaryimage"] ?? "").trim() || String(properties["imageURL"] ?? "").trim();
|
|
626
798
|
if (!image) {
|
|
@@ -858,6 +1030,46 @@ function normalizeSuggestionsResponse(response) {
|
|
|
858
1030
|
didYouMean: normalizeDidYouMean(r.didYouMean)
|
|
859
1031
|
};
|
|
860
1032
|
}
|
|
1033
|
+
function resolveTotalResultsCount2(response, fallback) {
|
|
1034
|
+
if (typeof response.totalResultsCount === "number") {
|
|
1035
|
+
return response.totalResultsCount;
|
|
1036
|
+
}
|
|
1037
|
+
if (typeof response.pageableInfo?.totalCount === "number") {
|
|
1038
|
+
return response.pageableInfo.totalCount;
|
|
1039
|
+
}
|
|
1040
|
+
return fallback;
|
|
1041
|
+
}
|
|
1042
|
+
var FETCH_FAILED = {
|
|
1043
|
+
suggestions: [],
|
|
1044
|
+
didYouMean: [],
|
|
1045
|
+
hasMoreResults: false,
|
|
1046
|
+
emptyFetched: true,
|
|
1047
|
+
failed: true
|
|
1048
|
+
};
|
|
1049
|
+
function toSettledResults(response) {
|
|
1050
|
+
const { suggestions: allSuggestions, didYouMean } = normalizeSuggestionsResponse(response);
|
|
1051
|
+
if (didYouMean.length > 0) {
|
|
1052
|
+
return {
|
|
1053
|
+
suggestions: [],
|
|
1054
|
+
didYouMean,
|
|
1055
|
+
hasMoreResults: false,
|
|
1056
|
+
emptyFetched: false,
|
|
1057
|
+
failed: false
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
const total = Math.max(
|
|
1061
|
+
resolveTotalResultsCount2(response, allSuggestions.length),
|
|
1062
|
+
allSuggestions.length
|
|
1063
|
+
);
|
|
1064
|
+
const suggestions = allSuggestions.slice(0, MAX_VISIBLE_SUGGESTIONS);
|
|
1065
|
+
return {
|
|
1066
|
+
suggestions,
|
|
1067
|
+
didYouMean,
|
|
1068
|
+
hasMoreResults: total > MAX_VISIBLE_SUGGESTIONS,
|
|
1069
|
+
emptyFetched: suggestions.length === 0,
|
|
1070
|
+
failed: false
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
861
1073
|
function useSearchSuggestions({
|
|
862
1074
|
service,
|
|
863
1075
|
enabled = true
|
|
@@ -869,7 +1081,6 @@ function useSearchSuggestions({
|
|
|
869
1081
|
const [emptyFetched, setEmptyFetched] = react.useState(false);
|
|
870
1082
|
const [suggestionFetchFailed, setSuggestionFetchFailed] = react.useState(false);
|
|
871
1083
|
const [hasMoreResults, setHasMoreResults] = react.useState(false);
|
|
872
|
-
const [totalResultsCount, setTotalResultsCount] = react.useState(0);
|
|
873
1084
|
const [settledQuery, setSettledQuery] = react.useState("");
|
|
874
1085
|
const fetchDebounced = react.useRef(null);
|
|
875
1086
|
const latestRequestId = react.useRef(0);
|
|
@@ -879,7 +1090,6 @@ function useSearchSuggestions({
|
|
|
879
1090
|
setEmptyFetched(false);
|
|
880
1091
|
setSuggestionFetchFailed(false);
|
|
881
1092
|
setHasMoreResults(false);
|
|
882
|
-
setTotalResultsCount(0);
|
|
883
1093
|
setSettledQuery("");
|
|
884
1094
|
setLoading(false);
|
|
885
1095
|
}, []);
|
|
@@ -897,6 +1107,14 @@ function useSearchSuggestions({
|
|
|
897
1107
|
resetSuggestionsState();
|
|
898
1108
|
setQuery("");
|
|
899
1109
|
}, [resetSuggestionsState]);
|
|
1110
|
+
const settle = react.useCallback((results, term) => {
|
|
1111
|
+
setDidYouMean(results.didYouMean);
|
|
1112
|
+
setSuggestions(results.suggestions);
|
|
1113
|
+
setHasMoreResults(results.hasMoreResults);
|
|
1114
|
+
setSuggestionFetchFailed(results.failed);
|
|
1115
|
+
setSettledQuery(term);
|
|
1116
|
+
setEmptyFetched(results.emptyFetched);
|
|
1117
|
+
}, []);
|
|
900
1118
|
const runFetch = react.useCallback(
|
|
901
1119
|
async (nextQuery) => {
|
|
902
1120
|
const trimmedQuery = nextQuery.trim();
|
|
@@ -908,53 +1126,23 @@ function useSearchSuggestions({
|
|
|
908
1126
|
setLoading(true);
|
|
909
1127
|
setEmptyFetched(false);
|
|
910
1128
|
setSuggestionFetchFailed(false);
|
|
1129
|
+
let results;
|
|
911
1130
|
try {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
const {
|
|
917
|
-
suggestions: allSuggestions,
|
|
918
|
-
didYouMean: suggestedTerms
|
|
919
|
-
} = normalizeSuggestionsResponse(fetchedSuggestions);
|
|
920
|
-
const visibleSuggestions = allSuggestions.slice(
|
|
921
|
-
0,
|
|
922
|
-
MAX_VISIBLE_SUGGESTIONS
|
|
923
|
-
);
|
|
924
|
-
const hasDidYouMean = suggestedTerms.length > 0;
|
|
925
|
-
const normalizedTotal = typeof fetchedSuggestions.totalResultsCount === "number" ? fetchedSuggestions.totalResultsCount : typeof fetchedSuggestions.pageableInfo?.totalCount === "number" ? fetchedSuggestions.pageableInfo.totalCount : allSuggestions.length;
|
|
926
|
-
const effectiveTotal = hasDidYouMean ? suggestedTerms.length : Math.max(normalizedTotal, allSuggestions.length);
|
|
927
|
-
setDidYouMean(suggestedTerms);
|
|
928
|
-
setSuggestions(hasDidYouMean ? [] : visibleSuggestions);
|
|
929
|
-
setHasMoreResults(
|
|
930
|
-
!hasDidYouMean && effectiveTotal > MAX_VISIBLE_SUGGESTIONS
|
|
931
|
-
);
|
|
932
|
-
setTotalResultsCount(effectiveTotal);
|
|
933
|
-
setSuggestionFetchFailed(false);
|
|
934
|
-
setSettledQuery(trimmedQuery);
|
|
935
|
-
setEmptyFetched(
|
|
936
|
-
hasDidYouMean ? false : visibleSuggestions.length === 0 && suggestedTerms.length === 0
|
|
1131
|
+
results = toSettledResults(
|
|
1132
|
+
await service.fetchSuggestions(trimmedQuery)
|
|
937
1133
|
);
|
|
938
|
-
} catch {
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
}
|
|
942
|
-
setSuggestions([]);
|
|
943
|
-
setDidYouMean([]);
|
|
944
|
-
setEmptyFetched(true);
|
|
945
|
-
setSuggestionFetchFailed(true);
|
|
946
|
-
setHasMoreResults(false);
|
|
947
|
-
setTotalResultsCount(0);
|
|
948
|
-
setSettledQuery(trimmedQuery);
|
|
949
|
-
} finally {
|
|
950
|
-
if (requestId === latestRequestId.current) {
|
|
951
|
-
setLoading(false);
|
|
952
|
-
}
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
console.warn("[search] Could not load suggestions:", error);
|
|
1136
|
+
results = FETCH_FAILED;
|
|
953
1137
|
}
|
|
1138
|
+
if (requestId !== latestRequestId.current) return;
|
|
1139
|
+
settle(results, trimmedQuery);
|
|
1140
|
+
setLoading(false);
|
|
954
1141
|
},
|
|
955
|
-
[enabled, service, resetSuggestionsState]
|
|
1142
|
+
[enabled, service, resetSuggestionsState, settle]
|
|
956
1143
|
);
|
|
957
1144
|
react.useEffect(() => {
|
|
1145
|
+
latestRequestId.current += 1;
|
|
958
1146
|
if (!enabled) {
|
|
959
1147
|
resetSuggestionsState();
|
|
960
1148
|
return void 0;
|
|
@@ -962,7 +1150,6 @@ function useSearchSuggestions({
|
|
|
962
1150
|
if (fetchDebounced.current) {
|
|
963
1151
|
clearTimeout(fetchDebounced.current);
|
|
964
1152
|
}
|
|
965
|
-
latestRequestId.current += 1;
|
|
966
1153
|
fetchDebounced.current = setTimeout(() => {
|
|
967
1154
|
void runFetch(query);
|
|
968
1155
|
}, DEBOUNCE_MS);
|
|
@@ -976,15 +1163,12 @@ function useSearchSuggestions({
|
|
|
976
1163
|
clearQuery,
|
|
977
1164
|
emptyFetched,
|
|
978
1165
|
hasMoreResults,
|
|
979
|
-
hasQuery: Boolean(query.trim()),
|
|
980
1166
|
didYouMean,
|
|
981
1167
|
loading,
|
|
982
1168
|
query,
|
|
983
1169
|
settledQuery,
|
|
984
|
-
setQuery,
|
|
985
1170
|
suggestionFetchFailed,
|
|
986
1171
|
suggestions,
|
|
987
|
-
totalResultsCount,
|
|
988
1172
|
updateQuery
|
|
989
1173
|
};
|
|
990
1174
|
}
|
|
@@ -1059,43 +1243,6 @@ function trackProductSearch(input) {
|
|
|
1059
1243
|
});
|
|
1060
1244
|
}
|
|
1061
1245
|
|
|
1062
|
-
// src/utils/nuiSearchGate.ts
|
|
1063
|
-
var NUI_SEARCH_FLAG = "enableNuiSearch";
|
|
1064
|
-
function parseFlagValue(raw) {
|
|
1065
|
-
const value = raw?.trim().toLowerCase();
|
|
1066
|
-
if (value === "true" || value === "1") return true;
|
|
1067
|
-
if (value === "false" || value === "0") return false;
|
|
1068
|
-
return void 0;
|
|
1069
|
-
}
|
|
1070
|
-
function readCookieFlag(cookie) {
|
|
1071
|
-
for (const part of cookie.split(";")) {
|
|
1072
|
-
const separator = part.indexOf("=");
|
|
1073
|
-
if (separator < 0) continue;
|
|
1074
|
-
if (part.slice(0, separator).trim() !== NUI_SEARCH_FLAG) continue;
|
|
1075
|
-
try {
|
|
1076
|
-
return parseFlagValue(
|
|
1077
|
-
decodeURIComponent(part.slice(separator + 1))
|
|
1078
|
-
);
|
|
1079
|
-
} catch {
|
|
1080
|
-
return void 0;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
return void 0;
|
|
1084
|
-
}
|
|
1085
|
-
function resolveNuiSearchFlag(env = {}) {
|
|
1086
|
-
const inBrowser = typeof window !== "undefined" && typeof document !== "undefined";
|
|
1087
|
-
const search = env.search ?? (inBrowser ? window.location.search : "");
|
|
1088
|
-
const cookie = env.cookie ?? (inBrowser ? document.cookie : "");
|
|
1089
|
-
return parseFlagValue(new URLSearchParams(search).get(NUI_SEARCH_FLAG)) ?? readCookieFlag(cookie);
|
|
1090
|
-
}
|
|
1091
|
-
function isNuiSearchEnabled({
|
|
1092
|
-
isAuthenticated,
|
|
1093
|
-
flag,
|
|
1094
|
-
allowGuests = false
|
|
1095
|
-
}) {
|
|
1096
|
-
return flag === true && (isAuthenticated === true || allowGuests);
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
1246
|
// src/utils/searchRouting.ts
|
|
1100
1247
|
function decideSearchRoute({
|
|
1101
1248
|
enabled,
|
|
@@ -1108,6 +1255,96 @@ function decideSearchRoute({
|
|
|
1108
1255
|
return snapshot.suggestions.length > 0 ? "search" : "handoff";
|
|
1109
1256
|
}
|
|
1110
1257
|
|
|
1258
|
+
// src/hooks/useSubmitRouting.ts
|
|
1259
|
+
var SUBMIT_WAIT_TIMEOUT_MS = 2e3;
|
|
1260
|
+
function useSubmitRouting({
|
|
1261
|
+
enabled,
|
|
1262
|
+
minimumSearchLength,
|
|
1263
|
+
search,
|
|
1264
|
+
gtmContext,
|
|
1265
|
+
pendingTerm,
|
|
1266
|
+
setPendingTerm,
|
|
1267
|
+
onSearch,
|
|
1268
|
+
onHandoff
|
|
1269
|
+
}) {
|
|
1270
|
+
const {
|
|
1271
|
+
didYouMean,
|
|
1272
|
+
loading,
|
|
1273
|
+
settledQuery,
|
|
1274
|
+
suggestionFetchFailed,
|
|
1275
|
+
suggestions
|
|
1276
|
+
} = search;
|
|
1277
|
+
const decide = react.useCallback(
|
|
1278
|
+
(term) => decideSearchRoute({
|
|
1279
|
+
enabled,
|
|
1280
|
+
snapshot: {
|
|
1281
|
+
query: term,
|
|
1282
|
+
loading,
|
|
1283
|
+
settledQuery,
|
|
1284
|
+
suggestions,
|
|
1285
|
+
suggestionFetchFailed
|
|
1286
|
+
}
|
|
1287
|
+
}),
|
|
1288
|
+
[enabled, loading, settledQuery, suggestions, suggestionFetchFailed]
|
|
1289
|
+
);
|
|
1290
|
+
const act = react.useCallback(
|
|
1291
|
+
(term, route) => {
|
|
1292
|
+
setPendingTerm(null);
|
|
1293
|
+
if (route === "handoff") {
|
|
1294
|
+
trackChatEntryPoint(gtmContext ?? {});
|
|
1295
|
+
onHandoff({
|
|
1296
|
+
query: term,
|
|
1297
|
+
didYouMean: didYouMean.map((item) => item.value)
|
|
1298
|
+
});
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
onSearch(term);
|
|
1302
|
+
},
|
|
1303
|
+
[didYouMean, gtmContext, onHandoff, onSearch, setPendingTerm]
|
|
1304
|
+
);
|
|
1305
|
+
const submit = react.useCallback(
|
|
1306
|
+
(value) => {
|
|
1307
|
+
const term = value.trim();
|
|
1308
|
+
if (!term) return;
|
|
1309
|
+
if (term.length < minimumSearchLength) {
|
|
1310
|
+
onSearch(term);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
const route = decide(term);
|
|
1314
|
+
if (route === "wait") {
|
|
1315
|
+
setPendingTerm(term);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
act(term, route);
|
|
1319
|
+
},
|
|
1320
|
+
[act, decide, minimumSearchLength, onSearch, setPendingTerm]
|
|
1321
|
+
);
|
|
1322
|
+
const latest = react.useRef({ act, decide });
|
|
1323
|
+
react.useEffect(() => {
|
|
1324
|
+
latest.current = { act, decide };
|
|
1325
|
+
});
|
|
1326
|
+
react.useEffect(() => {
|
|
1327
|
+
if (pendingTerm === null) return void 0;
|
|
1328
|
+
const route = latest.current.decide(pendingTerm);
|
|
1329
|
+
if (route !== "wait") {
|
|
1330
|
+
latest.current.act(pendingTerm, route);
|
|
1331
|
+
return void 0;
|
|
1332
|
+
}
|
|
1333
|
+
const timer = setTimeout(
|
|
1334
|
+
() => latest.current.act(pendingTerm, "search"),
|
|
1335
|
+
SUBMIT_WAIT_TIMEOUT_MS
|
|
1336
|
+
);
|
|
1337
|
+
return () => clearTimeout(timer);
|
|
1338
|
+
}, [
|
|
1339
|
+
pendingTerm,
|
|
1340
|
+
loading,
|
|
1341
|
+
settledQuery,
|
|
1342
|
+
suggestions,
|
|
1343
|
+
suggestionFetchFailed
|
|
1344
|
+
]);
|
|
1345
|
+
return submit;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1111
1348
|
// src/utils/cachedProductClient.ts
|
|
1112
1349
|
function withProductCache(client) {
|
|
1113
1350
|
const lookups = /* @__PURE__ */ new Map();
|
|
@@ -1175,7 +1412,8 @@ var AgentErrorBoundary = class extends react.Component {
|
|
|
1175
1412
|
static getDerivedStateFromError() {
|
|
1176
1413
|
return { failed: true };
|
|
1177
1414
|
}
|
|
1178
|
-
componentDidCatch(
|
|
1415
|
+
componentDidCatch(error) {
|
|
1416
|
+
console.warn("[search] The agent could not be shown:", error);
|
|
1179
1417
|
this.props.onError();
|
|
1180
1418
|
}
|
|
1181
1419
|
render() {
|
|
@@ -1188,12 +1426,16 @@ function useHandoff({
|
|
|
1188
1426
|
agentContext,
|
|
1189
1427
|
onUnavailable
|
|
1190
1428
|
}) {
|
|
1429
|
+
const onUnavailableRef = react.useRef(onUnavailable);
|
|
1430
|
+
react.useEffect(() => {
|
|
1431
|
+
onUnavailableRef.current = onUnavailable;
|
|
1432
|
+
});
|
|
1191
1433
|
react.useEffect(() => {
|
|
1192
|
-
let
|
|
1434
|
+
let canceled = false;
|
|
1193
1435
|
let timer;
|
|
1194
1436
|
const startedAt = Date.now();
|
|
1195
1437
|
const tryOpen = () => {
|
|
1196
|
-
if (
|
|
1438
|
+
if (canceled) return;
|
|
1197
1439
|
const handle = getSalesAIHandle();
|
|
1198
1440
|
if (handle) {
|
|
1199
1441
|
handle.open({
|
|
@@ -1203,14 +1445,17 @@ function useHandoff({
|
|
|
1203
1445
|
return;
|
|
1204
1446
|
}
|
|
1205
1447
|
if (Date.now() - startedAt >= HANDLE_TIMEOUT_MS) {
|
|
1206
|
-
|
|
1448
|
+
console.warn(
|
|
1449
|
+
`[search] The agent did not start within ${HANDLE_TIMEOUT_MS / 1e3} seconds.`
|
|
1450
|
+
);
|
|
1451
|
+
onUnavailableRef.current();
|
|
1207
1452
|
return;
|
|
1208
1453
|
}
|
|
1209
1454
|
timer = setTimeout(tryOpen, HANDLE_POLL_MS);
|
|
1210
1455
|
};
|
|
1211
1456
|
tryOpen();
|
|
1212
1457
|
return () => {
|
|
1213
|
-
|
|
1458
|
+
canceled = true;
|
|
1214
1459
|
if (timer !== void 0) clearTimeout(timer);
|
|
1215
1460
|
};
|
|
1216
1461
|
}, [query]);
|
|
@@ -1245,16 +1490,6 @@ function NuiAgentPanel({
|
|
|
1245
1490
|
}
|
|
1246
1491
|
) });
|
|
1247
1492
|
}
|
|
1248
|
-
var SUBMIT_WAIT_TIMEOUT_MS = 2e3;
|
|
1249
|
-
function uniqueRecentSearches(queries) {
|
|
1250
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1251
|
-
return queries.filter((query) => {
|
|
1252
|
-
const key = query.trim().toLowerCase();
|
|
1253
|
-
if (!key || seen.has(key)) return false;
|
|
1254
|
-
seen.add(key);
|
|
1255
|
-
return true;
|
|
1256
|
-
});
|
|
1257
|
-
}
|
|
1258
1493
|
function CommerceSearchView({
|
|
1259
1494
|
copy,
|
|
1260
1495
|
isOpen,
|
|
@@ -1269,11 +1504,9 @@ function CommerceSearchView({
|
|
|
1269
1504
|
minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH,
|
|
1270
1505
|
gtmContext
|
|
1271
1506
|
}) {
|
|
1272
|
-
const [flag, setFlag] = react.useState(void 0);
|
|
1273
1507
|
const [handoff, setHandoff] = react.useState(null);
|
|
1274
1508
|
const agentQuery = handoff?.query ?? null;
|
|
1275
1509
|
const [pendingTerm, setPendingTerm] = react.useState(null);
|
|
1276
|
-
const [recentSearches, setRecentSearches] = react.useState([]);
|
|
1277
1510
|
const search = useSearchSuggestions({
|
|
1278
1511
|
service,
|
|
1279
1512
|
enabled: isOpen && agentQuery === null
|
|
@@ -1285,31 +1518,25 @@ function CommerceSearchView({
|
|
|
1285
1518
|
hasMoreResults,
|
|
1286
1519
|
loading,
|
|
1287
1520
|
query,
|
|
1288
|
-
settledQuery,
|
|
1289
|
-
suggestionFetchFailed,
|
|
1290
1521
|
suggestions,
|
|
1291
1522
|
updateQuery
|
|
1292
1523
|
} = search;
|
|
1293
|
-
const
|
|
1524
|
+
const agentEnabled = useNuiAgentGate({
|
|
1525
|
+
hasAgent: agent !== void 0,
|
|
1526
|
+
isOpen,
|
|
1294
1527
|
isAuthenticated,
|
|
1295
|
-
|
|
1296
|
-
|
|
1528
|
+
allowGuestAgent
|
|
1529
|
+
});
|
|
1530
|
+
const { recentSearches, clearRecentSearches } = useRecentSearches({
|
|
1531
|
+
service,
|
|
1532
|
+
isOpen,
|
|
1533
|
+
refreshKey: agentQuery
|
|
1297
1534
|
});
|
|
1298
1535
|
const items = getVisibleSuggestions({
|
|
1299
1536
|
didYouMean,
|
|
1300
1537
|
suggestions
|
|
1301
1538
|
});
|
|
1302
1539
|
const showDidYouMean = didYouMean.length > 0;
|
|
1303
|
-
react.useEffect(() => {
|
|
1304
|
-
if (isOpen) setFlag(resolveNuiSearchFlag());
|
|
1305
|
-
}, [isOpen]);
|
|
1306
|
-
react.useEffect(() => {
|
|
1307
|
-
if (isOpen) {
|
|
1308
|
-
setRecentSearches(
|
|
1309
|
-
uniqueRecentSearches(service.getRecentSearches())
|
|
1310
|
-
);
|
|
1311
|
-
}
|
|
1312
|
-
}, [isOpen, service, agentQuery]);
|
|
1313
1540
|
const close = react.useCallback(() => {
|
|
1314
1541
|
setHandoff(null);
|
|
1315
1542
|
setPendingTerm(null);
|
|
@@ -1348,72 +1575,16 @@ function CommerceSearchView({
|
|
|
1348
1575
|
},
|
|
1349
1576
|
[items, navigateTo, service, trackSemantic]
|
|
1350
1577
|
);
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
loading,
|
|
1357
|
-
settledQuery,
|
|
1358
|
-
suggestions,
|
|
1359
|
-
suggestionFetchFailed
|
|
1360
|
-
}
|
|
1361
|
-
}),
|
|
1362
|
-
[enabled, loading, settledQuery, suggestions, suggestionFetchFailed]
|
|
1363
|
-
);
|
|
1364
|
-
const act = react.useCallback(
|
|
1365
|
-
(term, route) => {
|
|
1366
|
-
setPendingTerm(null);
|
|
1367
|
-
if (route === "handoff") {
|
|
1368
|
-
trackChatEntryPoint(gtmContext ?? {});
|
|
1369
|
-
setHandoff({
|
|
1370
|
-
query: term,
|
|
1371
|
-
didYouMean: didYouMean.map((item) => item.value)
|
|
1372
|
-
});
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
|
-
runSearch(term);
|
|
1376
|
-
},
|
|
1377
|
-
[didYouMean, gtmContext, runSearch]
|
|
1378
|
-
);
|
|
1379
|
-
const handleSubmit = react.useCallback(
|
|
1380
|
-
(value) => {
|
|
1381
|
-
const term = value.trim();
|
|
1382
|
-
if (!term) return;
|
|
1383
|
-
if (term.length < minimumSearchLength) {
|
|
1384
|
-
runSearch(term);
|
|
1385
|
-
return;
|
|
1386
|
-
}
|
|
1387
|
-
const route = decide(term);
|
|
1388
|
-
if (route === "wait") {
|
|
1389
|
-
setPendingTerm(term);
|
|
1390
|
-
return;
|
|
1391
|
-
}
|
|
1392
|
-
act(term, route);
|
|
1393
|
-
},
|
|
1394
|
-
[act, decide, minimumSearchLength, runSearch]
|
|
1395
|
-
);
|
|
1396
|
-
const latest = react.useRef({ act, decide });
|
|
1397
|
-
latest.current = { act, decide };
|
|
1398
|
-
react.useEffect(() => {
|
|
1399
|
-
if (pendingTerm === null) return void 0;
|
|
1400
|
-
const route = latest.current.decide(pendingTerm);
|
|
1401
|
-
if (route !== "wait") {
|
|
1402
|
-
latest.current.act(pendingTerm, route);
|
|
1403
|
-
return void 0;
|
|
1404
|
-
}
|
|
1405
|
-
const timer = setTimeout(
|
|
1406
|
-
() => latest.current.act(pendingTerm, "search"),
|
|
1407
|
-
SUBMIT_WAIT_TIMEOUT_MS
|
|
1408
|
-
);
|
|
1409
|
-
return () => clearTimeout(timer);
|
|
1410
|
-
}, [
|
|
1578
|
+
const handleSubmit = useSubmitRouting({
|
|
1579
|
+
enabled: agentEnabled,
|
|
1580
|
+
minimumSearchLength,
|
|
1581
|
+
search,
|
|
1582
|
+
...gtmContext ? { gtmContext } : {},
|
|
1411
1583
|
pendingTerm,
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
]);
|
|
1584
|
+
setPendingTerm,
|
|
1585
|
+
onSearch: runSearch,
|
|
1586
|
+
onHandoff: setHandoff
|
|
1587
|
+
});
|
|
1417
1588
|
const handleSelect = react.useCallback(
|
|
1418
1589
|
(option) => {
|
|
1419
1590
|
if (isSuggestionDisabled(
|
|
@@ -1438,10 +1609,11 @@ function CommerceSearchView({
|
|
|
1438
1609
|
)) {
|
|
1439
1610
|
return;
|
|
1440
1611
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1612
|
+
navigateTo(
|
|
1613
|
+
service.activateSuggestionAction(
|
|
1614
|
+
option
|
|
1615
|
+
)
|
|
1443
1616
|
);
|
|
1444
|
-
void Promise.resolve(result).then(navigateTo).catch(() => void 0);
|
|
1445
1617
|
},
|
|
1446
1618
|
[navigateTo, service]
|
|
1447
1619
|
);
|
|
@@ -1456,10 +1628,6 @@ function CommerceSearchView({
|
|
|
1456
1628
|
},
|
|
1457
1629
|
[navigateTo, service, trackSemantic]
|
|
1458
1630
|
);
|
|
1459
|
-
const handleClearRecent = react.useCallback(() => {
|
|
1460
|
-
service.clearRecentSearches();
|
|
1461
|
-
setRecentSearches([]);
|
|
1462
|
-
}, [service]);
|
|
1463
1631
|
const handleAgentUnavailable = react.useCallback(() => {
|
|
1464
1632
|
if (agentQuery !== null)
|
|
1465
1633
|
navigateTo(service.submitProductSearch(agentQuery));
|
|
@@ -1508,14 +1676,29 @@ function CommerceSearchView({
|
|
|
1508
1676
|
recentSearches: {
|
|
1509
1677
|
items: recentSearches,
|
|
1510
1678
|
onSelect: handleRecentSelect,
|
|
1511
|
-
onClear:
|
|
1679
|
+
onClear: clearRecentSearches
|
|
1512
1680
|
},
|
|
1513
1681
|
agentPanel
|
|
1514
1682
|
}
|
|
1515
1683
|
);
|
|
1516
1684
|
}
|
|
1517
1685
|
var GUEST = "guest";
|
|
1518
|
-
function CommerceSearch({
|
|
1686
|
+
function CommerceSearch(props) {
|
|
1687
|
+
if (!isValidMarket(props.country, props.language)) {
|
|
1688
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1689
|
+
CommerceSearchUnavailable,
|
|
1690
|
+
{
|
|
1691
|
+
isOpen: props.isOpen,
|
|
1692
|
+
onClose: props.onClose,
|
|
1693
|
+
country: props.country,
|
|
1694
|
+
language: props.language,
|
|
1695
|
+
logo: props.logo
|
|
1696
|
+
}
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ValidMarketSearch, { ...props });
|
|
1700
|
+
}
|
|
1701
|
+
function ValidMarketSearch({
|
|
1519
1702
|
isOpen,
|
|
1520
1703
|
onClose,
|
|
1521
1704
|
navigate,
|
|
@@ -1534,13 +1717,15 @@ function CommerceSearch({
|
|
|
1534
1717
|
language,
|
|
1535
1718
|
enabled: hasOpened
|
|
1536
1719
|
});
|
|
1537
|
-
const
|
|
1538
|
-
customerTypeRef
|
|
1720
|
+
const customerType = customer.isAuthenticated ? customer.accountType ?? GUEST : GUEST;
|
|
1721
|
+
const customerTypeRef = react.useRef(customerType);
|
|
1722
|
+
react.useEffect(() => {
|
|
1723
|
+
customerTypeRef.current = customerType;
|
|
1724
|
+
}, [customerType]);
|
|
1539
1725
|
const service = react.useMemo(
|
|
1540
1726
|
() => createSearchService({
|
|
1541
1727
|
country,
|
|
1542
1728
|
language,
|
|
1543
|
-
locale: `${country.toUpperCase()}-${language.toLowerCase()}`,
|
|
1544
1729
|
getMarketConfig: fetchMarketConfig,
|
|
1545
1730
|
minimumSearchLength,
|
|
1546
1731
|
getCustomerType: () => customerTypeRef.current
|
|
@@ -1548,7 +1733,17 @@ function CommerceSearch({
|
|
|
1548
1733
|
[country, language, minimumSearchLength]
|
|
1549
1734
|
);
|
|
1550
1735
|
const navigateToPath = react.useCallback(
|
|
1551
|
-
(path) =>
|
|
1736
|
+
(path) => {
|
|
1737
|
+
const target = toNavigationTarget(path);
|
|
1738
|
+
if (target) {
|
|
1739
|
+
navigate(target);
|
|
1740
|
+
} else {
|
|
1741
|
+
console.warn(
|
|
1742
|
+
"[search] Not navigating to a path the SDK does not recognize:",
|
|
1743
|
+
path
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
},
|
|
1552
1747
|
[navigate]
|
|
1553
1748
|
);
|
|
1554
1749
|
const { isAuthenticated, accountType, userId } = customer;
|