@wix/headless-stores 0.0.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.
Files changed (150) hide show
  1. package/astro/actions/package.json +4 -0
  2. package/cjs/dist/astro/actions/custom-checkout.d.ts +50 -0
  3. package/cjs/dist/astro/actions/custom-checkout.js +53 -0
  4. package/cjs/dist/astro/actions/index.d.ts +1 -0
  5. package/cjs/dist/astro/actions/index.js +1 -0
  6. package/cjs/dist/data-component-tags.d.ts +8 -0
  7. package/cjs/dist/data-component-tags.js +9 -0
  8. package/cjs/dist/enums/index.d.ts +2 -0
  9. package/cjs/dist/enums/index.js +2 -0
  10. package/cjs/dist/enums/social-platform-enums.d.ts +25 -0
  11. package/cjs/dist/enums/social-platform-enums.js +27 -0
  12. package/cjs/dist/enums/sort-enums.d.ts +17 -0
  13. package/cjs/dist/enums/sort-enums.js +18 -0
  14. package/cjs/dist/react/Category.d.ts +242 -0
  15. package/cjs/dist/react/Category.js +235 -0
  16. package/cjs/dist/react/CategoryList.d.ts +107 -0
  17. package/cjs/dist/react/CategoryList.js +91 -0
  18. package/cjs/dist/react/Choice.d.ts +211 -0
  19. package/cjs/dist/react/Choice.js +213 -0
  20. package/cjs/dist/react/Option.d.ts +242 -0
  21. package/cjs/dist/react/Option.js +346 -0
  22. package/cjs/dist/react/Product.d.ts +1065 -0
  23. package/cjs/dist/react/Product.js +1157 -0
  24. package/cjs/dist/react/ProductList.d.ts +400 -0
  25. package/cjs/dist/react/ProductList.js +368 -0
  26. package/cjs/dist/react/core/CategoryList.d.ts +194 -0
  27. package/cjs/dist/react/core/CategoryList.js +180 -0
  28. package/cjs/dist/react/core/Product.d.ts +225 -0
  29. package/cjs/dist/react/core/Product.js +190 -0
  30. package/cjs/dist/react/core/ProductList.d.ts +235 -0
  31. package/cjs/dist/react/core/ProductList.js +217 -0
  32. package/cjs/dist/react/core/ProductListFilters.d.ts +138 -0
  33. package/cjs/dist/react/core/ProductListFilters.js +242 -0
  34. package/cjs/dist/react/core/ProductListPagination.d.ts +49 -0
  35. package/cjs/dist/react/core/ProductListPagination.js +41 -0
  36. package/cjs/dist/react/core/ProductListSort.d.ts +19 -0
  37. package/cjs/dist/react/core/ProductListSort.js +52 -0
  38. package/cjs/dist/react/core/ProductModifiers.d.ts +416 -0
  39. package/cjs/dist/react/core/ProductModifiers.js +413 -0
  40. package/cjs/dist/react/core/ProductVariantSelector.d.ts +313 -0
  41. package/cjs/dist/react/core/ProductVariantSelector.js +291 -0
  42. package/cjs/dist/react/core/SelectedVariant.d.ts +230 -0
  43. package/cjs/dist/react/core/SelectedVariant.js +269 -0
  44. package/cjs/dist/react/index.d.ts +6 -0
  45. package/cjs/dist/react/index.js +6 -0
  46. package/cjs/dist/react/types.d.ts +8 -0
  47. package/cjs/dist/react/types.js +9 -0
  48. package/cjs/dist/server-actions/custom-checkout-action.d.ts +49 -0
  49. package/cjs/dist/server-actions/custom-checkout-action.js +64 -0
  50. package/cjs/dist/server-actions/index.d.ts +1 -0
  51. package/cjs/dist/server-actions/index.js +1 -0
  52. package/cjs/dist/services/buy-now-service.d.ts +346 -0
  53. package/cjs/dist/services/buy-now-service.js +197 -0
  54. package/cjs/dist/services/categories-list-service.d.ts +164 -0
  55. package/cjs/dist/services/categories-list-service.js +148 -0
  56. package/cjs/dist/services/index.d.ts +5 -0
  57. package/cjs/dist/services/index.js +5 -0
  58. package/cjs/dist/services/pay-now-service.d.ts +214 -0
  59. package/cjs/dist/services/pay-now-service.js +156 -0
  60. package/cjs/dist/services/product-modifiers-service.d.ts +34 -0
  61. package/cjs/dist/services/product-modifiers-service.js +107 -0
  62. package/cjs/dist/services/product-service.d.ts +177 -0
  63. package/cjs/dist/services/product-service.js +190 -0
  64. package/cjs/dist/services/products-list-search-service.d.ts +1 -0
  65. package/cjs/dist/services/products-list-search-service.js +1 -0
  66. package/cjs/dist/services/products-list-service.d.ts +429 -0
  67. package/cjs/dist/services/products-list-service.js +893 -0
  68. package/cjs/dist/services/selected-variant-service.d.ts +66 -0
  69. package/cjs/dist/services/selected-variant-service.js +527 -0
  70. package/cjs/dist/utils/index.d.ts +1 -0
  71. package/cjs/dist/utils/index.js +30 -0
  72. package/cjs/dist/utils/url-params.d.ts +73 -0
  73. package/cjs/dist/utils/url-params.js +114 -0
  74. package/cjs/package.json +3 -0
  75. package/dist/astro/actions/custom-checkout.d.ts +50 -0
  76. package/dist/astro/actions/custom-checkout.js +53 -0
  77. package/dist/astro/actions/index.d.ts +1 -0
  78. package/dist/astro/actions/index.js +1 -0
  79. package/dist/data-component-tags.d.ts +8 -0
  80. package/dist/data-component-tags.js +9 -0
  81. package/dist/enums/index.d.ts +2 -0
  82. package/dist/enums/index.js +2 -0
  83. package/dist/enums/social-platform-enums.d.ts +25 -0
  84. package/dist/enums/social-platform-enums.js +27 -0
  85. package/dist/enums/sort-enums.d.ts +17 -0
  86. package/dist/enums/sort-enums.js +18 -0
  87. package/dist/react/Category.d.ts +242 -0
  88. package/dist/react/Category.js +235 -0
  89. package/dist/react/CategoryList.d.ts +107 -0
  90. package/dist/react/CategoryList.js +91 -0
  91. package/dist/react/Choice.d.ts +211 -0
  92. package/dist/react/Choice.js +213 -0
  93. package/dist/react/Option.d.ts +242 -0
  94. package/dist/react/Option.js +346 -0
  95. package/dist/react/Product.d.ts +1065 -0
  96. package/dist/react/Product.js +1157 -0
  97. package/dist/react/ProductList.d.ts +400 -0
  98. package/dist/react/ProductList.js +368 -0
  99. package/dist/react/core/CategoryList.d.ts +194 -0
  100. package/dist/react/core/CategoryList.js +180 -0
  101. package/dist/react/core/Product.d.ts +225 -0
  102. package/dist/react/core/Product.js +190 -0
  103. package/dist/react/core/ProductList.d.ts +235 -0
  104. package/dist/react/core/ProductList.js +217 -0
  105. package/dist/react/core/ProductListFilters.d.ts +138 -0
  106. package/dist/react/core/ProductListFilters.js +242 -0
  107. package/dist/react/core/ProductListPagination.d.ts +49 -0
  108. package/dist/react/core/ProductListPagination.js +41 -0
  109. package/dist/react/core/ProductListSort.d.ts +19 -0
  110. package/dist/react/core/ProductListSort.js +52 -0
  111. package/dist/react/core/ProductModifiers.d.ts +416 -0
  112. package/dist/react/core/ProductModifiers.js +413 -0
  113. package/dist/react/core/ProductVariantSelector.d.ts +313 -0
  114. package/dist/react/core/ProductVariantSelector.js +291 -0
  115. package/dist/react/core/SelectedVariant.d.ts +230 -0
  116. package/dist/react/core/SelectedVariant.js +269 -0
  117. package/dist/react/index.d.ts +6 -0
  118. package/dist/react/index.js +6 -0
  119. package/dist/react/types.d.ts +8 -0
  120. package/dist/react/types.js +9 -0
  121. package/dist/server-actions/custom-checkout-action.d.ts +49 -0
  122. package/dist/server-actions/custom-checkout-action.js +64 -0
  123. package/dist/server-actions/index.d.ts +1 -0
  124. package/dist/server-actions/index.js +1 -0
  125. package/dist/services/buy-now-service.d.ts +346 -0
  126. package/dist/services/buy-now-service.js +197 -0
  127. package/dist/services/categories-list-service.d.ts +164 -0
  128. package/dist/services/categories-list-service.js +148 -0
  129. package/dist/services/index.d.ts +5 -0
  130. package/dist/services/index.js +5 -0
  131. package/dist/services/pay-now-service.d.ts +214 -0
  132. package/dist/services/pay-now-service.js +156 -0
  133. package/dist/services/product-modifiers-service.d.ts +34 -0
  134. package/dist/services/product-modifiers-service.js +107 -0
  135. package/dist/services/product-service.d.ts +177 -0
  136. package/dist/services/product-service.js +190 -0
  137. package/dist/services/products-list-search-service.d.ts +0 -0
  138. package/dist/services/products-list-search-service.js +1 -0
  139. package/dist/services/products-list-service.d.ts +429 -0
  140. package/dist/services/products-list-service.js +893 -0
  141. package/dist/services/selected-variant-service.d.ts +66 -0
  142. package/dist/services/selected-variant-service.js +527 -0
  143. package/dist/utils/index.d.ts +1 -0
  144. package/dist/utils/index.js +30 -0
  145. package/dist/utils/url-params.d.ts +73 -0
  146. package/dist/utils/url-params.js +114 -0
  147. package/package.json +89 -0
  148. package/react/package.json +4 -0
  149. package/server-actions/package.json +4 -0
  150. package/services/package.json +4 -0
