@jay-framework/wix-stores 0.17.0 → 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.
@@ -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,8 +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
- let variantStockApplied = /* @__PURE__ */ new Set();
430
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;
431
609
  const [submittedSearchTerm, setSubmittedSearchTerm] = createSignal(null);
432
610
  let currentCursor = null;
@@ -477,7 +655,7 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
477
655
  if (version !== searchVersion) {
478
656
  return;
479
657
  }
480
- variantStockApplied = /* @__PURE__ */ new Set();
658
+ resetCardCache();
481
659
  setSearchResults(result.products);
482
660
  setResultCount(result.products.length);
483
661
  setTotalCount(result.totalCount);
@@ -690,175 +868,7 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
690
868
  setSubmittedSearchTerm(suggestion.suggestionText);
691
869
  }
692
870
  });
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
- });
871
+ const { resetCache: resetCardCache } = setupCardInteractions(refs.searchResults, { get: searchResults, set: setSearchResults }, storesContext);
862
872
  return {
863
873
  render: () => ({
864
874
  searchExpression: searchExpression(),
@@ -878,6 +888,17 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
878
888
  };
879
889
  }
880
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);
881
902
  const categoryList = makeJayStackComponent().withProps();
882
903
  const init = makeJayInit().withClient(async (data) => {
883
904
  console.log("[wix-stores] Initializing client-side stores context...");
@@ -893,5 +914,6 @@ export {
893
914
  init,
894
915
  productPage,
895
916
  productSearch,
896
- provideWixStoresContext
917
+ provideWixStoresContext,
918
+ relatedProducts
897
919
  };
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,39 @@ 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
+ 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
+
868
902
  interface CategoryOfCategoryListViewState {
869
903
  _id: string,
870
904
  name: string,
@@ -1034,4 +1068,4 @@ declare function generateWixStoresReferences(ctx: PluginReferencesContext): Prom
1034
1068
  */
1035
1069
  declare const generator: _jay_framework_fullstack_component.DynamicContractGenerator<[WixStoresService]>;
1036
1070
 
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 };
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
@@ -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,78 @@ 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
+ 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);
1548
1624
  async function findCategoryBySlug(categoriesClient, slug) {
1549
1625
  const result = await categoriesClient.queryCategories({ treeReference: { appNamespace: "@wix/stores" } }).eq("slug", slug).eq("visible", true).limit(1).find();
1550
1626
  return result.items?.[0] ?? null;
@@ -1897,6 +1973,7 @@ tags:
1897
1973
  - {tag: description, type: data, dataType: string, description: Product description}
1898
1974
  - {tag: brand, type: data, dataType: string, description: Brand name}
1899
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}
1900
1977
  - {tag: productType, type: variant, dataType: "enum (PHYSICAL | DIGITAL)", description: Product type }
1901
1978
 
1902
1979
  - {tag: sku, type: data, dataType: string, phase: fast+interactive, description: Product SKU, or chosen variant SKU}
@@ -2029,6 +2106,7 @@ export {
2029
2106
  provideWixCartContext,
2030
2107
  provideWixCartService,
2031
2108
  provideWixStoresService,
2109
+ relatedProducts,
2032
2110
  searchProducts,
2033
2111
  setupWixStores
2034
2112
  };
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.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.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.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.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.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