@decocms/apps 0.20.1

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.
Files changed (113) hide show
  1. package/.github/workflows/release.yml +34 -0
  2. package/.releaserc.json +25 -0
  3. package/commerce/components/Image.tsx +209 -0
  4. package/commerce/components/JsonLd.tsx +285 -0
  5. package/commerce/sdk/analytics.ts +24 -0
  6. package/commerce/sdk/formatPrice.ts +23 -0
  7. package/commerce/sdk/url.ts +9 -0
  8. package/commerce/sdk/useOffer.ts +75 -0
  9. package/commerce/sdk/useVariantPossibilities.ts +43 -0
  10. package/commerce/types/commerce.ts +1105 -0
  11. package/commerce/utils/canonical.ts +11 -0
  12. package/commerce/utils/constants.ts +9 -0
  13. package/commerce/utils/filters.ts +10 -0
  14. package/commerce/utils/productToAnalyticsItem.ts +67 -0
  15. package/commerce/utils/stateByZip.ts +50 -0
  16. package/knip.json +19 -0
  17. package/package.json +77 -0
  18. package/shopify/actions/cart/addItems.ts +37 -0
  19. package/shopify/actions/cart/updateCoupons.ts +32 -0
  20. package/shopify/actions/cart/updateItems.ts +32 -0
  21. package/shopify/actions/user/signIn.ts +45 -0
  22. package/shopify/actions/user/signUp.ts +36 -0
  23. package/shopify/client.ts +58 -0
  24. package/shopify/index.ts +32 -0
  25. package/shopify/init.ts +40 -0
  26. package/shopify/loaders/ProductDetailsPage.ts +35 -0
  27. package/shopify/loaders/ProductList.ts +101 -0
  28. package/shopify/loaders/ProductListingPage.ts +180 -0
  29. package/shopify/loaders/RelatedProducts.ts +45 -0
  30. package/shopify/loaders/cart.ts +73 -0
  31. package/shopify/loaders/shop.ts +40 -0
  32. package/shopify/loaders/user.ts +44 -0
  33. package/shopify/utils/admin/admin.ts +57 -0
  34. package/shopify/utils/admin/queries.ts +29 -0
  35. package/shopify/utils/cart.ts +28 -0
  36. package/shopify/utils/cookies.ts +85 -0
  37. package/shopify/utils/enums.ts +438 -0
  38. package/shopify/utils/graphql.ts +69 -0
  39. package/shopify/utils/storefront/queries.ts +530 -0
  40. package/shopify/utils/storefront/storefront.graphql.gen.ts +113 -0
  41. package/shopify/utils/transform.ts +436 -0
  42. package/shopify/utils/types.ts +191 -0
  43. package/shopify/utils/user.ts +23 -0
  44. package/shopify/utils/utils.ts +164 -0
  45. package/tsconfig.json +11 -0
  46. package/vtex/README.md +6 -0
  47. package/vtex/actions/address.ts +211 -0
  48. package/vtex/actions/auth.ts +337 -0
  49. package/vtex/actions/checkout.ts +497 -0
  50. package/vtex/actions/index.ts +11 -0
  51. package/vtex/actions/masterData.ts +170 -0
  52. package/vtex/actions/misc.ts +196 -0
  53. package/vtex/actions/newsletter.ts +108 -0
  54. package/vtex/actions/orders.ts +37 -0
  55. package/vtex/actions/profile.ts +119 -0
  56. package/vtex/actions/session.ts +87 -0
  57. package/vtex/actions/trigger.ts +43 -0
  58. package/vtex/actions/wishlist.ts +116 -0
  59. package/vtex/client.ts +423 -0
  60. package/vtex/hooks/index.ts +4 -0
  61. package/vtex/hooks/useAutocomplete.ts +89 -0
  62. package/vtex/hooks/useCart.ts +219 -0
  63. package/vtex/hooks/useUser.ts +78 -0
  64. package/vtex/hooks/useWishlist.ts +119 -0
  65. package/vtex/index.ts +14 -0
  66. package/vtex/inline-loaders/productDetailsPage.ts +75 -0
  67. package/vtex/inline-loaders/productList.ts +163 -0
  68. package/vtex/inline-loaders/productListingPage.ts +447 -0
  69. package/vtex/inline-loaders/relatedProducts.ts +83 -0
  70. package/vtex/inline-loaders/suggestions.ts +49 -0
  71. package/vtex/inline-loaders/workflowProducts.ts +68 -0
  72. package/vtex/invoke.ts +202 -0
  73. package/vtex/loaders/address.ts +120 -0
  74. package/vtex/loaders/brands.ts +51 -0
  75. package/vtex/loaders/cart.ts +49 -0
  76. package/vtex/loaders/catalog.ts +165 -0
  77. package/vtex/loaders/collections.ts +57 -0
  78. package/vtex/loaders/index.ts +19 -0
  79. package/vtex/loaders/legacy.ts +671 -0
  80. package/vtex/loaders/logistics.ts +115 -0
  81. package/vtex/loaders/navbar.ts +29 -0
  82. package/vtex/loaders/orders.ts +103 -0
  83. package/vtex/loaders/pageType.ts +62 -0
  84. package/vtex/loaders/payment.ts +107 -0
  85. package/vtex/loaders/profile.ts +138 -0
  86. package/vtex/loaders/promotion.ts +33 -0
  87. package/vtex/loaders/search.ts +127 -0
  88. package/vtex/loaders/session.ts +91 -0
  89. package/vtex/loaders/user.ts +89 -0
  90. package/vtex/loaders/wishlist.ts +89 -0
  91. package/vtex/loaders/wishlistProducts.ts +81 -0
  92. package/vtex/loaders/workflow.ts +323 -0
  93. package/vtex/logo.png +0 -0
  94. package/vtex/middleware.ts +229 -0
  95. package/vtex/types.ts +248 -0
  96. package/vtex/utils/batch.ts +21 -0
  97. package/vtex/utils/cookies.ts +76 -0
  98. package/vtex/utils/enrichment.ts +540 -0
  99. package/vtex/utils/fetchCache.ts +150 -0
  100. package/vtex/utils/index.ts +17 -0
  101. package/vtex/utils/intelligentSearch.ts +84 -0
  102. package/vtex/utils/legacy.ts +155 -0
  103. package/vtex/utils/pickAndOmit.ts +30 -0
  104. package/vtex/utils/proxy.ts +196 -0
  105. package/vtex/utils/resourceRange.ts +10 -0
  106. package/vtex/utils/segment.ts +163 -0
  107. package/vtex/utils/similars.ts +38 -0
  108. package/vtex/utils/sitemap.ts +133 -0
  109. package/vtex/utils/slugCache.ts +32 -0
  110. package/vtex/utils/slugify.ts +13 -0
  111. package/vtex/utils/transform.ts +1331 -0
  112. package/vtex/utils/types.ts +1884 -0
  113. package/vtex/utils/vtexId.ts +103 -0