@@ -0,0 +1,893 @@
1
+ import { defineService, implementService } from '@wix/services-definitions';
2
+ import { SignalsServiceDefinition, } from '@wix/services-definitions/core-services/signals';
3
+ import { customizationsV3, productsV3, readOnlyVariantsV3 } from '@wix/stores';
4
+ import { loadCategoriesListServiceConfig } from './categories-list-service.js';
5
+ export const DEFAULT_QUERY_LIMIT = 100;
6
+ import { SortType } from './../enums/sort-enums.js';
7
+ export { SortType } from './../enums/sort-enums.js';
8
+ /**
9
+ * Enumeration of inventory status types available for filtering.
10
+ * Re-exports the Wix inventory availability status enum values.
11
+ */
12
+ export const InventoryStatusType = productsV3.InventoryAvailabilityStatus;
13
+ /**
14
+ * Loads products list service configuration from the Wix Stores API for SSR initialization.
15
+ * This function is designed to be used during Server-Side Rendering (SSR) to preload
16
+ * a list of products based on search criteria or URL parameters.
17
+ *
18
+ * @param {string | { searchOptions: productsV3.V3ProductSearch; initialSearchState: InitialSearchState }} input - Either a URL to parse or parsed URL result from parseUrlToSearchOptions
19
+ * @returns {Promise<ProductsListServiceConfig>} Promise that resolves to the products list configuration
20
+ *
21
+ * @example
22
+ * ```astro
23
+ * ---
24
+ * // Astro page example - pages/products.astro
25
+ * import { loadProductsListServiceConfig, parseUrlToSearchOptions, loadCategoriesListServiceConfig } from '@wix/stores/services';
26
+ * import { ProductList } from '@wix/stores/components';
27
+ *
28
+ * // Option 1: Load from URL (will parse filters, sort, pagination from URL params)
29
+ * const productsConfig = await loadProductsListServiceConfig(Astro.url.href);
30
+ *
31
+ * // Option 2: Custom parsing with defaults
32
+ * const categories = await loadCategoriesListServiceConfig();
33
+ * const parsed = await parseUrlToSearchOptions(
34
+ * Astro.url.href,
35
+ * categories.categories,
36
+ * {
37
+ * cursorPaging: { limit: 12 },
38
+ * filter: {},
39
+ * sort: [{ fieldName: 'name' as const, order: 'ASC' as const }]
40
+ * }
41
+ * );
42
+ * const productsConfig = await loadProductsListServiceConfig(parsed);
43
+ * ---
44
+ *
45
+ * <ProductList.Root productsConfig={productsConfig}>
46
+ * <ProductList.ItemContent>
47
+ * {({ product }) => (
48
+ * <div>
49
+ * <h3>{product.name}</h3>
50
+ * <p>{product.description}</p>
51
+ * </div>
52
+ * )}
53
+ * </ProductList.ItemContent>
54
+ * </ProductList.Root>
55
+ * ```
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * // Next.js page example - pages/products.tsx
60
+ * import { GetServerSideProps } from 'next';
61
+ * import { loadProductsListServiceConfig, parseUrlToSearchOptions, loadCategoriesListServiceConfig } from '@wix/stores/services';
62
+ * import { ProductsList } from '@wix/stores/components';
63
+ *
64
+ * interface ProductsPageProps {
65
+ * productsConfig: Awaited<ReturnType<typeof loadProductsListServiceConfig>>;
66
+ * }
67
+ *
68
+ * export const getServerSideProps: GetServerSideProps<ProductsPageProps> = async ({ req }) => {
69
+ * // Option 1: Parse from URL
70
+ * const productsConfig = await loadProductsListServiceConfig(`${req.url}`);
71
+ *
72
+ * // Option 2: Custom parsing with filters
73
+ * const categories = await loadCategoriesListServiceConfig();
74
+ * const parsed = await parseUrlToSearchOptions(
75
+ * `${req.url}`,
76
+ * categories.categories,
77
+ * {
78
+ * cursorPaging: { limit: 12 },
79
+ * filter: {
80
+ * 'allCategoriesInfo.categories': { $matchItems: [{ _id: { $in: [category._id] } }] }
81
+ * },
82
+ * sort: [{ fieldName: 'name' as const, order: 'ASC' as const }]
83
+ * }
84
+ * );
85
+ * const productsConfig = await loadProductsListServiceConfig(parsed);
86
+ *
87
+ * return {
88
+ * props: {
89
+ * productsConfig,
90
+ * },
91
+ * };
92
+ * };
93
+ *
94
+ * export default function ProductsPage({ productsConfig }: ProductsPageProps) {
95
+ * return (
96
+ * <ProductList.Root productsConfig={productsConfig}>
97
+ * <ProductList.ItemContent>
98
+ * {({ product }) => (
99
+ * <div>
100
+ * <h3>{product.name}</h3>
101
+ * <p>{product.description}</p>
102
+ * </div>
103
+ * )}
104
+ * </ProductList.ItemContent>
105
+ * </ProductList.Root>
106
+ * );
107
+ * }
108
+ * ```
109
+ *
110
+ * @example
111
+ * ```tsx
112
+ * // Advanced: Performance optimization when using both services
113
+ * import { parseUrlToSearchOptions, loadProductsListServiceConfig, loadProductsListSearchServiceConfig, loadCategoriesListServiceConfig } from '@wix/stores/services';
114
+ *
115
+ * const categories = await loadCategoriesListServiceConfig();
116
+ * const parsed = await parseUrlToSearchOptions(url, categories.categories);
117
+ *
118
+ * // Both services use the same parsed result (no duplicate URL parsing)
119
+ * const [productsConfig, searchConfig] = await Promise.all([
120
+ * loadProductsListServiceConfig(parsed),
121
+ * loadProductsListSearchServiceConfig(parsed)
122
+ * ]);
123
+ * ```
124
+ */
125
+ export async function loadProductsListServiceConfig(input) {
126
+ let searchOptions;
127
+ const { items: customizations = [] } = await customizationsV3
128
+ .queryCustomizations()
129
+ .find();
130
+ if (typeof input === 'string') {
131
+ // URL input - parse it
132
+ const categoriesListConfig = await loadCategoriesListServiceConfig();
133
+ const { searchOptions: parsedOptions } = await parseUrlToSearchOptions(input, categoriesListConfig.categories, customizations);
134
+ searchOptions = parsedOptions;
135
+ }
136
+ else {
137
+ // Parsed URL result - use searchOptions directly
138
+ searchOptions = input.searchOptions;
139
+ }
140
+ const searchWithoutFilter = { ...searchOptions, filter: {} };
141
+ const [resultWithoutFilter, resultWithFilter] = await Promise.all([
142
+ fetchProducts(searchWithoutFilter),
143
+ fetchProducts(searchOptions),
144
+ ]);
145
+ const products = resultWithFilter?.products ?? resultWithoutFilter?.products ?? [];
146
+ return {
147
+ products,
148
+ searchOptions,
149
+ pagingMetadata: resultWithFilter.pagingMetadata,
150
+ aggregations: resultWithoutFilter.aggregationData ?? {},
151
+ customizations,
152
+ };
153
+ }
154
+ /**
155
+ * Fetches products and their missing variants in one optimized request.
156
+ * This function wraps the standard searchProducts call and automatically
157
+ * fetches missing variant data for all products that need it.
158
+ *
159
+ * @param searchOptions - The search options for querying products
160
+ * @returns Promise that resolves to the search result with complete variant data
161
+ */
162
+ const fetchProducts = async (searchOptions) => {
163
+ const result = await productsV3.searchProducts(searchOptions);
164
+ // Fetch missing variants for all products in one batch request
165
+ if (result.products) {
166
+ result.products = await fetchMissingVariants(result.products);
167
+ }
168
+ return result;
169
+ };
170
+ /**
171
+ * Fetches missing variants for all products in one batch request.
172
+ * This function identifies products that need variant data and fetches
173
+ * all variants efficiently using the readOnlyVariantsV3 API.
174
+ *
175
+ * @param products - Array of products that may need variant data
176
+ * @returns Promise that resolves to products with complete variant information
177
+ */
178
+ const fetchMissingVariants = async (products) => {
179
+ // Find products that need variants (both single and multi-variant products)
180
+ const productsNeedingVariants = products.filter((product) => !product.variantsInfo?.variants &&
181
+ product.variantSummary?.variantCount &&
182
+ product.variantSummary.variantCount > 0);
183
+ if (productsNeedingVariants.length === 0) {
184
+ return products;
185
+ }
186
+ try {
187
+ const productIds = productsNeedingVariants
188
+ .map((p) => p._id)
189
+ .filter(Boolean);
190
+ if (productIds.length === 0) {
191
+ return products;
192
+ }
193
+ const items = [];
194
+ const res = await readOnlyVariantsV3
195
+ .queryVariants()
196
+ .in('productData.productId', productIds)
197
+ .limit(DEFAULT_QUERY_LIMIT)
198
+ .find();
199
+ items.push(...res.items);
200
+ let nextRes = res;
201
+ while (nextRes.hasNext()) {
202
+ nextRes = await nextRes.next();
203
+ items.push(...nextRes.items);
204
+ }
205
+ const variantsByProductId = new Map();
206
+ items.forEach((item) => {
207
+ const productId = item.productData?.productId;
208
+ if (productId) {
209
+ if (!variantsByProductId.has(productId)) {
210
+ variantsByProductId.set(productId, []);
211
+ }
212
+ variantsByProductId.get(productId).push({
213
+ ...item,
214
+ choices: item.optionChoices,
215
+ });
216
+ }
217
+ });
218
+ // Update products with their variants
219
+ return products.map((product) => {
220
+ const variants = variantsByProductId.get(product._id || '');
221
+ if (variants && variants.length > 0) {
222
+ return {
223
+ ...product,
224
+ variantsInfo: {
225
+ ...product.variantsInfo,
226
+ variants,
227
+ },
228
+ };
229
+ }
230
+ return product;
231
+ });
232
+ }
233
+ catch (error) {
234
+ console.error('Failed to fetch missing variants:', error);
235
+ return products;
236
+ }
237
+ };
238
+ /**
239
+ * Service definition for the Products List service.
240
+ * This defines the reactive API contract for managing a list of products with search, pagination, and filtering capabilities.
241
+ *
242
+ * @constant
243
+ */
244
+ export const ProductsListServiceDefinition = defineService('products-list');
245
+ /**
246
+ * Implementation of the Products List service that manages reactive products data.
247
+ * This service provides signals for products data, search options, pagination, aggregations,
248
+ * loading state, and error handling. It automatically re-fetches products when search options change.
249
+ *
250
+ * @example
251
+ * ```tsx
252
+ * import { ProductListService, ProductsListServiceDefinition } from '@wix/stores/services';
253
+ * import { useService } from '@wix/services-manager-react';
254
+ *
255
+ * function ProductsComponent({ productsConfig }) {
256
+ * return (
257
+ * <ServiceProvider services={createServicesMap([
258
+ * [ProductsListServiceDefinition, ProductListService.withConfig(productsConfig)]
259
+ * ])}>
260
+ * <ProductsDisplay />
261
+ * </ServiceProvider>
262
+ * );
263
+ * }
264
+ *
265
+ * function ProductsDisplay() {
266
+ * const productsService = useService(ProductsListServiceDefinition);
267
+ * const products = productsService.products.get();
268
+ * const isLoading = productsService.isLoading.get();
269
+ * const error = productsService.error.get();
270
+ *
271
+ * // Update search options to filter by category
272
+ * const filterByCategory = (categoryId: string) => {
273
+ * const currentOptions = productsService.searchOptions.get();
274
+ * productsService.setSearchOptions({
275
+ * ...currentOptions,
276
+ * filter: {
277
+ * ...currentOptions.filter,
278
+ * categoryIds: [categoryId]
279
+ * }
280
+ * });
281
+ * };
282
+ *
283
+ * if (isLoading) return <div>Loading products...</div>;
284
+ * if (error) return <div>Error: {error}</div>;
285
+ *
286
+ * return (
287
+ * <div>
288
+ * {products.map(product => (
289
+ * <div key={product._id}>
290
+ * <h3>{product.name}</h3>
291
+ * <p>{product.description}</p>
292
+ * </div>
293
+ * ))}
294
+ * </div>
295
+ * );
296
+ * }
297
+ * ```
298
+ */
299
+ export const ProductListService = implementService.withConfig()(ProductsListServiceDefinition, ({ getService, config }) => {
300
+ let firstRun = true;
301
+ const signalsService = getService(SignalsServiceDefinition);
302
+ const productsSignal = signalsService.signal(config.products);
303
+ const searchOptionsSignal = signalsService.signal(config.searchOptions);
304
+ const pagingMetadataSignal = signalsService.signal(config.pagingMetadata);
305
+ const minPriceSignal = signalsService.signal(getMinPrice(config.aggregations.results));
306
+ const maxPriceSignal = signalsService.signal(getMaxPrice(config.aggregations.results));
307
+ const availableProductOptionsSignal = signalsService.signal(getAvailableProductOptions(config.aggregations.results, config.customizations));
308
+ const availableInventoryStatusesSignal = signalsService.signal([
309
+ InventoryStatusType.IN_STOCK,
310
+ InventoryStatusType.OUT_OF_STOCK,
311
+ InventoryStatusType.PARTIALLY_OUT_OF_STOCK,
312
+ ]);
313
+ const aggregationsSignal = signalsService.signal(config.aggregations);
314
+ const isLoadingSignal = signalsService.signal(false);
315
+ const errorSignal = signalsService.signal(null);
316
+ if (typeof window !== 'undefined') {
317
+ signalsService.effect(async () => {
318
+ // CRITICAL: Read the signals FIRST to establish dependencies, even on first run
319
+ const searchOptions = searchOptionsSignal.get();
320
+ if (firstRun) {
321
+ firstRun = false;
322
+ return;
323
+ }
324
+ try {
325
+ isLoadingSignal.set(true);
326
+ const result = await fetchProducts(searchOptions);
327
+ productsSignal.set(result.products ?? []);
328
+ pagingMetadataSignal.set(result.pagingMetadata);
329
+ }
330
+ catch (error) {
331
+ errorSignal.set(error instanceof Error ? error.message : 'Unknown error');
332
+ }
333
+ finally {
334
+ isLoadingSignal.set(false);
335
+ }
336
+ });
337
+ }
338
+ firstRun = false;
339
+ const loadMoreCursor = async (count) => {
340
+ const affectiveSearchOptions = {
341
+ cursorPaging: {
342
+ cursor: pagingMetadataSignal.get().cursors?.next,
343
+ limit: DEFAULT_QUERY_LIMIT || count,
344
+ },
345
+ };
346
+ try {
347
+ isLoadingSignal.set(true);
348
+ const result = await fetchProducts(affectiveSearchOptions);
349
+ productsSignal.set([
350
+ ...productsSignal.get(),
351
+ ...(result.products ?? []),
352
+ ]);
353
+ pagingMetadataSignal.set(result.pagingMetadata);
354
+ }
355
+ catch (error) {
356
+ errorSignal.set(error instanceof Error ? error.message : 'Unknown error');
357
+ }
358
+ finally {
359
+ isLoadingSignal.set(false);
360
+ }
361
+ };
362
+ return {
363
+ products: productsSignal,
364
+ searchOptions: searchOptionsSignal,
365
+ pagingMetadata: pagingMetadataSignal,
366
+ aggregations: aggregationsSignal,
367
+ /* Metadata for products list */
368
+ minPrice: minPriceSignal,
369
+ maxPrice: maxPriceSignal,
370
+ availableInventoryStatuses: availableInventoryStatusesSignal,
371
+ availableProductOptions: availableProductOptionsSignal,
372
+ /* End of Metadata for products list */
373
+ setSearchOptions: (searchOptions) => {
374
+ searchOptionsSignal.set(searchOptions);
375
+ },
376
+ setSort: (sort) => {
377
+ const currentOptions = searchOptionsSignal.peek();
378
+ searchOptionsSignal.set({
379
+ ...currentOptions,
380
+ sort,
381
+ });
382
+ },
383
+ setFilter: (filter) => {
384
+ const currentOptions = searchOptionsSignal.peek();
385
+ searchOptionsSignal.set({
386
+ ...currentOptions,
387
+ filter,
388
+ });
389
+ },
390
+ resetFilter: () => {
391
+ const currentOptions = searchOptionsSignal.peek();
392
+ searchOptionsSignal.set({
393
+ ...currentOptions,
394
+ filter: {},
395
+ });
396
+ },
397
+ isFiltered: () => {
398
+ return signalsService.computed(() => {
399
+ const currentOptions = searchOptionsSignal.peek();
400
+ if (!currentOptions.filter)
401
+ return false;
402
+ return (currentOptions.filter !== undefined &&
403
+ Object.keys(currentOptions.filter).length > 0);
404
+ });
405
+ },
406
+ isLoading: isLoadingSignal,
407
+ error: errorSignal,
408
+ loadMore: (count) => {
409
+ loadMoreCursor(count);
410
+ },
411
+ hasMoreProducts: signalsService.computed(() => pagingMetadataSignal.get().hasNext ?? false),
412
+ };
413
+ });
414
+ function getMinPrice(aggregationData) {
415
+ const minPriceAggregation = aggregationData.find((data) => data.fieldPath === 'actualPriceRange.minValue.amount');
416
+ if (minPriceAggregation?.scalar?.value) {
417
+ return Number(minPriceAggregation.scalar.value) || 0;
418
+ }
419
+ return 0;
420
+ }
421
+ function getMaxPrice(aggregationData) {
422
+ const maxPriceAggregation = aggregationData.find((data) => data.fieldPath === 'actualPriceRange.maxValue.amount');
423
+ if (maxPriceAggregation?.scalar?.value) {
424
+ return Number(maxPriceAggregation.scalar.value) || 0;
425
+ }
426
+ return 0;
427
+ }
428
+ function getAvailableProductOptions(aggregationData = [], customizations = []) {
429
+ const matchesAggregationName = (name, aggregationNames) => {
430
+ return aggregationNames.some((aggName) => aggName.toLowerCase() === name.toLowerCase());
431
+ };
432
+ const sortChoicesIntelligently = (choices) => {
433
+ return [...choices].sort((a, b) => {
434
+ const aIsNumber = /^\d+$/.test(a.name);
435
+ const bIsNumber = /^\d+$/.test(b.name);
436
+ if (aIsNumber && bIsNumber) {
437
+ return parseInt(a.name) - parseInt(b.name);
438
+ }
439
+ if (aIsNumber && !bIsNumber)
440
+ return -1;
441
+ if (!aIsNumber && bIsNumber)
442
+ return 1;
443
+ return a.name.localeCompare(b.name);
444
+ });
445
+ };
446
+ const optionNames = [];
447
+ const choiceNames = [];
448
+ aggregationData.forEach((result) => {
449
+ if (result.name === 'optionNames' && result.values?.results) {
450
+ optionNames.push(...result.values.results
451
+ .map((item) => item.value)
452
+ .filter((value) => typeof value === 'string'));
453
+ }
454
+ if (result.name === 'choiceNames' && result.values?.results) {
455
+ choiceNames.push(...result.values.results
456
+ .map((item) => item.value)
457
+ .filter((value) => typeof value === 'string'));
458
+ }
459
+ });
460
+ const options = customizations
461
+ .filter((customization) => customization.name &&
462
+ customization._id &&
463
+ customization.customizationType ===
464
+ customizationsV3.CustomizationType.PRODUCT_OPTION &&
465
+ (optionNames.length === 0 ||
466
+ matchesAggregationName(customization.name, optionNames)))
467
+ .map((customization) => {
468
+ const choices = (customization.choicesSettings?.choices || [])
469
+ .filter((choice) => choice._id &&
470
+ choice.name &&
471
+ (choiceNames.length === 0 ||
472
+ matchesAggregationName(choice.name, choiceNames)))
473
+ .map((choice) => ({
474
+ id: choice._id,
475
+ name: choice.name,
476
+ colorCode: choice.colorCode,
477
+ }));
478
+ return {
479
+ id: customization._id,
480
+ name: customization.name,
481
+ choices: sortChoicesIntelligently(choices),
482
+ optionRenderType: customization.customizationRenderType,
483
+ };
484
+ })
485
+ .filter((option) => option.choices.length > 0);
486
+ return options;
487
+ }
488
+ /**
489
+ * Parse URL and build complete search options with all filters, sort, and pagination.
490
+ * This function extracts search parameters, filters, sorting, and pagination from a URL
491
+ * and converts them into the format expected by the Wix Stores API.
492
+ *
493
+ * @param {string} url - The URL to parse search parameters from
494
+ * @param {Category[]} categoriesList - List of available categories for category slug resolution
495
+ * @param {productsV3.V3ProductSearch} [defaultSearchOptions] - Default search options to merge with parsed URL parameters
496
+ * @returns {Promise<{searchOptions: productsV3.V3ProductSearch, initialSearchState: InitialSearchState}>}
497
+ * Object containing both API-ready search options and UI-ready initial state
498
+ *
499
+ * @example
500
+ * ```tsx
501
+ * // Parse URL with filters, sort, and pagination
502
+ * const categories = await loadCategoriesListServiceConfig();
503
+ * const { searchOptions, initialSearchState } = await parseUrlToSearchOptions(
504
+ * 'https://example.com/products?sort=price:desc&Color=red,blue&minPrice=50',
505
+ * categories.categories
506
+ * );
507
+ *
508
+ * // Use searchOptions for API calls
509
+ * const products = await productsV3.searchProducts(searchOptions);
510
+ *
511
+ * // Use initialSearchState for UI initialization
512
+ * const filterState = initialSearchState.productOptions; // { colorId: ['red-id', 'blue-id'] }
513
+ * ```
514
+ */
515
+ export async function parseUrlToSearchOptions(url, categoriesList, customizations, defaultSearchOptions) {
516
+ const urlObj = new URL(url);
517
+ const searchParams = urlObj.searchParams;
518
+ // Build search options
519
+ const searchOptions = {
520
+ cursorPaging: {
521
+ limit: DEFAULT_QUERY_LIMIT,
522
+ },
523
+ ...defaultSearchOptions,
524
+ };
525
+ // Initialize search state for service
526
+ const initialSearchState = {};
527
+ // Extract category slug from URL path
528
+ // The category slug is always the last segment of the path
529
+ const pathSegments = urlObj.pathname.split('/').filter(Boolean);
530
+ let category = undefined;
531
+ if (pathSegments.length > 0) {
532
+ const lastSegment = pathSegments[pathSegments.length - 1];
533
+ // Check if the last segment matches any category in the categories list
534
+ category = categoriesList.find((cat) => cat.slug === lastSegment);
535
+ if (category) {
536
+ initialSearchState.category = category;
537
+ }
538
+ }
539
+ // Handle text search (q parameter)
540
+ const query = searchParams.get('q');
541
+ if (query) {
542
+ searchOptions.search = {
543
+ expression: query,
544
+ };
545
+ }
546
+ // Handle sorting
547
+ const sort = searchParams.get('sort');
548
+ if (sort) {
549
+ const sortType = convertUrlSortToSortType(sort);
550
+ if (sortType) {
551
+ initialSearchState.sort = sortType;
552
+ // Apply sort to search options
553
+ switch (sortType) {
554
+ case SortType.NAME_ASC:
555
+ searchOptions.sort = [
556
+ { fieldName: 'name', order: productsV3.SortDirection.ASC },
557
+ ];
558
+ break;
559
+ case SortType.NAME_DESC:
560
+ searchOptions.sort = [
561
+ { fieldName: 'name', order: productsV3.SortDirection.DESC },
562
+ ];
563
+ break;
564
+ case SortType.PRICE_ASC:
565
+ searchOptions.sort = [
566
+ {
567
+ fieldName: 'actualPriceRange.minValue.amount',
568
+ order: productsV3.SortDirection.ASC,
569
+ },
570
+ ];
571
+ break;
572
+ case SortType.PRICE_DESC:
573
+ searchOptions.sort = [
574
+ {
575
+ fieldName: 'actualPriceRange.minValue.amount',
576
+ order: productsV3.SortDirection.DESC,
577
+ },
578
+ ];
579
+ break;
580
+ case SortType.RECOMMENDED:
581
+ searchOptions.sort = [
582
+ {
583
+ fieldName: 'name',
584
+ order: productsV3.SortDirection.DESC,
585
+ },
586
+ ];
587
+ break;
588
+ }
589
+ }
590
+ }
591
+ // Handle pagination
592
+ const limit = searchParams.get('limit');
593
+ const cursor = searchParams.get('cursor');
594
+ if (limit || cursor) {
595
+ searchOptions.cursorPaging = {};
596
+ if (limit) {
597
+ const limitNum = parseInt(limit, 10);
598
+ if (!isNaN(limitNum) && limitNum > 0) {
599
+ searchOptions.cursorPaging.limit = limitNum;
600
+ initialSearchState.limit = limitNum;
601
+ }
602
+ }
603
+ if (cursor) {
604
+ searchOptions.cursorPaging.cursor = cursor;
605
+ initialSearchState.cursor = cursor;
606
+ }
607
+ }
608
+ // Handle filtering for search options
609
+ const filter = {};
610
+ const visible = searchParams.get('visible');
611
+ if (visible !== null) {
612
+ filter['visible'] = visible === 'true';
613
+ initialSearchState.visible = visible === 'true';
614
+ }
615
+ const productType = searchParams.get('productType');
616
+ if (productType) {
617
+ filter['productType'] = productType;
618
+ initialSearchState.productType = productType;
619
+ }
620
+ // Add category filter if found
621
+ if (category) {
622
+ filter['allCategoriesInfo.categories'] = {
623
+ $matchItems: [{ _id: { $in: [category._id] } }],
624
+ };
625
+ }
626
+ // Price range filtering
627
+ const minPrice = searchParams.get('minPrice');
628
+ const maxPrice = searchParams.get('maxPrice');
629
+ if (minPrice || maxPrice) {
630
+ initialSearchState.priceRange = {};
631
+ if (minPrice) {
632
+ const minPriceNum = parseFloat(minPrice);
633
+ if (!isNaN(minPriceNum)) {
634
+ filter['actualPriceRange.minValue.amount'] = { $gte: minPriceNum };
635
+ initialSearchState.priceRange.min = minPriceNum;
636
+ }
637
+ }
638
+ if (maxPrice) {
639
+ const maxPriceNum = parseFloat(maxPrice);
640
+ if (!isNaN(maxPriceNum)) {
641
+ filter['actualPriceRange.maxValue.amount'] = { $lte: maxPriceNum };
642
+ initialSearchState.priceRange.max = maxPriceNum;
643
+ }
644
+ }
645
+ }
646
+ const inventoryStatus = searchParams.get('inventoryStatus');
647
+ if (inventoryStatus) {
648
+ filter['inventory.availabilityStatus'] = {
649
+ $in: inventoryStatus.split(','),
650
+ };
651
+ }
652
+ // Parse product options from URL parameters
653
+ const reservedParams = [
654
+ 'minPrice',
655
+ 'maxPrice',
656
+ 'inventory_status',
657
+ 'inventoryStatus',
658
+ 'visible',
659
+ 'productType',
660
+ 'q',
661
+ 'limit',
662
+ 'cursor',
663
+ 'sort',
664
+ ];
665
+ const productOptionsById = {};
666
+ for (const [optionName, optionValues] of searchParams.entries()) {
667
+ if (reservedParams.includes(optionName))
668
+ continue;
669
+ // Find the option by name in customizations
670
+ const option = customizations.find((c) => c.name === optionName &&
671
+ c.customizationType ===
672
+ customizationsV3.CustomizationType.PRODUCT_OPTION);
673
+ if (option && option._id) {
674
+ const choiceValues = optionValues.split(',').filter(Boolean);
675
+ const choiceIds = [];
676
+ // Convert choice names to IDs
677
+ for (const choiceName of choiceValues) {
678
+ const choice = option.choicesSettings?.choices?.find((c) => c.name === choiceName);
679
+ if (choice && choice._id) {
680
+ choiceIds.push(choice._id);
681
+ }
682
+ }
683
+ if (choiceIds.length > 0) {
684
+ productOptionsById[option._id] = choiceIds;
685
+ }
686
+ }
687
+ }
688
+ if (Object.keys(productOptionsById).length > 0) {
689
+ initialSearchState.productOptions = productOptionsById;
690
+ // Add product option filter to search options
691
+ filter[`options.choicesSettings.choices.choiceId`] = {
692
+ $hasSome: Object.values(productOptionsById).flat(),
693
+ };
694
+ }
695
+ // Add filter to search options if any filters were set
696
+ if (Object.keys(filter).length > 0) {
697
+ searchOptions.filter = filter;
698
+ }
699
+ // Add aggregations for getting filter options
700
+ searchOptions.aggregations = [
701
+ {
702
+ name: 'minPrice',
703
+ fieldPath: 'actualPriceRange.minValue.amount',
704
+ type: 'SCALAR',
705
+ scalar: { type: 'MIN' },
706
+ },
707
+ {
708
+ name: 'maxPrice',
709
+ fieldPath: 'actualPriceRange.maxValue.amount',
710
+ type: 'SCALAR',
711
+ scalar: { type: 'MAX' },
712
+ },
713
+ {
714
+ name: 'optionNames',
715
+ fieldPath: 'options.name',
716
+ type: productsV3.SortType.VALUE,
717
+ value: {
718
+ limit: 20,
719
+ sortType: productsV3.SortType.VALUE,
720
+ sortDirection: productsV3.SortDirection.ASC,
721
+ },
722
+ },
723
+ {
724
+ name: 'choiceNames',
725
+ fieldPath: 'options.choicesSettings.choices.name',
726
+ type: productsV3.SortType.VALUE,
727
+ value: {
728
+ limit: 50,
729
+ sortType: productsV3.SortType.VALUE,
730
+ sortDirection: productsV3.SortDirection.ASC,
731
+ },
732
+ },
733
+ {
734
+ name: 'inventoryStatus',
735
+ fieldPath: 'inventory.availabilityStatus',
736
+ type: productsV3.SortType.VALUE,
737
+ value: {
738
+ limit: 10,
739
+ sortType: productsV3.SortType.VALUE,
740
+ sortDirection: productsV3.SortDirection.ASC,
741
+ },
742
+ },
743
+ ];
744
+ return { searchOptions, initialSearchState };
745
+ }
746
+ /**
747
+ * Convert SortType enum to URL format
748
+ */
749
+ function convertSortTypeToUrl(sortType) {
750
+ switch (sortType) {
751
+ case SortType.NAME_ASC:
752
+ return 'name';
753
+ case SortType.NAME_DESC:
754
+ return 'name:desc';
755
+ case SortType.PRICE_ASC:
756
+ return 'price';
757
+ case SortType.PRICE_DESC:
758
+ return 'price:desc';
759
+ case SortType.NEWEST:
760
+ return 'newest';
761
+ case SortType.RECOMMENDED:
762
+ return 'recommended';
763
+ default:
764
+ return 'name';
765
+ }
766
+ }
767
+ /**
768
+ * Convert URL sort format to SortType enum
769
+ */
770
+ export function convertUrlSortToSortType(urlSort) {
771
+ const sortParts = urlSort.split(':');
772
+ const field = sortParts[0]?.toLowerCase();
773
+ const order = sortParts[1]?.toLowerCase() === 'desc' ? 'desc' : 'asc';
774
+ switch (field) {
775
+ case 'name':
776
+ return order === 'desc' ? SortType.NAME_DESC : SortType.NAME_ASC;
777
+ case 'price':
778
+ return order === 'desc' ? SortType.PRICE_DESC : SortType.PRICE_ASC;
779
+ case 'newest':
780
+ case 'created':
781
+ return SortType.NEWEST;
782
+ case 'recommended':
783
+ return SortType.RECOMMENDED;
784
+ default:
785
+ return null;
786
+ }
787
+ }
788
+ /**
789
+ * Update URL with current search state (sort, filters, pagination)
790
+ */
791
+ //@ts-expect-error
792
+ function updateUrlWithSearchState(searchState) {
793
+ if (typeof window === 'undefined')
794
+ return;
795
+ const { sort, filters, customizations, catalogBounds, categorySlug } = searchState;
796
+ // Convert filter IDs back to human-readable names for URL
797
+ const humanReadableOptions = {};
798
+ for (const [optionId, choiceIds] of Object.entries(filters?.productOptions ?? {})) {
799
+ const option = customizations.find((c) => c._id === optionId);
800
+ if (option && option.name) {
801
+ const choiceNames = [];
802
+ for (const choiceId of choiceIds) {
803
+ const choice = option.choicesSettings?.choices?.find((c) => c._id === choiceId);
804
+ if (choice && choice.name) {
805
+ choiceNames.push(choice.name);
806
+ }
807
+ }
808
+ if (choiceNames.length > 0) {
809
+ humanReadableOptions[option.name] = choiceNames;
810
+ }
811
+ }
812
+ }
813
+ // Start with current URL parameters to preserve non-search parameters
814
+ const params = new URLSearchParams(window.location.search);
815
+ // Define search-related parameters that we manage
816
+ const searchParams = [
817
+ 'sort',
818
+ 'limit',
819
+ 'cursor',
820
+ 'minPrice',
821
+ 'maxPrice',
822
+ 'inventoryStatus',
823
+ 'visible',
824
+ 'productType',
825
+ // Product option names will be dynamically added below
826
+ // Note: category is NOT included here as it's handled in the URL path
827
+ ];
828
+ // Remove existing search parameters first
829
+ searchParams.forEach((param) => params.delete(param));
830
+ // Remove existing product option parameters (they have dynamic names)
831
+ for (const customization of customizations) {
832
+ if (customization.customizationType ===
833
+ customizationsV3.CustomizationType.PRODUCT_OPTION &&
834
+ customization.name) {
835
+ params.delete(customization.name);
836
+ }
837
+ }
838
+ // Add sort parameter (only if not default)
839
+ const urlSort = convertSortTypeToUrl(sort);
840
+ if (sort !== SortType.NAME_ASC) {
841
+ params.set('sort', urlSort);
842
+ }
843
+ // Add price range parameters only if they differ from catalog bounds
844
+ if (filters.priceRange?.min &&
845
+ filters.priceRange.min > catalogBounds.minPrice) {
846
+ params.set('minPrice', filters.priceRange.min.toString());
847
+ }
848
+ if (filters.priceRange?.max &&
849
+ filters.priceRange.max < catalogBounds.maxPrice) {
850
+ params.set('maxPrice', filters.priceRange.max.toString());
851
+ }
852
+ // Add inventory status parameters
853
+ if (filters.inventoryStatuses && filters.inventoryStatuses.length > 0) {
854
+ params.set('inventoryStatus', filters.inventoryStatuses.join(','));
855
+ }
856
+ // Add visibility filter (only if explicitly false, since true is default)
857
+ if (filters.visible === false) {
858
+ params.set('visible', 'false');
859
+ }
860
+ // Add product type filter
861
+ if (filters.productType) {
862
+ params.set('productType', filters.productType);
863
+ }
864
+ // Add product options as individual parameters (Color=Red,Blue&Size=Large)
865
+ for (const [optionName, values] of Object.entries(humanReadableOptions)) {
866
+ if (values.length > 0) {
867
+ params.set(optionName, values.join(','));
868
+ }
869
+ }
870
+ // Handle URL path construction with category
871
+ let baseUrl = window.location.pathname;
872
+ // If categorySlug is provided, replace the last path segment (which represents the category)
873
+ if (categorySlug) {
874
+ const pathSegments = baseUrl.split('/').filter(Boolean);
875
+ if (pathSegments.length > 0) {
876
+ // Replace the last segment with the new category slug
877
+ pathSegments[pathSegments.length - 1] = categorySlug;
878
+ baseUrl = '/' + pathSegments.join('/');
879
+ }
880
+ else {
881
+ // If no segments, just use the category slug
882
+ baseUrl = `/${categorySlug}`;
883
+ }
884
+ }
885
+ // Build the new URL
886
+ const newUrl = params.toString()
887
+ ? `${baseUrl}?${params.toString()}`
888
+ : baseUrl;
889
+ // Only update if URL actually changed
890
+ if (newUrl !== window.location.pathname + window.location.search) {
891
+ window.history.pushState(null, '', newUrl);
892
+ }
893
+ }