@wix/headless-stores 0.0.54 → 0.0.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,235 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useService } from '@wix/services-manager-react';
3
+ import React from 'react';
4
+ import { ProductsListServiceDefinition } from '../services/products-list-service.js';
5
+ import * as CoreProductList from './core/ProductList.js';
6
+ import * as CoreProductListPagination from './core/ProductListPagination.js';
7
+ import * as Product from './Product.js';
8
+ import { renderAsChild } from '../utils/renderAsChild.js';
9
+ var TestIds;
10
+ (function (TestIds) {
11
+ TestIds["productListRoot"] = "product-list-root";
12
+ TestIds["productListProducts"] = "product-list-products";
13
+ TestIds["productListItem"] = "product-list-item";
14
+ TestIds["productListLoadMore"] = "product-list-load-more";
15
+ TestIds["productListTotalsDisplayed"] = "product-list-totals-displayed";
16
+ })(TestIds || (TestIds = {}));
17
+ /**
18
+ * Root component that provides the ProductList service context for rendering product lists.
19
+ *
20
+ * @order 1
21
+ * @component
22
+ * @example
23
+ * ```tsx
24
+ * import { ProductList } from '@wix/stores/components';
25
+ *
26
+ * function ProductListPage({ products }) {
27
+ * return (
28
+ * <ProductList.Root products={products}>
29
+ * <ProductList.Products>
30
+ * <ProductList.ProductRepeater>
31
+ * <Product.Name />
32
+ * <Product.Price />
33
+ * </ProductList.ProductRepeater>
34
+ * </ProductList.Products>
35
+ * </ProductList.Root>
36
+ * );
37
+ * }
38
+ * ```
39
+ */
40
+ export const Root = React.forwardRef((props, ref) => {
41
+ const { children, products, productsListConfig, productsListSearchConfig, className, } = props;
42
+ const serviceConfig = productsListConfig || {
43
+ products: products || [],
44
+ searchOptions: {
45
+ cursorPaging: { limit: 10 },
46
+ },
47
+ pagingMetadata: {
48
+ count: products?.length || 0,
49
+ },
50
+ aggregations: {}, // Empty aggregation data
51
+ };
52
+ return (_jsx(CoreProductList.Root, { productsListConfig: serviceConfig, productsListSearchConfig: productsListSearchConfig, children: _jsx(RootContent, { children: children, className: className, ref: ref }) }));
53
+ });
54
+ /**
55
+ * Internal component to handle the Root content with service access
56
+ */
57
+ const RootContent = React.forwardRef((props, ref) => {
58
+ const { children, className } = props;
59
+ const productsListService = useService(ProductsListServiceDefinition);
60
+ const contextProducts = productsListService.products.get();
61
+ const pagingMetadata = productsListService.pagingMetadata.get();
62
+ const displayedProducts = contextProducts.length;
63
+ const totalProducts = pagingMetadata.count || contextProducts.length;
64
+ const isFiltered = false; // TODO: Implement filtering detection
65
+ const attributes = {
66
+ 'data-testid': TestIds.productListRoot,
67
+ 'data-total-products': totalProducts,
68
+ 'data-displayed-products': displayedProducts,
69
+ 'data-filtered': isFiltered,
70
+ className,
71
+ };
72
+ return (_jsx("div", { ...attributes, ref: ref, children: children }));
73
+ });
74
+ /**
75
+ * Raw component that provides direct access to product list data.
76
+ * Similar to Product.Raw, this should only be used when you need custom access to list data.
77
+ *
78
+ * @component
79
+ * @example
80
+ * ```tsx
81
+ * <ProductList.Raw>
82
+ * {({ totalProducts, displayedProducts, isFiltered }) => (
83
+ * <div className="text-content-muted">
84
+ * Showing {displayedProducts} of {totalProducts} products
85
+ * {isFiltered && <span className="ml-2 text-brand-primary">(Filtered)</span>}
86
+ * </div>
87
+ * )}
88
+ * </ProductList.Raw>
89
+ * ```
90
+ */
91
+ export const Raw = React.forwardRef((props, _ref) => {
92
+ const { children } = props;
93
+ const productsListService = useService(ProductsListServiceDefinition);
94
+ const products = productsListService.products.get();
95
+ const pagingMetadata = productsListService.pagingMetadata.get();
96
+ const displayedProducts = products.length;
97
+ const totalProducts = pagingMetadata.count || products.length;
98
+ const isFiltered = false; // TODO: Implement filtering detection
99
+ return typeof children === 'function'
100
+ ? children({ totalProducts, displayedProducts, isFiltered })
101
+ : children;
102
+ });
103
+ /**
104
+ * Container for the product list with empty state support.
105
+ * Follows List Container Level pattern.
106
+ *
107
+ * @component
108
+ * @example
109
+ * ```tsx
110
+ * <ProductList.Products emptyState={<div>No products found</div>}>
111
+ * <ProductList.ProductRepeater>
112
+ * <Product.Name />
113
+ * <Product.Price />
114
+ * </ProductList.ProductRepeater>
115
+ * </ProductList.Products>
116
+ * ```
117
+ */
118
+ export const Products = React.forwardRef((props, ref) => {
119
+ const { children, emptyState, infiniteScroll = true, pageSize = 0, className, } = props;
120
+ const productsListService = useService(ProductsListServiceDefinition);
121
+ const products = productsListService.products.get();
122
+ const hasProducts = products.length > 0;
123
+ if (!hasProducts) {
124
+ return emptyState || null;
125
+ }
126
+ const attributes = {
127
+ 'data-testid': TestIds.productListProducts,
128
+ 'data-empty': !hasProducts,
129
+ 'data-infinite-scroll': infiniteScroll,
130
+ 'data-page-size': pageSize,
131
+ className,
132
+ };
133
+ return (_jsx("div", { ...attributes, ref: ref, children: children }));
134
+ });
135
+ /**
136
+ * Repeater component that renders Product.Root for each product.
137
+ * Follows Repeater Level pattern.
138
+ * Note: Repeater components do NOT support asChild as per architecture rules.
139
+ *
140
+ * @component
141
+ * @example
142
+ * ```tsx
143
+ * <ProductList.ProductRepeater>
144
+ * <Product.Name />
145
+ * <Product.Price />
146
+ * <Product.MediaGallery>
147
+ * <MediaGallery.Viewport />
148
+ * </Product.MediaGallery>
149
+ * </ProductList.ProductRepeater>
150
+ * ```
151
+ */
152
+ export const ProductRepeater = React.forwardRef((props, _ref) => {
153
+ const { children } = props;
154
+ const productsListService = useService(ProductsListServiceDefinition);
155
+ const products = productsListService.products.get();
156
+ const hasProducts = products.length > 0;
157
+ if (!hasProducts)
158
+ return null;
159
+ return (_jsx(_Fragment, { children: products.map((product) => (_jsx(Product.Root, { product: product, "data-testid": TestIds.productListItem, "data-product-id": product._id, "data-product-available": true, children: children }, product._id))) }));
160
+ });
161
+ /**
162
+ * Displays a button to load more products. Not rendered if infiniteScroll is false or no products are left to load.
163
+ * Follows the architecture rules - does not support asChild as it's a simple trigger component.
164
+ *
165
+ * @component
166
+ * @example
167
+ * ```tsx
168
+ * <ProductList.LoadMoreTrigger asChild>
169
+ * <button>Load More</button>
170
+ * </ProductList.LoadMoreTrigger>
171
+ * ```
172
+ */
173
+ export const LoadMoreTrigger = React.forwardRef((props, ref) => {
174
+ const { asChild, children, className } = props;
175
+ return (_jsx(CoreProductListPagination.LoadMoreTrigger, { children: ({ loadMore, hasMoreProducts, isLoading }) => {
176
+ // Don't render if no more products to load
177
+ if (!hasMoreProducts)
178
+ return null;
179
+ const handleClick = () => loadMore(10);
180
+ const attributes = {
181
+ 'data-testid': TestIds.productListLoadMore,
182
+ className,
183
+ onClick: handleClick,
184
+ disabled: isLoading,
185
+ };
186
+ if (asChild && React.isValidElement(children)) {
187
+ return React.cloneElement(children, {
188
+ ...attributes,
189
+ onClick: handleClick,
190
+ ref,
191
+ });
192
+ }
193
+ return (_jsx("button", { ...attributes, ref: ref, children: children }));
194
+ } }));
195
+ });
196
+ /**
197
+ * Displays the number of products currently displayed.
198
+ *
199
+ * @component
200
+ * @example
201
+ * ```tsx
202
+ * <ProductList.TotalsDisplayed />
203
+ * // or with asChild
204
+ * <ProductList.TotalsDisplayed asChild>
205
+ * <strong />
206
+ * </ProductList.TotalsDisplayed>
207
+ * // or with render function
208
+ * <ProductList.TotalsDisplayed asChild>
209
+ * {({ displayedProducts }, ref) => <strong ref={ref}>{displayedProducts}</strong>}
210
+ * </ProductList.TotalsDisplayed>
211
+ * ```
212
+ */
213
+ export const TotalsDisplayed = React.forwardRef((props, ref) => {
214
+ const { asChild, children, className } = props;
215
+ const productsListService = useService(ProductsListServiceDefinition);
216
+ const products = productsListService.products.get();
217
+ const displayedProducts = products.length;
218
+ const attributes = {
219
+ 'data-testid': TestIds.productListTotalsDisplayed,
220
+ 'data-displayed': displayedProducts,
221
+ className,
222
+ };
223
+ if (asChild) {
224
+ const rendered = renderAsChild({
225
+ children,
226
+ props: { displayedProducts },
227
+ ref,
228
+ content: displayedProducts.toString(),
229
+ attributes,
230
+ });
231
+ if (rendered)
232
+ return rendered;
233
+ }
234
+ return (_jsx("span", { ...attributes, ref: ref, children: displayedProducts }));
235
+ });
@@ -129,7 +129,8 @@ export interface ProductMediaProps {
129
129
  children: (props: ProductMediaRenderProps) => React.ReactNode;
130
130
  }
