@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,116 @@
1
+ /**
2
+ * VTEX Wishlist actions (wish-list graphql app).
3
+ * Ported from deco-cx/apps:
4
+ * - vtex/actions/wishlist/addItem.ts
5
+ * - vtex/actions/wishlist/removeItem.ts
6
+ * @see https://developers.vtex.com/docs/apps/vtex.wish-list
7
+ */
8
+ import { vtexFetch, getVtexConfig, vtexIOGraphQL } from "../client";
9
+ import { buildAuthCookieHeader } from "../utils/vtexId";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Types
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface WishlistItem {
16
+ id?: string;
17
+ productId: string;
18
+ sku: string;
19
+ title?: string;
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // GraphQL helper (myvtex.com private graphql)
24
+ // ---------------------------------------------------------------------------
25
+
26
+ interface GqlResponse<T> {
27
+ data: T;
28
+ errors?: Array<{ message: string }>;
29
+ }
30
+
31
+ function buildCookieHeader(authCookie: string): string {
32
+ return buildAuthCookieHeader(authCookie, getVtexConfig().account);
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Queries & Mutations
37
+ // ---------------------------------------------------------------------------
38
+
39
+ const ADD_TO_WISHLIST = `mutation AddToWishlist($listItem: ListItemInputType!, $shopperId: String!, $name: String!, $public: Boolean) {
40
+ addToList(listItem: $listItem, shopperId: $shopperId, name: $name, public: $public) @context(provider: "vtex.wish-list@1.x")
41
+ }`;
42
+
43
+ const REMOVE_FROM_WISHLIST = `mutation RemoveFromList($id: ID!, $shopperId: String!, $name: String) {
44
+ removeFromList(id: $id, shopperId: $shopperId, name: $name) @context(provider: "vtex.wish-list@1.x")
45
+ }`;
46
+
47
+ const VIEW_WISHLIST = `query ViewList($shopperId: String!, $name: String!, $from: Int!, $to: Int!) {
48
+ viewList(shopperId: $shopperId, name: $name, from: $from, to: $to) @context(provider: "vtex.wish-list@1.x") {
49
+ data { id productId sku title }
50
+ }
51
+ }`;
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Actions
55
+ // ---------------------------------------------------------------------------
56
+
57
+ async function fetchWishlist(shopperId: string, authCookie: string): Promise<WishlistItem[]> {
58
+ const data = await vtexIOGraphQL<{
59
+ viewList: { data: WishlistItem[] | null };
60
+ }>(
61
+ {
62
+ query: VIEW_WISHLIST,
63
+ variables: { shopperId, name: "Wishlist", from: 0, to: 500 },
64
+ },
65
+ { Cookie: buildCookieHeader(authCookie) },
66
+ );
67
+ return data.viewList?.data ?? [];
68
+ }
69
+
70
+ /**
71
+ * Add an item to the user's wishlist.
72
+ * Returns the updated full wishlist.
73
+ */
74
+ export async function addItem(
75
+ item: { productId: string; sku: string; title?: string },
76
+ shopperId: string,
77
+ authCookie: string,
78
+ ): Promise<WishlistItem[]> {
79
+ if (!authCookie) throw new Error("User must be logged in to add to wishlist");
80
+ await vtexIOGraphQL<unknown>(
81
+ {
82
+ query: ADD_TO_WISHLIST,
83
+ variables: {
84
+ name: "Wishlist",
85
+ shopperId,
86
+ listItem: item,
87
+ },
88
+ },
89
+ { Cookie: buildCookieHeader(authCookie) },
90
+ );
91
+ return fetchWishlist(shopperId, authCookie);
92
+ }
93
+
94
+ /**
95
+ * Remove an item from the user's wishlist by its list-entry ID.
96
+ * Returns the updated full wishlist.
97
+ */
98
+ export async function removeItem(
99
+ id: string,
100
+ shopperId: string,
101
+ authCookie: string,
102
+ ): Promise<WishlistItem[]> {
103
+ if (!authCookie) throw new Error("User must be logged in to remove from wishlist");
104
+ await vtexIOGraphQL<unknown>(
105
+ {
106
+ query: REMOVE_FROM_WISHLIST,
107
+ variables: {
108
+ id,
109
+ name: "Wishlist",
110
+ shopperId,
111
+ },
112
+ },
113
+ { Cookie: buildCookieHeader(authCookie) },
114
+ );
115
+ return fetchWishlist(shopperId, authCookie);
116
+ }
package/vtex/client.ts ADDED
@@ -0,0 +1,423 @@
1
+ /**
2
+ * VTEX API Client for TanStack Start.
3
+ * Uses VTEX's public REST APIs (Intelligent Search + Catalog + Checkout).
4
+ */
5
+
6
+ import { type FetchCacheOptions, fetchWithCache } from "./utils/fetchCache";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // URL sanitization (ported from deco-cx/apps vtex/utils/fetchVTEX.ts)
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const removeNonLatin1Chars = (str: string): string =>
13
+ str.replace(/[^\x00-\x7F]|["']/g, "");
14
+
15
+ const removeScriptChars = (str: string): string => {
16
+ return str
17
+ .replace(/\+/g, "")
18
+ .replaceAll(" ", "")
19
+ .replace(/[[\]{}()<>]/g, "")
20
+ .replace(/[/\\]/g, "")
21
+ .replace(/\./g, "")
22
+ .normalize("NFD")
23
+ .replace(/[\u0300-\u036f]/g, "");
24
+ };
25
+
26
+ function sanitizeUrl(input: string): string {
27
+ let url: URL;
28
+ try {
29
+ url = new URL(input);
30
+ } catch {
31
+ return input;
32
+ }
33
+
34
+ const QS_TO_SANITIZE = ["utm_campaign", "utm_medium", "utm_source", "map"];
35
+ for (const qs of QS_TO_SANITIZE) {
36
+ if (url.searchParams.has(qs)) {
37
+ const values = url.searchParams.getAll(qs);
38
+ url.searchParams.delete(qs);
39
+ for (const v of values) {
40
+ const sanitized = removeScriptChars(removeNonLatin1Chars(v));
41
+ if (sanitized) url.searchParams.append(qs, sanitized);
42
+ }
43
+ }
44
+ }
45
+
46
+ const QS_TO_ENCODE = ["ft"];
47
+ for (const qs of QS_TO_ENCODE) {
48
+ if (url.searchParams.has(qs)) {
49
+ const values = url.searchParams.getAll(qs);
50
+ url.searchParams.delete(qs);
51
+ for (const v of values) {
52
+ url.searchParams.append(qs, encodeURIComponent(v.trim()));
53
+ }
54
+ }
55
+ }
56
+
57
+ return url.toString();
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Config
62
+ // ---------------------------------------------------------------------------
63
+
64
+ export interface VtexConfig {
65
+ account: string;
66
+ publicUrl?: string;
67
+ salesChannel?: string;
68
+ locale?: string;
69
+ appKey?: string;
70
+ appToken?: string;
71
+ /**
72
+ * ISO 3166-1 alpha-3 country code used for simulation/checkout.
73
+ * @default "BRA"
74
+ */
75
+ country?: string;
76
+ /**
77
+ * VTEX domain suffix. Override for non-standard VTEX environments.
78
+ * @default "com.br"
79
+ */
80
+ domain?: string;
81
+ }
82
+
83
+ let _config: VtexConfig | null = null;
84
+ let _fetch: typeof fetch = globalThis.fetch;
85
+
86
+ export function configureVtex(config: VtexConfig) {
87
+ _config = config;
88
+ console.log(`[VTEX] Configured: ${config.account}.vtexcommercestable.com.br`);
89
+ }
90
+
91
+ /**
92
+ * Override the fetch function used by all VTEX client calls.
93
+ * Use this to plug in instrumented fetch for logging/tracing.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * import { createInstrumentedFetch } from "@decocms/start/sdk/instrumentedFetch";
98
+ * import { setVtexFetch } from "@decocms/apps/vtex";
99
+ * setVtexFetch(createInstrumentedFetch("vtex"));
100
+ * ```
101
+ */
102
+ export function setVtexFetch(fetchFn: typeof fetch) {
103
+ _fetch = fetchFn;
104
+ }
105
+
106
+ export function getVtexConfig(): VtexConfig {
107
+ if (!_config)
108
+ throw new Error("VTEX not configured. Call configureVtex() first.");
109
+ return _config;
110
+ }
111
+
112
+ /**
113
+ * Build the VTEX hostname for a given environment.
114
+ * Centralizes `{account}.{env}.{domain}` so nothing is hardcoded.
115
+ */
116
+ export function vtexHost(
117
+ environment: string = "vtexcommercestable",
118
+ config?: VtexConfig,
119
+ ): string {
120
+ const c = config ?? getVtexConfig();
121
+ const domain = c.domain ?? "com.br";
122
+ return `${c.account}.${environment}.${domain}`;
123
+ }
124
+
125
+ function baseUrl(): string {
126
+ return `https://${vtexHost()}`;
127
+ }
128
+
129
+ function isUrl(): string {
130
+ return `https://${vtexHost()}/api/io/_v/api/intelligent-search`;
131
+ }
132
+
133
+ function authHeaders(): Record<string, string> {
134
+ const c = getVtexConfig();
135
+ const headers: Record<string, string> = {
136
+ "Content-Type": "application/json",
137
+ Accept: "application/json",
138
+ };
139
+ if (c.appKey && c.appToken) {
140
+ headers["X-VTEX-API-AppKey"] = c.appKey;
141
+ headers["X-VTEX-API-AppToken"] = c.appToken;
142
+ }
143
+ return headers;
144
+ }
145
+
146
+ export async function vtexFetchResponse(
147
+ path: string,
148
+ init?: RequestInit,
149
+ ): Promise<Response> {
150
+ const raw = path.startsWith("http") ? path : `${baseUrl()}${path}`;
151
+ const url = sanitizeUrl(raw);
152
+ const response = await _fetch(url, {
153
+ ...init,
154
+ headers: { ...authHeaders(), ...init?.headers },
155
+ });
156
+ if (!response.ok) {
157
+ throw new Error(
158
+ `VTEX API error: ${response.status} ${response.statusText} - ${url}`,
159
+ );
160
+ }
161
+ return response;
162
+ }
163
+
164
+ export async function vtexFetch<T>(
165
+ path: string,
166
+ init?: RequestInit,
167
+ ): Promise<T> {
168
+ const response = await vtexFetchResponse(path, init);
169
+ return response.json();
170
+ }
171
+
172
+ export interface VtexCachedFetchOptions {
173
+ /** SWR cache TTL override in ms */
174
+ cacheTTL?: number;
175
+ }
176
+
177
+ /**
178
+ * Like vtexFetch but routes GET requests through the SWR in-memory cache.
179
+ * Uses in-flight dedup + stale-while-revalidate.
180
+ * Non-GET requests fall through to regular vtexFetch.
181
+ */
182
+ export async function vtexCachedFetch<T>(
183
+ path: string,
184
+ init?: RequestInit,
185
+ cacheOpts?: VtexCachedFetchOptions,
186
+ ): Promise<T | null> {
187
+ const method = (init?.method ?? "GET").toUpperCase();
188
+ if (method !== "GET") return vtexFetch<T>(path, init);
189
+
190
+ const raw = path.startsWith("http") ? path : `${baseUrl()}${path}`;
191
+ const url = sanitizeUrl(raw);
192
+ const opts: FetchCacheOptions | undefined = cacheOpts?.cacheTTL
193
+ ? { ttl: cacheOpts.cacheTTL }
194
+ : undefined;
195
+
196
+ return fetchWithCache<T>(
197
+ url,
198
+ () =>
199
+ _fetch(url, {
200
+ ...init,
201
+ headers: { ...authHeaders(), ...init?.headers },
202
+ }),
203
+ opts,
204
+ );
205
+ }
206
+
207
+ /**
208
+ * Result type for actions that need to propagate VTEX Set-Cookie headers.
209
+ * In TanStack Start, the caller (server function) is responsible for
210
+ * forwarding these cookies to the client via `setCookie` from vinxi/http.
211
+ */
212
+ export interface VtexFetchResult<T> {
213
+ data: T;
214
+ setCookies: string[];
215
+ }
216
+
217
+ /**
218
+ * Like vtexFetch, but also returns Set-Cookie headers from the response.
219
+ * Use for checkout, session, and auth actions that set cookies.
220
+ */
221
+ export async function vtexFetchWithCookies<T>(
222
+ path: string,
223
+ init?: RequestInit,
224
+ ): Promise<VtexFetchResult<T>> {
225
+ const response = await vtexFetchResponse(path, init);
226
+ const data = (await response.json()) as T;
227
+ const setCookies: string[] = [];
228
+ if (typeof response.headers.getSetCookie === "function") {
229
+ setCookies.push(...response.headers.getSetCookie());
230
+ } else {
231
+ response.headers.forEach((value, key) => {
232
+ if (key.toLowerCase() === "set-cookie") {
233
+ setCookies.push(value);
234
+ }
235
+ });
236
+ }
237
+ return { data, setCookies };
238
+ }
239
+
240
+ export async function intelligentSearch<T>(
241
+ path: string,
242
+ params?: Record<string, string>,
243
+ opts?: { cookieHeader?: string; locale?: string },
244
+ ): Promise<T> {
245
+ const url = new URL(`${isUrl()}${path}`);
246
+ if (params) {
247
+ for (const [k, v] of Object.entries(params)) {
248
+ url.searchParams.set(k, v);
249
+ }
250
+ }
251
+ const c = getVtexConfig();
252
+ if (c.salesChannel) url.searchParams.set("sc", c.salesChannel);
253
+
254
+ const locale = opts?.locale ?? c.locale;
255
+ if (locale && !url.searchParams.has("locale")) {
256
+ url.searchParams.set("locale", locale);
257
+ }
258
+
259
+ const headers: Record<string, string> = { ...authHeaders() };
260
+ if (opts?.cookieHeader) {
261
+ headers.cookie = opts.cookieHeader;
262
+ }
263
+
264
+ const fullUrl = url.toString();
265
+
266
+ // IS GET requests go through SWR cache (3 min TTL via status-based defaults).
267
+ // The doFetch callback throws on non-ok responses, so null is never returned.
268
+ return fetchWithCache<T>(fullUrl, async () => {
269
+ const response = await _fetch(fullUrl, { headers });
270
+ if (!response.ok) {
271
+ throw new Error(`VTEX IS error: ${response.status} - ${fullUrl}`);
272
+ }
273
+ return response;
274
+ }) as Promise<T>;
275
+ }
276
+
277
+ /**
278
+ * Execute a GraphQL query against the VTEX IO Runtime (myvtex.com).
279
+ * Used for private profile/session/wishlist/payment queries that the
280
+ * original Deco loaders called via `ctx.io.query(...)`.
281
+ */
282
+ export async function vtexIOGraphQL<T>(
283
+ body: {
284
+ query: string;
285
+ variables?: Record<string, unknown> | null;
286
+ operationName?: string;
287
+ },
288
+ headers?: Record<string, string>,
289
+ ): Promise<T> {
290
+ const { account } = getVtexConfig();
291
+ const res = await vtexFetch<{ data: T; errors?: Array<{ message: string }> }>(
292
+ `https://${account}.myvtex.com/_v/private/graphql/v1`,
293
+ {
294
+ method: "POST",
295
+ headers,
296
+ body: JSON.stringify(body),
297
+ },
298
+ );
299
+ if (res.errors?.length) {
300
+ throw new Error(
301
+ `VTEX IO GraphQL error: ${res.errors.map((e) => e.message).join(", ")}`,
302
+ );
303
+ }
304
+ return res.data;
305
+ }
306
+
307
+ // -- Page Type API (used by PLP to derive category facets from URL path) --
308
+
309
+ export interface PageType {
310
+ id: string;
311
+ name: string;
312
+ url: string;
313
+ title: string;
314
+ metaTagDescription: string;
315
+ pageType:
316
+ | "Brand"
317
+ | "Category"
318
+ | "Department"
319
+ | "SubCategory"
320
+ | "Collection"
321
+ | "Cluster"
322
+ | "Search"
323
+ | "Product"
324
+ | "NotFound"
325
+ | "FullText";
326
+ }
327
+
328
+ const PAGE_TYPE_TO_MAP_PARAM: Record<string, string | null> = {
329
+ Brand: "brand",
330
+ Collection: "productClusterIds",
331
+ Cluster: "productClusterIds",
332
+ Search: null,
333
+ Product: null,
334
+ NotFound: null,
335
+ FullText: null,
336
+ };
337
+
338
+ function pageTypeToMapParam(
339
+ type: PageType["pageType"],
340
+ index: number,
341
+ ): string | null {
342
+ if (type === "Category" || type === "Department" || type === "SubCategory") {
343
+ return `category-${index + 1}`;
344
+ }
345
+ return PAGE_TYPE_TO_MAP_PARAM[type] ?? null;
346
+ }
347
+
348
+ function cachedPageType(term: string): Promise<PageType | null> {
349
+ return vtexCachedFetch<PageType>(
350
+ `/api/catalog_system/pub/portal/pagetype/${term}`,
351
+ );
352
+ }
353
+
354
+ /**
355
+ * Query VTEX Page Type API for each path segment (cumulative).
356
+ * Mirrors deco-cx/apps `pageTypesFromUrl`.
357
+ * Uses in-flight deduplication to avoid duplicate calls for the same segment.
358
+ */
359
+ export async function pageTypesFromPath(pagePath: string): Promise<PageType[]> {
360
+ const segments = pagePath.split("/").filter(Boolean);
361
+ const results = await Promise.all(
362
+ segments.map((_, index) => {
363
+ const term = segments.slice(0, index + 1).join("/");
364
+ return cachedPageType(term);
365
+ }),
366
+ );
367
+ return results.filter((pt): pt is PageType => pt !== null);
368
+ }
369
+
370
+ const slugify = (str: string) =>
371
+ str
372
+ .replace(/,/g, "")
373
+ .replace(/[·/_,:]/g, "-")
374
+ .replace(/[*+~.()'"!:@&[\]`/ %$#?{}|><=_^]/g, "-")
375
+ .normalize("NFD")
376
+ .replace(/[\u0300-\u036f]/g, "")
377
+ .toLowerCase();
378
+
379
+ /**
380
+ * Convert page types to selectedFacets with correct IS facet keys.
381
+ * Mirrors deco-cx/apps `filtersFromPathname`.
382
+ */
383
+ export function filtersFromPageTypes(
384
+ pageTypes: PageType[],
385
+ ): Array<{ key: string; value: string }> {
386
+ return pageTypes
387
+ .map((page, index) => {
388
+ const key = pageTypeToMapParam(page.pageType, index);
389
+ if (!key || !page.name) return null;
390
+ return { key, value: slugify(page.name) };
391
+ })
392
+ .filter((f): f is { key: string; value: string } => f !== null);
393
+ }
394
+
395
+ /**
396
+ * Build the IS facet path string from selectedFacets.
397
+ * Mirrors deco-cx/apps `toPath`.
398
+ */
399
+ export function toFacetPath(
400
+ facets: Array<{ key: string; value: string }>,
401
+ ): string {
402
+ return facets
403
+ .map(({ key, value }) => (key ? `${key}/${value}` : value))
404
+ .join("/");
405
+ }
406
+
407
+ export function initVtexFromBlocks(blocks: Record<string, any>) {
408
+ const vtexBlock = blocks.vtex || blocks["deco-vtex"];
409
+ if (!vtexBlock) {
410
+ console.warn("[VTEX] No vtex.json block found.");
411
+ return;
412
+ }
413
+ configureVtex({
414
+ account: vtexBlock.account,
415
+ publicUrl: vtexBlock.publicUrl,
416
+ salesChannel: vtexBlock.salesChannel || "1",
417
+ locale: vtexBlock.locale || vtexBlock.defaultLocale,
418
+ appKey: vtexBlock.appKey,
419
+ appToken: vtexBlock.appToken,
420
+ country: vtexBlock.country,
421
+ domain: vtexBlock.domain,
422
+ });
423
+ }
@@ -0,0 +1,4 @@
1
+ export { useCart, type UseCartOptions, type OrderForm, type CartItem } from "./useCart";
2
+ export { useUser, type UseUserOptions, type VtexUser } from "./useUser";
3
+ export { useWishlist, type UseWishlistOptions, type WishlistItem } from "./useWishlist";
4
+ export { useAutocomplete, type UseAutocompleteOptions } from "./useAutocomplete";
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Client-side autocomplete hook for VTEX Intelligent Search.
3
+ *
4
+ * Uses TanStack Query with debounced input to fetch search suggestions.
5
+ * Ported from deco-cx/apps vtex/hooks/useAutocomplete.ts
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { useAutocomplete } from "@decocms/apps/vtex/hooks/useAutocomplete";
10
+ *
11
+ * function SearchBar() {
12
+ * const { setSearch, suggestions, loading } = useAutocomplete();
13
+ * return (
14
+ * <input
15
+ * onChange={(e) => setSearch(e.target.value)}
16
+ * placeholder="Search..."
17
+ * />
18
+ * );
19
+ * }
20
+ * ```
21
+ */
22
+
23
+ import { useState, useCallback, useRef, useEffect } from "react";
24
+ import { useQuery } from "@tanstack/react-query";
25
+ import type { Suggestion } from "../../commerce/types/commerce";
26
+
27
+ export interface UseAutocompleteOptions {
28
+ /** Debounce delay in ms @default 250 */
29
+ debounceMs?: number;
30
+ /** Max products to return @default 4 */
31
+ count?: number;
32
+ /** Custom fetch function — defaults to calling the inline-loader on the server */
33
+ fetchSuggestions?: (query: string, count: number) => Promise<Suggestion | null>;
34
+ }
35
+
36
+ const AUTOCOMPLETE_QUERY_KEY = "vtex-autocomplete";
37
+
38
+ function useDebounce<T>(value: T, delay: number): T {
39
+ const [debounced, setDebounced] = useState(value);
40
+ useEffect(() => {
41
+ const timer = setTimeout(() => setDebounced(value), delay);
42
+ return () => clearTimeout(timer);
43
+ }, [value, delay]);
44
+ return debounced;
45
+ }
46
+
47
+ async function defaultFetchSuggestions(
48
+ query: string,
49
+ _count: number,
50
+ ): Promise<Suggestion | null> {
51
+ const params = new URLSearchParams({ query });
52
+ const res = await fetch(`/api/vtex/suggestions?${params}`);
53
+ if (!res.ok) return null;
54
+ return res.json();
55
+ }
56
+
57
+ export function useAutocomplete(opts?: UseAutocompleteOptions) {
58
+ const debounceMs = opts?.debounceMs ?? 250;
59
+ const count = opts?.count ?? 4;
60
+ const fetchFn = opts?.fetchSuggestions ?? defaultFetchSuggestions;
61
+
62
+ const [rawQuery, setRawQuery] = useState("");
63
+ const debouncedQuery = useDebounce(rawQuery.trim(), debounceMs);
64
+
65
+ const { data, isLoading, isFetching } = useQuery({
66
+ queryKey: [AUTOCOMPLETE_QUERY_KEY, debouncedQuery, count],
67
+ queryFn: () => fetchFn(debouncedQuery, count),
68
+ enabled: debouncedQuery.length > 0,
69
+ staleTime: 60_000,
70
+ });
71
+
72
+ const setSearch = useCallback(
73
+ (query: string) => {
74
+ setRawQuery(query);
75
+ },
76
+ [],
77
+ );
78
+
79
+ return {
80
+ /** Set the search query (will be debounced automatically) */
81
+ setSearch,
82
+ /** Current raw (un-debounced) query */
83
+ query: rawQuery,
84
+ /** Suggestion result (searches + products) */
85
+ suggestions: data ?? null,
86
+ /** True while initial fetch is in progress */
87
+ loading: isLoading || isFetching,
88
+ };
89
+ }