@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.
- package/.github/workflows/release.yml +34 -0
- package/.releaserc.json +25 -0
- package/commerce/components/Image.tsx +209 -0
- package/commerce/components/JsonLd.tsx +285 -0
- package/commerce/sdk/analytics.ts +24 -0
- package/commerce/sdk/formatPrice.ts +23 -0
- package/commerce/sdk/url.ts +9 -0
- package/commerce/sdk/useOffer.ts +75 -0
- package/commerce/sdk/useVariantPossibilities.ts +43 -0
- package/commerce/types/commerce.ts +1105 -0
- package/commerce/utils/canonical.ts +11 -0
- package/commerce/utils/constants.ts +9 -0
- package/commerce/utils/filters.ts +10 -0
- package/commerce/utils/productToAnalyticsItem.ts +67 -0
- package/commerce/utils/stateByZip.ts +50 -0
- package/knip.json +19 -0
- package/package.json +77 -0
- package/shopify/actions/cart/addItems.ts +37 -0
- package/shopify/actions/cart/updateCoupons.ts +32 -0
- package/shopify/actions/cart/updateItems.ts +32 -0
- package/shopify/actions/user/signIn.ts +45 -0
- package/shopify/actions/user/signUp.ts +36 -0
- package/shopify/client.ts +58 -0
- package/shopify/index.ts +32 -0
- package/shopify/init.ts +40 -0
- package/shopify/loaders/ProductDetailsPage.ts +35 -0
- package/shopify/loaders/ProductList.ts +101 -0
- package/shopify/loaders/ProductListingPage.ts +180 -0
- package/shopify/loaders/RelatedProducts.ts +45 -0
- package/shopify/loaders/cart.ts +73 -0
- package/shopify/loaders/shop.ts +40 -0
- package/shopify/loaders/user.ts +44 -0
- package/shopify/utils/admin/admin.ts +57 -0
- package/shopify/utils/admin/queries.ts +29 -0
- package/shopify/utils/cart.ts +28 -0
- package/shopify/utils/cookies.ts +85 -0
- package/shopify/utils/enums.ts +438 -0
- package/shopify/utils/graphql.ts +69 -0
- package/shopify/utils/storefront/queries.ts +530 -0
- package/shopify/utils/storefront/storefront.graphql.gen.ts +113 -0
- package/shopify/utils/transform.ts +436 -0
- package/shopify/utils/types.ts +191 -0
- package/shopify/utils/user.ts +23 -0
- package/shopify/utils/utils.ts +164 -0
- package/tsconfig.json +11 -0
- package/vtex/README.md +6 -0
- package/vtex/actions/address.ts +211 -0
- package/vtex/actions/auth.ts +337 -0
- package/vtex/actions/checkout.ts +497 -0
- package/vtex/actions/index.ts +11 -0
- package/vtex/actions/masterData.ts +170 -0
- package/vtex/actions/misc.ts +196 -0
- package/vtex/actions/newsletter.ts +108 -0
- package/vtex/actions/orders.ts +37 -0
- package/vtex/actions/profile.ts +119 -0
- package/vtex/actions/session.ts +87 -0
- package/vtex/actions/trigger.ts +43 -0
- package/vtex/actions/wishlist.ts +116 -0
- package/vtex/client.ts +423 -0
- package/vtex/hooks/index.ts +4 -0
- package/vtex/hooks/useAutocomplete.ts +89 -0
- package/vtex/hooks/useCart.ts +219 -0
- package/vtex/hooks/useUser.ts +78 -0
- package/vtex/hooks/useWishlist.ts +119 -0
- package/vtex/index.ts +14 -0
- package/vtex/inline-loaders/productDetailsPage.ts +75 -0
- package/vtex/inline-loaders/productList.ts +163 -0
- package/vtex/inline-loaders/productListingPage.ts +447 -0
- package/vtex/inline-loaders/relatedProducts.ts +83 -0
- package/vtex/inline-loaders/suggestions.ts +49 -0
- package/vtex/inline-loaders/workflowProducts.ts +68 -0
- package/vtex/invoke.ts +202 -0
- package/vtex/loaders/address.ts +120 -0
- package/vtex/loaders/brands.ts +51 -0
- package/vtex/loaders/cart.ts +49 -0
- package/vtex/loaders/catalog.ts +165 -0
- package/vtex/loaders/collections.ts +57 -0
- package/vtex/loaders/index.ts +19 -0
- package/vtex/loaders/legacy.ts +671 -0
- package/vtex/loaders/logistics.ts +115 -0
- package/vtex/loaders/navbar.ts +29 -0
- package/vtex/loaders/orders.ts +103 -0
- package/vtex/loaders/pageType.ts +62 -0
- package/vtex/loaders/payment.ts +107 -0
- package/vtex/loaders/profile.ts +138 -0
- package/vtex/loaders/promotion.ts +33 -0
- package/vtex/loaders/search.ts +127 -0
- package/vtex/loaders/session.ts +91 -0
- package/vtex/loaders/user.ts +89 -0
- package/vtex/loaders/wishlist.ts +89 -0
- package/vtex/loaders/wishlistProducts.ts +81 -0
- package/vtex/loaders/workflow.ts +323 -0
- package/vtex/logo.png +0 -0
- package/vtex/middleware.ts +229 -0
- package/vtex/types.ts +248 -0
- package/vtex/utils/batch.ts +21 -0
- package/vtex/utils/cookies.ts +76 -0
- package/vtex/utils/enrichment.ts +540 -0
- package/vtex/utils/fetchCache.ts +150 -0
- package/vtex/utils/index.ts +17 -0
- package/vtex/utils/intelligentSearch.ts +84 -0
- package/vtex/utils/legacy.ts +155 -0
- package/vtex/utils/pickAndOmit.ts +30 -0
- package/vtex/utils/proxy.ts +196 -0
- package/vtex/utils/resourceRange.ts +10 -0
- package/vtex/utils/segment.ts +163 -0
- package/vtex/utils/similars.ts +38 -0
- package/vtex/utils/sitemap.ts +133 -0
- package/vtex/utils/slugCache.ts +32 -0
- package/vtex/utils/slugify.ts +13 -0
- package/vtex/utils/transform.ts +1331 -0
- package/vtex/utils/types.ts +1884 -0
- 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
|
+
}
|