@jay-framework/wix-stores 0.15.1 → 0.15.4

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/dist/index.js CHANGED
@@ -44,13 +44,49 @@ function getInventoryClient(wixClient) {
44
44
  }
45
45
  const WIX_STORES_SERVICE_MARKER = createJayService("Wix Store Service");
46
46
  function provideWixStoresService(wixClient, options) {
47
+ let cachedTree = null;
48
+ const categoriesClient = getCategoriesClient(wixClient);
47
49
  const service = {
48
50
  products: getProductsV3Client(wixClient),
49
- categories: getCategoriesClient(wixClient),
51
+ categories: categoriesClient,
50
52
  inventory: getInventoryClient(wixClient),
51
- // Keep cart for backward compatibility, but prefer WIX_CART_SERVICE
52
53
  cart: getCurrentCartClient(wixClient),
53
- categoryPrefixes: options?.categoryPrefixes ?? []
54
+ urls: options?.urls ?? { product: "/products/{slug}", category: null },
55
+ defaultCategory: options?.defaultCategory ?? null,
56
+ async getCategoryTree() {
57
+ if (cachedTree) return cachedTree;
58
+ const slugMap = /* @__PURE__ */ new Map();
59
+ const parentMap = /* @__PURE__ */ new Map();
60
+ const rootIds = /* @__PURE__ */ new Set();
61
+ const imageMap = /* @__PURE__ */ new Map();
62
+ try {
63
+ const processItems = (items) => {
64
+ for (const cat of items) {
65
+ if (!cat._id || !cat.slug) continue;
66
+ slugMap.set(cat._id, cat.slug);
67
+ if (cat.parentCategory?._id) {
68
+ parentMap.set(cat._id, cat.parentCategory._id);
69
+ } else {
70
+ rootIds.add(cat._id);
71
+ }
72
+ const imageUrl = cat.media?.mainMedia?.image?.url || cat.media?.mainMedia?.url;
73
+ if (imageUrl) {
74
+ imageMap.set(cat._id, imageUrl);
75
+ }
76
+ }
77
+ };
78
+ let result = await categoriesClient.queryCategories({ treeReference: { appNamespace: "@wix/stores" } }).eq("visible", true).limit(100).find();
79
+ processItems(result.items || []);
80
+ while (result.hasNext()) {
81
+ result = await result.next();
82
+ processItems(result.items || []);
83
+ }
84
+ } catch (error) {
85
+ console.error("[wix-stores] Failed to build category tree:", error);
86
+ }
87
+ cachedTree = { slugMap, parentMap, rootIds, imageMap };
88
+ return cachedTree;
89
+ }
54
90
  };
55
91
  registerService(WIX_STORES_SERVICE_MARKER, service);
56
92
  return service;
@@ -80,7 +116,8 @@ var ProductType$1 = /* @__PURE__ */ ((ProductType2) => {
80
116
  var QuickAddType = /* @__PURE__ */ ((QuickAddType2) => {
81
117
  QuickAddType2[QuickAddType2["SIMPLE"] = 0] = "SIMPLE";
82
118
  QuickAddType2[QuickAddType2["SINGLE_OPTION"] = 1] = "SINGLE_OPTION";
83
- QuickAddType2[QuickAddType2["NEEDS_CONFIGURATION"] = 2] = "NEEDS_CONFIGURATION";
119
+ QuickAddType2[QuickAddType2["COLOR_AND_TEXT_OPTIONS"] = 2] = "COLOR_AND_TEXT_OPTIONS";
120
+ QuickAddType2[QuickAddType2["NEEDS_CONFIGURATION"] = 3] = "NEEDS_CONFIGURATION";
84
121
  return QuickAddType2;
85
122
  })(QuickAddType || {});
86
123
  var OptionRenderType$1 = /* @__PURE__ */ ((OptionRenderType2) => {
@@ -136,33 +173,54 @@ function formatWixMediaUrl(_id, url, resize) {
136
173
  }
137
174
  return "";
138
175
  }
139
- function resolveProductPrefix(product, prefixConfig) {
140
- if (!prefixConfig?.length || !product.allCategoriesInfo?.categories) {
141
- return null;
142
- }
143
- const productCategoryIds = new Set(
144
- product.allCategoriesInfo.categories.map((c) => c._id).filter(Boolean)
145
- );
146
- for (const { categoryId, prefix } of prefixConfig) {
147
- if (productCategoryIds.has(categoryId)) {
148
- return prefix;
176
+ function findRootCategoryId(categoryId, tree) {
177
+ let current = categoryId;
178
+ for (let depth = 0; depth < 20; depth++) {
179
+ if (tree.rootIds.has(current)) {
180
+ return current;
149
181
  }
182
+ const parentId = tree.parentMap.get(current);
183
+ if (!parentId) return current;
184
+ current = parentId;
150
185
  }
151
- return null;
186
+ return current;
152
187
  }
153
- function resolveProductPrefixConfig(product, prefixConfig) {
154
- if (!prefixConfig?.length || !product.allCategoriesInfo?.categories) {
155
- return null;
188
+ function findRootCategorySlug(categoryId, tree) {
189
+ const rootId = findRootCategoryId(categoryId, tree);
190
+ return tree.slugMap.get(rootId) ?? "";
191
+ }
192
+ function findCategoryImage(categoryId, tree) {
193
+ let current = categoryId;
194
+ for (let depth = 0; depth < 20 && current; depth++) {
195
+ const image = tree.imageMap.get(current);
196
+ if (image) return image;
197
+ current = tree.parentMap.get(current);
156
198
  }
157
- const productCategoryIds = new Set(
158
- product.allCategoriesInfo.categories.map((c) => c._id).filter(Boolean)
159
- );
160
- for (const config of prefixConfig) {
161
- if (productCategoryIds.has(config.categoryId)) {
162
- return config;
163
- }
199
+ return "";
200
+ }
201
+ function buildProductUrl(urls, tree, slug, mainCategoryId) {
202
+ let url = urls.product;
203
+ url = url.replace("{slug}", slug);
204
+ if (url.includes("{category}")) {
205
+ const categorySlug = tree.slugMap.get(mainCategoryId);
206
+ url = url.replace("{category}", categorySlug);
164
207
  }
165
- return null;
208
+ if (url.includes("{prefix}")) {
209
+ const prefixSlug = findRootCategorySlug(mainCategoryId, tree);
210
+ url = url.replace("{prefix}", prefixSlug);
211
+ }
212
+ return url;
213
+ }
214
+ function buildCategoryUrl(urls, tree, categorySlug, categoryId) {
215
+ if (!urls.category) return null;
216
+ let url = urls.category;
217
+ url = url.replace("{category}", categorySlug);
218
+ if (url.includes("{prefix}")) {
219
+ const prefixSlug = findRootCategorySlug(categoryId, tree);
220
+ if (!prefixSlug) return null;
221
+ url = url.replace("{prefix}", prefixSlug);
222
+ }
223
+ return url.includes("{") ? null : url;
166
224
  }
167
225
  function mapAvailabilityStatus(status) {
168
226
  switch (status) {
@@ -198,7 +256,15 @@ function isValidPrice(amount) {
198
256
  function getQuickAddType(product) {
199
257
  const optionCount = product.options?.length ?? 0;
200
258
  const hasModifiers = (product.modifiers?.length ?? 0) > 0;
201
- if (hasModifiers || optionCount > 1) {
259
+ if (hasModifiers || optionCount > 2) {
260
+ return QuickAddType.NEEDS_CONFIGURATION;
261
+ }
262
+ if (optionCount === 2) {
263
+ const hasColor = product.options.some((o) => o.optionRenderType === "SWATCH_CHOICES");
264
+ const hasText = product.options.some((o) => o.optionRenderType === "TEXT_CHOICES");
265
+ if (hasColor && hasText) {
266
+ return QuickAddType.COLOR_AND_TEXT_OPTIONS;
267
+ }
202
268
  return QuickAddType.NEEDS_CONFIGURATION;
203
269
  }
204
270
  if (optionCount === 1) {
@@ -214,31 +280,85 @@ function mapChoiceType(choiceType) {
214
280
  }
215
281
  function mapQuickOption(option, variantsInfo) {
216
282
  if (!option) return null;
217
- const optionId = option._id;
218
283
  const choices = option.choicesSettings?.choices || [];
219
284
  return {
220
- _id: optionId,
285
+ _id: option._id || "",
221
286
  name: option.name || "",
222
287
  optionRenderType: mapOptionRenderType(option.optionRenderType),
223
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
224
- choices: choices.map((choice) => {
225
- return {
226
- choiceId: choice.choiceId,
227
- name: choice.name || "",
228
- choiceType: mapChoiceType(choice.choiceType),
229
- colorCode: choice.colorCode || "",
230
- inStock: choice.inStock,
231
- isSelected: false
232
- };
233
- })
288
+ choices: choices.map((choice) => ({
289
+ choiceId: choice.choiceId || "",
290
+ name: choice.name || "",
291
+ choiceType: mapChoiceType(choice.choiceType),
292
+ colorCode: choice.colorCode || "",
293
+ inStock: choice.inStock ?? true,
294
+ isSelected: false
295
+ }))
234
296
  };
235
297
  }
236
- const DEFAULT_PRODUCT_PAGE_PATH = "/products";
237
- function mapProductToCard(product, productPagePath = DEFAULT_PRODUCT_PAGE_PATH, prefixConfig) {
298
+ function mapQuickAddOptions(product) {
299
+ const quickAddType = getQuickAddType(product);
300
+ if (quickAddType === QuickAddType.COLOR_AND_TEXT_OPTIONS) {
301
+ const colorOption = product.options.find((o) => o.optionRenderType === "SWATCH_CHOICES");
302
+ const textOption = product.options.find((o) => o.optionRenderType === "TEXT_CHOICES");
303
+ const quickOption = mapQuickOption(colorOption, product.variantsInfo);
304
+ const secondQuickOption = mapQuickOption(textOption, product.variantsInfo);
305
+ if (quickOption?.choices) {
306
+ const firstInStock = quickOption.choices.find((c) => c.inStock);
307
+ if (firstInStock) {
308
+ firstInStock.isSelected = true;
309
+ }
310
+ }
311
+ if (secondQuickOption?.choices) {
312
+ for (const choice of secondQuickOption.choices) {
313
+ choice.inStock = false;
314
+ }
315
+ }
316
+ return { quickAddType, quickOption, secondQuickOption };
317
+ }
318
+ if (quickAddType === QuickAddType.SINGLE_OPTION) {
319
+ return {
320
+ quickAddType,
321
+ quickOption: mapQuickOption(product.options?.[0], product.variantsInfo),
322
+ secondQuickOption: null
323
+ };
324
+ }
325
+ return { quickAddType, quickOption: null, secondQuickOption: null };
326
+ }
327
+ function buildVariantStockMap(product) {
328
+ const stockMap = {};
329
+ const options = product.options;
330
+ const variants = product.variantsInfo?.variants;
331
+ if (!options || options.length !== 2 || !variants) return stockMap;
332
+ const colorOption = options.find((o) => o.optionRenderType === "COLOR_SWATCH_CHOICES");
333
+ const textOption = options.find((o) => o.optionRenderType !== "COLOR_SWATCH_CHOICES");
334
+ if (!colorOption || !textOption) return stockMap;
335
+ const colorOptionId = colorOption._id || "";
336
+ const textOptionId = textOption._id || "";
337
+ const colorChoices = colorOption.choicesSettings?.choices || [];
338
+ const textChoices = textOption.choicesSettings?.choices || [];
339
+ for (const colorChoice of colorChoices) {
340
+ const cId = colorChoice.choiceId || "";
341
+ stockMap[cId] = {};
342
+ for (const textChoice of textChoices) {
343
+ const tId = textChoice.choiceId || "";
344
+ const variant = variants.find(
345
+ (v) => v.choices?.some(
346
+ (c) => c.optionChoiceIds?.optionId === colorOptionId && c.optionChoiceIds?.choiceId === cId
347
+ ) && v.choices?.some(
348
+ (c) => c.optionChoiceIds?.optionId === textOptionId && c.optionChoiceIds?.choiceId === tId
349
+ )
350
+ );
351
+ stockMap[cId][tId] = variant?.inventoryStatus?.inStock ?? false;
352
+ }
353
+ }
354
+ return stockMap;
355
+ }
356
+ function mapProductToCard(product, urls, tree) {
238
357
  const mainMedia = product.media?.main;
239
358
  const slug = product.slug || "";
240
- const matchedPrefix = prefixConfig?.length ? resolveProductPrefixConfig(product, prefixConfig) : null;
241
- const productUrl = slug ? matchedPrefix ? `${productPagePath}/${matchedPrefix.prefix}/${slug}` : `${productPagePath}/${slug}` : "";
359
+ const mainCategoryId = product.mainCategoryId || "";
360
+ const productUrl = buildProductUrl(urls, tree, slug, mainCategoryId);
361
+ const categoryName = tree.slugMap.get(mainCategoryId);
242
362
  const firstVariant = product.variantsInfo?.variants?.[0];
243
363
  const variantPrice = firstVariant?.price;
244
364
  const actualAmount = variantPrice?.actualPrice?.amount || product.actualPriceRange?.minValue?.amount || "0";
@@ -251,7 +371,7 @@ function mapProductToCard(product, productPagePath = DEFAULT_PRODUCT_PAGE_PATH,
251
371
  name: product.name || "",
252
372
  slug,
253
373
  productUrl,
254
- categoryPrefix: matchedPrefix?.name ?? "",
374
+ categoryPrefix: categoryName,
255
375
  mainMedia: {
256
376
  url: mainMedia ? formatWixMediaUrl(mainMedia._id, mainMedia.url) : "",
257
377
  altText: mainMedia?.altText || product.name || "",
@@ -263,7 +383,6 @@ function mapProductToCard(product, productPagePath = DEFAULT_PRODUCT_PAGE_PATH,
263
383
  width: 300,
264
384
  height: 300
265
385
  },
266
- // Simplified price fields
267
386
  price: actualFormattedAmount,
268
387
  strikethroughPrice: hasDiscount ? compareAtFormattedAmount : "",
269
388
  hasDiscount,
@@ -282,11 +401,13 @@ function mapProductToCard(product, productPagePath = DEFAULT_PRODUCT_PAGE_PATH,
282
401
  },
283
402
  productType: mapProductType$1(product.productType),
284
403
  isAddingToCart: false,
285
- // Quick add behavior
286
- quickAddType: getQuickAddType(product),
287
- quickOption: getQuickAddType(product) === QuickAddType.SINGLE_OPTION ? mapQuickOption(product.options?.[0], product.variantsInfo) : null
404
+ ...mapQuickAddOptions(product)
288
405
  };
289
406
  }
407
+ function needsCategoryInfo(wixStores) {
408
+ const template = wixStores.urls.product;
409
+ return template.includes("{category}") || template.includes("{prefix}");
410
+ }
290
411
  const PRICE_BUCKET_BOUNDARIES = [
291
412
  0,
292
413
  20,
@@ -397,18 +518,16 @@ const searchProducts = makeJayQuery("wixStores.searchProducts").withServices(WIX
397
518
  const searchResult = await wixStores.products.searchProducts(
398
519
  {
399
520
  filter,
400
- // @ts-expect-error - Wix SDK types don't match actual API
401
521
  sort: sort.length > 0 ? sort : void 0,
402
522
  cursorPaging,
403
523
  search,
404
- // @ts-expect-error - Wix SDK types don't include aggregations
405
524
  aggregations
406
525
  },
407
526
  {
408
527
  fields: [
409
528
  "CURRENCY",
410
529
  "VARIANT_OPTION_CHOICE_NAMES",
411
- ...wixStores.categoryPrefixes.length > 0 ? ["ALL_CATEGORIES_INFO"] : []
530
+ ...needsCategoryInfo(wixStores) ? ["ALL_CATEGORIES_INFO"] : []
412
531
  ]
413
532
  }
414
533
  );
@@ -448,9 +567,9 @@ const searchProducts = makeJayQuery("wixStores.searchProducts").withServices(WIX
448
567
  };
449
568
  });
450
569
  priceRanges.push(...bucketRanges);
451
- const prefixConfig = wixStores.categoryPrefixes;
570
+ const tree = await wixStores.getCategoryTree();
452
571
  const mappedProducts = products.map(
453
- (p) => mapProductToCard(p, "/products", prefixConfig)
572
+ (p) => mapProductToCard(p, wixStores.urls, tree)
454
573
  );
455
574
  return {
456
575
  products: mappedProducts,
@@ -476,11 +595,10 @@ const getProductBySlug = makeJayQuery("wixStores.getProductBySlug").withServices
476
595
  throw new ActionError("INVALID_INPUT", "Product slug is required");
477
596
  }
478
597
  try {
479
- const prefixConfig = wixStores.categoryPrefixes;
480
598
  const fields = [
481
599
  "MEDIA_ITEMS_INFO",
482
600
  "VARIANT_OPTION_CHOICE_NAMES",
483
- ...prefixConfig.length > 0 ? ["ALL_CATEGORIES_INFO"] : []
601
+ ...needsCategoryInfo(wixStores) ? ["ALL_CATEGORIES_INFO"] : []
484
602
  ];
485
603
  const result = await wixStores.products.getProductBySlug(slug, {
486
604
  fields: [...fields]
@@ -489,13 +607,27 @@ const getProductBySlug = makeJayQuery("wixStores.getProductBySlug").withServices
489
607
  if (!product) {
490
608
  return null;
491
609
  }
492
- return mapProductToCard(product, "/products", prefixConfig);
610
+ const tree = await wixStores.getCategoryTree();
611
+ return mapProductToCard(product, wixStores.urls, tree);
493
612
  } catch (error) {
494
613
  console.error("[wixStores.getProductBySlug] Failed to get product:", error);
495
614
  return null;
496
615
  }
497
616
  }
498
617
  );
618
+ const getVariantStock = makeJayQuery("wixStores.getVariantStock").withServices(WIX_STORES_SERVICE_MARKER).withHandler(
619
+ async (input, wixStores) => {
620
+ try {
621
+ const product = await wixStores.products.getProduct(input.productId, {
622
+ fields: ["VARIANT_OPTION_CHOICE_NAMES"]
623
+ });
624
+ return buildVariantStockMap(product);
625
+ } catch (error) {
626
+ console.error("[wixStores.getVariantStock] Failed:", error);
627
+ return {};
628
+ }
629
+ }
630
+ );
499
631
  const getCategories = makeJayQuery("wixStores.getCategories").withServices(WIX_STORES_SERVICE_MARKER).withCaching({ maxAge: 3600 }).withHandler(
500
632
  async (_input, wixStores) => {
501
633
  try {
@@ -515,11 +647,153 @@ const getCategories = makeJayQuery("wixStores.getCategories").withServices(WIX_S
515
647
  }
516
648
  );
517
649
  const PAGE_SIZE = 12;
650
+ function mapSortToAction(sort) {
651
+ switch (sort) {
652
+ case CurrentSort.priceAsc:
653
+ return "price_asc";
654
+ case CurrentSort.priceDesc:
655
+ return "price_desc";
656
+ case CurrentSort.newest:
657
+ return "newest";
658
+ case CurrentSort.nameAsc:
659
+ return "name_asc";
660
+ case CurrentSort.nameDesc:
661
+ return "name_desc";
662
+ default:
663
+ return "relevance";
664
+ }
665
+ }
666
+ function parseUrlFilters(url) {
667
+ try {
668
+ const params = new URL(url, "http://x").searchParams;
669
+ return {
670
+ searchTerm: params.get("q") || "",
671
+ selectedCategorySlugs: params.get("cat")?.split(",").filter(Boolean) || [],
672
+ minPrice: params.has("min") ? Number(params.get("min")) : null,
673
+ maxPrice: params.has("max") ? Number(params.get("max")) : null,
674
+ inStockOnly: params.get("inStock") === "1",
675
+ sort: params.get("sort") || "relevance"
676
+ };
677
+ } catch {
678
+ return {
679
+ searchTerm: "",
680
+ selectedCategorySlugs: [],
681
+ minPrice: null,
682
+ maxPrice: null,
683
+ inStockOnly: false,
684
+ sort: "relevance"
685
+ };
686
+ }
687
+ }
688
+ function parseSortParam(sort) {
689
+ const sortMap = {
690
+ relevance: CurrentSort.relevance,
691
+ priceAsc: CurrentSort.priceAsc,
692
+ priceDesc: CurrentSort.priceDesc,
693
+ newest: CurrentSort.newest,
694
+ nameAsc: CurrentSort.nameAsc,
695
+ nameDesc: CurrentSort.nameDesc
696
+ };
697
+ return sortMap[sort] ?? CurrentSort.relevance;
698
+ }
699
+ const EMPTY_CATEGORY_HEADER = {
700
+ name: "",
701
+ description: "",
702
+ imageUrl: "",
703
+ hasImage: false,
704
+ productCount: 0,
705
+ breadcrumbs: [],
706
+ seoData: { tags: [], settings: { preventAutoRedirect: false, keywords: [] } }
707
+ };
708
+ async function findCategoryBySlug(categoriesClient, slug) {
709
+ const result = await categoriesClient.queryCategories({ treeReference: { appNamespace: "@wix/stores" } }).eq("slug", slug).eq("visible", true).limit(1).find();
710
+ return result.items?.[0] ?? null;
711
+ }
712
+ async function loadCategoryDetails(categoriesClient, categoryId) {
713
+ try {
714
+ return await categoriesClient.getCategory(categoryId, { appNamespace: "@wix/stores" }, { fields: ["DESCRIPTION", "BREADCRUMBS_INFO"] });
715
+ } catch {
716
+ return null;
717
+ }
718
+ }
719
+ async function buildCategoryHeader(wixStoreService, category, categoryUrlTemplate) {
720
+ const details = await loadCategoryDetails(wixStoreService.categories, category._id);
721
+ const cat = details || category;
722
+ const imageUrl = cat.image || "";
723
+ const description = cat.description || "";
724
+ const categoryTree = await wixStoreService.getCategoryTree();
725
+ const breadcrumbs = (cat.breadcrumbsInfo?.breadcrumbs || []).map((b) => ({
726
+ categoryId: b.categoryId,
727
+ name: b.categoryName,
728
+ slug: b.categorySlug,
729
+ url: categoryUrlTemplate ? buildCategoryUrl(wixStoreService.urls, categoryTree, b.categorySlug, b.categoryId) : ""
730
+ }));
731
+ const seoData = cat.seoData ? {
732
+ tags: (cat.seoData.tags || []).map((tag, index) => ({
733
+ position: index.toString().padStart(2, "0"),
734
+ type: tag.type || "",
735
+ props: Object.entries(tag.props || {}).map(([key, value]) => ({
736
+ key,
737
+ value
738
+ })),
739
+ meta: Object.entries(tag.meta || {}).map(([key, value]) => ({
740
+ key,
741
+ value
742
+ })),
743
+ children: tag.children || ""
744
+ })),
745
+ settings: {
746
+ preventAutoRedirect: cat.seoData.settings?.preventAutoRedirect || false,
747
+ keywords: (cat.seoData.settings?.keywords || []).map((k) => ({
748
+ term: k.term || "",
749
+ isMain: k.isMain || false,
750
+ origin: k.origin || ""
751
+ }))
752
+ }
753
+ } : EMPTY_CATEGORY_HEADER.seoData;
754
+ let header = {
755
+ name: cat.name || "",
756
+ description,
757
+ imageUrl,
758
+ hasImage: !!imageUrl,
759
+ productCount: cat.itemCounter || 0,
760
+ breadcrumbs,
761
+ seoData
762
+ };
763
+ if ((!description || !imageUrl) && cat.parentCategory?._id) {
764
+ const parent = await loadCategoryDetails(wixStoreService.categories, cat.parentCategory._id);
765
+ if (parent) {
766
+ if (!header.description && parent.description) {
767
+ header = { ...header, description: parent.description };
768
+ }
769
+ if (!header.imageUrl) {
770
+ const parentImage = parent.image || "";
771
+ if (parentImage) {
772
+ header = { ...header, imageUrl: parentImage, hasImage: true };
773
+ }
774
+ }
775
+ }
776
+ }
777
+ return header;
778
+ }
518
779
  async function renderSlowlyChanging$2(props, wixStores) {
519
780
  const Pipeline = RenderPipeline.for();
520
- const categoryPrefix = props.category;
521
- const categoryConfig = categoryPrefix ? wixStores.categoryPrefixes.find((c) => c.prefix === categoryPrefix) : null;
522
- const baseCategoryId = categoryConfig?.categoryId ?? null;
781
+ const subcategorySlug = props.subcategory ?? null;
782
+ const categorySlug = props.category ?? null;
783
+ const defaultCategorySlug = wixStores.defaultCategory;
784
+ let activeCategory = null;
785
+ let baseCategoryId = null;
786
+ if (subcategorySlug) {
787
+ activeCategory = await findCategoryBySlug(wixStores.categories, subcategorySlug);
788
+ baseCategoryId = activeCategory?._id ?? null;
789
+ } else if (categorySlug) {
790
+ activeCategory = await findCategoryBySlug(wixStores.categories, categorySlug);
791
+ baseCategoryId = activeCategory?._id ?? null;
792
+ } else if (defaultCategorySlug) {
793
+ activeCategory = await findCategoryBySlug(wixStores.categories, defaultCategorySlug);
794
+ }
795
+ const tree = await wixStores.getCategoryTree();
796
+ const categoryHeader = activeCategory ? await buildCategoryHeader(wixStores, activeCategory, wixStores.urls.category) : EMPTY_CATEGORY_HEADER;
523
797
  return Pipeline.try(async () => {
524
798
  let query = wixStores.categories.queryCategories({
525
799
  treeReference: { appNamespace: "@wix/stores" }
@@ -536,7 +810,8 @@ async function renderSlowlyChanging$2(props, wixStores) {
536
810
  const categoryInfos = categories2.map((cat) => ({
537
811
  categoryId: cat._id || "",
538
812
  categoryName: cat.name || "",
539
- categorySlug: cat.slug || ""
813
+ categorySlug: cat.slug || "",
814
+ categoryUrl: buildCategoryUrl(wixStores.urls, tree, cat.slug || "", cat._id || "") ?? ""
540
815
  }));
541
816
  return {
542
817
  viewState: {
@@ -547,7 +822,8 @@ async function renderSlowlyChanging$2(props, wixStores) {
547
822
  categoryFilter: {
548
823
  categories: categoryInfos
549
824
  }
550
- }
825
+ },
826
+ categoryHeader
551
827
  },
552
828
  carryForward: {
553
829
  searchFields: "name,description,sku",
@@ -560,11 +836,20 @@ async function renderSlowlyChanging$2(props, wixStores) {
560
836
  }
561
837
  async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
562
838
  const Pipeline = RenderPipeline.for();
839
+ const urlFilters = parseUrlFilters(props.url);
840
+ const initialSort = parseSortParam(urlFilters.sort);
841
+ const initialCategoryIds = urlFilters.selectedCategorySlugs.map((slug) => slowCarryForward.categories.find((c) => c.categorySlug === slug)?.categoryId).filter(Boolean);
563
842
  return Pipeline.try(async () => {
564
- const baseCategoryIds = slowCarryForward.baseCategoryId ? [slowCarryForward.baseCategoryId] : [];
843
+ const baseCategoryIds = slowCarryForward.baseCategoryId ? [slowCarryForward.baseCategoryId, ...initialCategoryIds] : initialCategoryIds;
565
844
  const result = await searchProducts({
566
- query: "",
567
- filters: baseCategoryIds.length > 0 ? { categoryIds: baseCategoryIds } : void 0,
845
+ query: urlFilters.searchTerm || "",
846
+ filters: {
847
+ categoryIds: baseCategoryIds.length > 0 ? baseCategoryIds : void 0,
848
+ minPrice: urlFilters.minPrice ?? void 0,
849
+ maxPrice: urlFilters.maxPrice ?? void 0,
850
+ inStockOnly: urlFilters.inStockOnly || void 0
851
+ },
852
+ sortBy: initialSort !== CurrentSort.relevance ? mapSortToAction(initialSort) : void 0,
568
853
  pageSize: PAGE_SIZE
569
854
  });
570
855
  return result;
@@ -607,20 +892,19 @@ async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
607
892
  };
608
893
  return {
609
894
  viewState: {
610
- searchExpression: "",
895
+ searchExpression: urlFilters.searchTerm,
611
896
  isSearching: false,
612
- hasSearched: false,
897
+ hasSearched: !!urlFilters.searchTerm,
613
898
  searchResults: result.products,
614
899
  resultCount: result.products.length,
615
900
  hasResults: result.products.length > 0,
616
901
  hasSuggestions: false,
617
902
  suggestions: [],
618
903
  filters: {
619
- inStockOnly: false,
904
+ inStockOnly: urlFilters.inStockOnly,
620
905
  priceRange: {
621
- // Initialize sliders to full range (bounds)
622
- minPrice: priceAgg.minBound,
623
- maxPrice: priceAgg.maxBound,
906
+ minPrice: urlFilters.minPrice ?? priceAgg.minBound,
907
+ maxPrice: urlFilters.maxPrice ?? priceAgg.maxBound,
624
908
  minBound: priceAgg.minBound,
625
909
  maxBound: priceAgg.maxBound,
626
910
  ranges: priceAgg.ranges
@@ -628,12 +912,12 @@ async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
628
912
  categoryFilter: {
629
913
  categories: slowCarryForward.categories.map((cat) => ({
630
914
  categoryId: cat.categoryId,
631
- isSelected: false
915
+ isSelected: initialCategoryIds.includes(cat.categoryId)
632
916
  }))
633
917
  }
634
918
  },
635
919
  sortBy: {
636
- currentSort: CurrentSort.relevance
920
+ currentSort: initialSort
637
921
  },
638
922
  hasMore: result.hasMore,
639
923
  loadedCount: result.products.length,
@@ -648,7 +932,29 @@ async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
648
932
  };
649
933
  });
650
934
  }
651
- const productSearch = makeJayStackComponent().withProps().withServices(WIX_STORES_SERVICE_MARKER).withSlowlyRender(renderSlowlyChanging$2).withFastRender(renderFastChanging$1);
935
+ async function* loadSearchParams([wixStores]) {
936
+ try {
937
+ const result = await wixStores.categories.queryCategories({
938
+ treeReference: { appNamespace: "@wix/stores" }
939
+ }).eq("visible", true).limit(100).find();
940
+ const categories2 = result.items || [];
941
+ yield categories2.filter((cat) => cat.slug && (cat.itemCounter ?? 0) > 0).map((cat) => ({
942
+ category: cat.slug
943
+ }));
944
+ for (const cat of categories2) {
945
+ if (!cat.slug || !cat.parentCategory?._id || (cat.itemCounter ?? 0) === 0)
946
+ continue;
947
+ const parent = categories2.find((c) => c._id === cat.parentCategory?._id);
948
+ if (parent?.slug) {
949
+ yield [{ category: parent.slug, subcategory: cat.slug }];
950
+ }
951
+ }
952
+ } catch (error) {
953
+ console.error("Failed to load category params:", error);
954
+ yield [];
955
+ }
956
+ }
957
+ const productSearch = makeJayStackComponent().withProps().withServices(WIX_STORES_SERVICE_MARKER).withLoadParams(loadSearchParams).withSlowlyRender(renderSlowlyChanging$2).withFastRender(renderFastChanging$1);
652
958
  var ProductType = /* @__PURE__ */ ((ProductType2) => {
653
959
  ProductType2[ProductType2["PHYSICAL"] = 0] = "PHYSICAL";
654
960
  ProductType2[ProductType2["DIGITAL"] = 1] = "DIGITAL";
@@ -691,28 +997,21 @@ var MediaType = /* @__PURE__ */ ((MediaType2) => {
691
997
  return MediaType2;
692
998
  })(MediaType || {});
693
999
  async function* loadProductParams([wixStores]) {
694
- const prefixConfig = wixStores.categoryPrefixes;
695
- const hasPrefixes = prefixConfig.length > 0;
696
- const fields = hasPrefixes ? ["ALL_CATEGORIES_INFO"] : [];
1000
+ const template = wixStores.urls.product;
1001
+ const needsCategories = template.includes("{category}") || template.includes("{prefix}");
1002
+ const fields = needsCategories ? ["ALL_CATEGORIES_INFO"] : [];
697
1003
  try {
698
1004
  let result = await wixStores.products.queryProducts({ fields: [...fields] }).find();
699
- yield result.items.map((product) => mapProductToParams(product, prefixConfig));
1005
+ yield result.items.map((product) => ({ slug: product.slug ?? "" })).filter((p) => p.slug);
700
1006
  while (result.hasNext()) {
701
1007
  result = await result.next();
702
- yield result.items.map((product) => mapProductToParams(product, prefixConfig));
1008
+ yield result.items.map((product) => ({ slug: product.slug ?? "" })).filter((p) => p.slug);
703
1009
  }
704
1010
  } catch (error) {
705
1011
  console.error("Failed to load product slugs:", error);
706
1012
  yield [];
707
1013
  }
708
1014
  }
709
- function mapProductToParams(product, prefixConfig) {
710
- const prefix = resolveProductPrefix(product, prefixConfig);
711
- return {
712
- slug: product.slug ?? "",
713
- ...prefix ? { category: prefix } : {}
714
- };
715
- }
716
1015
  function mapProductType(productType) {
717
1016
  return productType === "DIGITAL" ? ProductType.DIGITAL : ProductType.PHYSICAL;
718
1017
  }
@@ -848,8 +1147,8 @@ function mapVariants(variantsInfo) {
848
1147
  }
849
1148
  async function renderSlowlyChanging$1(props, wixStores) {
850
1149
  const Pipeline = RenderPipeline.for();
851
- const prefixConfig = wixStores.categoryPrefixes;
852
- const hasPrefixes = prefixConfig.length > 0;
1150
+ const template = wixStores.urls.product;
1151
+ const needsCategories = template.includes("{category}") || template.includes("{prefix}");
853
1152
  return Pipeline.try(async () => {
854
1153
  const fields = [
855
1154
  "INFO_SECTION",
@@ -857,17 +1156,11 @@ async function renderSlowlyChanging$1(props, wixStores) {
857
1156
  "MEDIA_ITEMS_INFO",
858
1157
  "PLAIN_DESCRIPTION",
859
1158
  "CURRENCY",
860
- ...hasPrefixes ? ["ALL_CATEGORIES_INFO"] : []
1159
+ ...needsCategories ? ["ALL_CATEGORIES_INFO"] : []
861
1160
  ];
862
1161
  const response = await wixStores.products.getProductBySlug(props.slug, {
863
1162
  fields: [...fields]
864
1163
  });
865
- if (props.category && hasPrefixes) {
866
- const actualPrefix = resolveProductPrefix(response.product, prefixConfig);
867
- if (actualPrefix !== props.category) {
868
- throw new Error("Category prefix mismatch");
869
- }
870
- }
871
1164
  return response;
872
1165
  }).recover((error) => {
873
1166
  console.log("product page error", error);
@@ -960,37 +1253,38 @@ const categoryList = makeJayStackComponent().withProps().withServices(WIX_STORES
960
1253
  const WIX_STORES_CONTEXT = createJayContext();
961
1254
  function loadWixStoresConfig() {
962
1255
  const configPath = path.join(process.cwd(), "config", ".wix-stores.yaml");
1256
+ const defaults = {
1257
+ urls: { product: "/products/{slug}", category: null },
1258
+ defaultCategory: null
1259
+ };
963
1260
  if (!fs.existsSync(configPath)) {
964
- return { categoryPrefixes: [] };
1261
+ return defaults;
965
1262
  }
966
1263
  const fileContents = fs.readFileSync(configPath, "utf8");
967
1264
  const raw = yaml.load(fileContents);
968
1265
  if (!raw) {
969
- return { categoryPrefixes: [] };
1266
+ return defaults;
970
1267
  }
971
- const prefixes = [];
972
- if (Array.isArray(raw.categoryPrefixes)) {
973
- for (const entry of raw.categoryPrefixes) {
974
- if (typeof entry.categoryId === "string" && typeof entry.prefix === "string") {
975
- prefixes.push({
976
- categoryId: entry.categoryId.trim(),
977
- prefix: entry.prefix.trim(),
978
- name: typeof entry.name === "string" ? entry.name.trim() : entry.prefix.trim()
979
- });
980
- }
981
- }
982
- }
983
- return { categoryPrefixes: prefixes };
1268
+ const urls = raw.urls;
1269
+ return {
1270
+ urls: {
1271
+ product: typeof urls?.product === "string" ? urls.product : defaults.urls.product,
1272
+ category: typeof urls?.category === "string" ? urls.category : null
1273
+ },
1274
+ defaultCategory: typeof raw.defaultCategory === "string" ? raw.defaultCategory : null
1275
+ };
984
1276
  }
985
1277
  const init = makeJayInit().withServer(async () => {
986
1278
  console.log("[wix-stores] Initializing Wix Stores service...");
987
1279
  const wixClient = getService(WIX_CLIENT_SERVICE);
988
1280
  const storesConfig = loadWixStoresConfig();
989
1281
  provideWixStoresService(wixClient, {
990
- categoryPrefixes: storesConfig.categoryPrefixes
1282
+ urls: storesConfig.urls,
1283
+ defaultCategory: storesConfig.defaultCategory
991
1284
  });
992
- if (storesConfig.categoryPrefixes.length > 0) {
993
- console.log(`[wix-stores] Category prefixes configured: ${storesConfig.categoryPrefixes.map((p) => p.prefix).join(", ")}`);
1285
+ console.log(`[wix-stores] URL templates: product="${storesConfig.urls.product}", category="${storesConfig.urls.category ?? "none"}"`);
1286
+ if (storesConfig.defaultCategory) {
1287
+ console.log(`[wix-stores] Default category: ${storesConfig.defaultCategory}`);
994
1288
  }
995
1289
  console.log("[wix-stores] Server initialization complete");
996
1290
  return {
@@ -1001,20 +1295,22 @@ const init = makeJayInit().withServer(async () => {
1001
1295
  const CONFIG_FILE_NAME = ".wix-stores.yaml";
1002
1296
  const CONFIG_TEMPLATE = `# Wix Stores Configuration
1003
1297
  #
1004
- # Category Prefixes (optional):
1005
- # Maps root Wix categories to URL prefix slugs.
1006
- # Products under a root category get URLs like /products/{prefix}/{product-slug}
1007
- # Each prefix gets its own search/listing page and product page templates.
1298
+ # URL templates for link generation.
1299
+ # Placeholders: {slug} (product), {category} (sub-category), {prefix} (root category)
1300
+ #
1301
+ # urls:
1302
+ # product: "/products/{slug}" # simple (default)
1303
+ # product: "/products/{category}/{slug}" # with categories
1304
+ # product: "/products/{prefix}/{category}/{slug}" # with prefixes + categories
1305
+ # category: "/products/{prefix}/{category}" # category deep-link pages
1008
1306
  #
1009
- # To find category IDs, use: jay-stack action wix-stores/getCategories
1307
+ # Fallback category for pages without category context:
1308
+ # defaultCategory: "all-products"
1010
1309
  #
1011
- # categoryPrefixes:
1012
- # - categoryId: "<root-category-id>"
1013
- # prefix: "<url-prefix>"
1014
- # name: "<display-name>"
1015
- # - categoryId: "<another-root-category-id>"
1016
- # prefix: "<another-url-prefix>"
1017
- # name: "<another-display-name>"
1310
+ # To see available categories: jay-stack setup wix-stores (generates category tree reference)
1311
+
1312
+ urls:
1313
+ product: "/products/{slug}"
1018
1314
  `;
1019
1315
  async function setupWixStores(ctx) {
1020
1316
  if (ctx.initError) {
@@ -1041,8 +1337,7 @@ async function setupWixStores(ctx) {
1041
1337
  configCreated.push(`config/${CONFIG_FILE_NAME}`);
1042
1338
  }
1043
1339
  const service = getService(WIX_STORES_SERVICE_MARKER);
1044
- const prefixCount = service.categoryPrefixes.length;
1045
- const message = prefixCount > 0 ? `Wix Stores configured with ${prefixCount} category prefix(es): ${service.categoryPrefixes.map((p) => p.prefix).join(", ")}` : "Wix Stores service verified";
1340
+ const message = `Wix Stores configured (product URL: ${service.urls.product})`;
1046
1341
  return {
1047
1342
  status: "configured",
1048
1343
  message,
@@ -1091,22 +1386,13 @@ async function generateWixStoresReferences(ctx) {
1091
1386
  roots.push(node);
1092
1387
  }
1093
1388
  }
1094
- const prefixConfig = storesService.categoryPrefixes;
1095
- const configuredPrefixes = prefixConfig.map((p) => ({
1096
- categoryId: p.categoryId,
1097
- prefix: p.prefix,
1098
- name: p.name,
1099
- categoryName: nodeMap.get(p.categoryId)?.name ?? "unknown"
1100
- }));
1101
1389
  const categoriesPath = path.join(ctx.referencesDir, "categories.yaml");
1102
1390
  fs.writeFileSync(
1103
1391
  categoriesPath,
1104
1392
  yaml.dump(
1105
1393
  {
1106
- _generated: (/* @__PURE__ */ new Date()).toISOString(),
1107
- _description: "Wix Stores category tree for agent discovery. Shows category hierarchy, IDs, product counts, and configured URL prefixes.",
1394
+ _description: "Wix Stores category tree for agent discovery. Shows category hierarchy, IDs, slugs, product counts, and parent-child relationships.",
1108
1395
  totalCategories: allCategories.length,
1109
- configuredPrefixes: configuredPrefixes.length > 0 ? configuredPrefixes : void 0,
1110
1396
  categoryTree: roots
1111
1397
  },
1112
1398
  { indent: 2, lineWidth: 120, noRefs: true }
@@ -1123,12 +1409,18 @@ export {
1123
1409
  WIX_CART_SERVICE,
1124
1410
  WIX_STORES_CONTEXT,
1125
1411
  WIX_STORES_SERVICE_MARKER,
1412
+ buildCategoryUrl,
1413
+ buildProductUrl,
1126
1414
  cartIndicator,
1127
1415
  cartPage,
1128
1416
  categoryList,
1417
+ findCategoryImage,
1418
+ findRootCategoryId,
1419
+ findRootCategorySlug,
1129
1420
  generateWixStoresReferences,
1130
1421
  getCategories,
1131
1422
  getProductBySlug,
1423
+ getVariantStock,
1132
1424
  init,
1133
1425
  productPage,
1134
1426
  productSearch,