@decocms/apps-vtex 8.1.0-next.0 → 8.1.0-next.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.
Files changed (163) hide show
  1. package/dist/index.d.ts +6 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +6 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/utils/constants.d.ts +28 -0
  6. package/dist/utils/constants.d.ts.map +1 -0
  7. package/dist/utils/constants.js +17 -0
  8. package/dist/utils/constants.js.map +1 -0
  9. package/dist/utils/cookieSanitizer.d.ts +87 -0
  10. package/dist/utils/cookieSanitizer.d.ts.map +1 -0
  11. package/dist/utils/cookieSanitizer.js +136 -0
  12. package/dist/utils/cookieSanitizer.js.map +1 -0
  13. package/dist/utils/operationRouter.d.ts +25 -0
  14. package/dist/utils/operationRouter.d.ts.map +1 -0
  15. package/dist/utils/operationRouter.js +120 -0
  16. package/dist/utils/operationRouter.js.map +1 -0
  17. package/dist/utils/types.d.ts +944 -0
  18. package/dist/utils/types.d.ts.map +1 -0
  19. package/dist/utils/types.js +25 -0
  20. package/dist/utils/types.js.map +1 -0
  21. package/dist/vtexClient.d.ts +173 -0
  22. package/dist/vtexClient.d.ts.map +1 -0
  23. package/dist/vtexClient.js +242 -0
  24. package/dist/vtexClient.js.map +1 -0
  25. package/package.json +16 -57
  26. package/src/index.ts +11 -29
  27. package/src/utils/constants.ts +2 -56
  28. package/src/utils/cookieSanitizer.ts +59 -59
  29. package/src/utils/operationRouter.ts +75 -65
  30. package/src/utils/types.ts +709 -1557
  31. package/src/vtexClient.ts +358 -347
  32. package/src/README.md +0 -6
  33. package/src/__conformance__/upstream.test.ts +0 -158
  34. package/src/__tests__/client-segment-cookie.test.ts +0 -255
  35. package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
  36. package/src/__tests__/schemas.test.ts +0 -71
  37. package/src/actions/address.ts +0 -250
  38. package/src/actions/analytics/sendEvent.ts +0 -86
  39. package/src/actions/auth.ts +0 -333
  40. package/src/actions/checkout.ts +0 -659
  41. package/src/actions/index.ts +0 -11
  42. package/src/actions/masterData.ts +0 -168
  43. package/src/actions/misc.ts +0 -188
  44. package/src/actions/newsletter.ts +0 -105
  45. package/src/actions/orders.ts +0 -34
  46. package/src/actions/profile.ts +0 -201
  47. package/src/actions/session.ts +0 -85
  48. package/src/actions/trigger.ts +0 -43
  49. package/src/actions/wishlist.ts +0 -114
  50. package/src/client.ts +0 -724
  51. package/src/commerceLoaders.ts +0 -267
  52. package/src/hooks/__tests__/createCart.render.test.ts +0 -114
  53. package/src/hooks/__tests__/createCart.test.ts +0 -92
  54. package/src/hooks/__tests__/createUseCart.test.ts +0 -184
  55. package/src/hooks/__tests__/createUseUser.test.ts +0 -48
  56. package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
  57. package/src/hooks/cartQuery.ts +0 -204
  58. package/src/hooks/createCart.ts +0 -427
  59. package/src/hooks/createUseCart.ts +0 -360
  60. package/src/hooks/createUseUser.ts +0 -153
  61. package/src/hooks/createUseWishlist.ts +0 -242
  62. package/src/hooks/index.ts +0 -25
  63. package/src/hooks/useAutocomplete.ts +0 -83
  64. package/src/hooks/useCart.ts +0 -243
  65. package/src/hooks/useUser.ts +0 -78
  66. package/src/hooks/useWishlist.ts +0 -119
  67. package/src/invoke.ts +0 -235
  68. package/src/loaders/ProductDetailsPage.ts +0 -1
  69. package/src/loaders/ProductList.ts +0 -1
  70. package/src/loaders/ProductListingPage.ts +0 -1
  71. package/src/loaders/__tests__/legacyProductList.test.ts +0 -157
  72. package/src/loaders/address.ts +0 -115
  73. package/src/loaders/autocomplete.ts +0 -56
  74. package/src/loaders/brands.ts +0 -44
  75. package/src/loaders/cart/attachments.ts +0 -53
  76. package/src/loaders/cart/full.ts +0 -67
  77. package/src/loaders/cart/gifts.ts +0 -43
  78. package/src/loaders/cart/orderFormId.ts +0 -31
  79. package/src/loaders/cart/shipping.test.ts +0 -125
  80. package/src/loaders/cart/shipping.ts +0 -136
  81. package/src/loaders/cart/summary.ts +0 -36
  82. package/src/loaders/cart.ts +0 -52
  83. package/src/loaders/catalog.ts +0 -163
  84. package/src/loaders/collections.ts +0 -55
  85. package/src/loaders/index.ts +0 -20
  86. package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
  87. package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -111
  88. package/src/loaders/intelligentSearch/productList.ts +0 -170
  89. package/src/loaders/intelligentSearch/productListingPage.ts +0 -525
  90. package/src/loaders/intelligentSearch/suggestions.ts +0 -44
  91. package/src/loaders/legacy/productDetailsPage.ts +0 -1
  92. package/src/loaders/legacy/productList.ts +0 -1
  93. package/src/loaders/legacy/relatedProductsLoader.ts +0 -79
  94. package/src/loaders/legacy.ts +0 -710
  95. package/src/loaders/logistics.ts +0 -111
  96. package/src/loaders/minicart.ts +0 -78
  97. package/src/loaders/navbar.ts +0 -26
  98. package/src/loaders/orders.ts +0 -92
  99. package/src/loaders/pageType.ts +0 -68
  100. package/src/loaders/paths/PDPDefaultPath.ts +0 -51
  101. package/src/loaders/payment.ts +0 -97
  102. package/src/loaders/productListFull.ts +0 -157
  103. package/src/loaders/profile.ts +0 -138
  104. package/src/loaders/promotion.ts +0 -30
  105. package/src/loaders/search.ts +0 -124
  106. package/src/loaders/session.ts +0 -83
  107. package/src/loaders/user.ts +0 -87
  108. package/src/loaders/wishlist.ts +0 -89
  109. package/src/loaders/wishlistProducts.ts +0 -69
  110. package/src/loaders/workflow/products.ts +0 -62
  111. package/src/loaders/workflow.ts +0 -316
  112. package/src/logo.png +0 -0
  113. package/src/manifest.gen.ts +0 -89
  114. package/src/middleware.cacheHeaders.test.ts +0 -44
  115. package/src/middleware.ts +0 -240
  116. package/src/mod.ts +0 -173
  117. package/src/registry.ts +0 -9
  118. package/src/schemas.gen.ts +0 -3191
  119. package/src/schemas.ts +0 -30
  120. package/src/types.ts +0 -248
  121. package/src/utils/__tests__/cartProjection.test.ts +0 -118
  122. package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
  123. package/src/utils/__tests__/fetch.test.ts +0 -80
  124. package/src/utils/__tests__/fetchCache.test.ts +0 -155
  125. package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
  126. package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
  127. package/src/utils/__tests__/minicart.test.ts +0 -184
  128. package/src/utils/__tests__/operationRouter.test.ts +0 -227
  129. package/src/utils/__tests__/resilience.test.ts +0 -205
  130. package/src/utils/__tests__/resourceRange.test.ts +0 -32
  131. package/src/utils/__tests__/simulationCache.test.ts +0 -40
  132. package/src/utils/__tests__/sitemap.test.ts +0 -185
  133. package/src/utils/__tests__/slugify.test.ts +0 -31
  134. package/src/utils/__tests__/storefrontBaseUrl.test.ts +0 -61
  135. package/src/utils/__tests__/transform.test.ts +0 -1111
  136. package/src/utils/accountLoaders.ts +0 -203
  137. package/src/utils/authHelpers.ts +0 -85
  138. package/src/utils/batch.ts +0 -18
  139. package/src/utils/buildOfferShelf.test.ts +0 -59
  140. package/src/utils/cartProjection.ts +0 -96
  141. package/src/utils/cookies.ts +0 -167
  142. package/src/utils/enrichment.ts +0 -556
  143. package/src/utils/fetch.ts +0 -111
  144. package/src/utils/fetchCache.ts +0 -55
  145. package/src/utils/index.ts +0 -19
  146. package/src/utils/instrumentedFetch.ts +0 -98
  147. package/src/utils/intelligentSearch.ts +0 -96
  148. package/src/utils/legacy.ts +0 -139
  149. package/src/utils/minicart.ts +0 -139
  150. package/src/utils/pickAndOmit.ts +0 -25
  151. package/src/utils/proxy.ts +0 -495
  152. package/src/utils/resilience.ts +0 -352
  153. package/src/utils/resourceRange.ts +0 -10
  154. package/src/utils/segment.ts +0 -152
  155. package/src/utils/similars.ts +0 -35
  156. package/src/utils/simulationCache.ts +0 -73
  157. package/src/utils/sitemap.ts +0 -283
  158. package/src/utils/slugCache.ts +0 -28
  159. package/src/utils/slugify.ts +0 -13
  160. package/src/utils/transform.ts +0 -1691
  161. package/src/utils/vtexId.ts +0 -151
  162. package/src/vtexClient.test.ts +0 -204
  163. package/tsconfig.json +0 -7