@@ -0,0 +1,671 @@
1
+ /**
2
+ * Legacy VTEX Catalog Search loaders.
3
+ * Pure async functions — require configureVtex() to have been called.
4
+ *
5
+ * Ported from deco-cx/apps:
6
+ * vtex/loaders/legacy/productDetailsPage.ts
7
+ * vtex/loaders/legacy/productList.ts
8
+ * vtex/loaders/legacy/productListingPage.ts
9
+ * vtex/loaders/legacy/suggestions.ts
10
+ *
11
+ * @see https://developers.vtex.com/docs/api-reference/search-api
12
+ */
13
+ import type {
14
+ Filter,
15
+ Product,
16
+ ProductDetailsPage,
17
+ ProductListingPage,
18
+ Suggestion,
19
+ } from "../../commerce/types/commerce";
20
+ import { getVtexConfig, vtexFetch, vtexFetchResponse } from "../client";
21
+ import {
22
+ getMapAndTerm,
23
+ getValidTypesFromPageTypes,
24
+ isFilterParam,
25
+ pageTypesFromPathname,
26
+ pageTypesToBreadcrumbList,
27
+ pageTypesToSeo,
28
+ } from "../utils/legacy";
29
+ import {
30
+ legacyFacetToFilter,
31
+ parsePageType,
32
+ pickSku,
33
+ sortProducts,
34
+ toProduct,
35
+ toProductPage,
36
+ } from "../utils/transform";
37
+ import type {
38
+ LegacyFacet,
39
+ LegacyItem,
40
+ LegacyProduct,
41
+ LegacySort,
42
+ PageType,
43
+ } from "../utils/types";
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Shared helpers
47
+ // ---------------------------------------------------------------------------
48
+
49
+ const MAX_ALLOWED_PAGES = 500;
50
+
51
+ function salesChannelParam(): string {
52
+ return getVtexConfig().salesChannel ?? "1";
53
+ }
54
+
55
+ function buildSearchParams(
56
+ extra: Record<string, string | string[] | number | undefined>,
57
+ ): URLSearchParams {
58
+ const params = new URLSearchParams();
59
+ const sc = salesChannelParam();
60
+ if (sc) params.set("sc", sc);
61
+
62
+ for (const [key, val] of Object.entries(extra)) {
63
+ if (val == null) continue;
64
+ if (Array.isArray(val)) {
65
+ for (const v of val) params.append(key, v);
66
+ } else {
67
+ params.set(key, String(val));
68
+ }
69
+ }
70
+ return params;
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // legacyProductDetailsPage
75
+ // ---------------------------------------------------------------------------
76
+
77
+ export interface LegacyPDPOptions {
78
+ slug: string;
79
+ skuId?: string;
80
+ baseUrl: string;
81
+ priceCurrency?: string;
82
+ includeOriginalAttributes?: string[];
83
+ preferDescription?: boolean;
84
+ /** When true, pages with ?skuId are still indexable */
85
+ indexingSkus?: boolean;
86
+ }
87
+
88
+ /**
89
+ * Fetch a product details page using the legacy Catalog search API.
90
+ *
91
+ * @see https://developers.vtex.com/docs/api-reference/search-api#get-/api/catalog_system/pub/products/search/-slug-/p
92
+ * Ported from: vtex/loaders/legacy/productDetailsPage.ts
93
+ */
94
+ export async function legacyProductDetailsPage(
95
+ opts: LegacyPDPOptions,
96
+ ): Promise<ProductDetailsPage | null> {
97
+ const {
98
+ slug,
99
+ skuId,
100
+ baseUrl,
101
+ priceCurrency = "BRL",
102
+ includeOriginalAttributes,
103
+ preferDescription,
104
+ indexingSkus,
105
+ } = opts;
106
+
107
+ const lowercaseSlug = slug.toLowerCase();
108
+ const qs = buildSearchParams({});
109
+
110
+ const response = await vtexFetch<LegacyProduct[]>(
111
+ `/api/catalog_system/pub/products/search/${lowercaseSlug}/p?${qs}`,
112
+ );
113
+
114
+ if (response && !Array.isArray(response)) {
115
+ throw new Error(
116
+ `Error while fetching VTEX data ${JSON.stringify(response)}`,
117
+ );
118
+ }
119
+
120
+ const [product] = response;
121
+ if (!product) return null;
122
+
123
+ const sku = pickSku(product, skuId);
124
+
125
+ const kitItems: LegacyProduct[] =
126
+ Array.isArray(sku.kitItems) && sku.kitItems.length > 0
127
+ ? await vtexFetch<LegacyProduct[]>(
128
+ `/api/catalog_system/pub/products/search/?${buildSearchParams({
129
+ _from: 0,
130
+ _to: 49,
131
+ fq: sku.kitItems.map((item) => `skuId:${item.itemId}`),
132
+ })}`,
133
+ )
134
+ : [];
135
+
136
+ const page = toProductPage(product, sku, kitItems, {
137
+ baseUrl,
138
+ priceCurrency,
139
+ includeOriginalAttributes,
140
+ });
141
+
142
+ const url = new URL(baseUrl);
143
+
144
+ return {
145
+ ...page,
146
+ seo: {
147
+ title: product.productTitle || product.productName,
148
+ description: preferDescription
149
+ ? product.description
150
+ : product.metaTagDescription,
151
+ canonical: new URL(`/${product.linkText}/p`, url.origin).href,
152
+ noIndexing: indexingSkus ? false : !!skuId,
153
+ },
154
+ };
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // legacyProductList
159
+ // ---------------------------------------------------------------------------
160
+
161
+ export type LegacyProductListQuery =
162
+ | { collection: string; count: number; sort?: LegacySort }
163
+ | { term?: string; count: number; sort?: LegacySort }
164
+ | { fq: string[]; count: number; sort?: LegacySort }
165
+ | { skuIds: string[] }
166
+ | { productIds: string[] };
167
+
168
+ export interface LegacyProductListOptions {
169
+ query: LegacyProductListQuery;
170
+ baseUrl: string;
171
+ priceCurrency?: string;
172
+ }
173
+
174
+ function isCollectionQuery(q: LegacyProductListQuery): q is { collection: string; count: number; sort?: LegacySort } {
175
+ return "collection" in q && typeof (q as any).collection === "string";
176
+ }
177
+ function isSkuIdsQuery(q: LegacyProductListQuery): q is { skuIds: string[] } {
178
+ return "skuIds" in q && Array.isArray((q as any).skuIds);
179
+ }
180
+ function isProductIdsQuery(q: LegacyProductListQuery): q is { productIds: string[] } {
181
+ return "productIds" in q && Array.isArray((q as any).productIds);
182
+ }
183
+ function isFqQuery(q: LegacyProductListQuery): q is { fq: string[]; count: number; sort?: LegacySort } {
184
+ return "fq" in q && Array.isArray((q as any).fq);
185
+ }
186
+ function isTermQuery(q: LegacyProductListQuery): q is { term?: string; count: number; sort?: LegacySort } {
187
+ return "term" in q || ("count" in q && !isCollectionQuery(q as any) && !isFqQuery(q as any));
188
+ }
189
+
190
+ function queryToSearchParams(query: LegacyProductListQuery): Record<string, string | string[] | number | undefined> {
191
+ if (isSkuIdsQuery(query)) {
192
+ return {
193
+ fq: query.skuIds.map((id) => `skuId:${id}`),
194
+ _from: 0,
195
+ _to: Math.max(query.skuIds.length - 1, 0),
196
+ };
197
+ }
198
+
199
+ if (isProductIdsQuery(query)) {
200
+ return {
201
+ fq: query.productIds.map((id) => `productId:${id}`),
202
+ _from: 0,
203
+ _to: Math.max(query.productIds.length - 1, 0),
204
+ };
205
+ }
206
+
207
+ const count = "count" in query ? (query.count ?? 12) : 12;
208
+ const sort = "sort" in query ? query.sort : undefined;
209
+ const base: Record<string, string | string[] | number | undefined> = {
210
+ _from: 0,
211
+ _to: Math.max(count - 1, 0),
212
+ O: sort,
213
+ };
214
+
215
+ if (isCollectionQuery(query)) {
216
+ base.fq = [`productClusterIds:${query.collection}`];
217
+ return base;
218
+ }
219
+
220
+ if (isFqQuery(query)) {
221
+ base.fq = query.fq;
222
+ return base;
223
+ }
224
+
225
+ if (isTermQuery(query) && query.term) {
226
+ base.ft = encodeURIComponent(query.term);
227
+ }
228
+
229
+ return base;
230
+ }
231
+
232
+ /**
233
+ * Fetch a product list using the legacy Catalog search API.
234
+ *
235
+ * @see https://developers.vtex.com/docs/api-reference/search-api#get-/api/catalog_system/pub/products/search
236
+ * Ported from: vtex/loaders/legacy/productList.ts
237
+ */
238
+ export async function legacyProductList(
239
+ opts: LegacyProductListOptions,
240
+ ): Promise<Product[] | null> {
241
+ const { query, baseUrl, priceCurrency = "BRL" } = opts;
242
+ const searchArgs = queryToSearchParams(query);
243
+ const qs = buildSearchParams(searchArgs);
244
+
245
+ const vtexProducts = await vtexFetch<LegacyProduct[]>(
246
+ `/api/catalog_system/pub/products/search/?${qs}`,
247
+ );
248
+
249
+ if (vtexProducts && !Array.isArray(vtexProducts)) {
250
+ throw new Error(
251
+ `Error while fetching VTEX data ${JSON.stringify(vtexProducts)}`,
252
+ );
253
+ }
254
+
255
+ const preferredSKU = (items: LegacyItem[]): LegacyItem => {
256
+ if (isSkuIdsQuery(query)) {
257
+ const fetchedSkus = new Set(query.skuIds);
258
+ return items.find((item) => fetchedSkus.has(item.itemId)) || items[0];
259
+ }
260
+ return items[0];
261
+ };
262
+
263
+ let products = vtexProducts.map((p) =>
264
+ toProduct(p, preferredSKU(p.items), 0, { baseUrl, priceCurrency }),
265
+ );
266
+
267
+ if (isSkuIdsQuery(query)) {
268
+ products = sortProducts(products, query.skuIds, "sku");
269
+ }
270
+ if (isProductIdsQuery(query)) {
271
+ products = sortProducts(products, query.productIds, "inProductGroupWithID");
272
+ }
273
+
274
+ return products;
275
+ }
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // legacyProductListingPage
279
+ // ---------------------------------------------------------------------------
280
+
281
+ export const LEGACY_SORT_OPTIONS = [
282
+ { label: "price:desc", value: "OrderByPriceDESC" },
283
+ { label: "price:asc", value: "OrderByPriceASC" },
284
+ { label: "orders:desc", value: "OrderByTopSaleDESC" },
285
+ { label: "name:desc", value: "OrderByNameDESC" },
286
+ { label: "name:asc", value: "OrderByNameASC" },
287
+ { label: "release:desc", value: "OrderByReleaseDateDESC" },
288
+ { label: "discount:desc", value: "OrderByBestDiscountDESC" },
289
+ { label: "relevance:desc", value: "OrderByScoreDESC" },
290
+ ] as const;
291
+
292
+ const IS_TO_LEGACY: Record<string, LegacySort> = {
293
+ "price:desc": "OrderByPriceDESC",
294
+ "price:asc": "OrderByPriceASC",
295
+ "orders:desc": "OrderByTopSaleDESC",
296
+ "name:desc": "OrderByNameDESC",
297
+ "name:asc": "OrderByNameASC",
298
+ "release:desc": "OrderByReleaseDateDESC",
299
+ "discount:desc": "OrderByBestDiscountDESC",
300
+ "relevance:desc": "OrderByScoreDESC",
301
+ };
302
+
303
+ const formatPriceFromPathToFacet = (term: string) =>
304
+ term.replace(/de-\d+[,]?[\d]+-a-\d+[,]?[\d]+/, (match) =>
305
+ match.replaceAll(",", "."),
306
+ );
307
+
308
+ const removeForwardSlash = (str: string) =>
309
+ str.slice(str.startsWith("/") ? 1 : 0);
310
+
311
+ const getTerm = (path: string, map: string) => {
312
+ const mapSegments = map.split(",");
313
+ const pathSegments = removeForwardSlash(path).split("/");
314
+ const term = pathSegments.slice(0, mapSegments.length).join("/");
315
+ return mapSegments.includes("priceFrom")
316
+ ? formatPriceFromPathToFacet(term)
317
+ : term;
318
+ };
319
+
320
+ const getFirstItemAvailable = (item: LegacyItem) =>
321
+ !!item?.sellers?.find((s) => s.commertialOffer?.AvailableQuantity > 0);
322
+
323
+ const getTermFallback = (url: URL, isPage: boolean, hasFilters: boolean) => {
324
+ const pathList = url.pathname.split("/").slice(1);
325
+ if (!isPage && !hasFilters && pathList.length === 1) return pathList[0];
326
+ return "";
327
+ };
328
+
329
+ export interface LegacyPLPOptions {
330
+ /** URL of the page being rendered (used for filter links, pagination, etc.) */
331
+ url: URL;
332
+ /** Override the search term (path). Defaults to url.pathname */
333
+ term?: string;
334
+ /** Items per page */
335
+ count?: number;
336
+ /** Current page number (0-indexed internally; see pageOffset) */
337
+ page?: number;
338
+ /** Starting page offset. Defaults to 1. */
339
+ pageOffset?: number;
340
+ sort?: LegacySort;
341
+ /** FullText search term */
342
+ ft?: string;
343
+ /** Filter query */
344
+ fq?: string;
345
+ /** Map parameter */
346
+ map?: string;
347
+ /** Filter behavior: dynamic (default) or static */
348
+ filters?: "dynamic" | "static";
349
+ /** Base URL for building canonical/absolute links */
350
+ baseUrl: string;
351
+ priceCurrency?: string;
352
+ /** Use collection name as page title */
353
+ useCollectionName?: boolean;
354
+ /** Ignore case when checking if a facet is selected */
355
+ ignoreCaseSelected?: boolean;
356
+ includeOriginalAttributes?: string[];
357
+ }
358
+
359
+ /**
360
+ * Fetch a product listing page (PLP) using the legacy Catalog search API.
361
+ * Handles categories, departments, brands, collections, and full-text search.
362
+ *
363
+ * @see https://developers.vtex.com/docs/api-reference/search-api#get-/api/catalog_system/pub/products/search
364
+ * Ported from: vtex/loaders/legacy/productListingPage.ts
365
+ */
366
+ export async function legacyProductListingPage(
367
+ opts: LegacyPLPOptions,
368
+ ): Promise<ProductListingPage | null> {
369
+ const {
370
+ url,
371
+ baseUrl,
372
+ priceCurrency = "BRL",
373
+ filters: filtersBehavior = "dynamic",
374
+ ignoreCaseSelected,
375
+ useCollectionName,
376
+ includeOriginalAttributes,
377
+ } = opts;
378
+
379
+ const currentPageOffset = opts.pageOffset ?? 1;
380
+ const countFromSearchParams = url.searchParams.get("PS");
381
+ const count = Number(countFromSearchParams ?? opts.count ?? 12);
382
+
383
+ const maybeMap = opts.map || url.searchParams.get("map") || undefined;
384
+ const maybeTerm = opts.term || url.pathname || "";
385
+
386
+ const pageParam = url.searchParams.get("page")
387
+ ? Number(url.searchParams.get("page")) - currentPageOffset
388
+ : 0;
389
+ const page = opts.page ?? pageParam;
390
+ const O: LegacySort =
391
+ (url.searchParams.get("O") as LegacySort) ??
392
+ IS_TO_LEGACY[url.searchParams.get("sort") ?? ""] ??
393
+ opts.sort ??
394
+ (LEGACY_SORT_OPTIONS[0].value as LegacySort);
395
+ const fq = [
396
+ ...new Set([
397
+ ...(opts.fq ? [opts.fq] : []),
398
+ ...url.searchParams.getAll("fq"),
399
+ ]),
400
+ ];
401
+ const _from = page * count;
402
+ const _to = (page + 1) * count - 1;
403
+
404
+ const allPageTypes = await pageTypesFromPathname(maybeTerm);
405
+ const pageTypes = getValidTypesFromPageTypes(allPageTypes);
406
+ const pageType: PageType = pageTypes.at(-1) || pageTypes[0];
407
+
408
+ const missingParams = typeof maybeMap !== "string" || !maybeTerm;
409
+ const [map, term] =
410
+ missingParams && fq.length > 0
411
+ ? ["", ""]
412
+ : missingParams
413
+ ? getMapAndTerm(pageTypes)
414
+ : [maybeMap, maybeTerm];
415
+
416
+ const isPage = pageTypes.length > 0;
417
+ const hasFilters = fq.length > 0 || !map;
418
+ const ftFallback = getTermFallback(url, isPage, hasFilters);
419
+ const ft =
420
+ opts.ft ||
421
+ url.searchParams.get("ft") ||
422
+ url.searchParams.get("q") ||
423
+ ftFallback;
424
+ const isInSearchFormat = ft;
425
+
426
+ if (!isPage && !hasFilters && !isInSearchFormat) return null;
427
+
428
+ const fmap = url.searchParams.get("fmap") ?? map;
429
+ const sc = salesChannelParam();
430
+ const searchBase: Record<string, string | string[] | number | undefined> = {
431
+ _from,
432
+ _to,
433
+ O,
434
+ ft: ft || undefined,
435
+ fq: fq.length > 0 ? fq : undefined,
436
+ map,
437
+ sc,
438
+ };
439
+
440
+ const [vtexProductsResponse, vtexFacets] = await Promise.all([
441
+ vtexFetchResponse(
442
+ `/api/catalog_system/pub/products/search/${getTerm(term, map)}?${buildSearchParams(searchBase)}`,
443
+ ),
444
+ vtexFetch<{
445
+ CategoriesTrees: LegacyFacet[];
446
+ Departments: LegacyFacet[];
447
+ Brands: LegacyFacet[];
448
+ SpecificationFilters: Record<string, LegacyFacet[]>;
449
+ PriceRanges: LegacyFacet[];
450
+ }>(
451
+ `/api/catalog_system/pub/facets/search/${getTerm(term, fmap)}?${buildSearchParams({
452
+ ...searchBase,
453
+ map: fmap,
454
+ })}`,
455
+ ),
456
+ ]);
457
+
458
+ const vtexProducts = (await vtexProductsResponse.json()) as LegacyProduct[];
459
+ const resources = vtexProductsResponse.headers.get("resources") ?? "";
460
+ const [, _total] = resources.split("/");
461
+
462
+ if (vtexProducts && !Array.isArray(vtexProducts)) {
463
+ throw new Error(
464
+ `Error while fetching VTEX data ${JSON.stringify(vtexProducts)}`,
465
+ );
466
+ }
467
+
468
+ const products = vtexProducts.map((p) =>
469
+ toProduct(
470
+ p,
471
+ p.items.find(getFirstItemAvailable) ?? p.items[0],
472
+ 0,
473
+ { baseUrl, priceCurrency, includeOriginalAttributes },
474
+ ),
475
+ );
476
+
477
+ const currentPageTypes = !useCollectionName
478
+ ? pageTypes
479
+ : pageTypes.map((pt) => {
480
+ if (pt.id !== pageTypes.at(-1)?.id) return pt;
481
+ const name =
482
+ products?.[0]?.additionalProperty?.find(
483
+ (property) =>
484
+ property.name === "cluster" &&
485
+ property.propertyID === pt.name,
486
+ )?.value ?? pt.name;
487
+ return { ...pt, name };
488
+ });
489
+
490
+ const getFlatCategories = (
491
+ trees: LegacyFacet[],
492
+ ): Record<string, LegacyFacet[]> => {
493
+ const flat: Record<string, LegacyFacet[]> = {};
494
+ trees.forEach((cat) => (flat[cat.Name] = cat.Children || []));
495
+ return flat;
496
+ };
497
+
498
+ const getCategoryFacets = (
499
+ trees: LegacyFacet[],
500
+ isDeptOrCat: boolean,
501
+ ): LegacyFacet[] => {
502
+ if (!isDeptOrCat) return [];
503
+ for (const category of trees) {
504
+ if (category.Id === Number(pageType?.id)) return category.Children || [];
505
+ if (category.Children?.length) {
506
+ const child = getCategoryFacets(category.Children, isDeptOrCat);
507
+ if (child.length) return child;
508
+ }
509
+ }
510
+ return [];
511
+ };
512
+
513
+ const isDeptOrCat =
514
+ pageType?.pageType === "Department" ||
515
+ pageType?.pageType === "Category" ||
516
+ pageType?.pageType === "SubCategory";
517
+
518
+ const flatCategories = !isDeptOrCat
519
+ ? getFlatCategories(vtexFacets.CategoriesTrees)
520
+ : {};
521
+
522
+ const filters = Object.entries({
523
+ Departments: vtexFacets.Departments,
524
+ Categories: getCategoryFacets(vtexFacets.CategoriesTrees, isDeptOrCat),
525
+ Brands: vtexFacets.Brands,
526
+ ...vtexFacets.SpecificationFilters,
527
+ PriceRanges: vtexFacets.PriceRanges,
528
+ ...flatCategories,
529
+ })
530
+ .map(([name, facets]) =>
531
+ legacyFacetToFilter(
532
+ name,
533
+ facets,
534
+ url,
535
+ map,
536
+ term,
537
+ filtersBehavior,
538
+ ignoreCaseSelected,
539
+ name === "Categories",
540
+ ),
541
+ )
542
+ .flat()
543
+ .filter((x): x is Filter => Boolean(x));
544
+
545
+ const itemListElement = pageTypesToBreadcrumbList(pageTypes, baseUrl);
546
+ const totalRecords = parseInt(_total, 10);
547
+ const hasMoreResources = _to < totalRecords - 1;
548
+ const hasNextPage = page < MAX_ALLOWED_PAGES && hasMoreResources;
549
+ const hasPreviousPage = page > 0;
550
+
551
+ const nextPage = new URLSearchParams(url.searchParams);
552
+ const previousPage = new URLSearchParams(url.searchParams);
553
+ if (hasNextPage) nextPage.set("page", String(page + currentPageOffset + 1));
554
+ if (hasPreviousPage) previousPage.set("page", String(page + currentPageOffset - 1));
555
+
556
+ const currentPage = page + currentPageOffset;
557
+
558
+ return {
559
+ "@type": "ProductListingPage",
560
+ breadcrumb: {
561
+ "@type": "BreadcrumbList",
562
+ itemListElement,
563
+ numberOfItems: itemListElement.length,
564
+ },
565
+ filters,
566
+ products,
567
+ pageInfo: {
568
+ nextPage: hasNextPage ? `?${nextPage.toString()}` : undefined,
569
+ previousPage: hasPreviousPage
570
+ ? `?${previousPage.toString()}`
571
+ : undefined,
572
+ currentPage,
573
+ records: totalRecords,
574
+ recordPerPage: count,
575
+ pageTypes: allPageTypes.map(parsePageType),
576
+ },
577
+ sortOptions: LEGACY_SORT_OPTIONS.map((o) => ({ ...o })),
578
+ seo: pageTypesToSeo(
579
+ currentPageTypes,
580
+ baseUrl,
581
+ hasPreviousPage ? currentPage : undefined,
582
+ ),
583
+ };
584
+ }
585
+
586
+ // ---------------------------------------------------------------------------
587
+ // legacySuggestions
588
+ // ---------------------------------------------------------------------------
589
+
590
+ export interface LegacySuggestionsOptions {
591
+ query?: string;
592
+ /** Max results. Defaults to 4. */
593
+ count?: number;
594
+ }
595
+
596
+ interface AutocompleteItem {
597
+ productId: string;
598
+ itemId: string;
599
+ name: string;
600
+ nameComplete: string;
601
+ imageUrl: string;
602
+ }
603
+
604
+ interface AutocompleteResult {
605
+ name: string;
606
+ href: string;
607
+ items: AutocompleteItem[];
608
+ }
609
+
610
+ interface AutocompleteResponse {
611
+ itemsReturned: AutocompleteResult[];
612
+ }
613
+
614
+ /**
615
+ * Fetch legacy autocomplete/search suggestions.
616
+ *
617
+ * @see https://developers.vtex.com/docs/api-reference/search-api#get-/buscaautocomplete
618
+ * Ported from: vtex/loaders/legacy/suggestions.ts
619
+ */
620
+ export async function legacySuggestions(
621
+ opts: LegacySuggestionsOptions = {},
622
+ ): Promise<Suggestion | null> {
623
+ const { count = 4, query } = opts;
624
+
625
+ const params = new URLSearchParams({
626
+ maxRows: String(count),
627
+ productNameContains: encodeURIComponent(query ?? ""),
628
+ suggestionsStack: "",
629
+ });
630
+
631
+ const { salesChannel } = getVtexConfig();
632
+ if (salesChannel) params.set("sc", salesChannel);
633
+
634
+ const suggestions = await vtexFetch<AutocompleteResponse>(
635
+ `/buscaautocomplete?${params}`,
636
+ );
637
+
638
+ const searches: Suggestion["searches"] = suggestions.itemsReturned
639
+ .filter(({ items }) => !items?.length)
640
+ .map(({ name, href }) => ({ term: name, href }));
641
+
642
+ const products: Suggestion["products"] = suggestions.itemsReturned
643
+ .filter(({ items }) => !!items.length)
644
+ .map(
645
+ ({
646
+ items: [{ productId, itemId, imageUrl, name, nameComplete }],
647
+ href,
648
+ }): Product => {
649
+ const parsedUrl = new URL(href, "https://placeholder.com");
650
+ return {
651
+ "@type": "Product",
652
+ productID: itemId,
653
+ sku: itemId,
654
+ inProductGroupWithID: productId,
655
+ isVariantOf: {
656
+ "@type": "ProductGroup",
657
+ name: nameComplete,
658
+ url: parsedUrl.pathname,
659
+ hasVariant: [],
660
+ additionalProperty: [],
661
+ productGroupID: productId,
662
+ },
663
+ image: [{ "@type": "ImageObject", url: imageUrl }],
664
+ name,
665
+ url: parsedUrl.pathname + parsedUrl.search + parsedUrl.hash,
666
+ };
667
+ },
668
+ );
669
+
670
+ return { searches, products };
671
+ }