@jay-framework/wix-stores 0.16.5 → 0.17.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.
@@ -183,6 +183,11 @@ tags:
183
183
  description: Secondary option for quick selection (text choices for COLOR_AND_TEXT_OPTIONS). Rendered as buttons or dropdown. Selection adds to cart.
184
184
  link: ./product-options
185
185
 
186
+ - tag: cardContainer
187
+ type: interactive
188
+ elementType: HTMLElement
189
+ description: The product card root element. Must be placed on the outermost card wrapper so that mouseenter preloads variant stock for quick-add options.
190
+
186
191
  - tag: viewOptionsButton
187
192
  type: interactive
188
193
  elementType: HTMLButtonElement
@@ -116,6 +116,7 @@ export type ProductCardInteractiveViewState = Pick<ProductCardViewState, 'price'
116
116
  export interface ProductCardRefs {
117
117
  productLink: HTMLElementProxy<ProductCardViewState, HTMLAnchorElement>,
118
118
  addToCartButton: HTMLElementProxy<ProductCardViewState, HTMLButtonElement>,
119
+ cardContainer: HTMLElementProxy<ProductCardViewState, HTMLElement>,
119
120
  viewOptionsButton: HTMLElementProxy<ProductCardViewState, HTMLButtonElement>,
120
121
  quickOption: ProductOptionsRefs,
121
122
  secondQuickOption: ProductOptionsRefs
@@ -125,6 +126,7 @@ export interface ProductCardRefs {
125
126
  export interface ProductCardRepeatedRefs {
126
127
  productLink: HTMLElementCollectionProxy<ProductCardViewState, HTMLAnchorElement>,
127
128
  addToCartButton: HTMLElementCollectionProxy<ProductCardViewState, HTMLButtonElement>,
129
+ cardContainer: HTMLElementCollectionProxy<ProductCardViewState, HTMLElement>,
128
130
  viewOptionsButton: HTMLElementCollectionProxy<ProductCardViewState, HTMLButtonElement>,
129
131
  quickOption: ProductOptionsRepeatedRefs,
130
132
  secondQuickOption: ProductOptionsRepeatedRefs
@@ -16,6 +16,7 @@ tags:
16
16
  - {tag: description, type: data, dataType: string, description: Product description}
17
17
  - {tag: brand, type: data, dataType: string, description: Brand name}
18
18
  - {tag: ribbon, type: data, dataType: string, description: "Ribbon text (e.g., \"New\", \"Sale\")"}
19
+ - {tag: categorySlug, type: data, dataType: string, description: Slug of the product's main category}
19
20
  - {tag: productType, type: variant, dataType: "enum (PHYSICAL | DIGITAL)", description: Product type }
20
21
 
21
22
  - {tag: sku, type: data, dataType: string, phase: fast+interactive, description: Product SKU, or chosen variant SKU}
@@ -84,6 +84,7 @@ export interface ProductPageViewState {
84
84
  description: string,
85
85
  brand: string,
86
86
  ribbon: string,
87
+ categorySlug: string,
87
88
  productType: ProductType,
88
89
  sku: string,
89
90
  price: string,
@@ -98,7 +99,7 @@ export interface ProductPageViewState {
98
99
  modifiers: Array<ModifierOfProductPageViewState>
99
100
  }
100
101
 
101
- export type ProductPageSlowViewState = Pick<ProductPageViewState, '_id' | 'productName' | 'description' | 'brand' | 'ribbon' | 'productType'> & {
102
+ export type ProductPageSlowViewState = Pick<ProductPageViewState, '_id' | 'productName' | 'description' | 'brand' | 'ribbon' | 'categorySlug' | 'productType'> & {
102
103
  options: Array<Pick<ProductPageViewState['options'][number], '_id' | 'name' | 'optionRenderType'> & {
103
104
  choices: Array<Pick<ProductPageViewState['options'][number]['choices'][number], 'choiceId' | 'choiceType' | 'name' | 'colorCode' | 'inStock'>>;
104
105
  }>;
@@ -0,0 +1,31 @@
1
+ name: related-products
2
+ description: Related products grid showing products from the same category. Use on product pages alongside the product-page component.
3
+ props:
4
+ - name: productId
5
+ type: string
6
+ description: Product ID to exclude from results
7
+ - name: categorySlug
8
+ type: string
9
+ description: Category slug to filter related products by
10
+ - name: limit
11
+ type: number
12
+ description: Maximum number of related products to show (default 4)
13
+ tags:
14
+ - tag: products
15
+ type: sub-contract
16
+ repeated: true
17
+ trackBy: _id
18
+ phase: fast+interactive
19
+ description: Related product cards
20
+ link: ./product-card
21
+
22
+ - tag: hasProducts
23
+ type: variant
24
+ dataType: boolean
25
+ phase: fast+interactive
26
+ description: Whether there are related products to show
27
+
28
+ - tag: categoryName
29
+ type: data
30
+ dataType: string
31
+ description: Name of the category these products belong to
@@ -0,0 +1,36 @@
1
+ import {JayContract} from "@jay-framework/runtime";
2
+ import {ProductCardViewState, ProductCardRefs, ProductCardRepeatedRefs} from "./product-card.jay-contract";
3
+
4
+ export interface RelatedProductsViewState {
5
+ products: Array<ProductCardViewState>,
6
+ hasProducts: boolean,
7
+ categoryName: string
8
+ }
9
+
10
+ export type RelatedProductsSlowViewState = Pick<RelatedProductsViewState, 'categoryName'>;
11
+
12
+ export type RelatedProductsFastViewState = Pick<RelatedProductsViewState, 'hasProducts'> & {
13
+ products: Array<RelatedProductsViewState['products'][number]>;
14
+ };
15
+
16
+ export type RelatedProductsInteractiveViewState = Pick<RelatedProductsViewState, 'hasProducts'> & {
17
+ products: Array<RelatedProductsViewState['products'][number]>;
18
+ };
19
+
20
+
21
+ export interface RelatedProductsRefs {
22
+ products: ProductCardRepeatedRefs
23
+ }
24
+
25
+
26
+ export interface RelatedProductsRepeatedRefs {
27
+ products: ProductCardRepeatedRefs
28
+ }
29
+
30
+ export interface RelatedProductsProps {
31
+ productId?: string;
32
+ categorySlug?: string;
33
+ limit?: number;
34
+ }
35
+
36
+ export type RelatedProductsContract = JayContract<RelatedProductsViewState, RelatedProductsRefs, RelatedProductsSlowViewState, RelatedProductsFastViewState, RelatedProductsInteractiveViewState, RelatedProductsProps>
@@ -339,6 +339,186 @@ var QuickAddType = /* @__PURE__ */ ((QuickAddType2) => {
339
339
  QuickAddType2[QuickAddType2["NEEDS_CONFIGURATION"] = 3] = "NEEDS_CONFIGURATION";
340
340
  return QuickAddType2;
341
341
  })(QuickAddType || {});
342
+ function setupCardInteractions(refs, cards, storesContext) {
343
+ const variantStockCache = {};
344
+ let variantStockApplied = /* @__PURE__ */ new Set();
345
+ const variantStockLoading = /* @__PURE__ */ new Set();
346
+ const loadVariantStock = async (productId) => {
347
+ if (variantStockApplied.has(productId) || variantStockLoading.has(productId)) return;
348
+ variantStockLoading.add(productId);
349
+ try {
350
+ const currentResults = cards.get();
351
+ const productIndex = currentResults.findIndex((p) => p._id === productId);
352
+ if (productIndex === -1) return;
353
+ const product = currentResults[productIndex];
354
+ if (product?.quickAddType !== QuickAddType.COLOR_AND_TEXT_OPTIONS) return;
355
+ const stockMap = variantStockCache[productId] ?? await getVariantStock({ productId });
356
+ variantStockCache[productId] = stockMap;
357
+ variantStockApplied.add(productId);
358
+ const selectedColor = product.quickOption?.choices?.find((c) => c.isSelected);
359
+ const textChoices = product.secondQuickOption?.choices;
360
+ if (!selectedColor || !textChoices) return;
361
+ const colorStock = stockMap[selectedColor.choiceId];
362
+ const updatedTextChoices = textChoices.map((c) => ({
363
+ ...c,
364
+ inStock: colorStock?.[c.choiceId] ?? false
365
+ }));
366
+ cards.set(
367
+ patch(cards.get(), [
368
+ {
369
+ op: REPLACE,
370
+ path: [productIndex, "secondQuickOption", "choices"],
371
+ value: updatedTextChoices
372
+ }
373
+ ])
374
+ );
375
+ } finally {
376
+ variantStockLoading.delete(productId);
377
+ }
378
+ };
379
+ refs.addToCartButton.onclick(async ({ coordinate }) => {
380
+ const [productId] = coordinate;
381
+ const currentResults = cards.get();
382
+ const productIndex = currentResults.findIndex((p) => p._id === productId);
383
+ if (productIndex === -1) return;
384
+ cards.set(
385
+ patch(currentResults, [
386
+ { op: REPLACE, path: [productIndex, "isAddingToCart"], value: true }
387
+ ])
388
+ );
389
+ try {
390
+ await storesContext.addToCart(productId, 1);
391
+ } catch (error) {
392
+ console.error("Failed to add to cart:", error);
393
+ } finally {
394
+ cards.set(
395
+ patch(cards.get(), [
396
+ { op: REPLACE, path: [productIndex, "isAddingToCart"], value: false }
397
+ ])
398
+ );
399
+ }
400
+ });
401
+ refs.cardContainer.onmouseenter(({ coordinate }) => {
402
+ const [productId] = coordinate;
403
+ loadVariantStock(productId);
404
+ });
405
+ refs.quickOption.choices.choiceButton.onclick(async ({ coordinate }) => {
406
+ const [productId, choiceId] = coordinate;
407
+ const currentResults = cards.get();
408
+ const productIndex = currentResults.findIndex((p) => p._id === productId);
409
+ if (productIndex === -1) return;
410
+ const product = currentResults[productIndex];
411
+ if (product.quickAddType === QuickAddType.COLOR_AND_TEXT_OPTIONS) {
412
+ const choices = product.quickOption?.choices;
413
+ if (!choices) return;
414
+ const updatedChoices = choices.map((c) => ({
415
+ ...c,
416
+ isSelected: c.choiceId === choiceId
417
+ }));
418
+ let updated = patch(currentResults, [
419
+ {
420
+ op: REPLACE,
421
+ path: [productIndex, "quickOption", "choices"],
422
+ value: updatedChoices
423
+ }
424
+ ]);
425
+ const stockMap = variantStockCache[productId];
426
+ if (stockMap) {
427
+ const colorStock = stockMap[choiceId];
428
+ const textChoices = product.secondQuickOption?.choices;
429
+ if (textChoices) {
430
+ const updatedTextChoices = textChoices.map((c) => ({
431
+ ...c,
432
+ inStock: colorStock?.[c.choiceId] ?? false
433
+ }));
434
+ updated = patch(updated, [
435
+ {
436
+ op: REPLACE,
437
+ path: [productIndex, "secondQuickOption", "choices"],
438
+ value: updatedTextChoices
439
+ }
440
+ ]);
441
+ }
442
+ } else {
443
+ loadVariantStock(productId);
444
+ }
445
+ cards.set(updated);
446
+ return;
447
+ }
448
+ const choice = product.quickOption?.choices?.find((c) => c.choiceId === choiceId);
449
+ if (!choice || !choice.inStock) return;
450
+ cards.set(
451
+ patch(currentResults, [
452
+ { op: REPLACE, path: [productIndex, "isAddingToCart"], value: true }
453
+ ])
454
+ );
455
+ try {
456
+ const optionId = product.quickOption._id;
457
+ await storesContext.addToCart(productId, 1, {
458
+ options: { [optionId]: choice.choiceId },
459
+ modifiers: {},
460
+ customTextFields: {}
461
+ });
462
+ } catch (error) {
463
+ console.error("Failed to add to cart:", error);
464
+ } finally {
465
+ cards.set(
466
+ patch(cards.get(), [
467
+ { op: REPLACE, path: [productIndex, "isAddingToCart"], value: false }
468
+ ])
469
+ );
470
+ }
471
+ });
472
+ refs.secondQuickOption.choices.choiceButton.onclick(async ({ coordinate }) => {
473
+ const [productId, choiceId] = coordinate;
474
+ const currentResults = cards.get();
475
+ const productIndex = currentResults.findIndex((p) => p._id === productId);
476
+ if (productIndex === -1) return;
477
+ const product = currentResults[productIndex];
478
+ const textChoice = product.secondQuickOption?.choices?.find(
479
+ (c) => c.choiceId === choiceId
480
+ );
481
+ const selectedColor = product.quickOption?.choices?.find((c) => c.isSelected);
482
+ if (!textChoice || !textChoice.inStock) return;
483
+ cards.set(
484
+ patch(currentResults, [
485
+ { op: REPLACE, path: [productIndex, "isAddingToCart"], value: true }
486
+ ])
487
+ );
488
+ try {
489
+ const colorOptionId = product.quickOption?._id || "";
490
+ const textOptionId = product.secondQuickOption?._id || "";
491
+ await storesContext.addToCart(productId, 1, {
492
+ options: {
493
+ [colorOptionId]: selectedColor?.choiceId || "",
494
+ [textOptionId]: textChoice.choiceId
495
+ },
496
+ modifiers: {},
497
+ customTextFields: {}
498
+ });
499
+ } catch (error) {
500
+ console.error("Failed to add to cart:", error);
501
+ } finally {
502
+ cards.set(
503
+ patch(cards.get(), [
504
+ { op: REPLACE, path: [productIndex, "isAddingToCart"], value: false }
505
+ ])
506
+ );
507
+ }
508
+ });
509
+ refs.viewOptionsButton.onclick(({ coordinate }) => {
510
+ const [productId] = coordinate;
511
+ const product = cards.get().find((p) => p._id === productId);
512
+ if (product?.productUrl) {
513
+ window.location.href = product.productUrl;
514
+ }
515
+ });
516
+ return {
517
+ resetCache() {
518
+ variantStockApplied = /* @__PURE__ */ new Set();
519
+ }
520
+ };
521
+ }
342
522
  const PAGE_SIZE = 12;
343
523
  function mapSortToAction(sort) {
344
524
  switch (sort) {
@@ -425,7 +605,6 @@ function buildOptionFiltersViewState(baseOptionFilters, filteredResult, optionSe
425
605
  }
426
606
  function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForward, storesContext) {
427
607
  const baseCategoryId = fastCarryForward.baseCategoryId;
428
- const variantStockCache = {};
429
608
  const { searchExpression: [searchExpression, setSearchExpression], isSearching: [isSearching, setIsSearching], hasSearched: [hasSearched, setHasSearched], searchResults: [searchResults, setSearchResults], resultCount: [resultCount, setResultCount], hasResults: [hasResults, setHasResults], hasSuggestions: [hasSuggestions, setHasSuggestions], suggestions: [suggestions, setSuggestions], filters: [filters, setFilters], sortBy: [sortBy, setSortBy], hasMore: [hasMore, setHasMore], loadedCount: [loadedCount, setLoadedCount], totalCount: [totalCount, setTotalCount] } = viewStateSignals;
430
609
  const [submittedSearchTerm, setSubmittedSearchTerm] = createSignal(null);
431
610
  let currentCursor = null;
@@ -476,6 +655,7 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
476
655
  if (version !== searchVersion) {
477
656
  return;
478
657
  }
658
+ resetCardCache();
479
659
  setSearchResults(result.products);
480
660
  setResultCount(result.products.length);
481
661
  setTotalCount(result.totalCount);
@@ -688,182 +868,7 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
688
868
  setSubmittedSearchTerm(suggestion.suggestionText);
689
869
  }
690
870
  });
691
- refs.searchResults.addToCartButton.onclick(async ({ coordinate }) => {
692
- const [productId] = coordinate;
693
- const currentResults = searchResults();
694
- const productIndex = currentResults.findIndex((p) => p._id === productId);
695
- if (productIndex === -1)
696
- return;
697
- setSearchResults(patch(currentResults, [
698
- { op: REPLACE, path: [productIndex, "isAddingToCart"], value: true }
699
- ]));
700
- try {
701
- await storesContext.addToCart(productId, 1);
702
- } catch (error) {
703
- console.error("Failed to add to cart:", error);
704
- } finally {
705
- setSearchResults(patch(searchResults(), [
706
- { op: REPLACE, path: [productIndex, "isAddingToCart"], value: false }
707
- ]));
708
- }
709
- });
710
- const variantStockLoading = /* @__PURE__ */ new Set();
711
- const loadVariantStock = async (productId) => {
712
- if (variantStockCache[productId] || variantStockLoading.has(productId))
713
- return;
714
- variantStockLoading.add(productId);
715
- try {
716
- const currentResults = searchResults();
717
- const productIndex = currentResults.findIndex((p) => p._id === productId);
718
- if (productIndex === -1)
719
- return;
720
- const product = currentResults[productIndex];
721
- if (product?.quickAddType !== QuickAddType.COLOR_AND_TEXT_OPTIONS)
722
- return;
723
- const stockMap = await getVariantStock({ productId });
724
- variantStockCache[productId] = stockMap;
725
- const selectedColor = product.quickOption?.choices?.find((c) => c.isSelected);
726
- const textChoices = product.secondQuickOption?.choices;
727
- if (!selectedColor || !textChoices)
728
- return;
729
- const colorStock = stockMap[selectedColor.choiceId];
730
- const updatedTextChoices = textChoices.map((c) => ({
731
- ...c,
732
- inStock: colorStock?.[c.choiceId] ?? false
733
- }));
734
- setSearchResults(patch(searchResults(), [
735
- {
736
- op: REPLACE,
737
- path: [productIndex, "secondQuickOption", "choices"],
738
- value: updatedTextChoices
739
- }
740
- ]));
741
- } finally {
742
- variantStockLoading.delete(productId);
743
- }
744
- };
745
- refs.searchResults.productLink.onmouseenter(({ coordinate }) => {
746
- const [productId] = coordinate;
747
- loadVariantStock(productId);
748
- });
749
- refs.searchResults.quickOption.choices.choiceButton.onmouseenter(({ coordinate }) => {
750
- const [productId] = coordinate;
751
- loadVariantStock(productId);
752
- });
753
- refs.searchResults.quickOption.choices.choiceButton.onclick(async ({ coordinate }) => {
754
- const [productId, choiceId] = coordinate;
755
- const currentResults = searchResults();
756
- const productIndex = currentResults.findIndex((p) => p._id === productId);
757
- if (productIndex === -1)
758
- return;
759
- const product = currentResults[productIndex];
760
- if (product.quickAddType === QuickAddType.COLOR_AND_TEXT_OPTIONS) {
761
- const choices = product.quickOption?.choices;
762
- if (!choices)
763
- return;
764
- const updatedChoices = choices.map((c) => ({
765
- ...c,
766
- isSelected: c.choiceId === choiceId
767
- }));
768
- let updated = patch(currentResults, [
769
- {
770
- op: REPLACE,
771
- path: [productIndex, "quickOption", "choices"],
772
- value: updatedChoices
773
- }
774
- ]);
775
- const stockMap = variantStockCache[productId];
776
- if (stockMap) {
777
- const colorStock = stockMap[choiceId];
778
- const textChoices = product.secondQuickOption?.choices;
779
- if (textChoices) {
780
- const updatedTextChoices = textChoices.map((c) => ({
781
- ...c,
782
- inStock: colorStock?.[c.choiceId] ?? false
783
- }));
784
- updated = patch(updated, [
785
- {
786
- op: REPLACE,
787
- path: [productIndex, "secondQuickOption", "choices"],
788
- value: updatedTextChoices
789
- }
790
- ]);
791
- }
792
- } else {
793
- loadVariantStock(productId);
794
- }
795
- setSearchResults(updated);
796
- return;
797
- }
798
- const choice = product.quickOption?.choices?.find((c) => c.choiceId === choiceId);
799
- if (!choice || !choice.inStock) {
800
- console.warn("Choice not available or out of stock");
801
- return;
802
- }
803
- setSearchResults(patch(currentResults, [
804
- { op: REPLACE, path: [productIndex, "isAddingToCart"], value: true }
805
- ]));
806
- try {
807
- const optionId = product.quickOption._id;
808
- await storesContext.addToCart(productId, 1, {
809
- options: { [optionId]: choice.choiceId },
810
- modifiers: {},
811
- customTextFields: {}
812
- });
813
- } catch (error) {
814
- console.error("Failed to add to cart:", error);
815
- } finally {
816
- setSearchResults(patch(searchResults(), [
817
- { op: REPLACE, path: [productIndex, "isAddingToCart"], value: false }
818
- ]));
819
- }
820
- });
821
- refs.searchResults.secondQuickOption.choices.choiceButton.onclick(async ({ coordinate }) => {
822
- const [productId, choiceId] = coordinate;
823
- const currentResults = searchResults();
824
- const productIndex = currentResults.findIndex((p) => p._id === productId);
825
- if (productIndex === -1)
826
- return;
827
- const product = currentResults[productIndex];
828
- const textChoice = product.secondQuickOption?.choices?.find((c) => c.choiceId === choiceId);
829
- const selectedColor = product.quickOption?.choices?.find((c) => c.isSelected);
830
- if (!textChoice || !textChoice.inStock) {
831
- console.warn("Text choice not available or out of stock");
832
- return;
833
- }
834
- setSearchResults(patch(currentResults, [
835
- { op: REPLACE, path: [productIndex, "isAddingToCart"], value: true }
836
- ]));
837
- try {
838
- const colorOptionId = product.quickOption?._id || "";
839
- const textOptionId = product.secondQuickOption?._id || "";
840
- await storesContext.addToCart(productId, 1, {
841
- options: {
842
- [colorOptionId]: selectedColor?.choiceId || "",
843
- [textOptionId]: textChoice.choiceId
844
- },
845
- modifiers: {},
846
- customTextFields: {}
847
- });
848
- } catch (error) {
849
- console.error("Failed to add to cart:", error);
850
- } finally {
851
- setSearchResults(patch(searchResults(), [
852
- { op: REPLACE, path: [productIndex, "isAddingToCart"], value: false }
853
- ]));
854
- }
855
- });
856
- refs.searchResults.secondQuickOption.choices.choiceButton.onmouseenter(({ coordinate }) => {
857
- const [productId] = coordinate;
858
- loadVariantStock(productId);
859
- });
860
- refs.searchResults.viewOptionsButton.onclick(({ coordinate }) => {
861
- const [productId] = coordinate;
862
- const product = searchResults().find((p) => p._id === productId);
863
- if (product?.productUrl) {
864
- window.location.href = product.productUrl;
865
- }
866
- });
871
+ const { resetCache: resetCardCache } = setupCardInteractions(refs.searchResults, { get: searchResults, set: setSearchResults }, storesContext);
867
872
  return {
868
873
  render: () => ({
869
874
  searchExpression: searchExpression(),
@@ -883,6 +888,17 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
883
888
  };
884
889
  }
885
890
  const productSearch = makeJayStackComponent().withProps().withContexts(WIX_STORES_CONTEXT).withInteractive(ProductSearchInteractive);
891
+ function RelatedProductsInteractive(_props, refs, viewStateSignals, _fastCarryForward, storesContext) {
892
+ const { products: [products, setProducts], hasProducts: [hasProducts] } = viewStateSignals;
893
+ setupCardInteractions(refs.products, { get: products, set: setProducts }, storesContext);
894
+ return {
895
+ render: () => ({
896
+ products: products(),
897
+ hasProducts: hasProducts()
898
+ })
899
+ };
900
+ }
901
+ const relatedProducts = makeJayStackComponent().withProps().withContexts(WIX_STORES_CONTEXT).withInteractive(RelatedProductsInteractive);
886
902
  const categoryList = makeJayStackComponent().withProps();
887
903
  const init = makeJayInit().withClient(async (data) => {
888
904
  console.log("[wix-stores] Initializing client-side stores context...");
@@ -898,5 +914,6 @@ export {
898
914
  init,
899
915
  productPage,
900
916
  productSearch,
901
- provideWixStoresContext
917
+ provideWixStoresContext,
918
+ relatedProducts
902
919
  };
package/dist/index.d.ts CHANGED
@@ -130,6 +130,7 @@ interface ProductCardViewState {
130
130
  interface ProductCardRepeatedRefs {
131
131
  productLink: HTMLElementCollectionProxy<ProductCardViewState, HTMLAnchorElement>,
132
132
  addToCartButton: HTMLElementCollectionProxy<ProductCardViewState, HTMLButtonElement>,
133
+ cardContainer: HTMLElementCollectionProxy<ProductCardViewState, HTMLElement>,
133
134
  viewOptionsButton: HTMLElementCollectionProxy<ProductCardViewState, HTMLButtonElement>,
134
135
  quickOption: ProductOptionsRepeatedRefs,
135
136
  secondQuickOption: ProductOptionsRepeatedRefs
@@ -744,6 +745,7 @@ interface ProductPageViewState {
744
745
  description: string,
745
746
  brand: string,
746
747
  ribbon: string,
748
+ categorySlug: string,
747
749
  productType: ProductType,
748
750
  sku: string,
749
751
  price: string,
@@ -758,7 +760,7 @@ interface ProductPageViewState {
758
760
  modifiers: Array<ModifierOfProductPageViewState>
759
761
  }
760
762
 
761
- type ProductPageSlowViewState = Pick<ProductPageViewState, '_id' | 'productName' | 'description' | 'brand' | 'ribbon' | 'productType'> & {
763
+ type ProductPageSlowViewState = Pick<ProductPageViewState, '_id' | 'productName' | 'description' | 'brand' | 'ribbon' | 'categorySlug' | 'productType'> & {
762
764
  options: Array<Pick<ProductPageViewState['options'][number], '_id' | 'name' | 'optionRenderType'> & {
763
765
  choices: Array<Pick<ProductPageViewState['options'][number]['choices'][number], 'choiceId' | 'choiceType' | 'name' | 'colorCode' | 'inStock'>>;
764
766
  }>;
@@ -864,6 +866,39 @@ interface ProductFastCarryForward {
864
866
  */
865
867
  declare const productPage: _jay_framework_fullstack_component.JayStackComponentDefinition<ProductPageRefs, ProductPageSlowViewState, ProductPageFastViewState, ProductPageInteractiveViewState, [ProductSlowCarryForward, WixStoresService], [Signals<ProductPageFastViewState>, ProductFastCarryForward, WixStoresContext], PageProps & ProductPageParams, ProductPageParams, _jay_framework_component.JayComponentCore<PageProps & ProductPageParams, ProductPageInteractiveViewState>>;
866
868
 
869
+ interface RelatedProductsViewState {
870
+ products: Array<ProductCardViewState>,
871
+ hasProducts: boolean,
872
+ categoryName: string
873
+ }
874
+
875
+ type RelatedProductsSlowViewState = Pick<RelatedProductsViewState, 'categoryName'>;
876
+
877
+ type RelatedProductsFastViewState = Pick<RelatedProductsViewState, 'hasProducts'> & {
878
+ products: Array<RelatedProductsViewState['products'][number]>;
879
+ };
880
+
881
+ type RelatedProductsInteractiveViewState = Pick<RelatedProductsViewState, 'hasProducts'> & {
882
+ products: Array<RelatedProductsViewState['products'][number]>;
883
+ };
884
+
885
+
886
+ interface RelatedProductsRefs {
887
+ products: ProductCardRepeatedRefs
888
+ }
889
+
890
+ interface RelatedProductsProps {
891
+ productId: string;
892
+ categorySlug?: string;
893
+ limit?: number;
894
+ }
895
+ interface RelatedSlowCarryForward {
896
+ categoryId: string;
897
+ productId: string;
898
+ limit: number;
899
+ }
900
+ declare const relatedProducts: _jay_framework_fullstack_component.JayStackComponentDefinition<RelatedProductsRefs, RelatedProductsSlowViewState, RelatedProductsFastViewState, RelatedProductsInteractiveViewState, [RelatedSlowCarryForward, WixStoresService], [Signals<RelatedProductsFastViewState>, Record<string, never>, WixStoresContext], PageProps & RelatedProductsProps, {}, _jay_framework_component.JayComponentCore<PageProps & RelatedProductsProps, RelatedProductsInteractiveViewState>>;
901
+
867
902
  interface CategoryOfCategoryListViewState {
868
903
  _id: string,
869
904
  name: string,
@@ -1033,4 +1068,4 @@ declare function generateWixStoresReferences(ctx: PluginReferencesContext): Prom
1033
1068
  */
1034
1069
  declare const generator: _jay_framework_fullstack_component.DynamicContractGenerator<[WixStoresService]>;
1035
1070
 
1036
- export { type CategoryListParams, type CategoryTree, type GetProductBySlugInput, type ProductPageParams, type ProductSearchParams, WIX_STORES_CONTEXT, WIX_STORES_SERVICE_MARKER, type WixStoresContext, type WixStoresInitData, type WixStoresService, type WixStoresServiceOptions, buildCategoryUrl, buildProductUrl, categoryList, findCategoryImage, findRootCategoryId, findRootCategorySlug, generateWixStoresReferences, getCategories, getProductBySlug, getVariantStock, init, productPage, generator as productPageContractGenerator, productSearch, provideWixStoresService, searchProducts, setupWixStores };
1071
+ export { type CategoryListParams, type CategoryTree, type GetProductBySlugInput, type ProductPageParams, type ProductSearchParams, type RelatedProductsProps, WIX_STORES_CONTEXT, WIX_STORES_SERVICE_MARKER, type WixStoresContext, type WixStoresInitData, type WixStoresService, type WixStoresServiceOptions, buildCategoryUrl, buildProductUrl, categoryList, findCategoryImage, findRootCategoryId, findRootCategorySlug, generateWixStoresReferences, getCategories, getProductBySlug, getVariantStock, init, productPage, generator as productPageContractGenerator, productSearch, provideWixStoresService, relatedProducts, searchProducts, setupWixStores };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { WIX_CART_CONTEXT, WIX_CART_SERVICE, cartIndicator, cartPage, provideWixCartContext, provideWixCartService } from "@jay-framework/wix-cart";
2
- import { createJayService, makeJayQuery, ActionError, makeJayStackComponent, RenderPipeline, makeJayInit, makeContractGenerator } from "@jay-framework/fullstack-component";
2
+ import { createJayService, makeJayQuery, ActionError, RenderPipeline, makeJayStackComponent, makeJayInit, makeContractGenerator } from "@jay-framework/fullstack-component";
3
3
  import { customizationsV3, inventoryItemsV3, productsV3 } from "@wix/stores";
4
4
  import { categories } from "@wix/categories";
5
5
  import { registerService, getService } from "@jay-framework/stack-server-runtime";
@@ -763,6 +763,19 @@ const getCategories = makeJayQuery("wixStores.getCategories").withServices(WIX_S
763
763
  }
764
764
  }
765
765
  );
766
+ function handleError(error) {
767
+ const errorCode = error["details"]?.applicationError?.code;
768
+ if (!!errorCode) {
769
+ error["details"]?.requestId;
770
+ if (errorCode === "NOT_FOUND") {
771
+ return RenderPipeline.for().clientError(404, "not found");
772
+ }
773
+ return RenderPipeline.for().serverError(
774
+ 500,
775
+ "Wix SDK Error: " + error.message
776
+ );
777
+ } else return RenderPipeline.for().serverError(500, "unknown server error");
778
+ }
766
779
  const PAGE_SIZE = 12;
767
780
  function mapSortToAction(sort) {
768
781
  switch (sort) {
@@ -941,7 +954,7 @@ async function buildCategoryHeader(wixStoreService, category, categoryUrlTemplat
941
954
  }
942
955
  return header;
943
956
  }
944
- async function renderSlowlyChanging$2(props, wixStores) {
957
+ async function renderSlowlyChanging$3(props, wixStores) {
945
958
  const Pipeline = RenderPipeline.for();
946
959
  const categorySlug = props.category ?? null;
947
960
  const prefixSlug = props.prefix ?? null;
@@ -982,11 +995,7 @@ async function renderSlowlyChanging$2(props, wixStores) {
982
995
  productsResult
983
996
  };
984
997
  }).recover((error) => {
985
- console.error("Failed to load categories/products:", error);
986
- return Pipeline.ok({
987
- categories: [],
988
- productsResult: null
989
- });
998
+ return handleError(error);
990
999
  }).toPhaseOutput(({ categories: categories2, productsResult }) => {
991
1000
  const categoryInfos = categories2.map((cat) => ({
992
1001
  categoryId: cat._id || "",
@@ -1018,7 +1027,7 @@ async function renderSlowlyChanging$2(props, wixStores) {
1018
1027
  };
1019
1028
  });
1020
1029
  }
1021
- async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
1030
+ async function renderFastChanging$2(props, slowCarryForward, _wixStores) {
1022
1031
  const Pipeline = RenderPipeline.for();
1023
1032
  const urlFilters = parseUrlFilters(props.url);
1024
1033
  const initialSort = parseSortParam(urlFilters.sort);
@@ -1134,15 +1143,22 @@ async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
1134
1143
  }
1135
1144
  async function* loadSearchParams([wixStores]) {
1136
1145
  try {
1137
- let findRootParent = function(cat) {
1138
- let current = cat;
1139
- while (current.parentCategory?._id) {
1140
- const parent = categoryById.get(current.parentCategory._id);
1141
- if (!parent)
1142
- break;
1143
- current = parent;
1146
+ let sumItems = function(node) {
1147
+ for (const child of node.children) {
1148
+ node.itemCount += sumItems(child);
1149
+ }
1150
+ return node.itemCount;
1151
+ }, collectParams = function(node, rootSlug) {
1152
+ if (!node.slug || node.itemCount === 0)
1153
+ return;
1154
+ if (rootSlug) {
1155
+ params.push({ prefix: rootSlug, category: node.slug });
1156
+ } else {
1157
+ params.push({ prefix: node.slug });
1158
+ }
1159
+ for (const child of node.children) {
1160
+ collectParams(child, rootSlug ?? node.slug);
1144
1161
  }
1145
- return current._id !== cat._id ? current : null;
1146
1162
  };
1147
1163
  const allCategories = [];
1148
1164
  let result = await wixStores.categories.queryCategories({
@@ -1153,29 +1169,52 @@ async function* loadSearchParams([wixStores]) {
1153
1169
  result = await result.next();
1154
1170
  allCategories.push(...result.items || []);
1155
1171
  }
1156
- const categoryById = /* @__PURE__ */ new Map();
1172
+ const nodeById = /* @__PURE__ */ new Map();
1173
+ const roots = [];
1174
+ const childPendingParent = /* @__PURE__ */ new Map();
1157
1175
  for (const cat of allCategories) {
1158
- if (cat._id)
1159
- categoryById.set(cat._id, cat);
1160
- }
1161
- const params = [];
1162
- for (const cat of allCategories) {
1163
- if (!cat.slug || (cat.itemCounter ?? 0) === 0)
1176
+ if (!cat._id)
1164
1177
  continue;
1165
- const rootParent = findRootParent(cat);
1166
- if (rootParent?.slug) {
1167
- params.push({ prefix: rootParent.slug, category: cat.slug });
1178
+ const node = {
1179
+ slug: cat.slug || "",
1180
+ itemCount: cat.itemCounter ?? 0,
1181
+ children: []
1182
+ };
1183
+ nodeById.set(cat._id, node);
1184
+ const pending = childPendingParent.get(cat._id);
1185
+ if (pending) {
1186
+ node.children.push(...pending);
1187
+ childPendingParent.delete(cat._id);
1188
+ }
1189
+ if (cat.parentCategory?._id) {
1190
+ const parent = nodeById.get(cat.parentCategory._id);
1191
+ if (parent) {
1192
+ parent.children.push(node);
1193
+ } else {
1194
+ const siblings = childPendingParent.get(cat.parentCategory._id);
1195
+ if (siblings)
1196
+ siblings.push(node);
1197
+ else
1198
+ childPendingParent.set(cat.parentCategory._id, [node]);
1199
+ }
1168
1200
  } else {
1169
- params.push({ prefix: cat.slug });
1201
+ roots.push(node);
1170
1202
  }
1171
1203
  }
1204
+ for (const root of roots) {
1205
+ sumItems(root);
1206
+ }
1207
+ const params = [];
1208
+ for (const root of roots) {
1209
+ collectParams(root, null);
1210
+ }
1172
1211
  yield params;
1173
1212
  } catch (error) {
1174
1213
  console.error("Failed to load category params:", error);
1175
1214
  yield [];
1176
1215
  }
1177
1216
  }
1178
- const productSearch = makeJayStackComponent().withProps().withServices(WIX_STORES_SERVICE_MARKER).withLoadParams(loadSearchParams).withSlowlyRender(renderSlowlyChanging$2).withFastRender(renderFastChanging$1);
1217
+ const productSearch = makeJayStackComponent().withProps().withServices(WIX_STORES_SERVICE_MARKER).withLoadParams(loadSearchParams).withSlowlyRender(renderSlowlyChanging$3).withFastRender(renderFastChanging$2);
1179
1218
  var ProductType = /* @__PURE__ */ ((ProductType2) => {
1180
1219
  ProductType2[ProductType2["PHYSICAL"] = 0] = "PHYSICAL";
1181
1220
  ProductType2[ProductType2["DIGITAL"] = 1] = "DIGITAL";
@@ -1404,7 +1443,7 @@ function mapVariants(variantsInfo) {
1404
1443
  strikethroughPrice: variant.price.compareAtPrice?.formattedAmount || ""
1405
1444
  })) || [];
1406
1445
  }
1407
- async function renderSlowlyChanging$1(props, wixStores) {
1446
+ async function renderSlowlyChanging$2(props, wixStores) {
1408
1447
  const Pipeline = RenderPipeline.for();
1409
1448
  const template = wixStores.urls.product;
1410
1449
  const needsCategories = template.includes("{category}") || template.includes("{prefix}");
@@ -1417,21 +1456,25 @@ async function renderSlowlyChanging$1(props, wixStores) {
1417
1456
  "CURRENCY",
1418
1457
  ...needsCategories ? ["ALL_CATEGORIES_INFO"] : []
1419
1458
  ];
1420
- const response = await wixStores.products.getProductBySlug(props.slug, {
1421
- fields: [...fields]
1422
- });
1423
- return response;
1459
+ const [response, tree] = await Promise.all([
1460
+ wixStores.products.getProductBySlug(props.slug, {
1461
+ fields: [...fields]
1462
+ }),
1463
+ wixStores.getCategoryTree()
1464
+ ]);
1465
+ return { response, tree };
1424
1466
  }).recover((error) => {
1425
- console.log("product page error", error);
1426
- return Pipeline.clientError(404, "not found");
1427
- }).toPhaseOutput((getProductResponse) => {
1467
+ return handleError(error);
1468
+ }).toPhaseOutput(({ response: getProductResponse, tree }) => {
1428
1469
  const product = getProductResponse.product;
1429
1470
  const { _id, name, plainDescription, options, modifiers, actualPriceRange, compareAtPriceRange, media, productType, brand, ribbon, infoSections, seoData, physicalProperties, inventory, variantsInfo } = product;
1471
+ const categorySlug = tree.slugMap.get(product.mainCategoryId) || "";
1430
1472
  return {
1431
1473
  viewState: {
1432
1474
  _id,
1433
1475
  productName: name || "",
1434
1476
  description: plainDescription,
1477
+ categorySlug,
1435
1478
  brand: brand?.name || "",
1436
1479
  ribbon: ribbon?.name || "",
1437
1480
  productType: mapProductType(productType),
@@ -1456,7 +1499,7 @@ async function renderSlowlyChanging$1(props, wixStores) {
1456
1499
  };
1457
1500
  });
1458
1501
  }
1459
- async function renderFastChanging(props, slowCarryForward, wixStores) {
1502
+ async function renderFastChanging$1(props, slowCarryForward, wixStores) {
1460
1503
  const Pipeline = RenderPipeline.for();
1461
1504
  const { variants } = slowCarryForward;
1462
1505
  const defaultVariant = variants.find((v) => v.inventoryStatus === StockStatus.IN_STOCK) || variants[0];
@@ -1506,7 +1549,78 @@ async function renderFastChanging(props, slowCarryForward, wixStores) {
1506
1549
  }
1507
1550
  }));
1508
1551
  }
1509
- const productPage = makeJayStackComponent().withProps().withServices(WIX_STORES_SERVICE_MARKER).withLoadParams(loadProductParams).withSlowlyRender(renderSlowlyChanging$1).withFastRender(renderFastChanging);
1552
+ const productPage = makeJayStackComponent().withProps().withServices(WIX_STORES_SERVICE_MARKER).withLoadParams(loadProductParams).withSlowlyRender(renderSlowlyChanging$2).withFastRender(renderFastChanging$1);
1553
+ const DEFAULT_LIMIT = 4;
1554
+ async function renderSlowlyChanging$1(props, wixStores) {
1555
+ const Pipeline = RenderPipeline.for();
1556
+ const limit = props.limit ?? DEFAULT_LIMIT;
1557
+ return Pipeline.try(async () => {
1558
+ const categorySlug = props.categorySlug;
1559
+ if (!categorySlug) {
1560
+ return { categoryId: "", categoryName: "" };
1561
+ }
1562
+ const tree = await wixStores.getCategoryTree();
1563
+ let categoryId = "";
1564
+ let categoryName = "";
1565
+ for (const [id, slug] of tree.slugMap) {
1566
+ if (slug === categorySlug) {
1567
+ categoryId = id;
1568
+ categoryName = slug;
1569
+ break;
1570
+ }
1571
+ }
1572
+ if (categoryId) {
1573
+ try {
1574
+ const result = await wixStores.categories.queryCategories({ treeReference: { appNamespace: "@wix/stores" } }).eq("_id", categoryId).limit(1).find();
1575
+ if (result.items?.[0]?.name) {
1576
+ categoryName = result.items[0].name;
1577
+ }
1578
+ } catch {
1579
+ }
1580
+ }
1581
+ return { categoryId, categoryName };
1582
+ }).recover((error) => {
1583
+ return handleError(error);
1584
+ }).toPhaseOutput(({ categoryId, categoryName }) => ({
1585
+ viewState: {
1586
+ categoryName
1587
+ },
1588
+ carryForward: {
1589
+ categoryId,
1590
+ productId: props.productId,
1591
+ limit
1592
+ }
1593
+ }));
1594
+ }
1595
+ async function renderFastChanging(_props, slowCarryForward, _wixStores) {
1596
+ const Pipeline = RenderPipeline.for();
1597
+ const { categoryId, productId, limit } = slowCarryForward;
1598
+ if (!categoryId) {
1599
+ return Pipeline.ok({
1600
+ products: [],
1601
+ hasProducts: false
1602
+ }).toPhaseOutput((viewState) => ({ viewState, carryForward: {} }));
1603
+ }
1604
+ return Pipeline.try(async () => {
1605
+ const result = await searchProducts({
1606
+ query: "",
1607
+ filters: { categoryIds: [categoryId] },
1608
+ pageSize: limit + 1
1609
+ });
1610
+ const products = result.products.filter((p) => p._id !== productId).slice(0, limit);
1611
+ return {
1612
+ products,
1613
+ hasProducts: products.length > 0
1614
+ };
1615
+ }).recover((error) => {
1616
+ console.error("Failed to load related products:", error);
1617
+ return Pipeline.ok({
1618
+ products: [],
1619
+ hasProducts: false
1620
+ });
1621
+ }).toPhaseOutput((viewState) => ({ viewState, carryForward: {} }));
1622
+ }
1623
+ const relatedProducts = makeJayStackComponent().withProps().withServices(WIX_STORES_SERVICE_MARKER).withSlowlyRender(renderSlowlyChanging$1).withFastRender(renderFastChanging);
1510
1624
  async function findCategoryBySlug(categoriesClient, slug) {
1511
1625
  const result = await categoriesClient.queryCategories({ treeReference: { appNamespace: "@wix/stores" } }).eq("slug", slug).eq("visible", true).limit(1).find();
1512
1626
  return result.items?.[0] ?? null;
@@ -1859,6 +1973,7 @@ tags:
1859
1973
  - {tag: description, type: data, dataType: string, description: Product description}
1860
1974
  - {tag: brand, type: data, dataType: string, description: Brand name}
1861
1975
  - {tag: ribbon, type: data, dataType: string, description: "Ribbon text (e.g., \\"New\\", \\"Sale\\")"}
1976
+ - {tag: categorySlug, type: data, dataType: string, description: Slug of the product's main category}
1862
1977
  - {tag: productType, type: variant, dataType: "enum (PHYSICAL | DIGITAL)", description: Product type }
1863
1978
 
1864
1979
  - {tag: sku, type: data, dataType: string, phase: fast+interactive, description: Product SKU, or chosen variant SKU}
@@ -1991,6 +2106,7 @@ export {
1991
2106
  provideWixCartContext,
1992
2107
  provideWixCartService,
1993
2108
  provideWixStoresService,
2109
+ relatedProducts,
1994
2110
  searchProducts,
1995
2111
  setupWixStores
1996
2112
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/wix-stores",
3
- "version": "0.16.5",
3
+ "version": "0.17.1",
4
4
  "type": "module",
5
5
  "description": "Wix Stores API client for Jay Framework",
6
6
  "license": "Apache-2.0",
@@ -17,6 +17,7 @@
17
17
  "./media.jay-contract": "./dist/contracts/media.jay-contract",
18
18
  "./media-gallery.jay-contract": "./dist/contracts/media-gallery.jay-contract",
19
19
  "./product-search.jay-contract": "./dist/contracts/product-search.jay-contract",
20
+ "./related-products.jay-contract": "./dist/contracts/related-products.jay-contract",
20
21
  "./category-list.jay-contract": "./dist/contracts/category-list.jay-contract",
21
22
  "./search-products.jay-action": "./dist/actions/search-products.jay-action",
22
23
  "./get-product-by-slug.jay-action": "./dist/actions/get-product-by-slug.jay-action",
@@ -37,16 +38,16 @@
37
38
  "test": ":"
38
39
  },
39
40
  "dependencies": {
40
- "@jay-framework/component": "^0.16.5",
41
- "@jay-framework/fullstack-component": "^0.16.5",
42
- "@jay-framework/reactive": "^0.16.5",
43
- "@jay-framework/runtime": "^0.16.5",
44
- "@jay-framework/secure": "^0.16.5",
45
- "@jay-framework/stack-client-runtime": "^0.16.5",
46
- "@jay-framework/stack-server-runtime": "^0.16.5",
47
- "@jay-framework/wix-cart": "^0.16.5",
48
- "@jay-framework/wix-server-client": "^0.16.5",
49
- "@jay-framework/wix-utils": "^0.16.5",
41
+ "@jay-framework/component": "^0.17.1",
42
+ "@jay-framework/fullstack-component": "^0.17.1",
43
+ "@jay-framework/reactive": "^0.17.1",
44
+ "@jay-framework/runtime": "^0.17.1",
45
+ "@jay-framework/secure": "^0.17.1",
46
+ "@jay-framework/stack-client-runtime": "^0.17.1",
47
+ "@jay-framework/stack-server-runtime": "^0.17.1",
48
+ "@jay-framework/wix-cart": "^0.17.1",
49
+ "@jay-framework/wix-server-client": "^0.17.1",
50
+ "@jay-framework/wix-utils": "^0.17.1",
50
51
  "@wix/categories": "^1.0.185",
51
52
  "@wix/data-extension-schema": "^1.0.221",
52
53
  "@wix/sdk": "^1.21.5",
@@ -58,10 +59,10 @@
58
59
  "@babel/core": "^7.23.7",
59
60
  "@babel/preset-env": "^7.23.8",
60
61
  "@babel/preset-typescript": "^7.23.3",
61
- "@jay-framework/compiler-jay-stack": "^0.16.5",
62
- "@jay-framework/jay-cli": "^0.16.5",
63
- "@jay-framework/jay-stack-cli": "^0.16.5",
64
- "@jay-framework/vite-plugin": "^0.16.5",
62
+ "@jay-framework/compiler-jay-stack": "^0.17.1",
63
+ "@jay-framework/jay-cli": "^0.17.1",
64
+ "@jay-framework/jay-stack-cli": "^0.17.1",
65
+ "@jay-framework/vite-plugin": "^0.17.1",
65
66
  "nodemon": "^3.0.3",
66
67
  "rimraf": "^5.0.5",
67
68
  "tslib": "^2.6.2",
package/plugin.yaml CHANGED
@@ -7,6 +7,10 @@ contracts:
7
7
  component: productSearch
8
8
  description: Headless product search page
9
9
  # category-page removed — use product-search with prefix/category params instead
10
+ - name: related-products
11
+ contract: related-products.jay-contract
12
+ component: relatedProducts
13
+ description: Related products from the same category
10
14
  - name: category-list
11
15
  contract: category-list.jay-contract
12
16
  component: categoryList