@jay-framework/wix-stores 0.17.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,184 @@ 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((c) => c.choiceId === choiceId);
479
+ const selectedColor = product.quickOption?.choices?.find((c) => c.isSelected);
480
+ if (!textChoice || !textChoice.inStock) return;
481
+ cards.set(
482
+ patch(currentResults, [
483
+ { op: REPLACE, path: [productIndex, "isAddingToCart"], value: true }
484
+ ])
485
+ );
486
+ try {
487
+ const colorOptionId = product.quickOption?._id || "";
488
+ const textOptionId = product.secondQuickOption?._id || "";
489
+ await storesContext.addToCart(productId, 1, {
490
+ options: {
491
+ [colorOptionId]: selectedColor?.choiceId || "",
492
+ [textOptionId]: textChoice.choiceId
493
+ },
494
+ modifiers: {},
495
+ customTextFields: {}
496
+ });
497
+ } catch (error) {
498
+ console.error("Failed to add to cart:", error);
499
+ } finally {
500
+ cards.set(
501
+ patch(cards.get(), [
502
+ { op: REPLACE, path: [productIndex, "isAddingToCart"], value: false }
503
+ ])
504
+ );
505
+ }
506
+ });
507
+ refs.viewOptionsButton.onclick(({ coordinate }) => {
508
+ const [productId] = coordinate;
509
+ const product = cards.get().find((p) => p._id === productId);
510
+ if (product?.productUrl) {
511
+ window.location.href = product.productUrl;
512
+ }
513
+ });
514
+ return {
515
+ resetCache() {
516
+ variantStockApplied = /* @__PURE__ */ new Set();
517
+ }
518
+ };
519
+ }
342
520
  const PAGE_SIZE = 12;
343
521
  function mapSortToAction(sort) {
344
522
  switch (sort) {
@@ -425,8 +603,6 @@ function buildOptionFiltersViewState(baseOptionFilters, filteredResult, optionSe
425
603
  }
426
604
  function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForward, storesContext) {
427
605
  const baseCategoryId = fastCarryForward.baseCategoryId;
428
- const variantStockCache = {};
429
- let variantStockApplied = /* @__PURE__ */ new Set();
430
606
  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;
431
607
  const [submittedSearchTerm, setSubmittedSearchTerm] = createSignal(null);
432
608
  let currentCursor = null;
@@ -477,7 +653,7 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
477
653
  if (version !== searchVersion) {
478
654
  return;
479
655
  }
480
- variantStockApplied = /* @__PURE__ */ new Set();
656
+ resetCardCache();
481
657
  setSearchResults(result.products);
482
658
  setResultCount(result.products.length);
483
659
  setTotalCount(result.totalCount);
@@ -690,175 +866,7 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
690
866
  setSubmittedSearchTerm(suggestion.suggestionText);
691
867
  }
692
868
  });