@@ -1,525 +0,0 @@
1
- import {
2
- filtersFromPageTypes,
3
- getVtexConfig,
4
- intelligentSearch,
5
- type PageType,
6
- pageTypesFromPath,
7
- storefrontBaseUrl,
8
- toFacetPath,
9
- } from "../../client";
10
- import { pickSku, toProduct } from "../../utils/transform";
11
- import type { Product as ProductVTEX, Sort } from "../../utils/types";
12
-
13
- export interface SelectedFacet {
14
- key: string;
15
- value: string;
16
- }
17
-
18
- /**
19
- * Friendly fuzzy labels for CMS UIs. Translate to the raw IS API value via
20
- * {@link mapLabelledFuzzyToFuzzy} before passing into a loader's `fuzzy` field.
21
- */
22
- export type LabelledFuzzy = "automatic" | "disabled" | "enabled";
23
-
24
- /**
25
- * Translate a friendly fuzzy label to the value the VTEX Intelligent Search
26
- * API expects. Returns `undefined` when the label is omitted so callers can
27
- * skip the param entirely.
28
- *
29
- * @example
30
- * intelligentSearch({ fuzzy: mapLabelledFuzzyToFuzzy(props.fuzzy) })
31
- */
32
- /**
33
- * Resolve the 0-indexed internal `page` from the CMS prop and/or the
34
- * 1-indexed URL `?page=` param.
35
- *
36
- * `props.page` may arrive as a string (e.g. auto-injected from a URL search
37
- * param upstream) — `Number.isFinite` does NOT coerce, so a bare
38
- * `Number.isFinite(propsPage)` check would silently treat `"3"` as invalid
39
- * and hard-default to page 0 (see #391). Coerce first, and treat a
40
- * non-finite coerced value as "absent" so it falls through to the URL
41
- * parse instead of hard-defaulting.
42
- */
43
- export function resolvePage(propsPage: number | string | undefined, pageFromUrl: string | null | undefined): number {
44
- const coercedPropsPage = propsPage !== undefined ? Number(propsPage) : undefined;
45
- const rawPage =
46
- coercedPropsPage !== undefined && Number.isFinite(coercedPropsPage)
47
- ? coercedPropsPage
48
- : pageFromUrl
49
- ? Number(pageFromUrl) - 1
50
- : 0;
51
- return Number.isFinite(rawPage) && rawPage >= 0 ? Math.floor(rawPage) : 0;
52
- }
53
-
54
- export const mapLabelledFuzzyToFuzzy = (label?: LabelledFuzzy): "0" | "1" | "auto" | undefined => {
55
- switch (label) {
56
- case "automatic":
57
- return "auto";
58
- case "enabled":
59
- return "1";
60
- case "disabled":
61
- return "0";
62
- default:
63
- return undefined;
64
- }
65
- };
66
-
67
- export interface PLPProps {
68
- /**
69
- * @title Query
70
- * @description Overrides the search term used to fetch the listing.
71
- */
72
- query?: string;
73
- /**
74
- * @title Items per page
75
- * @description Number of products per page to display.
76
- */
77
- count?: number;
78
- /**
79
- * @title Sorting
80
- * @description Order in which products are returned.
81
- */
82
- sort?: Sort;
83
- /**
84
- * @title Fuzzy
85
- * @description Controls Intelligent Search typo tolerance.
86
- */
87
- fuzzy?: LabelledFuzzy;
88
- /**
89
- * @title Page offset
90
- * @description Starting page (0-indexed) for the listing query.
91
- */
92
- page?: number;
93
- /**
94
- * @title Selected Facets
95
- * @description Override selected facets from the URL (e.g. force a collection).
96
- */
97
- selectedFacets?: SelectedFacet[];
98
- /**
99
- * @title Hide Unavailable Items
100
- * @description Do not return out-of-stock items.
101
- */
102
- hideUnavailableItems?: boolean;
103
- /** Injected by CMS resolve — the matched page path (e.g. "/pisos/piso-vinilico-clicado") */
104
- __pagePath?: string;
105
- /** Injected by CMS resolve — the full request URL (e.g. "https://site.com/s?q=telha&sort=price:asc") */
106
- __pageUrl?: string;
107
- }
108
-
109
- // -- Types matching VTEX IS API responses --
110
-
111
- interface ISPaginationItem {
112
- index: number;
113
- proxyUrl?: string;
114
- }
115
-
116
- interface ISPagination {
117
- count: number;
118
- current: ISPaginationItem;
119
- before: ISPaginationItem[];
120
- after: ISPaginationItem[];
121
- perPage: number;
122
- next: ISPaginationItem;
123
- previous: ISPaginationItem;
124
- first: ISPaginationItem;
125
- last: ISPaginationItem;
126
- }
127
-
128
- interface ISProductSearchResult {
129
- products: any[];
130
- recordsFiltered: number;
131
- pagination: ISPagination;
132
- correction?: { misspelled?: boolean };
133
- operator?: string;
134
- redirect?: string;
135
- }
136
-
137
- interface ISFacetValueBoolean {
138
- quantity: number;
139
- name: string;
140
- value: string;
141
- selected: boolean;
142
- }
143
-
144
- interface ISFacetValueRange {
145
- quantity: number;
146
- name: string;
147
- selected: boolean;
148
- range: { from: number; to: number };
149
- }
150
-
151
- interface ISFacet {
152
- key: string;
153
- name: string;
154
- type: "TEXT" | "PRICERANGE";
155
- hidden: boolean;
156
- quantity: number;
157
- values: Array<ISFacetValueBoolean | ISFacetValueRange>;
158
- }
159
-
160
- interface ISFacetsResult {
161
- facets: ISFacet[];
162
- }
163
-
164
- // Valid page types for filtering (matching original getValidTypesFromPageTypes)
165
- const VALID_PAGE_TYPES = new Set([
166
- "Brand",
167
- "Category",
168
- "Department",
169
- "SubCategory",
170
- "Collection",
171
- "Cluster",
172
- "Search",
173
- "FullText",
174
- "Product",
175
- ]);
176
-
177
- function getValidPageTypes(pageTypes: PageType[]): PageType[] {
178
- return pageTypes.filter((pt) => VALID_PAGE_TYPES.has(pt.pageType));
179
- }
180
-
181
- // -- Filter transformation (mirrors original toFilter + facetToToggle) --
182
-
183
- function formatRange(from: number, to: number): string {
184
- return `${from}:${to}`;
185
- }
186
-
187
- function isRangeValue(val: any): val is ISFacetValueRange {
188
- return Boolean(val.range);
189
- }
190
-
191
- function filtersToSearchParams(
192
- facets: SelectedFacet[],
193
- paramsToPersist?: URLSearchParams,
194
- ): URLSearchParams {
195
- const searchParams = new URLSearchParams(paramsToPersist);
196
- for (const { key, value } of facets) {
197
- searchParams.append(`filter.${key}`, value);
198
- }
199
- return searchParams;
200
- }
201
-
202
- function facetToToggle(
203
- selectedFacets: SelectedFacet[],
204
- key: string,
205
- paramsToPersist?: URLSearchParams,
206
- basePath = "",
207
- ) {
208
- return (item: ISFacetValueBoolean | ISFacetValueRange) => {
209
- const { quantity, selected } = item;
210
- const isRange = isRangeValue(item);
211
- const value = isRange
212
- ? formatRange(item.range.from, item.range.to)
213
- : (item as ISFacetValueBoolean).value;
214
- const label = isRange ? value : (item as ISFacetValueBoolean).name;
215
- const facet = { key, value };
216
-
217
- const filters = selected
218
- ? selectedFacets.filter((f) => f.key !== key || f.value !== value)
219
- : [...selectedFacets, facet];
220
-
221
- return {
222
- value,
223
- quantity,
224
- selected,
225
- url: `${basePath}?${filtersToSearchParams(filters, paramsToPersist)}`,
226
- label,
227
- };
228
- };
229
- }
230
-
231
- function toFilter(
232
- selectedFacets: SelectedFacet[],
233
- paramsToPersist?: URLSearchParams,
234
- basePath = "",
235
- ) {
236
- return (facet: ISFacet) => ({
237
- "@type": "FilterToggle" as const,
238
- key: facet.key,
239
- label: facet.name,
240
- quantity: facet.quantity,
241
- values: facet.values.map(facetToToggle(selectedFacets, facet.key, paramsToPersist, basePath)),
242
- });
243
- }
244
-
245
- // -- Breadcrumb from page types (mirrors original pageTypesToBreadcrumbList) --
246
-
247
- function pageTypesToBreadcrumb(pageTypes: PageType[]) {
248
- const filtered = pageTypes.filter(
249
- (pt) =>
250
- pt.pageType === "Category" || pt.pageType === "Department" || pt.pageType === "SubCategory",
251
- );
252
- return filtered.map((page, index) => {
253
- const position = index + 1;
254
- const slugParts = filtered.slice(0, position).map((x) => {
255
- const urlPath = x.url ? new URL(`http://${x.url}`).pathname : "";
256
- const segments = urlPath.split("/").filter(Boolean);
257
- return segments[segments.length - 1]?.toLowerCase() ?? "";
258
- });
259
- return {
260
- "@type": "ListItem" as const,
261
- name: page.name,
262
- item: `/${slugParts.join("/")}`,
263
- position,
264
- };
265
- });
266
- }
267
-
268
- // -- SEO from page types (mirrors original pageTypesToSeo) --
269
-
270
- function pageTypesToSeo(pageTypes: PageType[]) {
271
- const current = pageTypes[pageTypes.length - 1];
272
- if (!current) return undefined;
273
- return {
274
- title: current.title || current.name || "",
275
- description: current.metaTagDescription || "",
276
- };
277
- }
278
-
279
- // -- Build IS query params (mirrors original withDefaultParams) --
280
-
281
- function buildISParams(opts: {
282
- query: string;
283
- page: number;
284
- count: number;
285
- sort: string;
286
- fuzzy?: string;
287
- locale: string;
288
- hideUnavailableItems: boolean;
289
- }): Record<string, string> {
290
- const params: Record<string, string> = {
291
- page: String(opts.page + 1), // IS API is 1-indexed
292
- count: String(opts.count),
293
- query: opts.query,
294
- sort: opts.sort,
295
- locale: opts.locale,
296
- hideUnavailableItems: String(opts.hideUnavailableItems),
297
- };
298
- if (opts.fuzzy) params.fuzzy = opts.fuzzy;
299
- return params;
300
- }
301
-
302
- const INVALID_PLP_PREFIXES = [
303
- "/image/",
304
- "/.well-known/",
305
- "/assets/",
306
- "/favicon",
307
- "/_serverFn/",
308
- "/_build/",
309
- "/node_modules/",
310
- ];
311
-
312
- function isValidPLPPath(path: string): boolean {
313
- const lower = path.toLowerCase();
314
- if (INVALID_PLP_PREFIXES.some((p) => lower.startsWith(p))) return false;
315
- const ext = lower.split("/").pop()?.split(".")?.pop();
316
- if (
317
- ext &&
318
- [
319
- "png",
320
- "jpg",
321
- "jpeg",
322
- "gif",
323
- "svg",
324
- "webp",
325
- "ico",
326
- "css",
327
- "js",
328
- "woff",
329
- "woff2",
330
- "ttf",
331
- ].includes(ext)
332
- ) {
333
- return false;
334
- }
335
- return true;
336
- }
337
-
338
- /**
339
- * Mirrors the original deco-cx/apps PLP loader:
340
- *
341
- * 1. Resolve facets from CMS props or Page Type API
342
- * 2. Call product_search AND facets APIs in parallel (same params)
343
- * 3. Transform products to schema.org format
344
- * 4. Transform facets to FilterToggle format
345
- * 5. Build pagination from IS response
346
- */
347
- export default async function vtexProductListingPage(props: PLPProps): Promise<any | null> {
348
- const pageUrl = props.__pageUrl ? new URL(props.__pageUrl, "https://localhost") : null;
349
-
350
- const query = props.query ?? pageUrl?.searchParams.get("q") ?? "";
351
- const countFromUrl = pageUrl?.searchParams.get("PS");
352
- const rawCount = Number(countFromUrl ?? props.count ?? 12);
353
- const count = Number.isFinite(rawCount) && rawCount > 0 ? rawCount : 12;
354
- const sort = props.sort || pageUrl?.searchParams.get("sort") || "";
355
- // props.fuzzy is a friendly LabelledFuzzy ("automatic"|…) — translate it to the
356
- // raw IS API value. The URL param is already a raw value, so it passes through.
357
- const fuzzy =
358
- mapLabelledFuzzyToFuzzy(props.fuzzy) ?? pageUrl?.searchParams.get("fuzzy") ?? undefined;
359
- const page = resolvePage(props.page, pageUrl?.searchParams.get("page"));
360
-
361
- const { selectedFacets: cmsSelectedFacets, hideUnavailableItems = false, __pagePath } = props;
362
-
363
- try {
364
- // 1. Resolve selected facets (CMS + URL filter.* params, matching original)
365
- let facets: SelectedFacet[] =
366
- cmsSelectedFacets && cmsSelectedFacets.length > 0 ? [...cmsSelectedFacets] : [];
367
-
368
- // Extract filter.* params from URL (e.g. filter.category-1=telhas)
369
- if (pageUrl) {
370
- for (const [name, value] of pageUrl.searchParams.entries()) {
371
- const dotIndex = name.indexOf(".");
372
- if (dotIndex > 0 && name.slice(0, dotIndex) === "filter") {
373
- const key = name.slice(dotIndex + 1);
374
- if (key && !facets.some((f) => f.key === key && f.value === value)) {
375
- facets.push({ key, value });
376
- }
377
- }
378
- }
379
- }
380
-
381
- // Handle VTEX `map` query param (e.g. /1368?map=productClusterIds).
382
- // The `map` param tells IS how to interpret each path segment as a facet type.
383
- // Segments and map values are positionally matched (comma-separated).
384
- if (facets.length === 0 && pageUrl && __pagePath) {
385
- const mapParam = pageUrl.searchParams.get("map");
386
- if (mapParam) {
387
- const segments = __pagePath.split("/").filter(Boolean);
388
- const mapValues = mapParam.split(",");
389
- for (let i = 0; i < Math.min(segments.length, mapValues.length); i++) {
390
- const key = mapValues[i].trim();
391
- const value = decodeURIComponent(segments[i]);
392
- if (key && value) {
393
- facets.push({ key, value });
394
- }
395
- }
396
- }
397
- }
398
-
399
- let pageTypes: PageType[] = [];
400
-
401
- if (
402
- facets.length === 0 &&
403
- !query &&
404
- __pagePath &&
405
- __pagePath !== "/" &&
406
- __pagePath !== "/*" &&
407
- isValidPLPPath(__pagePath)
408
- ) {
409
- const allPageTypes = await pageTypesFromPath(__pagePath);
410
- pageTypes = getValidPageTypes(allPageTypes);
411
- facets = filtersFromPageTypes(pageTypes);
412
- }
413
-
414
- if (!facets.length && !query) {
415
- return null;
416
- }
417
-
418
- const facetPath = toFacetPath(facets);
419
- const config = getVtexConfig();
420
- const locale = config.locale ?? "pt-BR";
421
-
422
- const params = buildISParams({
423
- query,
424
- page,
425
- count,
426
- sort,
427
- fuzzy,
428
- locale,
429
- hideUnavailableItems,
430
- });
431
-
432
- const productEndpoint = facetPath ? `/product_search/${facetPath}` : "/product_search/";
433
-
434
- const facetsEndpoint = facetPath ? `/facets/${facetPath}` : "/facets/";
435
-
436
- // 2. Parallel calls — exactly like the original
437
- const [productsResult, facetsResult] = await Promise.all([
438
- intelligentSearch<ISProductSearchResult>(productEndpoint, params),
439
- intelligentSearch<ISFacetsResult>(facetsEndpoint, params),
440
- ]);
441
-
442
- const { products: vtexProducts, pagination, recordsFiltered } = productsResult;
443
-
444
- // 3. Transform products using shared transform pipeline (same as deco-cx/apps)
445
- const baseUrl = storefrontBaseUrl(config);
446
-
447
- const schemaProducts = (vtexProducts as ProductVTEX[]).map((p) => {
448
- const sku = pickSku(p);
449
- return toProduct(p, sku, 0, { baseUrl, priceCurrency: "BRL" });
450
- });
451
-
452
- // Persist URL params (q, sort, filter.*) across filter toggles and pagination links
453
- const paramsToPersist = new URLSearchParams();
454
- if (pageUrl) {
455
- for (const [k, v] of pageUrl.searchParams.entries()) {
456
- if (k !== "page" && k !== "PS" && !k.startsWith("filter.")) {
457
- paramsToPersist.append(k, v);
458
- }
459
- }
460
- } else {
461
- if (query) paramsToPersist.set("q", query);
462
- if (sort) paramsToPersist.set("sort", sort);
463
- }
464
-
465
- // 4. Transform facets to filters (matching original toFilter)
466
- const visibleFacets = facetsResult.facets.filter((f) => !f.hidden);
467
- const basePath = __pagePath && __pagePath !== "/" ? __pagePath : "";
468
- const filters = visibleFacets.map(toFilter(facets, paramsToPersist, basePath));
469
-
470
- // 5. Build pagination (matching original logic)
471
- const currentPageoffset = 1;
472
- const hasNextPage = Boolean(pagination.next?.proxyUrl);
473
- const hasPreviousPage = page > 0;
474
-
475
- const nextPageParams = new URLSearchParams(paramsToPersist);
476
- const prevPageParams = new URLSearchParams(paramsToPersist);
477
-
478
- // Re-add active filter.* params so pagination links preserve selected filters
479
- for (const { key, value } of facets) {
480
- nextPageParams.append(`filter.${key}`, value);
481
- prevPageParams.append(`filter.${key}`, value);
482
- }
483
-
484
- if (hasNextPage) {
485
- nextPageParams.set("page", String(page + currentPageoffset + 1));
486
- }
487
- if (hasPreviousPage) {
488
- prevPageParams.set("page", String(page + currentPageoffset - 1));
489
- }
490
-
491
- const breadcrumbItems = pageTypesToBreadcrumb(pageTypes);
492
-
493
- return {
494
- "@type": "ProductListingPage",
495
- breadcrumb: {
496
- "@type": "BreadcrumbList",
497
- itemListElement: breadcrumbItems,
498
- numberOfItems: breadcrumbItems.length,
499
- },
500
- filters,
501
- products: schemaProducts,
502
- pageInfo: {
503
- nextPage: hasNextPage ? `${basePath}?${nextPageParams}` : undefined,
504
- previousPage: hasPreviousPage ? `${basePath}?${prevPageParams}` : undefined,
505
- currentPage: page + currentPageoffset,
506
- records: recordsFiltered,
507
- recordPerPage: pagination.perPage,
508
- },
509
- sortOptions: [
510
- { value: "", label: "relevance:desc" },
511
- { value: "price:desc", label: "price:desc" },
512
- { value: "price:asc", label: "price:asc" },
513
- { value: "orders:desc", label: "orders:desc" },
514
- { value: "name:desc", label: "name:desc" },
515
- { value: "name:asc", label: "name:asc" },
516
- { value: "release:desc", label: "release:desc" },
517
- { value: "discount:desc", label: "discount:desc" },
518
- ],
519
- seo: pageTypesToSeo(pageTypes),
520
- };
521
- } catch (error) {
522
- console.error("[VTEX] PLP error:", error);
523
- return null;
524
- }
525
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * IS autocomplete suggestions loader.
3
- * Maps VTEX IS response to commerce Suggestion type.
4
- */
5
-
6
- import type { Product, Suggestion } from "@decocms/apps-commerce/types";
7
- import { getVtexConfig, intelligentSearch, storefrontBaseUrl } from "../../client";
8
- import { pickSku, toProduct } from "../../utils/transform";
9
- import type { Product as ProductVTEX } from "../../utils/types";
10
-
11
- export interface SuggestionsProps {
12
- query?: string;
13
- count?: number;
14
- }
15
-
16
- export default async function vtexSuggestions(props: SuggestionsProps): Promise<Suggestion | null> {
17
- const query = props.query || "";
18
- if (!query.trim()) return { searches: [], products: [] };
19
-
20
- try {
21
- const data = await intelligentSearch<{
22
- searches: Array<{ term: string; count: number }>;
23
- products: ProductVTEX[];
24
- }>("/autocomplete_suggestions/", { query });
25
-
26
- const searches = (data.searches ?? []).map((s) => ({
27
- term: s.term,
28
- hits: s.count || 0,
29
- }));
30
-
31
- const config = getVtexConfig();
32
- const baseUrl = storefrontBaseUrl(config);
33
-
34
- const products: Product[] = (data.products ?? []).slice(0, props.count ?? 4).map((p) => {
35
- const sku = pickSku(p);
36
- return toProduct(p, sku, 0, { baseUrl, priceCurrency: "BRL" });
37
- });
38
-
39
- return { searches, products };
40
- } catch (error) {
41
- console.error("[VTEX] Suggestions error:", error);
42
- return null;
43
- }
44
- }
@@ -1 +0,0 @@
1
- export { default, type PDPProps } from "../intelligentSearch/productDetailsPage";
@@ -1 +0,0 @@
1
- export { default, type ProductListProps } from "../intelligentSearch/productList";
@@ -1,79 +0,0 @@
1
- /**
2
- * Related/cross-selling products loader using Legacy Catalog API + shared transform.
3
- * Maps VTEX catalog response to schema.org Product[] following deco-cx/apps pattern.
4
- *
5
- * Includes in-flight dedup for slug→productId resolution so multiple sections
6
- * on the same page (similars, suggestions, whoboughtalsobought, etc.) share a
7
- * single search/{slug}/p call instead of each doing their own.
8
- */
9
-
10
- import type { Product } from "@decocms/apps-commerce/types";
11
- import { getVtexConfig, storefrontBaseUrl, vtexCachedFetch } from "../../client";
12
- import { resolveProductIdBySlug } from "../../utils/slugCache";
13
- import { pickSku, toProduct } from "../../utils/transform";
14
- import type { LegacyProduct } from "../../utils/types";
15
-
16
- export type CrossSellingType =
17
- | "similars"
18
- | "suggestions"
19
- | "accessories"
20
- | "whosawalsosaw"
21
- | "whosawalsobought"
22
- | "whoboughtalsobought"
23
- | "showtogether";
24
-
25
- export interface RelatedProductsProps {
26
- slug?: string;
27
- productId?: string;
28
- crossSelling?: CrossSellingType;
29
- count?: number;
30
- hideUnavailableItems?: boolean;
31
- }
32
-
33
- function fetchCrossSelling(
34
- type: CrossSellingType,
35
- productId: string,
36
- ): Promise<LegacyProduct[] | null> {
37
- return vtexCachedFetch<LegacyProduct[]>(
38
- `/api/catalog_system/pub/products/crossselling/${type}/${productId}`,
39
- );
40
- }
41
-
42
- export default async function vtexRelatedProducts(
43
- props: RelatedProductsProps,
44
- ): Promise<Product[] | null> {
45
- const { slug, crossSelling = "similars", count = 8 } = props;
46
-
47
- let productId = props.productId;
48
-
49
- if (!productId) {
50
- if (!slug) return null;
51
- const linkText = slug.replace(/\/p$/, "").replace(/^\//, "");
52
- productId = (await resolveProductIdBySlug(linkText)) ?? undefined;
53
- if (!productId) return null;
54
- }
55
-
56
- try {
57
- const related = await fetchCrossSelling(crossSelling, productId);
58
- if (!related?.length) return [];
59
-
60
- const config = getVtexConfig();
61
- const baseUrl = storefrontBaseUrl(config);
62
-
63
- let result = related.slice(0, count).map((p) => {
64
- const sku = pickSku(p);
65
- return toProduct(p, sku, 0, { baseUrl, priceCurrency: "BRL" });
66
- });
67
-
68
- if (props.hideUnavailableItems) {
69
- result = result.filter((p) =>
70
- p.offers?.offers?.some((o) => o.availability === "https://schema.org/InStock"),
71
- );
72
- }
73
-
74
- return result;
75
- } catch (error) {
76
- console.error("[VTEX] Related products error:", error);
77
- return null;
78
- }
79
- }