@jay-framework/wix-stores 0.15.1 → 0.15.5

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.
@@ -1,10 +1,10 @@
1
- import { WIX_CART_CONTEXT, cartIndicator, cartPage } from "@jay-framework/wix-cart/client";
1
+ import { WIX_CART_CONTEXT } from "@jay-framework/wix-cart/client";
2
+ import { WIX_CART_CONTEXT as WIX_CART_CONTEXT2, cartIndicator, cartPage } from "@jay-framework/wix-cart/client";
2
3
  import { makeJayStackComponent, makeJayInit } from "@jay-framework/fullstack-component";
3
4
  import { registerReactiveGlobalContext, createSignal, createMemo, createEffect } from "@jay-framework/component";
4
5
  import { patch, REPLACE } from "@jay-framework/json-patch";
5
6
  import { createJayContext, useGlobalContext } from "@jay-framework/runtime";
6
7
  import { WIX_CLIENT_CONTEXT } from "@jay-framework/wix-server-client/client";
7
- import { WIX_CART_CONTEXT as WIX_CART_CONTEXT2 } from "@jay-framework/wix-cart";
8
8
  import { productsV3 } from "@wix/stores";
9
9
  import "@wix/categories";
10
10
  import { createActionCaller } from "@jay-framework/stack-client-runtime";