693
- refs.searchResults.addToCartButton.onclick(async ({ coordinate }) => {
694
- const [productId] = coordinate;
695
- const currentResults = searchResults();
696
- const productIndex = currentResults.findIndex((p) => p._id === productId);
697
- if (productIndex === -1)
698
- return;
699
- setSearchResults(patch(currentResults, [
700
- { op: REPLACE, path: [productIndex, "isAddingToCart"], value: true }
701
- ]));
702
- try {
703
- await storesContext.addToCart(productId, 1);
704
- } catch (error) {
705
- console.error("Failed to add to cart:", error);
706
- } finally {
707
- setSearchResults(patch(searchResults(), [
708
- { op: REPLACE, path: [productIndex, "isAddingToCart"], value: false }
709
- ]));
710
- }
711
- });
712
- const variantStockLoading = /* @__PURE__ */ new Set();
713
- const loadVariantStock = async (productId) => {
714
- if (variantStockApplied.has(productId) || variantStockLoading.has(productId))
715
- return;
716
- variantStockLoading.add(productId);
717
- try {
718
- const currentResults = searchResults();
719
- const productIndex = currentResults.findIndex((p) => p._id === productId);
720
- if (productIndex === -1)
721
- return;
722
- const product = currentResults[productIndex];
723
- if (product?.quickAddType !== QuickAddType.COLOR_AND_TEXT_OPTIONS)
724
- return;
725
- const stockMap = variantStockCache[productId] ?? await getVariantStock({ productId });
726
- variantStockCache[productId] = stockMap;
727
- variantStockApplied.add(productId);
728
- const selectedColor = product.quickOption?.choices?.find((c) => c.isSelected);
729
- const textChoices = product.secondQuickOption?.choices;
730
- if (!selectedColor || !textChoices)
731
- return;
732
- const colorStock = stockMap[selectedColor.choiceId];
733
- const updatedTextChoices = textChoices.map((c) => ({
734
- ...c,
735
- inStock: colorStock?.[c.choiceId] ?? false
736
- }));
737
- setSearchResults(patch(searchResults(), [
738
- {
739
- op: REPLACE,
740
- path: [productIndex, "secondQuickOption", "choices"],
741
- value: updatedTextChoices
742
- }
743
- ]));
744
- } finally {
745
- variantStockLoading.delete(productId);
746
- }
747
- };
748
- refs.searchResults.cardContainer.onmouseenter(({ coordinate }) => {
749
- const [productId] = coordinate;
750
- loadVariantStock(productId);
751
- });
752
- refs.searchResults.quickOption.choices.choiceButton.onclick(async ({ coordinate }) => {
753
- const [productId, choiceId] = coordinate;
754
- const currentResults = searchResults();
755
- const productIndex = currentResults.findIndex((p) => p._id === productId);
756
- if (productIndex === -1)
757
- return;
758
- const product = currentResults[productIndex];
759
- if (product.quickAddType === QuickAddType.COLOR_AND_TEXT_OPTIONS) {
760
- const choices = product.quickOption?.choices;
761
- if (!choices)
762
- return;
763
- const updatedChoices = choices.map((c) => ({
764
- ...c,
765
- isSelected: c.choiceId === choiceId
766
- }));
767
- let updated = patch(currentResults, [
768
- {
769
- op: REPLACE,
770
- path: [productIndex, "quickOption", "choices"],
771
- value: updatedChoices
772
- }
773
- ]);
774
- const stockMap = variantStockCache[productId];
775
- if (stockMap) {
776
- const colorStock = stockMap[choiceId];
777
- const textChoices = product.secondQuickOption?.choices;
778
- if (textChoices) {
779
- const updatedTextChoices = textChoices.map((c) => ({
780
- ...c,
781
- inStock: colorStock?.[c.choiceId] ?? false
782
- }));
783
- updated = patch(updated, [
784
- {
785
- op: REPLACE,
786
- path: [productIndex, "secondQuickOption", "choices"],
787
- value: updatedTextChoices
788
- }
789
- ]);
790
- }
791
- } else {
792
- loadVariantStock(productId);
793
- }
794
- setSearchResults(updated);
795
- return;
796
- }
797
- const choice = product.quickOption?.choices?.find((c) => c.choiceId === choiceId);
798
- if (!choice || !choice.inStock) {
799
- console.warn("Choice not available or out of stock");
800
- return;
801
- }
802
- setSearchResults(patch(currentResults, [
803
- { op: REPLACE, path: [productIndex, "isAddingToCart"], value: true }
804
- ]));
805
- try {
806
- const optionId = product.quickOption._id;
807
- await storesContext.addToCart(productId, 1, {
808
- options: { [optionId]: choice.choiceId },
809
- modifiers: {},
810
- customTextFields: {}
811
- });
812
- } catch (error) {
813
- console.error("Failed to add to cart:", error);
814
- } finally {
815
- setSearchResults(patch(searchResults(), [
816
- { op: REPLACE, path: [productIndex, "isAddingToCart"], value: false }
817
- ]));
818
- }
819
- });
820
- refs.searchResults.secondQuickOption.choices.choiceButton.onclick(async ({ coordinate }) => {
821
- const [productId, choiceId] = coordinate;
822
- const currentResults = searchResults();
823
- const productIndex = currentResults.findIndex((p) => p._id === productId);
824
- if (productIndex === -1)
825
- return;
826
- const product = currentResults[productIndex];
827
- const textChoice = product.secondQuickOption?.choices?.find((c) => c.choiceId === choiceId);
828
- const selectedColor = product.quickOption?.choices?.find((c) => c.isSelected);
829
- if (!textChoice || !textChoice.inStock) {
830
- console.warn("Text choice not available or out of stock");
831
- return;
832
- }
833
- setSearchResults(patch(currentResults, [
834
- { op: REPLACE, path: [productIndex, "isAddingToCart"], value: true }
835
- ]));
836
- try {
837
- const colorOptionId = product.quickOption?._id || "";
838
- const textOptionId = product.secondQuickOption?._id || "";
839
- await storesContext.addToCart(productId, 1, {
840
- options: {
841
- [colorOptionId]: selectedColor?.choiceId || "",
842
- [textOptionId]: textChoice.choiceId
843
- },
844
- modifiers: {},
845
- customTextFields: {}
846
- });
847
- } catch (error) {
848
- console.error("Failed to add to cart:", error);
849
- } finally {
850
- setSearchResults(patch(searchResults(), [
851
- { op: REPLACE, path: [productIndex, "isAddingToCart"], value: false }
852
- ]));
853
- }
854
- });
855
- refs.searchResults.viewOptionsButton.onclick(({ coordinate }) => {
856
- const [productId] = coordinate;
857
- const product = searchResults().find((p) => p._id === productId);
858
- if (product?.productUrl) {
859
- window.location.href = product.productUrl;
860
- }
861
- });
869
+ const { resetCache: resetCardCache } = setupCardInteractions(refs.searchResults, { get: searchResults, set: setSearchResults }, storesContext);
862
870
  return {
863
871
  render: () => ({
864
872
  searchExpression: searchExpression(),
@@ -878,6 +886,17 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
878
886
  };
879
887
  }
880
888
  const productSearch = makeJayStackComponent().withProps().withContexts(WIX_STORES_CONTEXT).withInteractive(ProductSearchInteractive);
889
+ function RelatedProductsInteractive(_props, refs, viewStateSignals, _fastCarryForward, storesContext) {
890
+ const { products: [products, setProducts], hasProducts: [hasProducts] } = viewStateSignals;
891
+ setupCardInteractions(refs.products, { get: products, set: setProducts }, storesContext);
892
+ return {
893
+ render: () => ({
894
+ products: products(),
895
+ hasProducts: hasProducts()
896
+ })
897
+ };
898
+ }
899
+ const relatedProducts = makeJayStackComponent().withProps().withContexts(WIX_STORES_CONTEXT).withInteractive(RelatedProductsInteractive);
881
900
  const categoryList = makeJayStackComponent().withProps();
882
901
  const init = makeJayInit().withClient(async (data) => {
883
902
  console.log("[wix-stores] Initializing client-side stores context...");
@@ -893,5 +912,6 @@ export {
893
912
  init,
894
913
  productPage,
895
914
  productSearch,
896
- provideWixStoresContext
915
+ provideWixStoresContext,
916
+ relatedProducts
897
917
  };
package/dist/index.d.ts CHANGED
@@ -745,6 +745,7 @@ interface ProductPageViewState {
745
745
  description: string,
746
746
  brand: string,
747
747
  ribbon: string,
748
+ categorySlug: string,
748
749
  productType: ProductType,
749
750
  sku: string,
750
751
  price: string,
@@ -759,7 +760,7 @@ interface ProductPageViewState {
759
760
  modifiers: Array<ModifierOfProductPageViewState>
760
761
  }
761
762
 
762
- type ProductPageSlowViewState = Pick<ProductPageViewState, '_id' | 'productName' | 'description' | 'brand' | 'ribbon' | 'productType'> & {
763
+ type ProductPageSlowViewState = Pick<ProductPageViewState, '_id' | 'productName' | 'description' | 'brand' | 'ribbon' | 'categorySlug' | 'productType'> & {
763
764
  options: Array<Pick<ProductPageViewState['options'][number], '_id' | 'name' | 'optionRenderType'> & {
764
765
  choices: Array<Pick<ProductPageViewState['options'][number]['choices'][number], 'choiceId' | 'choiceType' | 'name' | 'colorCode' | 'inStock'>>;
765
766
  }>;
@@ -865,6 +866,37 @@ interface ProductFastCarryForward {
865
866
  */
866
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>>;
867
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
+ products: ProductCardViewState[];
897
+ }
898
+ 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>>;
899
+
868
900
  interface CategoryOfCategoryListViewState {
869
901
  _id: string,
870
902
  name: string,
@@ -1034,4 +1066,4 @@ declare function generateWixStoresReferences(ctx: PluginReferencesContext): Prom
1034
1066
  */
1035
1067
  declare const generator: _jay_framework_fullstack_component.DynamicContractGenerator<[WixStoresService]>;
1036
1068
 
1037
- 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 };
1069
+ 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
@@ -954,7 +954,7 @@ async function buildCategoryHeader(wixStoreService, category, categoryUrlTemplat
954
954
  }
955
955
  return header;
956
956
  }
957
- async function renderSlowlyChanging$2(props, wixStores) {
957
+ async function renderSlowlyChanging$3(props, wixStores) {
958
958
  const Pipeline = RenderPipeline.for();
959
959
  const categorySlug = props.category ?? null;
960
960
  const prefixSlug = props.prefix ?? null;
@@ -1027,7 +1027,7 @@ async function renderSlowlyChanging$2(props, wixStores) {
1027
1027
  };
1028
1028
  });
1029
1029
  }
1030
- async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
1030
+ async function renderFastChanging$2(props, slowCarryForward, _wixStores) {
1031
1031
  const Pipeline = RenderPipeline.for();
1032
1032
  const urlFilters = parseUrlFilters(props.url);
1033
1033
  const initialSort = parseSortParam(urlFilters.sort);
@@ -1214,7 +1214,7 @@ async function* loadSearchParams([wixStores]) {
1214
1214
  yield [];
1215
1215
  }
1216
1216
  }
1217
- 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);
1218
1218
  var ProductType = /* @__PURE__ */ ((ProductType2) => {
1219
1219
  ProductType2[ProductType2["PHYSICAL"] = 0] = "PHYSICAL";
1220
1220
  ProductType2[ProductType2["DIGITAL"] = 1] = "DIGITAL";
@@ -1443,7 +1443,7 @@ function mapVariants(variantsInfo) {
1443
1443
  strikethroughPrice: variant.price.compareAtPrice?.formattedAmount || ""
1444
1444
  })) || [];
1445
1445
  }
1446
- async function renderSlowlyChanging$1(props, wixStores) {
1446
+ async function renderSlowlyChanging$2(props, wixStores) {
1447
1447
  const Pipeline = RenderPipeline.for();
1448
1448
  const template = wixStores.urls.product;
1449
1449
  const needsCategories = template.includes("{category}") || template.includes("{prefix}");
@@ -1456,20 +1456,25 @@ async function renderSlowlyChanging$1(props, wixStores) {
1456
1456
  "CURRENCY",
1457
1457
  ...needsCategories ? ["ALL_CATEGORIES_INFO"] : []
1458
1458
  ];
1459
- const response = await wixStores.products.getProductBySlug(props.slug, {
1460
- fields: [...fields]
1461
- });
1462
- 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 };
1463
1466
  }).recover((error) => {
1464
1467
  return handleError(error);
1465
- }).toPhaseOutput((getProductResponse) => {
1468
+ }).toPhaseOutput(({ response: getProductResponse, tree }) => {
1466
1469
  const product = getProductResponse.product;
1467
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) || "";
1468
1472
  return {
1469
1473
  viewState: {
1470
1474
  _id,
1471
1475
  productName: name || "",
1472
1476
  description: plainDescription,
1477
+ categorySlug,
1473
1478
  brand: brand?.name || "",
1474
1479
  ribbon: ribbon?.name || "",
1475
1480
  productType: mapProductType(productType),
@@ -1494,7 +1499,7 @@ async function renderSlowlyChanging$1(props, wixStores) {
1494
1499
  };
1495
1500
  });
1496
1501
  }
1497
- async function renderFastChanging(props, slowCarryForward, wixStores) {
1502
+ async function renderFastChanging$1(props, slowCarryForward, wixStores) {
1498
1503
  const Pipeline = RenderPipeline.for();
1499
1504
  const { variants } = slowCarryForward;
1500
1505
  const defaultVariant = variants.find((v) => v.inventoryStatus === StockStatus.IN_STOCK) || variants[0];
@@ -1544,7 +1549,61 @@ async function renderFastChanging(props, slowCarryForward, wixStores) {
1544
1549
  }
1545
1550
  }));
