@nuskin/react-search-sdk 1.0.1-code-cleanup.1 → 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 +140 -50
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +140 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
# @nuskin/react-search-sdk
|
|
2
2
|
|
|
3
|
-
The search experience for the
|
|
3
|
+
The search experience for the Nu Skin storefront: typeahead and recent searches over the Equinox
|
|
4
4
|
catalog, and the NUi agent when a signed-in shopper searches for something with no products.
|
|
5
5
|
|
|
6
6
|
A host renders one component, `CommerceSearch`, and gives it the market, who the customer is, how to
|
|
7
7
|
navigate, and its logo. The package works out the rest itself: the market's catalog lookup, the
|
|
8
8
|
minimum search length, which NUi app to use, the panel's text and the analytics events. The panel UI
|
|
9
|
-
comes from
|
|
9
|
+
comes from `@nuskin/react-search-components` and the agent from
|
|
10
10
|
`@nuskin/react-nui-chat-sdk`.
|
|
11
11
|
|
|
12
12
|
## Requirements
|
package/dist/index.d.mts
CHANGED
|
@@ -16,8 +16,8 @@ type NavigationTarget = {
|
|
|
16
16
|
/**
|
|
17
17
|
* The account types the suggestions API distinguishes, sent as `X-Customer-Type`.
|
|
18
18
|
*
|
|
19
|
-
* TODO: this set is a best guess from the values the storefront sends
|
|
20
|
-
*
|
|
19
|
+
* TODO: PA-1254 validates this set, which is a best guess from the values the storefront sends,
|
|
20
|
+
* against the storefront's customer types and what the suggestions API accepts.
|
|
21
21
|
*/
|
|
22
22
|
type CustomerAccountType = 'guest' | 'Brand Affiliate' | 'Retail Customer' | 'Preferred Customer/Member';
|
|
23
23
|
/** Who is searching. */
|
|
@@ -58,6 +58,6 @@ interface CommerceSearchProps {
|
|
|
58
58
|
* The package works out the rest itself: the market's catalog lookup, the minimum search length,
|
|
59
59
|
* which NUi app to use, the panel's text and the analytics fields.
|
|
60
60
|
*/
|
|
61
|
-
declare function CommerceSearch(
|
|
61
|
+
declare function CommerceSearch(props: Readonly<CommerceSearchProps>): ReactElement | null;
|
|
62
62
|
|
|
63
63
|
export { CommerceSearch, type CommerceSearchCustomer, type CommerceSearchProps, type CustomerAccountType, type NavigationTarget };
|
package/dist/index.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ type NavigationTarget = {
|
|
|
16
16
|
/**
|
|
17
17
|
* The account types the suggestions API distinguishes, sent as `X-Customer-Type`.
|
|
18
18
|
*
|
|
19
|
-
* TODO: this set is a best guess from the values the storefront sends
|
|
20
|
-
*
|
|
19
|
+
* TODO: PA-1254 validates this set, which is a best guess from the values the storefront sends,
|
|
20
|
+
* against the storefront's customer types and what the suggestions API accepts.
|
|
21
21
|
*/
|
|
22
22
|
type CustomerAccountType = 'guest' | 'Brand Affiliate' | 'Retail Customer' | 'Preferred Customer/Member';
|
|
23
23
|
/** Who is searching. */
|
|
@@ -58,6 +58,6 @@ interface CommerceSearchProps {
|
|
|
58
58
|
* The package works out the rest itself: the market's catalog lookup, the minimum search length,
|
|
59
59
|
* which NUi app to use, the panel's text and the analytics fields.
|
|
60
60
|
*/
|
|
61
|
-
declare function CommerceSearch(
|
|
61
|
+
declare function CommerceSearch(props: Readonly<CommerceSearchProps>): ReactElement | null;
|
|
62
62
|
|
|
63
63
|
export { CommerceSearch, type CommerceSearchCustomer, type CommerceSearchProps, type CustomerAccountType, type NavigationTarget };
|
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
|
|
|
@@ -180,25 +180,38 @@ function useSearchPanelCopy({
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
// src/services/localeParts.ts
|
|
183
|
-
|
|
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({
|
|
184
196
|
country,
|
|
185
|
-
language
|
|
186
|
-
locale
|
|
197
|
+
language
|
|
187
198
|
}) {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
const
|
|
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();
|
|
191
203
|
return {
|
|
192
|
-
country: normalizedCountry
|
|
193
|
-
language: normalizedLanguage
|
|
194
|
-
locale:
|
|
204
|
+
country: normalizedCountry,
|
|
205
|
+
language: normalizedLanguage,
|
|
206
|
+
locale: `${normalizedCountry}-${normalizedLanguage}`
|
|
195
207
|
};
|
|
196
208
|
}
|
|
197
|
-
function toCatalogLocale(
|
|
198
|
-
const language = String(localeParts.language ?? "en").toLowerCase();
|
|
199
|
-
const country = String(localeParts.country ?? "US").toUpperCase();
|
|
209
|
+
function toCatalogLocale({ country, language }) {
|
|
200
210
|
return `${language}_${country}`;
|
|
201
211
|
}
|
|
212
|
+
function toPathPrefix({ country, language }) {
|
|
213
|
+
return `/${country.toLowerCase()}/${language}`;
|
|
214
|
+
}
|
|
202
215
|
|
|
203
216
|
// src/services/catalogRequest.ts
|
|
204
217
|
function resolveApiGatewayPath(apiGatewayPath, apiName = STOREFRONT_SHOPPING_CONTEXT) {
|
|
@@ -241,12 +254,21 @@ async function resolveCatalogRequestConfig({
|
|
|
241
254
|
getMarketConfig
|
|
242
255
|
}) {
|
|
243
256
|
const marketConfig = getMarketConfig ? await getMarketConfig({ locationLike, country }) ?? null : null;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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 };
|
|
250
272
|
}
|
|
251
273
|
function resolveCustomerTypeHeaders(getCustomerType) {
|
|
252
274
|
const customerType = getCustomerType?.()?.trim();
|
|
@@ -291,7 +313,7 @@ function getSessionStorage(providedStorage) {
|
|
|
291
313
|
return null;
|
|
292
314
|
}
|
|
293
315
|
function getRecentHeaderSearchesStorageKey(localeParts) {
|
|
294
|
-
const { country, language, locale } =
|
|
316
|
+
const { country, language, locale } = localeParts;
|
|
295
317
|
return `${RECENT_SEARCHES_PREFIX}:${locale}:${country}:${language}`;
|
|
296
318
|
}
|
|
297
319
|
function getRecentHeaderSearches(localeParts, storage) {
|
|
@@ -354,13 +376,14 @@ function buildSearchResultsPath(query, localeParts) {
|
|
|
354
376
|
/%2D/gi,
|
|
355
377
|
"-"
|
|
356
378
|
);
|
|
357
|
-
|
|
358
|
-
const language = localeParts.language.toLowerCase();
|
|
359
|
-
return `/${country}/${language}${DEFAULT_SEARCH_PATH}/${encodedQuery}`;
|
|
379
|
+
return `${toPathPrefix(localeParts)}${DEFAULT_SEARCH_PATH}/${encodedQuery}`;
|
|
360
380
|
}
|
|
381
|
+
var SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
361
382
|
function normalizeOptionUrl(rawValue) {
|
|
362
383
|
if (typeof rawValue !== "string") return "";
|
|
363
|
-
|
|
384
|
+
const value = rawValue.trim();
|
|
385
|
+
if (!value || value.startsWith("/") || SCHEME.test(value)) return value;
|
|
386
|
+
return `/${value}`;
|
|
364
387
|
}
|
|
365
388
|
function safeDecodeURIComponent(value) {
|
|
366
389
|
try {
|
|
@@ -390,16 +413,14 @@ function slugifyPathSegment(value) {
|
|
|
390
413
|
function buildProductSlugPath(slugValue, localeParts) {
|
|
391
414
|
const slug = slugifyPathSegment(slugValue);
|
|
392
415
|
if (!slug) return "";
|
|
393
|
-
|
|
394
|
-
const language = String(localeParts.language ?? "en").toLowerCase();
|
|
395
|
-
return `/${country}/${language}/product/${slug}`;
|
|
416
|
+
return `${toPathPrefix(localeParts)}/product/${slug}`;
|
|
396
417
|
}
|
|
397
418
|
function isNavigableUrl(value) {
|
|
398
419
|
if (!value) return false;
|
|
399
420
|
return value.startsWith("/") || /^https?:\/\//i.test(value);
|
|
400
421
|
}
|
|
401
422
|
function stripLocalePrefix(path, localeParts) {
|
|
402
|
-
const prefix =
|
|
423
|
+
const prefix = toPathPrefix(localeParts);
|
|
403
424
|
return path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
|
|
404
425
|
}
|
|
405
426
|
function resolveOptionHref(option, localeParts) {
|
|
@@ -453,26 +474,20 @@ function getBrowserLocation(providedLocation) {
|
|
|
453
474
|
return window.location;
|
|
454
475
|
return null;
|
|
455
476
|
}
|
|
456
|
-
function
|
|
457
|
-
if (fetchImplOption !== void 0) return fetchImplOption;
|
|
458
|
-
return typeof fetch === "function" ? fetch.bind(globalThis) : null;
|
|
459
|
-
}
|
|
460
|
-
function createSearchService(options = {}) {
|
|
477
|
+
function createSearchService(options) {
|
|
461
478
|
const {
|
|
462
479
|
fetchImpl: fetchImplOption,
|
|
463
480
|
location: providedLocation,
|
|
464
481
|
storage: providedStorage,
|
|
465
482
|
country,
|
|
466
483
|
language,
|
|
467
|
-
locale,
|
|
468
484
|
getMarketConfig,
|
|
469
485
|
getCustomerType,
|
|
470
486
|
minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH
|
|
471
487
|
} = options;
|
|
472
|
-
const fetchImpl = resolveFetch(fetchImplOption);
|
|
473
488
|
const locationLike = getBrowserLocation(providedLocation);
|
|
474
489
|
const storage = getSessionStorage(providedStorage);
|
|
475
|
-
const localeParts =
|
|
490
|
+
const localeParts = createLocaleParts({ country, language });
|
|
476
491
|
let catalogRequestConfigPromise = null;
|
|
477
492
|
function loadCatalogRequestConfig() {
|
|
478
493
|
if (!catalogRequestConfigPromise) {
|
|
@@ -494,13 +509,12 @@ function createSearchService(options = {}) {
|
|
|
494
509
|
const service = {
|
|
495
510
|
async fetchSuggestions(searchTerm) {
|
|
496
511
|
const trimmedSearchTerm = String(searchTerm ?? "").trim();
|
|
497
|
-
if (!trimmedSearchTerm || trimmedSearchTerm.length < minimumSearchLength
|
|
512
|
+
if (!trimmedSearchTerm || trimmedSearchTerm.length < minimumSearchLength) {
|
|
498
513
|
return emptySuggestions();
|
|
499
514
|
}
|
|
500
515
|
const { suggestionsUrl, storeId } = await loadCatalogRequestConfig();
|
|
501
|
-
if (!suggestionsUrl || !storeId) return emptySuggestions();
|
|
502
516
|
const responsePayload = await fetchJson(
|
|
503
|
-
|
|
517
|
+
fetchImplOption ?? fetch.bind(globalThis),
|
|
504
518
|
buildSuggestionsRequestUrl({
|
|
505
519
|
suggestionsUrl,
|
|
506
520
|
searchTerm: trimmedSearchTerm,
|
|
@@ -554,7 +568,7 @@ function createSearchService(options = {}) {
|
|
|
554
568
|
|
|
555
569
|
// src/services/marketConfig.ts
|
|
556
570
|
var fetchMarketConfig = async ({ country }) => {
|
|
557
|
-
const countryCode =
|
|
571
|
+
const countryCode = country.trim().toUpperCase();
|
|
558
572
|
const configuration = await getConfiguration([EQUINOX_MARKETS_MAP], {
|
|
559
573
|
country: countryCode,
|
|
560
574
|
...getEnvironmentCode() ? { environment: getEnvironmentCode() } : {},
|
|
@@ -577,14 +591,25 @@ var SEARCH_GTM_USER_STATUS = {
|
|
|
577
591
|
var GTM_FALLBACK_NA = "NA";
|
|
578
592
|
|
|
579
593
|
// src/utils/navigationTarget.ts
|
|
594
|
+
var EXTERNAL_URL = /^https?:\/\//i;
|
|
595
|
+
var INTERNAL_PATH = /^\/(?![/\\])[^\t\n\r]*$/;
|
|
580
596
|
function toNavigationTarget(path) {
|
|
581
|
-
|
|
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;
|
|
582
600
|
}
|
|
583
601
|
|
|
584
602
|
// src/utils/productEnvironment.ts
|
|
585
|
-
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
|
+
}
|
|
586
611
|
function toProductEnvironment(environmentCode) {
|
|
587
|
-
return environmentCode &&
|
|
612
|
+
return environmentCode !== void 0 && isProductEnvironment(environmentCode) ? environmentCode : void 0;
|
|
588
613
|
}
|
|
589
614
|
function getProductEnvironment() {
|
|
590
615
|
try {
|
|
@@ -593,6 +618,47 @@ function getProductEnvironment() {
|
|
|
593
618
|
return void 0;
|
|
594
619
|
}
|
|
595
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
|
+
}
|
|
596
662
|
|
|
597
663
|
// src/utils/nuiSearchGate.ts
|
|
598
664
|
var NUI_SEARCH_FLAG = "enableNuiSearch";
|
|
@@ -1076,6 +1142,7 @@ function useSearchSuggestions({
|
|
|
1076
1142
|
[enabled, service, resetSuggestionsState, settle]
|
|
1077
1143
|
);
|
|
1078
1144
|
react.useEffect(() => {
|
|
1145
|
+
latestRequestId.current += 1;
|
|
1079
1146
|
if (!enabled) {
|
|
1080
1147
|
resetSuggestionsState();
|
|
1081
1148
|
return void 0;
|
|
@@ -1083,7 +1150,6 @@ function useSearchSuggestions({
|
|
|
1083
1150
|
if (fetchDebounced.current) {
|
|
1084
1151
|
clearTimeout(fetchDebounced.current);
|
|
1085
1152
|
}
|
|
1086
|
-
latestRequestId.current += 1;
|
|
1087
1153
|
fetchDebounced.current = setTimeout(() => {
|
|
1088
1154
|
void runFetch(query);
|
|
1089
1155
|
}, DEBOUNCE_MS);
|
|
@@ -1365,11 +1431,11 @@ function useHandoff({
|
|
|
1365
1431
|
onUnavailableRef.current = onUnavailable;
|
|
1366
1432
|
});
|
|
1367
1433
|
react.useEffect(() => {
|
|
1368
|
-
let
|
|
1434
|
+
let canceled = false;
|
|
1369
1435
|
let timer;
|
|
1370
1436
|
const startedAt = Date.now();
|
|
1371
1437
|
const tryOpen = () => {
|
|
1372
|
-
if (
|
|
1438
|
+
if (canceled) return;
|
|
1373
1439
|
const handle = getSalesAIHandle();
|
|
1374
1440
|
if (handle) {
|
|
1375
1441
|
handle.open({
|
|
@@ -1389,7 +1455,7 @@ function useHandoff({
|
|
|
1389
1455
|
};
|
|
1390
1456
|
tryOpen();
|
|
1391
1457
|
return () => {
|
|
1392
|
-
|
|
1458
|
+
canceled = true;
|
|
1393
1459
|
if (timer !== void 0) clearTimeout(timer);
|
|
1394
1460
|
};
|
|
1395
1461
|
}, [query]);
|
|
@@ -1617,7 +1683,22 @@ function CommerceSearchView({
|
|
|
1617
1683
|
);
|
|
1618
1684
|
}
|
|
1619
1685
|
var GUEST = "guest";
|
|
1620
|
-
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({
|
|
1621
1702
|
isOpen,
|
|
1622
1703
|
onClose,
|
|
1623
1704
|
navigate,
|
|
@@ -1645,7 +1726,6 @@ function CommerceSearch({
|
|
|
1645
1726
|
() => createSearchService({
|
|
1646
1727
|
country,
|
|
1647
1728
|
language,
|
|
1648
|
-
locale: `${country.toUpperCase()}-${language.toLowerCase()}`,
|
|
1649
1729
|
getMarketConfig: fetchMarketConfig,
|
|
1650
1730
|
minimumSearchLength,
|
|
1651
1731
|
getCustomerType: () => customerTypeRef.current
|
|
@@ -1653,7 +1733,17 @@ function CommerceSearch({
|
|
|
1653
1733
|
[country, language, minimumSearchLength]
|
|
1654
1734
|
);
|
|
1655
1735
|
const navigateToPath = react.useCallback(
|
|
1656
|
-
(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
|
+
},
|
|
1657
1747
|
[navigate]
|
|
1658
1748
|
);
|
|
1659
1749
|
const { isAuthenticated, accountType, userId } = customer;
|