@@ -31,7 +31,7 @@ const WIX_STORES_CONTEXT = createJayContext();
31
31
  function provideWixStoresContext() {
32
32
  const wixClientContext = useGlobalContext(WIX_CLIENT_CONTEXT);
33
33
  const wixClient = wixClientContext.client;
34
- const cartContext = useGlobalContext(WIX_CART_CONTEXT2);
34
+ const cartContext = useGlobalContext(WIX_CART_CONTEXT);
35
35
  const catalogClient = getProductsV3Client(wixClient);
36
36
  const storesContext = registerReactiveGlobalContext(WIX_STORES_CONTEXT, () => {
37
37
  async function addToCart(productId, quantity = 1, selections) {
@@ -313,6 +313,11 @@ function ProductPageInteractive(props, refs, viewStateSignals, fastCarryForward,
313
313
  };
314
314
  }
315
315
  const productPage = makeJayStackComponent().withProps().withContexts(WIX_STORES_CONTEXT).withInteractive(ProductPageInteractive);
316
+ var OptionRenderType = /* @__PURE__ */ ((OptionRenderType2) => {
317
+ OptionRenderType2[OptionRenderType2["TEXT_CHOICES"] = 0] = "TEXT_CHOICES";
318
+ OptionRenderType2[OptionRenderType2["SWATCH_CHOICES"] = 1] = "SWATCH_CHOICES";
319
+ return OptionRenderType2;
320
+ })(OptionRenderType || {});
316
321
  var CurrentSort = /* @__PURE__ */ ((CurrentSort2) => {
317
322
  CurrentSort2[CurrentSort2["relevance"] = 0] = "relevance";
318
323
  CurrentSort2[CurrentSort2["priceAsc"] = 1] = "priceAsc";
@@ -324,10 +329,102 @@ var CurrentSort = /* @__PURE__ */ ((CurrentSort2) => {
324
329
  })(CurrentSort || {});
325
330
  const searchProducts = createActionCaller("wixStores.searchProducts", "GET");
326
331
  createActionCaller("wixStores.getProductBySlug", "GET");
332
+ const getVariantStock = createActionCaller("wixStores.getVariantStock", "GET");
327
333
  createActionCaller("wixStores.getCategories", "GET");
334
+ var QuickAddType = /* @__PURE__ */ ((QuickAddType2) => {
335
+ QuickAddType2[QuickAddType2["SIMPLE"] = 0] = "SIMPLE";
336
+ QuickAddType2[QuickAddType2["SINGLE_OPTION"] = 1] = "SINGLE_OPTION";
337
+ QuickAddType2[QuickAddType2["COLOR_AND_TEXT_OPTIONS"] = 2] = "COLOR_AND_TEXT_OPTIONS";
338
+ QuickAddType2[QuickAddType2["NEEDS_CONFIGURATION"] = 3] = "NEEDS_CONFIGURATION";
339
+ return QuickAddType2;
340
+ })(QuickAddType || {});
328
341
  const PAGE_SIZE = 12;
342
+ function mapSortToAction(sort) {
343
+ switch (sort) {
344
+ case CurrentSort.priceAsc:
345
+ return "price_asc";
346
+ case CurrentSort.priceDesc:
347
+ return "price_desc";
348
+ case CurrentSort.newest:
349
+ return "newest";
350
+ case CurrentSort.nameAsc:
351
+ return "name_asc";
352
+ case CurrentSort.nameDesc:
353
+ return "name_desc";
354
+ default:
355
+ return "relevance";
356
+ }
357
+ }
358
+ function updateUrlFilters(searchTerm, filters, sort, categories) {
359
+ if (typeof window === "undefined")
360
+ return;
361
+ const params = new URLSearchParams();
362
+ if (searchTerm)
363
+ params.set("q", searchTerm);
364
+ const selectedSlugs = filters.categoryFilter.categories.filter((c) => c.isSelected).map((c) => {
365
+ const info = categories.find((cat) => cat.categoryId === c.categoryId);
366
+ return info?.categorySlug;
367
+ }).filter(Boolean);
368
+ if (selectedSlugs.length)
369
+ params.set("cat", selectedSlugs.join(","));
370
+ if (filters.priceRange.minPrice > 0)
371
+ params.set("min", String(filters.priceRange.minPrice));
372
+ if (filters.priceRange.maxPrice > 0 && filters.priceRange.maxPrice < filters.priceRange.maxBound) {
373
+ params.set("max", String(filters.priceRange.maxPrice));
374
+ }
375
+ if (filters.inStockOnly)
376
+ params.set("inStock", "1");
377
+ const optSegments = [];
378
+ for (const opt of filters.optionFilters || []) {
379
+ const selected = opt.choices.filter((c) => c.isSelected).map((c) => encodeURIComponent(c.choiceName));
380
+ if (selected.length > 0) {
381
+ optSegments.push(`${encodeURIComponent(opt.optionName)}:${selected.join(",")}`);
382
+ }
383
+ }
384
+ if (optSegments.length > 0)
385
+ params.set("opt", optSegments.join(";"));
386
+ if (sort !== CurrentSort.relevance) {
387
+ const sortNames = {
388
+ [CurrentSort.priceAsc]: "priceAsc",
389
+ [CurrentSort.priceDesc]: "priceDesc",
390
+ [CurrentSort.newest]: "newest",
391
+ [CurrentSort.nameAsc]: "nameAsc",
392
+ [CurrentSort.nameDesc]: "nameDesc"
393
+ };
394
+ const sortName = sortNames[sort];
395
+ if (sortName)
396
+ params.set("sort", sortName);
397
+ }
398
+ const query = params.toString();
399
+ window.history.replaceState(null, "", query ? `?${query}` : window.location.pathname);
400
+ }
401
+ function buildOptionFiltersViewState(baseOptionFilters, filteredResult, optionSelections) {
402
+ const filteredChoiceCounts = /* @__PURE__ */ new Map();
403
+ for (const opt of filteredResult.optionFilters || []) {
404
+ for (const ch of opt.choices) {
405
+ filteredChoiceCounts.set(ch.choiceName.toLowerCase(), ch.productCount);
406
+ }
407
+ }
408
+ return baseOptionFilters.map((opt) => ({
409
+ optionId: opt.optionId,
410
+ optionName: opt.optionName,
411
+ optionRenderType: opt.optionRenderType === "SWATCH_CHOICES" ? OptionRenderType.SWATCH_CHOICES : OptionRenderType.TEXT_CHOICES,
412
+ choices: opt.choices.map((ch) => {
413
+ const count = filteredChoiceCounts.get(ch.choiceName.toLowerCase()) ?? 0;
414
+ return {
415
+ choiceId: ch.choiceId,
416
+ choiceName: ch.choiceName,
417
+ colorCode: ch.colorCode,
418
+ productCount: count,
419
+ isSelected: optionSelections.get(opt.optionName)?.has(ch.choiceName) ?? false,
420
+ isDisabled: count === 0
421
+ };
422
+ })
423
+ }));
424
+ }
329
425
  function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForward, storesContext) {
330
426
  const baseCategoryId = fastCarryForward.baseCategoryId;
427
+ const variantStockCache = {};
331
428
  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;
332
429
  const [submittedSearchTerm, setSubmittedSearchTerm] = createSignal(null);
333
430
  let currentCursor = null;
@@ -335,35 +432,41 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
335
432
  let debounceTimeout = null;
336
433
  let searchVersion = 0;
337
434
  const DEBOUNCE_MS = 300;
338
- const mapSortToAction = (sort) => {
339
- switch (sort) {
340
- case CurrentSort.priceAsc:
341
- return "price_asc";
342
- case CurrentSort.priceDesc:
343
- return "price_desc";
344
- case CurrentSort.newest:
345
- return "newest";
346
- case CurrentSort.nameAsc:
347
- return "name_asc";
348
- case CurrentSort.nameDesc:
349
- return "name_desc";
350
- default:
351
- return "relevance";
435
+ const [latestSearchResult, setLatestSearchResult] = createSignal(null);
436
+ const mergedFilters = createMemo(() => {
437
+ const f = filters();
438
+ const result = latestSearchResult();
439
+ if (!result)
440
+ return f;
441
+ const selections = /* @__PURE__ */ new Map();
442
+ for (const opt of f.optionFilters || []) {
443
+ const selected = opt.choices.filter((c) => c.isSelected).map((c) => c.choiceName);
444
+ if (selected.length > 0)
445
+ selections.set(opt.optionName, new Set(selected));
352
446
  }
353
- };
447
+ return {
448
+ ...f,
449
+ optionFilters: buildOptionFiltersViewState(fastCarryForward.baseOptionFilters, result, selections)
450
+ };
451
+ });
354
452
  const performSearch = async (version, searchTerm, currentFilters, currentSort) => {
355
453
  setIsSearching(true);
356
454
  setHasSearched(true);
357
455
  try {
358
456
  const userSelectedCategoryIds = currentFilters.categoryFilter.categories.filter((c) => c.isSelected).map((c) => c.categoryId);
359
457
  const categoryIds = baseCategoryId ? [baseCategoryId, ...userSelectedCategoryIds] : userSelectedCategoryIds;
458
+ const activeOptionFilters = (currentFilters.optionFilters || []).map((opt) => ({
459
+ optionName: opt.optionName,
460
+ choiceNames: opt.choices.filter((c) => c.isSelected).map((c) => c.choiceName)
461
+ })).filter((o) => o.choiceNames.length > 0);
360
462
  const result = await searchProducts({
361
463
  query: searchTerm || "",
362
464
  filters: {
363
465
  minPrice: currentFilters.priceRange.minPrice || void 0,
364
466
  maxPrice: currentFilters.priceRange.maxPrice || void 0,
365
467
  categoryIds,
366
- inStockOnly: currentFilters.inStockOnly
468
+ inStockOnly: currentFilters.inStockOnly,
469
+ optionFilters: activeOptionFilters.length > 0 ? activeOptionFilters : void 0
367
470
  },
368
471
  sortBy: mapSortToAction(currentSort),
369
472
  // No cursor = start from beginning
@@ -378,7 +481,9 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
378
481
  setLoadedCount(result.products.length);
379
482
  setHasMore(result.hasMore);
380
483
  setHasResults(result.products.length > 0);
484
+ setLatestSearchResult(result);
381
485
  currentCursor = result.nextCursor;
486
+ updateUrlFilters(searchTerm, currentFilters, currentSort, fastCarryForward.categories);
382
487
  } catch (error) {
383
488
  if (version === searchVersion) {
384
489
  console.error("Search failed:", error);
@@ -399,13 +504,18 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
399
504
  const searchTerm = submittedSearchTerm();
400
505
  const userSelectedCategoryIds = currentFilters.categoryFilter.categories.filter((c) => c.isSelected).map((c) => c.categoryId);
401
506
  const categoryIds = baseCategoryId ? [baseCategoryId, ...userSelectedCategoryIds] : userSelectedCategoryIds;
507
+ const activeOptionFilters = (currentFilters.optionFilters || []).map((opt) => ({
508
+ optionName: opt.optionName,
509
+ choiceNames: opt.choices.filter((c) => c.isSelected).map((c) => c.choiceName)
510
+ })).filter((o) => o.choiceNames.length > 0);
402
511
  const result = await searchProducts({
403
512
  query: searchTerm || "",
404
513
  filters: {
405
514
  minPrice: currentFilters.priceRange.minPrice || void 0,
406
515
  maxPrice: currentFilters.priceRange.maxPrice || void 0,
407
516
  categoryIds,
408
- inStockOnly: currentFilters.inStockOnly
517
+ inStockOnly: currentFilters.inStockOnly,
518
+ optionFilters: activeOptionFilters.length > 0 ? activeOptionFilters : void 0
409
519
  },
410
520
  sortBy: mapSortToAction(currentSort),
411
521
  cursor: currentCursor,
@@ -520,6 +630,24 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
520
630
  const isChecked = event.target.checked;
521
631
  setFilters(patch(filters(), [{ op: REPLACE, path: ["inStockOnly"], value: isChecked }]));
522
632
  });
633
+ refs.filters.optionFilters.choices.isSelected.oninput(({ event, coordinate }) => {
634
+ const [optionId, choiceId] = coordinate;
635
+ const currentFilters = filters();
636
+ const optionIndex = currentFilters.optionFilters.findIndex((o) => o.optionId === optionId);
637
+ if (optionIndex === -1)
638
+ return;
639
+ const choiceIndex = currentFilters.optionFilters[optionIndex].choices.findIndex((c) => c.choiceId === choiceId);
640
+ if (choiceIndex === -1)
641
+ return;
642
+ const isChecked = event.target.checked;
643
+ setFilters(patch(currentFilters, [
644
+ {
645
+ op: REPLACE,
646
+ path: ["optionFilters", optionIndex, "choices", choiceIndex, "isSelected"],
647
+ value: isChecked
648
+ }
649
+ ]));
650
+ });
523
651
  refs.filters.clearFilters.onclick(() => {
524
652
  const currentFilters = filters();
525
653
  const clearedCategories = currentFilters.categoryFilter.categories.map((cat) => ({
@@ -531,6 +659,10 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
531
659
  isSelected: i === 0
532
660
  // First one is "Show all"
533
661
  }));
662
+ const clearedOptionFilters = (currentFilters.optionFilters || []).map((opt) => ({
663
+ ...opt,
664
+ choices: opt.choices.map((ch) => ({ ...ch, isSelected: false }))
665
+ }));
534
666
  setFilters({
535
667
  priceRange: {
536
668
  minPrice: 0,
@@ -540,7 +672,8 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
540
672
  ranges: clearedRanges
541
673
  },
542
674
  categoryFilter: { categories: clearedCategories },
543
- inStockOnly: false
675
+ inStockOnly: false,
676
+ optionFilters: clearedOptionFilters
544
677
  });
545
678
  });
546
679
  refs.loadMoreButton.onclick(() => {
@@ -573,6 +706,49 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
573
706
  ]));
574
707
  }
575
708
  });
709
+ const variantStockLoading = /* @__PURE__ */ new Set();
710
+ const loadVariantStock = async (productId) => {
711
+ if (variantStockCache[productId] || variantStockLoading.has(productId))
712
+ return;
713
+ variantStockLoading.add(productId);
714
+ try {
715
+ const currentResults = searchResults();
716
+ const productIndex = currentResults.findIndex((p) => p._id === productId);
717
+ if (productIndex === -1)
718
+ return;
719
+ const product = currentResults[productIndex];
720
+ if (product?.quickAddType !== QuickAddType.COLOR_AND_TEXT_OPTIONS)
721
+ return;
722
+ const stockMap = await getVariantStock({ productId });
723
+ variantStockCache[productId] = stockMap;
724
+ const selectedColor = product.quickOption?.choices?.find((c) => c.isSelected);
725
+ const textChoices = product.secondQuickOption?.choices;
726
+ if (!selectedColor || !textChoices)
727
+ return;
728
+ const colorStock = stockMap[selectedColor.choiceId];
729
+ const updatedTextChoices = textChoices.map((c) => ({
730
+ ...c,
731
+ inStock: colorStock?.[c.choiceId] ?? false
732
+ }));
733
+ setSearchResults(patch(searchResults(), [
734
+ {
735
+ op: REPLACE,
736
+ path: [productIndex, "secondQuickOption", "choices"],
737
+ value: updatedTextChoices
738
+ }
739
+ ]));
740
+ } finally {
741
+ variantStockLoading.delete(productId);
742
+ }
743
+ };
744
+ refs.searchResults.productLink.onmouseenter(({ coordinate }) => {
745
+ const [productId] = coordinate;
746
+ loadVariantStock(productId);
747
+ });
748
+ refs.searchResults.quickOption.choices.choiceButton.onmouseenter(({ coordinate }) => {
749
+ const [productId] = coordinate;
750
+ loadVariantStock(productId);
751
+ });
576
752
  refs.searchResults.quickOption.choices.choiceButton.onclick(async ({ coordinate }) => {
577
753
  const [productId, choiceId] = coordinate;
578
754
  const currentResults = searchResults();
@@ -580,6 +756,44 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
580
756
  if (productIndex === -1)
581
757
  return;
582
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
+ }
583
797
  const choice = product.quickOption?.choices?.find((c) => c.choiceId === choiceId);
584
798
  if (!choice || !choice.inStock) {
585
799
  console.warn("Choice not available or out of stock");
@@ -603,6 +817,45 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
603
817
  ]));
604
818
  }
605
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.secondQuickOption.choices.choiceButton.onmouseenter(({ coordinate }) => {
856
+ const [productId] = coordinate;
857
+ loadVariantStock(productId);
858
+ });
606
859
  refs.searchResults.viewOptionsButton.onclick(({ coordinate }) => {
607
860
  const [productId] = coordinate;
608
861
  const product = searchResults().find((p) => p._id === productId);
@@ -620,7 +873,7 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
620
873
  hasResults: hasResults(),
621
874
  hasSuggestions: hasSuggestions(),
622
875
  suggestions: suggestions(),
623
- filters: filters(),
876
+ filters: mergedFilters(),
624
877
  sortBy: sortBy(),
625
878
  hasMore: hasMore(),
626
879
  loadedCount: loadedCount(),
@@ -634,10 +887,9 @@ const init = makeJayInit().withClient(async (data) => {
634
887
  console.log("[wix-stores] Initializing client-side stores context...");
635
888
  provideWixStoresContext();
636
889
  console.log("[wix-stores] Client initialization complete");
637
- console.log(`[wix-stores] Search enabled: ${data.enableClientSearch}`);
638
890
  });
639
891
  export {
640
- WIX_CART_CONTEXT,
892
+ WIX_CART_CONTEXT2 as WIX_CART_CONTEXT,
641
893
  WIX_STORES_CONTEXT,
642
894
  cartIndicator,
643
895
  cartPage,