@jay-framework/wix-stores 0.15.0

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.
@@ -0,0 +1,649 @@
1
+ import { WIX_CART_CONTEXT, cartIndicator, cartPage } from "@jay-framework/wix-cart/client";
2
+ import { makeJayStackComponent, makeJayInit } from "@jay-framework/fullstack-component";
3
+ import { registerReactiveGlobalContext, createSignal, createMemo, createEffect } from "@jay-framework/component";
4
+ import { patch, REPLACE } from "@jay-framework/json-patch";
5
+ import { createJayContext, useGlobalContext } from "@jay-framework/runtime";
6
+ 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
+ import { productsV3 } from "@wix/stores";
9
+ import "@wix/categories";
10
+ import { createActionCaller } from "@jay-framework/stack-client-runtime";
11
+ var StockStatus = /* @__PURE__ */ ((StockStatus2) => {
12
+ StockStatus2[StockStatus2["OUT_OF_STOCK"] = 0] = "OUT_OF_STOCK";
13
+ StockStatus2[StockStatus2["IN_STOCK"] = 1] = "IN_STOCK";
14
+ return StockStatus2;
15
+ })(StockStatus || {});
16
+ var Selected = /* @__PURE__ */ ((Selected2) => {
17
+ Selected2[Selected2["selected"] = 0] = "selected";
18
+ Selected2[Selected2["notSelected"] = 1] = "notSelected";
19
+ return Selected2;
20
+ })(Selected || {});
21
+ const instances = {
22
+ productsV3ClientInstance: void 0
23
+ };
24
+ function getProductsV3Client(wixClient) {
25
+ if (!instances.productsV3ClientInstance) {
26
+ instances.productsV3ClientInstance = wixClient.use(productsV3);
27
+ }
28
+ return instances.productsV3ClientInstance;
29
+ }
30
+ const WIX_STORES_CONTEXT = createJayContext();
31
+ function provideWixStoresContext() {
32
+ const wixClientContext = useGlobalContext(WIX_CLIENT_CONTEXT);
33
+ const wixClient = wixClientContext.client;
34
+ const cartContext = useGlobalContext(WIX_CART_CONTEXT2);
35
+ const catalogClient = getProductsV3Client(wixClient);
36
+ const storesContext = registerReactiveGlobalContext(WIX_STORES_CONTEXT, () => {
37
+ async function addToCart(productId, quantity = 1, selections) {
38
+ console.log(`[WixStores] Adding to cart: ${productId} x ${quantity}`, selections);
39
+ const product = await catalogClient.getProduct(productId, {
40
+ fields: ["VARIANT_OPTION_CHOICE_NAMES"]
41
+ });
42
+ let variant = product.variantsInfo?.variants?.find(
43
+ (v) => v.choices?.every(
44
+ (choice) => selections?.options?.[choice.optionChoiceIds?.optionId] === choice.optionChoiceIds?.choiceId
45
+ )
46
+ );
47
+ if (!variant && product.variantsInfo?.variants?.length > 0) {
48
+ variant = product.variantsInfo.variants[0];
49
+ }
50
+ if (!variant) {
51
+ console.warn(`[WixStores] No variant found for product ${productId}`);
52
+ return { cartState: await cartContext.getEstimatedCart() };
53
+ }
54
+ const translatedModifiers = {};
55
+ const translatedCustomTextFields = {};
56
+ if (selections?.modifiers || selections?.customTextFields) {
57
+ for (const modifier of product.modifiers || []) {
58
+ const modifierId = modifier._id;
59
+ const modifierKey = modifier["key"];
60
+ if (!modifierKey) continue;
61
+ const selectedChoiceId = selections?.modifiers?.[modifierId];
62
+ if (selectedChoiceId && modifier.choicesSettings?.choices) {
63
+ const choice = modifier.choicesSettings.choices.find(
64
+ (c) => c.choiceId === selectedChoiceId
65
+ );
66
+ if (choice?.key) {
67
+ translatedModifiers[modifierKey] = choice.key;
68
+ }
69
+ }
70
+ const customText = selections?.customTextFields?.[modifierId];
71
+ if (customText) {
72
+ translatedCustomTextFields[modifierKey] = customText;
73
+ }
74
+ }
75
+ }
76
+ return cartContext.addToCart(productId, quantity, {
77
+ variantId: variant._id,
78
+ modifiers: translatedModifiers,
79
+ customTextFields: translatedCustomTextFields,
80
+ productSlug: product.slug
81
+ });
82
+ }
83
+ return {
84
+ // Delegate cart indicator and operations to WIX_CART_CONTEXT
85
+ cartIndicator: cartContext.cartIndicator,
86
+ refreshCartIndicator: () => cartContext.refreshCartIndicator(),
87
+ getEstimatedCart: () => cartContext.getEstimatedCart(),
88
+ addToCart,
89
+ // Custom implementation that resolves V3 variants first
90
+ removeLineItems: (ids) => cartContext.removeLineItems(ids),
91
+ updateLineItemQuantity: (id, qty) => cartContext.updateLineItemQuantity(id, qty),
92
+ clearCart: () => cartContext.clearCart(),
93
+ applyCoupon: (code) => cartContext.applyCoupon(code),
94
+ removeCoupon: () => cartContext.removeCoupon()
95
+ };
96
+ });
97
+ console.log("[wix-stores] Client stores context initialized (delegating cart to wix-cart)");
98
+ return storesContext;
99
+ }
100
+ function buildSelectionsFromViewState(optionsVS, modifiersVS) {
101
+ const options = {};
102
+ for (const option of optionsVS) {
103
+ if (option.textChoiceSelection) {
104
+ options[option._id] = option.textChoiceSelection;
105
+ } else {
106
+ const selectedChoice = option.choices.find((c) => c.isSelected);
107
+ if (selectedChoice) {
108
+ options[option._id] = selectedChoice.choiceId;
109
+ }
110
+ }
111
+ }
112
+ const modifiers = {};
113
+ const customTextFields = {};
114
+ for (const modifier of modifiersVS) {
115
+ const selectedChoice = modifier.choices.find((c) => c.isSelected);
116
+ if (selectedChoice) {
117
+ modifiers[modifier._id] = selectedChoice.choiceId;
118
+ } else if (modifier.textModifierSelection) {
119
+ if (modifier.choices.length > 0) {
120
+ modifiers[modifier._id] = modifier.textModifierSelection;
121
+ } else {
122
+ customTextFields[modifier._id] = modifier.textModifierSelection;
123
+ }
124
+ }
125
+ }
126
+ return { options, modifiers, customTextFields };
127
+ }
128
+ function ProductPageInteractive(props, refs, viewStateSignals, fastCarryForward, storesContext) {
129
+ const [quantity, setQuantity] = createSignal(viewStateSignals.quantity[0]().quantity);
130
+ const { productId, variants } = fastCarryForward;
131
+ const {
132
+ actionsEnabled: [actionsEnabled, setActionsEnabled],
133
+ options: [options, setOptions],
134
+ modifiers: [modifiers, setModifiers],
135
+ mediaGallery: [mediaGallery, setMediaGallery],
136
+ // sku: [sku, setSKU],
137
+ // price: [price, setPrice],
138
+ // stockStatus: [stockStatus, setStockStatus],
139
+ pricePerUnit: [pricePerUnit, setPricePerUnit]
140
+ } = viewStateSignals;
141
+ const [isAddingToCart, setIsAddingToCart] = createSignal(false);
142
+ const [selectedMediaId, setSelectedMediaId] = createSignal(null);
143
+ const selectedOptionsRecord = createMemo(() => {
144
+ const result = {};
145
+ for (const option of options()) {
146
+ if (option.textChoiceSelection) {
147
+ result[option._id] = option.textChoiceSelection;
148
+ } else {
149
+ const selectedChoice = option.choices.find((c) => c.isSelected);
150
+ if (selectedChoice) {
151
+ result[option._id] = selectedChoice.choiceId;
152
+ }
153
+ }
154
+ }
155
+ return result;
156
+ });
157
+ const selectedVariant = createMemo(() => findVariant(variants, selectedOptionsRecord()));
158
+ const sku = createMemo(() => selectedVariant().sku);
159
+ const price = createMemo(() => selectedVariant().price);
160
+ const strikethroughPrice = createMemo(() => selectedVariant().strikethroughPrice);
161
+ const stockStatus = createMemo(() => selectedVariant().inventoryStatus);
162
+ const computedActionsEnabled = createMemo(() => stockStatus() === StockStatus.IN_STOCK);
163
+ const interactiveMedia = createMemo((prev) => {
164
+ prev = prev || mediaGallery();
165
+ const oldSelectedMediaIndex = prev.availableMedia.findIndex((_) => _.selected === Selected.selected);
166
+ const newSelectedMediaIndex = Math.max(0, prev.availableMedia.findIndex((_) => _.mediaId === selectedMediaId()));
167
+ if (oldSelectedMediaIndex === newSelectedMediaIndex)
168
+ return prev;
169
+ const newSelectedMedia = prev.availableMedia[newSelectedMediaIndex];
170
+ return patch(prev, [
171
+ { op: REPLACE, path: ["selectedMedia"], value: newSelectedMedia.media },
172
+ {
173
+ op: REPLACE,
174
+ path: ["availableMedia", oldSelectedMediaIndex, "selected"],
175
+ value: Selected.notSelected
176
+ },
177
+ {
178
+ op: REPLACE,
179
+ path: ["availableMedia", newSelectedMediaIndex, "selected"],
180
+ value: Selected.selected
181
+ }
182
+ ]);
183
+ });
184
+ refs.quantity.decrementButton.onclick(() => {
185
+ setQuantity((prev) => Math.max(1, prev - 1));
186
+ });
187
+ refs.quantity.incrementButton.onclick(() => {
188
+ setQuantity((prev) => prev + 1);
189
+ });
190
+ refs.quantity.quantity.oninput(({ event }) => {
191
+ const value = parseInt(event.target.value, 10);
192
+ if (!isNaN(value) && value > 0) {
193
+ setQuantity(value);
194
+ }
195
+ });
196
+ function findVariant(variants2, options2) {
197
+ const foundFullMatch = variants2.find((variant) => variant.choices.every((choice) => options2[choice.optionChoiceIds.optionId] === choice.optionChoiceIds.choiceId));
198
+ if (foundFullMatch && foundFullMatch.mediaId)
199
+ setSelectedMediaId(foundFullMatch.mediaId);
200
+ return foundFullMatch || variants2[0];
201
+ }
202
+ refs.mediaGallery.availableMedia.selected.onclick(({ coordinate }) => {
203
+ const mediaId = coordinate[0];
204
+ setSelectedMediaId(mediaId);
205
+ });
206
+ refs.options.choices.choiceButton.onclick(({ event, viewState, coordinate }) => {
207
+ const [optionId, choiceId] = coordinate;
208
+ const optionIndex = options().findIndex((_) => _._id === optionId);
209
+ const option = options()[optionIndex];
210
+ const newChoiceIndex = option.choices.findIndex((_) => _.choiceId === choiceId);
211
+ const oldChoiceIndex = option.choices.findIndex((_) => _.isSelected);
212
+ const removeSelectedPatch = oldChoiceIndex > -1 && oldChoiceIndex !== newChoiceIndex ? [
213
+ {
214
+ op: REPLACE,
215
+ path: [optionIndex, "choices", oldChoiceIndex, "isSelected"],
216
+ value: false
217
+ }
218
+ ] : [];
219
+ setOptions(patch(options(), [
220
+ {
221
+ op: REPLACE,
222
+ path: [optionIndex, "choices", newChoiceIndex, "isSelected"],
223
+ value: true
224
+ },
225
+ ...removeSelectedPatch
226
+ ]));
227
+ });
228
+ refs.options.textChoice.oninput(({ event, viewState, coordinate }) => {
229
+ const [optionId] = coordinate;
230
+ const optionIndex = options().findIndex((_) => _._id === optionId);
231
+ const selectedChoiceId = event.target.value;
232
+ setOptions(patch(options(), [
233
+ {
234
+ op: REPLACE,
235
+ path: [optionIndex, "textChoiceSelection"],
236
+ value: selectedChoiceId
237
+ }
238
+ ]));
239
+ });
240
+ refs.modifiers.choices.choiceButton.onclick(({ event, viewState, coordinate }) => {
241
+ const [modifierId, choiceId] = coordinate;
242
+ const modifierIndex = modifiers().findIndex((_) => _._id === modifierId);
243
+ const modifier = modifiers()[modifierIndex];
244
+ const newChoiceIndex = modifier.choices.findIndex((_) => _.choiceId === choiceId);
245
+ const oldChoiceIndex = modifier.choices.findIndex((_) => _.isSelected);
246
+ const removeSelectedPatch = oldChoiceIndex > -1 && oldChoiceIndex !== newChoiceIndex ? [
247
+ {
248
+ op: REPLACE,
249
+ path: [modifierIndex, "choices", oldChoiceIndex, "isSelected"],
250
+ value: false
251
+ }
252
+ ] : [];
253
+ setModifiers(patch(modifiers(), [
254
+ {
255
+ op: REPLACE,
256
+ path: [modifierIndex, "choices", newChoiceIndex, "isSelected"],
257
+ value: true
258
+ },
259
+ ...removeSelectedPatch
260
+ ]));
261
+ });
262
+ refs.modifiers.textInput.oninput(({ event, viewState, coordinate }) => {
263
+ const [modifierId] = coordinate;
264
+ const modifierIndex = modifiers().findIndex((_) => _._id === modifierId);
265
+ const textValue = event.target.value;
266
+ setModifiers(patch(modifiers(), [
267
+ { op: REPLACE, path: [modifierIndex, "textModifierSelection"], value: textValue }
268
+ ]));
269
+ });
270
+ refs.modifiers.textModifier.oninput(({ event, viewState, coordinate }) => {
271
+ const [modifierId] = coordinate;
272
+ const modifierIndex = modifiers().findIndex((_) => _._id === modifierId);
273
+ const selectedChoiceId = event.target.value;
274
+ setModifiers(patch(modifiers(), [
275
+ {
276
+ op: REPLACE,
277
+ path: [modifierIndex, "textModifierSelection"],
278
+ value: selectedChoiceId
279
+ }
280
+ ]));
281
+ });
282
+ refs.addToCartButton.onclick(async () => {
283
+ if (stockStatus() === StockStatus.OUT_OF_STOCK) {
284
+ console.warn("Product is out of stock");
285
+ return;
286
+ }
287
+ setIsAddingToCart(true);
288
+ try {
289
+ const selections = buildSelectionsFromViewState(options(), modifiers());
290
+ await storesContext.addToCart(fastCarryForward.productId, quantity(), selections);
291
+ console.log("Added to cart:", quantity(), "items with selections:", selections);
292
+ } catch (error) {
293
+ console.error("Failed to add to cart:", error);
294
+ } finally {
295
+ setIsAddingToCart(false);
296
+ }
297
+ });
298
+ return {
299
+ render: () => ({
300
+ quantity: {
301
+ quantity: quantity()
302
+ },
303
+ actionsEnabled: computedActionsEnabled,
304
+ options,
305
+ modifiers,
306
+ mediaGallery: interactiveMedia,
307
+ sku,
308
+ price,
309
+ pricePerUnit,
310
+ stockStatus,
311
+ strikethroughPrice
312
+ })
313
+ };
314
+ }
315
+ const productPage = makeJayStackComponent().withProps().withContexts(WIX_STORES_CONTEXT).withInteractive(ProductPageInteractive);
316
+ var CurrentSort = /* @__PURE__ */ ((CurrentSort2) => {
317
+ CurrentSort2[CurrentSort2["relevance"] = 0] = "relevance";
318
+ CurrentSort2[CurrentSort2["priceAsc"] = 1] = "priceAsc";
319
+ CurrentSort2[CurrentSort2["priceDesc"] = 2] = "priceDesc";
320
+ CurrentSort2[CurrentSort2["newest"] = 3] = "newest";
321
+ CurrentSort2[CurrentSort2["nameAsc"] = 4] = "nameAsc";
322
+ CurrentSort2[CurrentSort2["nameDesc"] = 5] = "nameDesc";
323
+ return CurrentSort2;
324
+ })(CurrentSort || {});
325
+ const searchProducts = createActionCaller("wixStores.searchProducts", "GET");
326
+ createActionCaller("wixStores.getProductBySlug", "GET");
327
+ createActionCaller("wixStores.getCategories", "GET");
328
+ const PAGE_SIZE = 12;
329
+ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForward, storesContext) {
330
+ const baseCategoryId = fastCarryForward.baseCategoryId;
331
+ 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
+ const [submittedSearchTerm, setSubmittedSearchTerm] = createSignal(null);
333
+ let currentCursor = null;
334
+ let isFirst = true;
335
+ let debounceTimeout = null;
336
+ let searchVersion = 0;
337
+ 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";
352
+ }
353
+ };
354
+ const performSearch = async (version, searchTerm, currentFilters, currentSort) => {
355
+ setIsSearching(true);
356
+ setHasSearched(true);
357
+ try {
358
+ const userSelectedCategoryIds = currentFilters.categoryFilter.categories.filter((c) => c.isSelected).map((c) => c.categoryId);
359
+ const categoryIds = baseCategoryId ? [baseCategoryId, ...userSelectedCategoryIds] : userSelectedCategoryIds;
360
+ const result = await searchProducts({
361
+ query: searchTerm || "",
362
+ filters: {
363
+ minPrice: currentFilters.priceRange.minPrice || void 0,
364
+ maxPrice: currentFilters.priceRange.maxPrice || void 0,
365
+ categoryIds,
366
+ inStockOnly: currentFilters.inStockOnly
367
+ },
368
+ sortBy: mapSortToAction(currentSort),
369
+ // No cursor = start from beginning
370
+ pageSize: PAGE_SIZE
371
+ });
372
+ if (version !== searchVersion) {
373
+ return;
374
+ }
375
+ setSearchResults(result.products);
376
+ setResultCount(result.products.length);
377
+ setTotalCount(result.totalCount);
378
+ setLoadedCount(result.products.length);
379
+ setHasMore(result.hasMore);
380
+ setHasResults(result.products.length > 0);
381
+ currentCursor = result.nextCursor;
382
+ } catch (error) {
383
+ if (version === searchVersion) {
384
+ console.error("Search failed:", error);
385
+ }
386
+ } finally {
387
+ if (version === searchVersion) {
388
+ setIsSearching(false);
389
+ }
390
+ }
391
+ };
392
+ const performLoadMore = async () => {
393
+ if (isSearching() || !hasMore() || !currentCursor)
394
+ return;
395
+ setIsSearching(true);
396
+ try {
397
+ const currentFilters = filters();
398
+ const currentSort = sortBy().currentSort;
399
+ const searchTerm = submittedSearchTerm();
400
+ const userSelectedCategoryIds = currentFilters.categoryFilter.categories.filter((c) => c.isSelected).map((c) => c.categoryId);
401
+ const categoryIds = baseCategoryId ? [baseCategoryId, ...userSelectedCategoryIds] : userSelectedCategoryIds;
402
+ const result = await searchProducts({
403
+ query: searchTerm || "",
404
+ filters: {
405
+ minPrice: currentFilters.priceRange.minPrice || void 0,
406
+ maxPrice: currentFilters.priceRange.maxPrice || void 0,
407
+ categoryIds,
408
+ inStockOnly: currentFilters.inStockOnly
409
+ },
410
+ sortBy: mapSortToAction(currentSort),
411
+ cursor: currentCursor,
412
+ pageSize: PAGE_SIZE
413
+ });
414
+ const currentResults = searchResults();
415
+ const newResults = [...currentResults, ...result.products];
416
+ setSearchResults(newResults);
417
+ setResultCount(newResults.length);
418
+ setLoadedCount(newResults.length);
419
+ setHasMore(result.hasMore);
420
+ currentCursor = result.nextCursor;
421
+ } catch (error) {
422
+ console.error("Load more failed:", error);
423
+ } finally {
424
+ setIsSearching(false);
425
+ }
426
+ };
427
+ createEffect(() => {
428
+ const searchTerm = submittedSearchTerm();
429
+ const currentFilters = filters();
430
+ const currentSort = sortBy().currentSort;
431
+ if (isFirst) {
432
+ isFirst = false;
433
+ return;
434
+ }
435
+ if (debounceTimeout) {
436
+ clearTimeout(debounceTimeout);
437
+ }
438
+ debounceTimeout = setTimeout(() => {
439
+ searchVersion++;
440
+ const version = searchVersion;
441
+ performSearch(version, searchTerm, currentFilters, currentSort);
442
+ }, DEBOUNCE_MS);
443
+ });
444
+ refs.searchExpression.oninput(({ event }) => {
445
+ const value = event.target.value;
446
+ setSearchExpression(value);
447
+ });
448
+ refs.searchExpression.onkeydown(({ event }) => {
449
+ if (event.key === "Enter") {
450
+ event.preventDefault();
451
+ setSubmittedSearchTerm(searchExpression().trim());
452
+ }
453
+ });
454
+ refs.searchButton.onclick(() => {
455
+ setSubmittedSearchTerm(searchExpression().trim());
456
+ });
457
+ refs.clearSearchButton.onclick(() => {
458
+ setSearchExpression("");
459
+ setSubmittedSearchTerm(null);
460
+ setHasSearched(false);
461
+ });
462
+ refs.sortBy.sortDropdown.oninput(({ event }) => {
463
+ const value = event.target.value;
464
+ const sortMap = {
465
+ relevance: CurrentSort.relevance,
466
+ priceAsc: CurrentSort.priceAsc,
467
+ priceDesc: CurrentSort.priceDesc,
468
+ newest: CurrentSort.newest,
469
+ nameAsc: CurrentSort.nameAsc,
470
+ nameDesc: CurrentSort.nameDesc
471
+ };
472
+ const newSort = sortMap[value] ?? CurrentSort.relevance;
473
+ setSortBy({ currentSort: newSort });
474
+ });
475
+ refs.filters.priceRange.minPrice.oninput(({ event }) => {
476
+ const value = parseFloat(event.target.value);
477
+ const newValue = isNaN(value) ? 0 : value;
478
+ setFilters(patch(filters(), [{ op: REPLACE, path: ["priceRange", "minPrice"], value: newValue }]));
479
+ });
480
+ refs.filters.priceRange.maxPrice.oninput(({ event }) => {
481
+ const value = parseFloat(event.target.value);
482
+ const newValue = isNaN(value) ? 0 : value;
483
+ setFilters(patch(filters(), [{ op: REPLACE, path: ["priceRange", "maxPrice"], value: newValue }]));
484
+ });
485
+ refs.filters.priceRange.ranges.isSelected.oninput(({ event, coordinate }) => {
486
+ const [rangeId] = coordinate;
487
+ const currentFilters = filters();
488
+ const ranges = currentFilters.priceRange.ranges || [];
489
+ const selectedRange = ranges.find((r) => r.rangeId === rangeId);
490
+ if (!selectedRange)
491
+ return;
492
+ const updatedRanges = ranges.map((r) => ({
493
+ ...r,
494
+ isSelected: r.rangeId === rangeId
495
+ }));
496
+ const newMinPrice = selectedRange.minValue ?? 0;
497
+ const newMaxPrice = selectedRange.maxValue ?? 0;
498
+ setFilters(patch(currentFilters, [
499
+ { op: REPLACE, path: ["priceRange", "ranges"], value: updatedRanges },
500
+ { op: REPLACE, path: ["priceRange", "minPrice"], value: newMinPrice },
501
+ { op: REPLACE, path: ["priceRange", "maxPrice"], value: newMaxPrice }
502
+ ]));
503
+ });
504
+ refs.filters.categoryFilter.categories.isSelected.oninput(({ event, coordinate }) => {
505
+ const [categoryId] = coordinate;
506
+ const currentFilters = filters();
507
+ const categoryIndex = currentFilters.categoryFilter.categories.findIndex((c) => c.categoryId === categoryId);
508
+ if (categoryIndex !== -1) {
509
+ const isChecked = event.target.checked;
510
+ setFilters(patch(currentFilters, [
511
+ {
512
+ op: REPLACE,
513
+ path: ["categoryFilter", "categories", categoryIndex, "isSelected"],
514
+ value: isChecked
515
+ }
516
+ ]));
517
+ }
518
+ });
519
+ refs.filters.inStockOnly.oninput(({ event }) => {
520
+ const isChecked = event.target.checked;
521
+ setFilters(patch(filters(), [{ op: REPLACE, path: ["inStockOnly"], value: isChecked }]));
522
+ });
523
+ refs.filters.clearFilters.onclick(() => {
524
+ const currentFilters = filters();
525
+ const clearedCategories = currentFilters.categoryFilter.categories.map((cat) => ({
526
+ ...cat,
527
+ isSelected: false
528
+ }));
529
+ const clearedRanges = (currentFilters.priceRange.ranges || []).map((r, i) => ({
530
+ ...r,
531
+ isSelected: i === 0
532
+ // First one is "Show all"
533
+ }));
534
+ setFilters({
535
+ priceRange: {
536
+ minPrice: 0,
537
+ maxPrice: 0,
538
+ minBound: currentFilters.priceRange.minBound,
539
+ maxBound: currentFilters.priceRange.maxBound,
540
+ ranges: clearedRanges
541
+ },
542
+ categoryFilter: { categories: clearedCategories },
543
+ inStockOnly: false
544
+ });
545
+ });
546
+ refs.loadMoreButton.onclick(() => {
547
+ performLoadMore();
548
+ });
549
+ refs.suggestions.suggestionButton.onclick(({ coordinate }) => {
550
+ const [suggestionId] = coordinate;
551
+ const suggestion = suggestions().find((s) => s.suggestionId === suggestionId);
552
+ if (suggestion) {
553
+ setSearchExpression(suggestion.suggestionText);
554
+ setSubmittedSearchTerm(suggestion.suggestionText);
555
+ }
556
+ });
557
+ refs.searchResults.addToCartButton.onclick(async ({ coordinate }) => {
558
+ const [productId] = coordinate;
559
+ const currentResults = searchResults();
560
+ const productIndex = currentResults.findIndex((p) => p._id === productId);
561
+ if (productIndex === -1)
562
+ return;
563
+ setSearchResults(patch(currentResults, [
564
+ { op: REPLACE, path: [productIndex, "isAddingToCart"], value: true }
565
+ ]));
566
+ try {
567
+ await storesContext.addToCart(productId, 1);
568
+ } catch (error) {
569
+ console.error("Failed to add to cart:", error);
570
+ } finally {
571
+ setSearchResults(patch(searchResults(), [
572
+ { op: REPLACE, path: [productIndex, "isAddingToCart"], value: false }
573
+ ]));
574
+ }
575
+ });
576
+ refs.searchResults.quickOption.choices.choiceButton.onclick(async ({ coordinate }) => {
577
+ const [productId, choiceId] = coordinate;
578
+ const currentResults = searchResults();
579
+ const productIndex = currentResults.findIndex((p) => p._id === productId);
580
+ if (productIndex === -1)
581
+ return;
582
+ const product = currentResults[productIndex];
583
+ const choice = product.quickOption?.choices?.find((c) => c.choiceId === choiceId);
584
+ if (!choice || !choice.inStock) {
585
+ console.warn("Choice not available or out of stock");
586
+ return;
587
+ }
588
+ setSearchResults(patch(currentResults, [
589
+ { op: REPLACE, path: [productIndex, "isAddingToCart"], value: true }
590
+ ]));
591
+ try {
592
+ const optionId = product.quickOption._id;
593
+ await storesContext.addToCart(productId, 1, {
594
+ options: { [optionId]: choice.choiceId },
595
+ modifiers: {},
596
+ customTextFields: {}
597
+ });
598
+ } catch (error) {
599
+ console.error("Failed to add to cart:", error);
600
+ } finally {
601
+ setSearchResults(patch(searchResults(), [
602
+ { op: REPLACE, path: [productIndex, "isAddingToCart"], value: false }
603
+ ]));
604
+ }
605
+ });
606
+ refs.searchResults.viewOptionsButton.onclick(({ coordinate }) => {
607
+ const [productId] = coordinate;
608
+ const product = searchResults().find((p) => p._id === productId);
609
+ if (product?.productUrl) {
610
+ window.location.href = product.productUrl;
611
+ }
612
+ });
613
+ return {
614
+ render: () => ({
615
+ searchExpression: searchExpression(),
616
+ isSearching: isSearching(),
617
+ hasSearched: hasSearched(),
618
+ searchResults: searchResults(),
619
+ resultCount: resultCount(),
620
+ hasResults: hasResults(),
621
+ hasSuggestions: hasSuggestions(),
622
+ suggestions: suggestions(),
623
+ filters: filters(),
624
+ sortBy: sortBy(),
625
+ hasMore: hasMore(),
626
+ loadedCount: loadedCount(),
627
+ totalCount: totalCount()
628
+ })
629
+ };
630
+ }
631
+ const productSearch = makeJayStackComponent().withProps().withContexts(WIX_STORES_CONTEXT).withInteractive(ProductSearchInteractive);
632
+ const categoryList = makeJayStackComponent().withProps();
633
+ const init = makeJayInit().withClient(async (data) => {
634
+ console.log("[wix-stores] Initializing client-side stores context...");
635
+ provideWixStoresContext();
636
+ console.log("[wix-stores] Client initialization complete");
637
+ console.log(`[wix-stores] Search enabled: ${data.enableClientSearch}`);
638
+ });
639
+ export {
640
+ WIX_CART_CONTEXT,
641
+ WIX_STORES_CONTEXT,
642
+ cartIndicator,
643
+ cartPage,
644
+ categoryList,
645
+ init,
646
+ productPage,
647
+ productSearch,
648
+ provideWixStoresContext
649
+ };