@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.
- package/README.md +184 -0
- package/dist/actions/get-categories.jay-action +8 -0
- package/dist/actions/get-product-by-slug.jay-action +10 -0
- package/dist/actions/search-products.jay-action +33 -0
- package/dist/contracts/category-list.jay-contract +50 -0
- package/dist/contracts/category-list.jay-contract.d.ts +40 -0
- package/dist/contracts/category-page.jay-contract +193 -0
- package/dist/contracts/category-page.jay-contract.d.ts +124 -0
- package/dist/contracts/media-gallery.jay-contract +22 -0
- package/dist/contracts/media-gallery.jay-contract.d.ts +53 -0
- package/dist/contracts/media.jay-contract +5 -0
- package/dist/contracts/media.jay-contract.d.ts +25 -0
- package/dist/contracts/product-card.jay-contract +182 -0
- package/dist/contracts/product-card.jay-contract.d.ts +120 -0
- package/dist/contracts/product-options.jay-contract +66 -0
- package/dist/contracts/product-options.jay-contract.d.ts +57 -0
- package/dist/contracts/product-page.jay-contract +151 -0
- package/dist/contracts/product-page.jay-contract.d.ts +218 -0
- package/dist/contracts/product-search.jay-contract +252 -0
- package/dist/contracts/product-search.jay-contract.d.ts +169 -0
- package/dist/index.client.js +649 -0
- package/dist/index.d.ts +943 -0
- package/dist/index.js +1140 -0
- package/package.json +66 -0
- package/plugin.yaml +34 -0
|
@@ -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
|
+
};
|