131
131
  export interface ProductMediaRenderProps {
132
- media: ProductMedia[];
132
+ mediaItems: ProductMedia[];
133
+ mainMedia?: ProductMedia;
133
134
  }
134
135
  export declare function Media(props: ProductMediaProps): import("react").ReactNode;
135
136
  export interface ProductProps {
@@ -146,3 +147,39 @@ export interface LoadingRenderProps {
146
147
  isLoading: boolean;
147
148
  }
148
149
  export declare function Loading(props: LoadingProps): import("react").ReactNode;
150
+ /**
151
+ * Props for ProductSlug headless component
152
+ */
153
+ export interface ProductSlugProps {
154
+ /** Render prop function that receives product slug data */
155
+ children: (props: ProductSlugRenderProps) => React.ReactNode;
156
+ }
157
+ /**
158
+ * Render props for ProductSlug component
159
+ */
160
+ export interface ProductSlugRenderProps {
161
+ /** Product slug */
162
+ slug: string;
163
+ }
164
+ /**
165
+ * Headless component for product slug display
166
+ *
167
+ * @component
168
+ * @example
169
+ * ```tsx
170
+ * import { Product } from '@wix/stores/components';
171
+ *
172
+ * function ProductSlugDisplay() {
173
+ * return (
174
+ * <Product.Slug>
175
+ * {({ slug }) => (
176
+ * <a href={`/product/${slug}`}>
177
+ * View Product
178
+ * </a>
179
+ * )}
180
+ * </Product.Slug>
181
+ * );
182
+ * }
183
+ * ```
184
+ */
185
+ export declare function Slug(props: ProductSlugProps): import("react").ReactNode;
@@ -105,9 +105,11 @@ export function Description(props) {
105
105
  export function Media(props) {
106
106
  const service = useService(ProductServiceDefinition);
107
107
  const product = service.product.get();
108
- const media = product.media?.itemsInfo?.items ?? [];
108
+ const mainMedia = product.media?.main;
109
+ const mediaItems = product.media?.itemsInfo?.items ?? [];
109
110
  return props.children({
110
- media,
111
+ mediaItems,
112
+ mainMedia,
111
113
  });
112
114
  }
113
115
  export function Content(props) {
@@ -124,3 +126,32 @@ export function Loading(props) {
124
126
  isLoading,
125
127
  });
126
128
  }
129
+ /**
130
+ * Headless component for product slug display
131
+ *
132
+ * @component
133
+ * @example
134
+ * ```tsx
135
+ * import { Product } from '@wix/stores/components';
136
+ *
137
+ * function ProductSlugDisplay() {
138
+ * return (
139
+ * <Product.Slug>
140
+ * {({ slug }) => (
141
+ * <a href={`/product/${slug}`}>
142
+ * View Product
143
+ * </a>
144
+ * )}
145
+ * </Product.Slug>
146
+ * );
147
+ * }
148
+ * ```
149
+ */
150
+ export function Slug(props) {
151
+ const service = useService(ProductServiceDefinition);
152
+ const product = service.product.get();
153
+ const slug = product.slug;
154
+ return props.children({
155
+ slug,
156
+ });
157
+ }
@@ -2,13 +2,14 @@ export * as CategoryListCore from './core/CategoryList.js';
2
2
  export * as CategoryCore from './core/Category.js';
3
3
  export * as ProductCore from './core/Product.js';
4
4
  export * as ProductModifiers from './core/ProductModifiers.js';
5
- export * as ProductList from './core/ProductList.js';
5
+ export * as ProductListCore from './core/ProductList.js';
6
6
  export * as ProductListFilters from './core/ProductListFilters.js';
7
7
  export * as ProductListPagination from './core/ProductListPagination.js';
8
8
  export * as ProductListSort from './core/ProductListSort.js';
9
9
  export * as ProductVariantSelector from './core/ProductVariantSelector.js';
10
10
  export * as SelectedVariant from './core/SelectedVariant.js';
11
11
  export * as Product from './Product.js';
12
+ export * as ProductList from './ProductList.js';
12
13
  export * as Option from './Option.js';
13
14
  export * as Choice from './Choice.js';
14
15
  export * as CategoryList from './CategoryList.js';
@@ -2,13 +2,14 @@ export * as CategoryListCore from './core/CategoryList.js';
2
2
  export * as CategoryCore from './core/Category.js';
3
3
  export * as ProductCore from './core/Product.js';
4
4
  export * as ProductModifiers from './core/ProductModifiers.js';
5
- export * as ProductList from './core/ProductList.js';
5
+ export * as ProductListCore from './core/ProductList.js';
6
6
  export * as ProductListFilters from './core/ProductListFilters.js';
7
7
  export * as ProductListPagination from './core/ProductListPagination.js';
8
8
  export * as ProductListSort from './core/ProductListSort.js';
9
9
  export * as ProductVariantSelector from './core/ProductVariantSelector.js';
10
10
  export * as SelectedVariant from './core/SelectedVariant.js';
11
11
  export * as Product from './Product.js';
12
+ export * as ProductList from './ProductList.js';
12
13
  export * as Option from './Option.js';
13
14
  export * as Choice from './Choice.js';
14
15
  export * as CategoryList from './CategoryList.js';
@@ -249,7 +249,7 @@ export const SelectedVariantService = implementService.withConfig()(SelectedVari
249
249
  else if (prod?.compareAtPriceRange?.minValue?.amount) {
250
250
  rawAmount = prod.compareAtPriceRange.minValue.amount;
251
251
  }
252
- return rawAmount ? `$${rawAmount}` : null;
252
+ return rawAmount && rawAmount !== '0' ? `$${rawAmount}` : null;
253
253
  });
254
254
  const isInStock = signalsService.computed(() => {
255
255
  const variant = currentVariant.get();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/headless-stores",
3
- "version": "0.0.54",
3
+ "version": "0.0.56",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "prebuild": "cd ../media && yarn build && cd ../ecom && yarn build",
@@ -63,7 +63,7 @@
63
63
  "@wix/ecom": "^1.0.1278",
64
64
  "@wix/essentials": "^0.1.24",
65
65
  "@wix/headless-ecom": "^0.0.14",
66
- "@wix/headless-media": "^0.0.8",
66
+ "@wix/headless-media": "^0.0.10",
67
67
  "@wix/redirects": "^1.0.83",
68
68
  "@wix/services-definitions": "^0.1.4",
69
69
  "@wix/services-manager-react": "^0.1.26"