@wix/headless-stores 0.0.9 → 0.0.11
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/cjs/dist/enums/index.d.ts +2 -0
- package/cjs/dist/enums/index.js +18 -0
- package/cjs/dist/enums/social-platform-enums.d.ts +25 -0
- package/cjs/dist/enums/social-platform-enums.js +30 -0
- package/cjs/dist/enums/sort-enums.d.ts +17 -0
- package/cjs/dist/enums/sort-enums.js +21 -0
- package/cjs/dist/react/BuyNow.d.ts +2 -4
- package/cjs/dist/react/Category.d.ts +15 -0
- package/cjs/dist/react/Category.js +22 -0
- package/cjs/dist/react/Collection.d.ts +151 -0
- package/cjs/dist/react/Collection.js +209 -0
- package/cjs/dist/react/FilteredCollection.d.ts +95 -0
- package/cjs/dist/react/FilteredCollection.js +150 -0
- package/cjs/dist/react/PayNow.d.ts +2 -4
- package/cjs/dist/react/Product.d.ts +43 -0
- package/cjs/dist/react/Product.js +35 -0
- package/cjs/dist/react/ProductActions.d.ts +42 -0
- package/cjs/dist/react/ProductActions.js +83 -0
- package/cjs/dist/react/ProductModifiers.d.ts +164 -0
- package/cjs/dist/react/ProductModifiers.js +168 -0
- package/cjs/dist/react/ProductVariantSelector.d.ts +153 -0
- package/cjs/dist/react/ProductVariantSelector.js +138 -0
- package/cjs/dist/react/RelatedProducts.d.ts +64 -0
- package/cjs/dist/react/RelatedProducts.js +73 -0
- package/cjs/dist/react/SelectedVariant.d.ts +66 -0
- package/cjs/dist/react/SelectedVariant.js +52 -0
- package/cjs/dist/react/SocialSharing.d.ts +112 -0
- package/cjs/dist/react/SocialSharing.js +86 -0
- package/cjs/dist/react/Sort.d.ts +14 -0
- package/cjs/dist/react/Sort.js +17 -0
- package/cjs/dist/react/index.d.ts +11 -0
- package/cjs/dist/react/index.js +34 -0
- package/cjs/dist/services/buy-now-service.js +2 -2
- package/cjs/dist/services/catalog-options-service.d.ts +30 -0
- package/cjs/dist/services/catalog-options-service.js +156 -0
- package/cjs/dist/services/catalog-price-range-service.d.ts +23 -0
- package/cjs/dist/services/catalog-price-range-service.js +97 -0
- package/cjs/dist/services/category-service.d.ts +26 -0
- package/cjs/dist/services/category-service.js +71 -0
- package/cjs/dist/services/collection-service.d.ts +37 -0
- package/cjs/dist/services/collection-service.js +569 -0
- package/cjs/dist/services/filter-service.d.ts +56 -0
- package/cjs/dist/services/filter-service.js +147 -0
- package/cjs/dist/services/product-media-gallery-service.d.ts +25 -0
- package/cjs/dist/services/product-media-gallery-service.js +105 -0
- package/cjs/dist/services/product-modifiers-service.d.ts +34 -0
- package/cjs/dist/services/product-modifiers-service.js +111 -0
- package/cjs/dist/services/product-service.d.ts +28 -0
- package/cjs/dist/services/product-service.js +68 -0
- package/cjs/dist/services/related-products-service.d.ts +25 -0
- package/cjs/dist/services/related-products-service.js +54 -0
- package/cjs/dist/services/selected-variant-service.d.ts +59 -0
- package/cjs/dist/services/selected-variant-service.js +541 -0
- package/cjs/dist/services/social-sharing-service.d.ts +41 -0
- package/cjs/dist/services/social-sharing-service.js +141 -0
- package/cjs/dist/services/sort-service.d.ts +20 -0
- package/cjs/dist/services/sort-service.js +32 -0
- package/cjs/dist/utils/url-params.d.ts +5 -0
- package/cjs/dist/utils/url-params.js +50 -0
- package/dist/enums/index.d.ts +2 -0
- package/dist/enums/index.js +2 -0
- package/dist/enums/social-platform-enums.d.ts +25 -0
- package/dist/enums/social-platform-enums.js +27 -0
- package/dist/enums/sort-enums.d.ts +17 -0
- package/dist/enums/sort-enums.js +18 -0
- package/dist/react/BuyNow.d.ts +2 -4
- package/dist/react/Category.d.ts +15 -0
- package/dist/react/Category.js +18 -0
- package/dist/react/Collection.d.ts +151 -0
- package/dist/react/Collection.js +201 -0
- package/dist/react/FilteredCollection.d.ts +95 -0
- package/dist/react/FilteredCollection.js +140 -0
- package/dist/react/PayNow.d.ts +2 -4
- package/dist/react/Product.d.ts +43 -0
- package/dist/react/Product.js +30 -0
- package/dist/react/ProductActions.d.ts +42 -0
- package/dist/react/ProductActions.js +79 -0
- package/dist/react/ProductModifiers.d.ts +164 -0
- package/dist/react/ProductModifiers.js +160 -0
- package/dist/react/ProductVariantSelector.d.ts +153 -0
- package/dist/react/ProductVariantSelector.js +130 -0
- package/dist/react/RelatedProducts.d.ts +64 -0
- package/dist/react/RelatedProducts.js +65 -0
- package/dist/react/SelectedVariant.d.ts +66 -0
- package/dist/react/SelectedVariant.js +46 -0
- package/dist/react/SocialSharing.d.ts +112 -0
- package/dist/react/SocialSharing.js +77 -0
- package/dist/react/Sort.d.ts +14 -0
- package/dist/react/Sort.js +14 -0
- package/dist/react/index.d.ts +11 -0
- package/dist/react/index.js +11 -0
- package/dist/services/buy-now-service.js +2 -2
- package/dist/services/catalog-options-service.d.ts +30 -0
- package/dist/services/catalog-options-service.js +152 -0
- package/dist/services/catalog-price-range-service.d.ts +23 -0
- package/dist/services/catalog-price-range-service.js +93 -0
- package/dist/services/category-service.d.ts +26 -0
- package/dist/services/category-service.js +67 -0
- package/dist/services/collection-service.d.ts +37 -0
- package/dist/services/collection-service.js +532 -0
- package/dist/services/filter-service.d.ts +56 -0
- package/dist/services/filter-service.js +144 -0
- package/dist/services/product-media-gallery-service.d.ts +25 -0
- package/dist/services/product-media-gallery-service.js +101 -0
- package/dist/services/product-modifiers-service.d.ts +34 -0
- package/dist/services/product-modifiers-service.js +107 -0
- package/dist/services/product-service.d.ts +28 -0
- package/dist/services/product-service.js +64 -0
- package/dist/services/related-products-service.d.ts +25 -0
- package/dist/services/related-products-service.js +50 -0
- package/dist/services/selected-variant-service.d.ts +59 -0
- package/dist/services/selected-variant-service.js +538 -0
- package/dist/services/social-sharing-service.d.ts +41 -0
- package/dist/services/social-sharing-service.js +137 -0
- package/dist/services/sort-service.d.ts +20 -0
- package/dist/services/sort-service.js +29 -0
- package/dist/utils/url-params.d.ts +5 -0
- package/dist/utils/url-params.js +46 -0
- package/package.json +8 -1
- package/dist/astro/BuyNowServiceContext.d.ts +0 -2
- package/dist/astro/BuyNowServiceContext.js +0 -6
- package/dist/astro/ManagerProviderContext.d.ts +0 -2
- package/dist/astro/ManagerProviderContext.js +0 -7
- package/dist/astro/withBuyButtonService.d.ts +0 -2
- package/dist/astro/withBuyButtonService.js +0 -16
- package/dist/react/CurrentCartServiceProvider.d.ts +0 -5
- package/dist/react/CurrentCartServiceProvider.js +0 -12
- package/dist/react/VariantSelectorServiceProvider.d.ts +0 -7
- package/dist/react/VariantSelectorServiceProvider.js +0 -22
- package/dist/react/hookim/index.d.ts +0 -5
- package/dist/react/hookim/index.js +0 -22
- package/dist/services/CurrentCartService.d.ts +0 -18
- package/dist/services/CurrentCartService.js +0 -9
- package/dist/services/VariantSelectorServices.d.ts +0 -8
- package/dist/services/VariantSelectorServices.js +0 -20
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import { defineService, implementService, } from '@wix/services-definitions';
|
|
2
|
+
import { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';
|
|
3
|
+
import { searchProducts as searchProductsSDK, SortDirection, } from '@wix/auto_sdk_stores_products-v-3';
|
|
4
|
+
import { queryVariants } from '@wix/auto_sdk_stores_read-only-variants-v-3';
|
|
5
|
+
import { FilterServiceDefinition } from './filter-service';
|
|
6
|
+
import { CategoryServiceDefinition } from './category-service';
|
|
7
|
+
import { SortServiceDefinition } from './sort-service';
|
|
8
|
+
import { URLParamsUtils } from '../utils/url-params';
|
|
9
|
+
import { SortType } from '../enums/sort-enums';
|
|
10
|
+
const searchProducts = async (searchOptions) => {
|
|
11
|
+
const searchParams = {
|
|
12
|
+
filter: searchOptions.search?.filter,
|
|
13
|
+
sort: searchOptions.search?.sort,
|
|
14
|
+
...(searchOptions.paging && { cursorPaging: searchOptions.paging }),
|
|
15
|
+
};
|
|
16
|
+
const options = {
|
|
17
|
+
fields: searchOptions.fields || [],
|
|
18
|
+
};
|
|
19
|
+
const result = await searchProductsSDK(searchParams, options);
|
|
20
|
+
// Fetch missing variants for all products in one batch request
|
|
21
|
+
if (result.products) {
|
|
22
|
+
result.products = await fetchMissingVariants(result.products);
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
// Helper to build search options with supported filters
|
|
27
|
+
const buildSearchOptions = (filters, selectedCategory, sortBy) => {
|
|
28
|
+
const searchOptions = {
|
|
29
|
+
search: {},
|
|
30
|
+
fields: [
|
|
31
|
+
'DESCRIPTION',
|
|
32
|
+
'DIRECT_CATEGORIES_INFO',
|
|
33
|
+
'BREADCRUMBS_INFO',
|
|
34
|
+
'INFO_SECTION',
|
|
35
|
+
'MEDIA_ITEMS_INFO',
|
|
36
|
+
'PLAIN_DESCRIPTION',
|
|
37
|
+
'THUMBNAIL',
|
|
38
|
+
'URL',
|
|
39
|
+
'VARIANT_OPTION_CHOICE_NAMES',
|
|
40
|
+
'WEIGHT_MEASUREMENT_UNIT_INFO',
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
// Build filter conditions array
|
|
44
|
+
const filterConditions = [];
|
|
45
|
+
// Add category filter if selected
|
|
46
|
+
if (selectedCategory) {
|
|
47
|
+
filterConditions.push({
|
|
48
|
+
'allCategoriesInfo.categories': {
|
|
49
|
+
$matchItems: [
|
|
50
|
+
{
|
|
51
|
+
id: {
|
|
52
|
+
$in: [selectedCategory],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Add price range filter if provided
|
|
60
|
+
if (filters?.priceRange) {
|
|
61
|
+
const { min, max } = filters.priceRange;
|
|
62
|
+
if (min > 0) {
|
|
63
|
+
filterConditions.push({
|
|
64
|
+
'actualPriceRange.minValue.amount': { $gte: min.toString() },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (max > 0 && max < 999999) {
|
|
68
|
+
filterConditions.push({
|
|
69
|
+
'actualPriceRange.maxValue.amount': { $lte: max.toString() },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Add product options filter if provided
|
|
74
|
+
if (filters?.selectedOptions &&
|
|
75
|
+
Object.keys(filters.selectedOptions).length > 0) {
|
|
76
|
+
for (const [optionId, choiceIds] of Object.entries(filters.selectedOptions)) {
|
|
77
|
+
if (choiceIds.length > 0) {
|
|
78
|
+
// Handle inventory filter separately
|
|
79
|
+
if (optionId === 'inventory-filter') {
|
|
80
|
+
filterConditions.push({
|
|
81
|
+
'inventory.availabilityStatus': {
|
|
82
|
+
$in: choiceIds,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// Regular product options filter
|
|
88
|
+
filterConditions.push({
|
|
89
|
+
'options.choicesSettings.choices.choiceId': {
|
|
90
|
+
$hasSome: choiceIds,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Apply filters
|
|
98
|
+
if (filterConditions.length > 0) {
|
|
99
|
+
if (filterConditions.length === 1) {
|
|
100
|
+
// Single condition - no need for $and wrapper
|
|
101
|
+
searchOptions.search.filter = filterConditions[0];
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Multiple conditions - use $and
|
|
105
|
+
searchOptions.search.filter = {
|
|
106
|
+
$and: filterConditions,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Add sort if provided
|
|
111
|
+
if (sortBy) {
|
|
112
|
+
switch (sortBy) {
|
|
113
|
+
case SortType.NAME_ASC:
|
|
114
|
+
searchOptions.search.sort = [
|
|
115
|
+
{ fieldName: 'name', order: SortDirection.ASC },
|
|
116
|
+
];
|
|
117
|
+
break;
|
|
118
|
+
case SortType.NAME_DESC:
|
|
119
|
+
searchOptions.search.sort = [
|
|
120
|
+
{ fieldName: 'name', order: SortDirection.DESC },
|
|
121
|
+
];
|
|
122
|
+
break;
|
|
123
|
+
case SortType.PRICE_ASC:
|
|
124
|
+
searchOptions.search.sort = [
|
|
125
|
+
{
|
|
126
|
+
fieldName: 'actualPriceRange.minValue.amount',
|
|
127
|
+
order: SortDirection.ASC,
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
break;
|
|
131
|
+
case SortType.PRICE_DESC:
|
|
132
|
+
searchOptions.search.sort = [
|
|
133
|
+
{
|
|
134
|
+
fieldName: 'actualPriceRange.minValue.amount',
|
|
135
|
+
order: SortDirection.DESC,
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
break;
|
|
139
|
+
case SortType.RECOMMENDED:
|
|
140
|
+
searchOptions.search.sort = [
|
|
141
|
+
{
|
|
142
|
+
fieldName: 'allCategoriesInfo.categories.index',
|
|
143
|
+
selectItemsBy: [
|
|
144
|
+
{
|
|
145
|
+
'allCategoriesInfo.categories.id': selectedCategory,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return searchOptions;
|
|
154
|
+
};
|
|
155
|
+
export const CollectionServiceDefinition = defineService('collection');
|
|
156
|
+
export const CollectionService = implementService.withConfig()(CollectionServiceDefinition, ({ getService, config }) => {
|
|
157
|
+
const signalsService = getService(SignalsServiceDefinition);
|
|
158
|
+
const collectionFilters = getService(FilterServiceDefinition);
|
|
159
|
+
const categoryService = getService(CategoryServiceDefinition);
|
|
160
|
+
const sortService = getService(SortServiceDefinition);
|
|
161
|
+
const hasMoreProducts = signalsService.signal((config.initialHasMore ?? true));
|
|
162
|
+
let nextCursor = config.initialCursor;
|
|
163
|
+
const initialProducts = config.initialProducts || [];
|
|
164
|
+
// Signal declarations
|
|
165
|
+
const productsList = signalsService.signal(initialProducts);
|
|
166
|
+
const isLoading = signalsService.signal(false);
|
|
167
|
+
const error = signalsService.signal(null);
|
|
168
|
+
const totalProducts = signalsService.signal(initialProducts.length);
|
|
169
|
+
const hasProducts = signalsService.signal((initialProducts.length > 0));
|
|
170
|
+
const pageSize = config.pageSize || 12;
|
|
171
|
+
let allProducts = initialProducts;
|
|
172
|
+
// Debouncing mechanism to prevent multiple simultaneous refreshes
|
|
173
|
+
let refreshTimeout = null;
|
|
174
|
+
let isRefreshing = false;
|
|
175
|
+
let isInitializingCatalogData = true;
|
|
176
|
+
const loadMore = async () => {
|
|
177
|
+
// Don't load more if there are no more products available
|
|
178
|
+
if (!hasMoreProducts.get()) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
isLoading.set(true);
|
|
183
|
+
error.set(null);
|
|
184
|
+
// For loadMore, use no filters or sorting to work with cursor pagination
|
|
185
|
+
const searchOptions = buildSearchOptions(undefined, undefined, undefined);
|
|
186
|
+
// Add pagination
|
|
187
|
+
searchOptions.paging = { limit: pageSize };
|
|
188
|
+
if (nextCursor) {
|
|
189
|
+
searchOptions.paging.cursor = nextCursor;
|
|
190
|
+
}
|
|
191
|
+
const currentProducts = productsList.get();
|
|
192
|
+
const productResults = await searchProducts(searchOptions);
|
|
193
|
+
// Update cursor for next pagination
|
|
194
|
+
nextCursor = productResults.pagingMetadata?.cursors?.next || undefined;
|
|
195
|
+
// Check if there are more products to load
|
|
196
|
+
const hasMore = Boolean(nextCursor &&
|
|
197
|
+
productResults.products &&
|
|
198
|
+
productResults.products.length === pageSize);
|
|
199
|
+
hasMoreProducts.set(hasMore);
|
|
200
|
+
// Update allProducts with the new data
|
|
201
|
+
allProducts = [...allProducts, ...(productResults.products || [])];
|
|
202
|
+
// Add new products to the list
|
|
203
|
+
const additionalProducts = productResults.products || [];
|
|
204
|
+
productsList.set([...currentProducts, ...additionalProducts]);
|
|
205
|
+
totalProducts.set(currentProducts.length + additionalProducts.length);
|
|
206
|
+
hasProducts.set(currentProducts.length + additionalProducts.length > 0);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
error.set(err instanceof Error ? err.message : 'Failed to load more products');
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
isLoading.set(false);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const refresh = async (setTotalProducts = true) => {
|
|
216
|
+
if (isRefreshing)
|
|
217
|
+
return;
|
|
218
|
+
try {
|
|
219
|
+
isRefreshing = true;
|
|
220
|
+
isLoading.set(true);
|
|
221
|
+
error.set(null);
|
|
222
|
+
const filters = collectionFilters.currentFilters.get();
|
|
223
|
+
const selectedCategory = categoryService.selectedCategory.get();
|
|
224
|
+
const sortBy = sortService.currentSort.get();
|
|
225
|
+
// Use regular search for all sorting options including recommended
|
|
226
|
+
const searchOptions = buildSearchOptions(filters, selectedCategory, sortBy);
|
|
227
|
+
// Add pagination
|
|
228
|
+
searchOptions.paging = { limit: pageSize };
|
|
229
|
+
const productResults = await searchProducts(searchOptions);
|
|
230
|
+
const isPriceSort = sortBy === SortType.PRICE_ASC || sortBy === SortType.PRICE_DESC;
|
|
231
|
+
if (isPriceSort) {
|
|
232
|
+
productResults.products = productResults.products?.sort((a, b) => {
|
|
233
|
+
const aPrice = Number(a.actualPriceRange?.minValue?.amount) || 0;
|
|
234
|
+
const bPrice = Number(b.actualPriceRange?.minValue?.amount) || 0;
|
|
235
|
+
return sortBy === SortType.PRICE_ASC
|
|
236
|
+
? aPrice - bPrice
|
|
237
|
+
: bPrice - aPrice;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
// Reset pagination state
|
|
241
|
+
nextCursor = productResults.pagingMetadata?.cursors?.next || undefined;
|
|
242
|
+
const hasMore = Boolean(productResults.pagingMetadata?.cursors?.next &&
|
|
243
|
+
productResults.products &&
|
|
244
|
+
productResults.products.length === pageSize);
|
|
245
|
+
hasMoreProducts.set(hasMore);
|
|
246
|
+
// Update allProducts with the new data
|
|
247
|
+
allProducts = productResults.products || [];
|
|
248
|
+
// All filtering is handled server-side
|
|
249
|
+
productsList.set(allProducts);
|
|
250
|
+
if (setTotalProducts) {
|
|
251
|
+
totalProducts.set(allProducts.length);
|
|
252
|
+
}
|
|
253
|
+
hasProducts.set(allProducts.length > 0);
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
error.set(err instanceof Error ? err.message : 'Failed to refresh products');
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
isLoading.set(false);
|
|
260
|
+
isRefreshing = false;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
// Debounced refresh function
|
|
264
|
+
const debouncedRefresh = async (setTotalProducts = true) => {
|
|
265
|
+
if (refreshTimeout) {
|
|
266
|
+
clearTimeout(refreshTimeout);
|
|
267
|
+
}
|
|
268
|
+
return new Promise(resolve => {
|
|
269
|
+
refreshTimeout = setTimeout(async () => {
|
|
270
|
+
await refresh(setTotalProducts);
|
|
271
|
+
resolve();
|
|
272
|
+
}, 50); // 50ms debounce delay
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
// Refresh with server-side filtering when any filters change
|
|
276
|
+
collectionFilters.currentFilters.subscribe(() => {
|
|
277
|
+
// Skip refresh during catalog data initialization to prevent double API calls
|
|
278
|
+
if (isInitializingCatalogData) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// All filtering (categories, price, options) is now handled server-side
|
|
282
|
+
debouncedRefresh(false);
|
|
283
|
+
});
|
|
284
|
+
// Initialize catalog data when the service starts
|
|
285
|
+
const initializeCatalogData = async () => {
|
|
286
|
+
const selectedCategory = categoryService.selectedCategory.get();
|
|
287
|
+
await collectionFilters.loadCatalogPriceRange(selectedCategory || undefined);
|
|
288
|
+
await collectionFilters.loadCatalogOptions(selectedCategory || undefined);
|
|
289
|
+
// Reset flag to allow filter changes to trigger refreshes
|
|
290
|
+
isInitializingCatalogData = false;
|
|
291
|
+
};
|
|
292
|
+
// Load catalog data on initialization
|
|
293
|
+
void initializeCatalogData();
|
|
294
|
+
sortService.currentSort.subscribe(() => {
|
|
295
|
+
debouncedRefresh(false);
|
|
296
|
+
});
|
|
297
|
+
categoryService.selectedCategory.subscribe(() => {
|
|
298
|
+
debouncedRefresh(true).then(() => {
|
|
299
|
+
initializeCatalogData();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
return {
|
|
303
|
+
products: productsList,
|
|
304
|
+
isLoading,
|
|
305
|
+
error,
|
|
306
|
+
totalProducts,
|
|
307
|
+
hasProducts,
|
|
308
|
+
hasMoreProducts,
|
|
309
|
+
loadMore,
|
|
310
|
+
refresh: debouncedRefresh,
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
// Helper function to parse URL parameters
|
|
314
|
+
function parseURLParams(searchParams, products = []) {
|
|
315
|
+
const defaultFilters = {
|
|
316
|
+
priceRange: { min: 0, max: 0 },
|
|
317
|
+
selectedOptions: {},
|
|
318
|
+
};
|
|
319
|
+
if (!searchParams) {
|
|
320
|
+
return {
|
|
321
|
+
initialSort: SortType.NEWEST,
|
|
322
|
+
initialFilters: defaultFilters,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const urlParams = URLParamsUtils.parseSearchParams(searchParams);
|
|
326
|
+
// Parse sort parameter
|
|
327
|
+
const sortMap = {
|
|
328
|
+
name_asc: SortType.NAME_ASC,
|
|
329
|
+
name_desc: SortType.NAME_DESC,
|
|
330
|
+
price_asc: SortType.PRICE_ASC,
|
|
331
|
+
price_desc: SortType.PRICE_DESC,
|
|
332
|
+
recommended: SortType.RECOMMENDED,
|
|
333
|
+
};
|
|
334
|
+
const initialSort = sortMap[urlParams['sort']] || SortType.NEWEST;
|
|
335
|
+
// Check if there are any filter parameters (excluding sort)
|
|
336
|
+
const filterParams = Object.keys(urlParams).filter(key => key !== 'sort');
|
|
337
|
+
if (filterParams.length === 0 || products.length === 0) {
|
|
338
|
+
return { initialSort, initialFilters: defaultFilters };
|
|
339
|
+
}
|
|
340
|
+
// Calculate available price range from products
|
|
341
|
+
const priceRange = calculatePriceRange(products);
|
|
342
|
+
const initialFilters = {
|
|
343
|
+
priceRange,
|
|
344
|
+
selectedOptions: {},
|
|
345
|
+
};
|
|
346
|
+
// Apply price filters from URL
|
|
347
|
+
if (urlParams['minPrice']) {
|
|
348
|
+
const min = parseFloat(urlParams['minPrice']);
|
|
349
|
+
if (!isNaN(min))
|
|
350
|
+
initialFilters.priceRange.min = min;
|
|
351
|
+
}
|
|
352
|
+
if (urlParams['maxPrice']) {
|
|
353
|
+
const max = parseFloat(urlParams['maxPrice']);
|
|
354
|
+
if (!isNaN(max))
|
|
355
|
+
initialFilters.priceRange.max = max;
|
|
356
|
+
}
|
|
357
|
+
// Parse option filters
|
|
358
|
+
const optionsMap = buildOptionsMap(products);
|
|
359
|
+
parseOptionFilters(urlParams, optionsMap, initialFilters);
|
|
360
|
+
// Parse inventory filter from 'availability' URL parameter
|
|
361
|
+
if (urlParams['availability']) {
|
|
362
|
+
const availabilityValues = Array.isArray(urlParams['availability'])
|
|
363
|
+
? urlParams['availability']
|
|
364
|
+
: [urlParams['availability']];
|
|
365
|
+
const inventoryStatusValues = availabilityValues.map(value => value.replace(/\s+/g, '_').toUpperCase());
|
|
366
|
+
initialFilters.selectedOptions['inventory-filter'] = inventoryStatusValues;
|
|
367
|
+
}
|
|
368
|
+
return { initialSort, initialFilters };
|
|
369
|
+
}
|
|
370
|
+
// Helper function to calculate price range from products
|
|
371
|
+
function calculatePriceRange(products) {
|
|
372
|
+
if (products.length === 0) {
|
|
373
|
+
return { min: 0, max: 1000 };
|
|
374
|
+
}
|
|
375
|
+
let minPrice = Infinity;
|
|
376
|
+
let maxPrice = 0;
|
|
377
|
+
products.forEach(product => {
|
|
378
|
+
const min = parseFloat(product.actualPriceRange?.minValue?.amount || '0');
|
|
379
|
+
const max = parseFloat(product.actualPriceRange?.maxValue?.amount || '0');
|
|
380
|
+
if (min > 0)
|
|
381
|
+
minPrice = Math.min(minPrice, min);
|
|
382
|
+
if (max > 0)
|
|
383
|
+
maxPrice = Math.max(maxPrice, max);
|
|
384
|
+
});
|
|
385
|
+
// If no valid prices found, return default range
|
|
386
|
+
if (minPrice === Infinity) {
|
|
387
|
+
minPrice = 0;
|
|
388
|
+
maxPrice = 1000;
|
|
389
|
+
}
|
|
390
|
+
return { min: minPrice, max: maxPrice };
|
|
391
|
+
}
|
|
392
|
+
// Helper function to build options map from products
|
|
393
|
+
function buildOptionsMap(products) {
|
|
394
|
+
const optionsMap = new Map();
|
|
395
|
+
products.forEach(product => {
|
|
396
|
+
product.options?.forEach(option => {
|
|
397
|
+
if (!option._id || !option.name)
|
|
398
|
+
return;
|
|
399
|
+
if (!optionsMap.has(option.name)) {
|
|
400
|
+
optionsMap.set(option.name, { id: option._id, choices: [] });
|
|
401
|
+
}
|
|
402
|
+
const optionData = optionsMap.get(option.name);
|
|
403
|
+
option.choicesSettings?.choices?.forEach(choice => {
|
|
404
|
+
if (choice.choiceId &&
|
|
405
|
+
choice.name &&
|
|
406
|
+
!optionData.choices.find(c => c.id === choice.choiceId)) {
|
|
407
|
+
optionData.choices.push({ id: choice.choiceId, name: choice.name });
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
return optionsMap;
|
|
413
|
+
}
|
|
414
|
+
// Helper function to parse option filters from URL parameters
|
|
415
|
+
function parseOptionFilters(urlParams, optionsMap, filters) {
|
|
416
|
+
Object.entries(urlParams).forEach(([key, value]) => {
|
|
417
|
+
if (['sort', 'minPrice', 'maxPrice'].includes(key))
|
|
418
|
+
return;
|
|
419
|
+
const option = optionsMap.get(key);
|
|
420
|
+
if (option) {
|
|
421
|
+
const values = Array.isArray(value) ? value : [value];
|
|
422
|
+
const matchingChoices = option.choices.filter(choice => values.includes(choice.name));
|
|
423
|
+
if (matchingChoices.length > 0) {
|
|
424
|
+
filters.selectedOptions[option.id] = matchingChoices.map(c => c.id);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
export async function loadCollectionServiceConfig(categoryId, searchParams, preloadedCategories) {
|
|
430
|
+
try {
|
|
431
|
+
// Use pre-loaded categories if provided, otherwise load them
|
|
432
|
+
let categories;
|
|
433
|
+
if (preloadedCategories) {
|
|
434
|
+
categories = preloadedCategories;
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
const { loadCategoriesConfig } = await import('./category-service');
|
|
438
|
+
const categoriesConfig = await loadCategoriesConfig();
|
|
439
|
+
categories = categoriesConfig.categories;
|
|
440
|
+
}
|
|
441
|
+
// Build search options with category filter
|
|
442
|
+
const searchOptions = buildSearchOptions(undefined, categoryId, undefined);
|
|
443
|
+
const pageSize = 12;
|
|
444
|
+
searchOptions.paging = { limit: pageSize };
|
|
445
|
+
const productResults = await searchProducts(searchOptions);
|
|
446
|
+
// Parse URL parameters for initial state
|
|
447
|
+
const { initialSort, initialFilters } = parseURLParams(searchParams, productResults.products || []);
|
|
448
|
+
return {
|
|
449
|
+
initialProducts: productResults.products || [],
|
|
450
|
+
pageSize,
|
|
451
|
+
initialCursor: productResults.pagingMetadata?.cursors?.next || undefined,
|
|
452
|
+
initialHasMore: Boolean(productResults.pagingMetadata?.cursors?.next &&
|
|
453
|
+
productResults.products &&
|
|
454
|
+
productResults.products.length === pageSize),
|
|
455
|
+
initialSort,
|
|
456
|
+
initialFilters,
|
|
457
|
+
categories,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
console.warn('Failed to load initial products:', error);
|
|
462
|
+
const { initialSort, initialFilters } = parseURLParams(searchParams);
|
|
463
|
+
return {
|
|
464
|
+
initialProducts: [],
|
|
465
|
+
pageSize: 12,
|
|
466
|
+
initialHasMore: false,
|
|
467
|
+
initialSort,
|
|
468
|
+
initialFilters,
|
|
469
|
+
categories: [],
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// Add function to fetch missing variants for all products in one request
|
|
474
|
+
const fetchMissingVariants = async (products) => {
|
|
475
|
+
// Find products that need variants (both single and multi-variant products)
|
|
476
|
+
const productsNeedingVariants = products.filter(product => !product.variantsInfo?.variants &&
|
|
477
|
+
product.variantSummary?.variantCount &&
|
|
478
|
+
product.variantSummary.variantCount > 0);
|
|
479
|
+
if (productsNeedingVariants.length === 0) {
|
|
480
|
+
return products;
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
const productIds = productsNeedingVariants
|
|
484
|
+
.map(p => p._id)
|
|
485
|
+
.filter(Boolean);
|
|
486
|
+
if (productIds.length === 0) {
|
|
487
|
+
return products;
|
|
488
|
+
}
|
|
489
|
+
const items = [];
|
|
490
|
+
const res = await queryVariants({})
|
|
491
|
+
.in('productData.productId', productIds)
|
|
492
|
+
.limit(100)
|
|
493
|
+
.find();
|
|
494
|
+
items.push(...res.items);
|
|
495
|
+
let nextRes = res;
|
|
496
|
+
while (nextRes.hasNext()) {
|
|
497
|
+
nextRes = await nextRes.next();
|
|
498
|
+
items.push(...nextRes.items);
|
|
499
|
+
}
|
|
500
|
+
const variantsByProductId = new Map();
|
|
501
|
+
items.forEach(item => {
|
|
502
|
+
const productId = item.productData?.productId;
|
|
503
|
+
if (productId) {
|
|
504
|
+
if (!variantsByProductId.has(productId)) {
|
|
505
|
+
variantsByProductId.set(productId, []);
|
|
506
|
+
}
|
|
507
|
+
variantsByProductId.get(productId).push({
|
|
508
|
+
...item,
|
|
509
|
+
choices: item.optionChoices,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
// Update products with their variants
|
|
514
|
+
return products.map(product => {
|
|
515
|
+
const variants = variantsByProductId.get(product._id || '');
|
|
516
|
+
if (variants && variants.length > 0) {
|
|
517
|
+
return {
|
|
518
|
+
...product,
|
|
519
|
+
variantsInfo: {
|
|
520
|
+
...product.variantsInfo,
|
|
521
|
+
variants,
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
return product;
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
console.error('Failed to fetch missing variants:', error);
|
|
530
|
+
return products;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Signal } from '../../Signal';
|
|
2
|
+
export interface ProductOption {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
choices: {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
colorCode?: string;
|
|
9
|
+
}[];
|
|
10
|
+
optionRenderType?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PriceRange {
|
|
13
|
+
min: number;
|
|
14
|
+
max: number;
|
|
15
|
+
}
|
|
16
|
+
export interface AvailableOptions {
|
|
17
|
+
productOptions: ProductOption[];
|
|
18
|
+
priceRange: PriceRange;
|
|
19
|
+
}
|
|
20
|
+
export interface Filter {
|
|
21
|
+
priceRange: {
|
|
22
|
+
min: number;
|
|
23
|
+
max: number;
|
|
24
|
+
};
|
|
25
|
+
selectedOptions: {
|
|
26
|
+
[optionId: string]: string[];
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface FilterServiceAPI {
|
|
30
|
+
currentFilters: Signal<Filter>;
|
|
31
|
+
applyFilters: (filters: Filter) => Promise<void>;
|
|
32
|
+
clearFilters: () => Promise<void>;
|
|
33
|
+
availableOptions: Signal<{
|
|
34
|
+
productOptions: ProductOption[];
|
|
35
|
+
priceRange: {
|
|
36
|
+
min: number;
|
|
37
|
+
max: number;
|
|
38
|
+
};
|
|
39
|
+
}>;
|
|
40
|
+
loadCatalogPriceRange: (categoryId?: string) => Promise<void>;
|
|
41
|
+
loadCatalogOptions: (categoryId?: string) => Promise<void>;
|
|
42
|
+
isFullyLoaded: Signal<boolean>;
|
|
43
|
+
}
|
|
44
|
+
export declare const FilterServiceDefinition: string & {
|
|
45
|
+
__api: FilterServiceAPI;
|
|
46
|
+
__config: {};
|
|
47
|
+
isServiceDefinition?: boolean;
|
|
48
|
+
} & FilterServiceAPI;
|
|
49
|
+
export declare const defaultFilter: Filter;
|
|
50
|
+
export declare const FilterService: import("@wix/services-definitions").ServiceFactory<string & {
|
|
51
|
+
__api: FilterServiceAPI;
|
|
52
|
+
__config: {};
|
|
53
|
+
isServiceDefinition?: boolean;
|
|
54
|
+
} & FilterServiceAPI, {
|
|
55
|
+
initialFilters?: Filter;
|
|
56
|
+
}, import("@wix/services-definitions").ThreadMode.MAIN>;
|