1546
1551
  }
1547
- 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
+ const categorySlug = props.categorySlug;
1558
+ if (!categorySlug) {
1559
+ return Pipeline.ok({ categoryName: "" }).toPhaseOutput((viewState) => ({
1560
+ viewState,
1561
+ carryForward: { products: [] }
1562
+ }));
1563
+ }
1564
+ return Pipeline.try(async () => {
1565
+ const tree = await wixStores.getCategoryTree();
1566
+ let categoryId = "";
1567
+ let categoryName = "";
1568
+ for (const [id, slug] of tree.slugMap) {
1569
+ if (slug === categorySlug) {
1570
+ categoryId = id;
1571
+ categoryName = slug;
1572
+ break;
1573
+ }
1574
+ }
1575
+ if (!categoryId) {
1576
+ return { categoryName: "", products: [] };
1577
+ }
1578
+ const [categoryResult, searchResult] = await Promise.all([
1579
+ wixStores.categories.queryCategories({ treeReference: { appNamespace: "@wix/stores" } }).eq("_id", categoryId).limit(1).find().catch(() => null),
1580
+ searchProducts({
1581
+ query: "",
1582
+ filters: { categoryIds: [categoryId] },
1583
+ pageSize: limit + 1
1584
+ })
1585
+ ]);
1586
+ if (categoryResult?.items?.[0]?.name) {
1587
+ categoryName = categoryResult.items[0].name;
1588
+ }
1589
+ const products = searchResult.products.filter((p) => p._id !== props.productId).slice(0, limit);
1590
+ return { categoryName, products };
1591
+ }).recover((error) => {
1592
+ return handleError(error);
1593
+ }).toPhaseOutput(({ categoryName, products }) => ({
1594
+ viewState: { categoryName },
1595
+ carryForward: { products }
1596
+ }));
1597
+ }
1598
+ async function renderFastChanging(_props, slowCarryForward, _wixStores) {
1599
+ const Pipeline = RenderPipeline.for();
1600
+ const { products } = slowCarryForward;
1601
+ return Pipeline.ok({
1602
+ products,
1603
+ hasProducts: products.length > 0
1604
+ }).toPhaseOutput((viewState) => ({ viewState, carryForward: {} }));
1605
+ }
1606
+ const relatedProducts = makeJayStackComponent().withProps().withServices(WIX_STORES_SERVICE_MARKER).withSlowlyRender(renderSlowlyChanging$1).withFastRender(renderFastChanging);
1548
1607
  async function findCategoryBySlug(categoriesClient, slug) {
1549
1608
  const result = await categoriesClient.queryCategories({ treeReference: { appNamespace: "@wix/stores" } }).eq("slug", slug).eq("visible", true).limit(1).find();
1550
1609
  return result.items?.[0] ?? null;
@@ -1897,6 +1956,7 @@ tags:
1897
1956
  - {tag: description, type: data, dataType: string, description: Product description}
1898
1957
  - {tag: brand, type: data, dataType: string, description: Brand name}
1899
1958
  - {tag: ribbon, type: data, dataType: string, description: "Ribbon text (e.g., \\"New\\", \\"Sale\\")"}
1959
+ - {tag: categorySlug, type: data, dataType: string, description: Slug of the product's main category}
1900
1960
  - {tag: productType, type: variant, dataType: "enum (PHYSICAL | DIGITAL)", description: Product type }
1901
1961
 
1902
1962
  - {tag: sku, type: data, dataType: string, phase: fast+interactive, description: Product SKU, or chosen variant SKU}
@@ -2029,6 +2089,7 @@ export {
2029
2089
  provideWixCartContext,
2030
2090
  provideWixCartService,
2031
2091
  provideWixStoresService,
2092
+ relatedProducts,
2032
2093
  searchProducts,
2033
2094
  setupWixStores
2034
2095
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/wix-stores",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
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.17.0",
41
- "@jay-framework/fullstack-component": "^0.17.0",
42
- "@jay-framework/reactive": "^0.17.0",
43
- "@jay-framework/runtime": "^0.17.0",
44
- "@jay-framework/secure": "^0.17.0",
45
- "@jay-framework/stack-client-runtime": "^0.17.0",
46
- "@jay-framework/stack-server-runtime": "^0.17.0",
47
- "@jay-framework/wix-cart": "^0.17.0",
48
- "@jay-framework/wix-server-client": "^0.17.0",
49
- "@jay-framework/wix-utils": "^0.17.0",
41
+ "@jay-framework/component": "^0.17.2",
42
+ "@jay-framework/fullstack-component": "^0.17.2",
43
+ "@jay-framework/reactive": "^0.17.2",
44
+ "@jay-framework/runtime": "^0.17.2",
45
+ "@jay-framework/secure": "^0.17.2",
46
+ "@jay-framework/stack-client-runtime": "^0.17.2",
47
+ "@jay-framework/stack-server-runtime": "^0.17.2",
48
+ "@jay-framework/wix-cart": "^0.17.2",
49
+ "@jay-framework/wix-server-client": "^0.17.2",
50
+ "@jay-framework/wix-utils": "^0.17.2",
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.17.0",
62
- "@jay-framework/jay-cli": "^0.17.0",
63
- "@jay-framework/jay-stack-cli": "^0.17.0",
64
- "@jay-framework/vite-plugin": "^0.17.0",
62
+ "@jay-framework/compiler-jay-stack": "^0.17.2",
63
+ "@jay-framework/jay-cli": "^0.17.2",
64
+ "@jay-framework/jay-stack-cli": "^0.17.2",
65
+ "@jay-framework/vite-plugin": "^0.17.2",
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