@faststore/core 3.77.3 → 3.78.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 (168) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +49 -49
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/config.json +3 -3
  5. package/.next/cache/webpack/client-production/0.pack +0 -0
  6. package/.next/cache/webpack/client-production/index.pack +0 -0
  7. package/.next/cache/webpack/server-production/0.pack +0 -0
  8. package/.next/cache/webpack/server-production/index.pack +0 -0
  9. package/.next/prerender-manifest.js +1 -1
  10. package/.next/prerender-manifest.json +1 -1
  11. package/.next/react-loadable-manifest.json +71 -57
  12. package/.next/routes-manifest.json +1 -1
  13. package/.next/server/chunks/1270.js +1 -0
  14. package/.next/server/chunks/1911.js +1 -0
  15. package/.next/server/chunks/2430.js +1 -0
  16. package/.next/server/chunks/3006.js +1 -1
  17. package/.next/server/chunks/3836.js +1 -1
  18. package/.next/server/chunks/3945.js +1 -0
  19. package/.next/server/chunks/5402.js +1 -0
  20. package/.next/server/chunks/6698.js +1 -0
  21. package/.next/server/chunks/6789.js +1 -1
  22. package/.next/server/chunks/{7178.js → 8071.js} +1 -1
  23. package/.next/server/chunks/83.js +1 -1
  24. package/.next/server/chunks/831.js +1 -1
  25. package/.next/server/chunks/8474.js +9 -0
  26. package/.next/server/chunks/8563.js +1 -0
  27. package/.next/server/chunks/9088.js +1 -1
  28. package/.next/server/chunks/9117.js +1 -0
  29. package/.next/server/chunks/948.js +1 -1
  30. package/.next/server/chunks/9563.js +2 -2
  31. package/.next/server/chunks/9630.js +101 -4
  32. package/.next/server/chunks/9853.js +1 -1
  33. package/.next/server/chunks/UIBannerText.js +1 -1
  34. package/.next/server/chunks/UISKUMatrixSidebar.js +1 -1
  35. package/.next/server/middleware-build-manifest.js +1 -1
  36. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  37. package/.next/server/pages/404.js.nft.json +1 -1
  38. package/.next/server/pages/500.js.nft.json +1 -1
  39. package/.next/server/pages/[...slug].js +1 -1
  40. package/.next/server/pages/[...slug].js.nft.json +1 -1
  41. package/.next/server/pages/[slug]/p.js +1 -1
  42. package/.next/server/pages/[slug]/p.js.nft.json +1 -1
  43. package/.next/server/pages/_app.js.nft.json +1 -1
  44. package/.next/server/pages/_document.js.nft.json +1 -1
  45. package/.next/server/pages/_error.js.nft.json +1 -1
  46. package/.next/server/pages/account/403.js.nft.json +1 -1
  47. package/.next/server/pages/account/404.js.nft.json +1 -1
  48. package/.next/server/pages/account/[...unknown].js.nft.json +1 -1
  49. package/.next/server/pages/account/orders/[id].js.nft.json +1 -1
  50. package/.next/server/pages/account/orders.js +1 -1
  51. package/.next/server/pages/account/orders.js.nft.json +1 -1
  52. package/.next/server/pages/account/profile.js.nft.json +1 -1
  53. package/.next/server/pages/account/security.js.nft.json +1 -1
  54. package/.next/server/pages/account/user-details.js.nft.json +1 -1
  55. package/.next/server/pages/account.js.nft.json +1 -1
  56. package/.next/server/pages/api/graphql.js +2 -2
  57. package/.next/server/pages/api/graphql.js.nft.json +1 -1
  58. package/.next/server/pages/api/health/live.js.nft.json +1 -1
  59. package/.next/server/pages/api/health/ready.js.nft.json +1 -1
  60. package/.next/server/pages/api/preview.js.nft.json +1 -1
  61. package/.next/server/pages/checkout.js.nft.json +1 -1
  62. package/.next/server/pages/en-US/404.html +1 -1
  63. package/.next/server/pages/en-US/500.html +1 -1
  64. package/.next/server/pages/en-US/checkout.html +1 -1
  65. package/.next/server/pages/en-US/login.html +1 -1
  66. package/.next/server/pages/en-US/s.html +1 -1
  67. package/.next/server/pages/en-US.html +1 -1
  68. package/.next/server/pages/index.js.nft.json +1 -1
  69. package/.next/server/pages/login.js.nft.json +1 -1
  70. package/.next/server/pages/s.js +1 -1
  71. package/.next/server/pages/s.js.nft.json +1 -1
  72. package/.next/server/pages-manifest.json +1 -1
  73. package/.next/static/{xFgIjKNUaH5r_Yo0uIAnc → 6VSKTBCq7vRe86nXQK6nv}/_buildManifest.js +1 -1
  74. package/.next/static/chunks/3155.c3fa96f983101956.js +1 -0
  75. package/.next/static/chunks/3166-a1d98d71987c90d6.js +1 -0
  76. package/.next/static/chunks/3399.60aae5ddb9123ef5.js +1 -0
  77. package/.next/static/chunks/3836.a2f49cd66f78bcb2.js +1 -0
  78. package/.next/static/chunks/4836.ef87204971e182f4.js +1 -0
  79. package/.next/static/chunks/6789.634a5fcc1ed30b8d.js +1 -0
  80. package/.next/static/chunks/7026.e990acc86d95259d.js +1 -0
  81. package/.next/static/chunks/7191-2b424236f6799274.js +1 -0
  82. package/.next/static/chunks/7351.8ef7b6b1d6e1505a.js +1 -0
  83. package/.next/static/chunks/83.affac11ef34a0c11.js +1 -0
  84. package/.next/static/chunks/8325.b3ddbb43feda1a85.js +1 -0
  85. package/.next/static/chunks/8587.1d6d458c9b697351.js +1 -0
  86. package/.next/static/chunks/9173-acced9d62b9088c8.js +1 -0
  87. package/.next/static/chunks/9666-9be3f1e484eeb148.js +1 -0
  88. package/.next/static/chunks/{7351.e90a4cc21797c136.js → 9714.68af2d4bf27f560b.js} +1 -1
  89. package/.next/static/chunks/9781.dd4028663db7414b.js +1 -0
  90. package/.next/static/chunks/BannerNewsletter.d91d065d644d15f3.js +1 -0
  91. package/.next/static/chunks/BannerText.3bc2f0af3687e984.js +1 -0
  92. package/.next/static/chunks/CartSidebar.f2f885b6d9a227e2.js +1 -0
  93. package/.next/static/chunks/ProductShelf.dcdeffe85dca1ace.js +1 -0
  94. package/.next/static/chunks/ProductTiles.12e553830401871d.js +1 -0
  95. package/.next/static/chunks/RegionModal.0aff964cb36eb49a.js +1 -0
  96. package/.next/static/chunks/RegionSlider.cbf2ac28eeac8dbe.js +1 -0
  97. package/.next/static/chunks/Toast.c06d4e2e2e7913c5.js +1 -0
  98. package/.next/static/chunks/UIBannerText.08c65db460bd1695.js +1 -0
  99. package/.next/static/chunks/UIToast.55af19d2eba3d8a1.js +1 -0
  100. package/.next/static/chunks/pages/{[...slug]-b3a9bdfcf0127006.js → [...slug]-1e68a914dfca7da9.js} +1 -1
  101. package/.next/static/chunks/pages/[slug]/p-2c73921b1ab00c27.js +1 -0
  102. package/.next/static/chunks/pages/_app-57478e0d1d2ddf62.js +1 -0
  103. package/.next/static/chunks/pages/{s-989fccebe1b60a4e.js → s-e7745eef6c834c3e.js} +1 -1
  104. package/.next/static/chunks/webpack-d29ca7e9684f9a33.js +1 -0
  105. package/.next/static/css/{70353bf19c496790.css → 297be4be3be36ff0.css} +1 -1
  106. package/.next/static/css/{0a57ee6c7a57788c.css → 4b8252ed2f23ac67.css} +1 -1
  107. package/.next/static/css/{6831395ff5fd317a.css → 6d92375b6ee8276a.css} +1 -1
  108. package/.next/static/css/8f6350925b347380.css +1 -0
  109. package/.next/trace +136 -135
  110. package/.turbo/turbo-build.log +19 -19
  111. package/.turbo/turbo-test.log +5 -5
  112. package/@generated/gql.ts +20 -4
  113. package/@generated/graphql.ts +310 -6
  114. package/@generated/persisted-documents.json +6 -5
  115. package/@generated/schema.graphql +39 -1
  116. package/CHANGELOG.md +6 -0
  117. package/cms/faststore/sections.json +99 -0
  118. package/package.json +4 -4
  119. package/src/components/product/ProductCard/ProductCard.tsx +74 -35
  120. package/src/components/product/ProductGrid/ProductGrid.tsx +16 -0
  121. package/src/components/sections/ProductDetails/ProductDetails.tsx +4 -0
  122. package/src/components/sections/ProductGallery/DefaultComponents.ts +15 -0
  123. package/src/components/sections/ProductGallery/ProductGallery.tsx +1 -0
  124. package/src/components/sections/ProductGallery/section.module.scss +16 -3
  125. package/src/components/ui/ProductComparison/ProductComparisonSidebar.tsx +256 -0
  126. package/src/components/ui/ProductComparison/index.ts +1 -0
  127. package/src/components/ui/ProductGallery/ProductGallery.tsx +251 -173
  128. package/src/components/ui/ProductGallery/ProductGalleryPage.tsx +6 -0
  129. package/src/sdk/product/useProductsSelected.tsx +45 -0
  130. package/src/typings/overrides.ts +30 -1
  131. package/test/server/index.test.ts +1 -0
  132. package/.next/server/chunks/1333.js +0 -1
  133. package/.next/server/chunks/2295.js +0 -1
  134. package/.next/server/chunks/2778.js +0 -9
  135. package/.next/server/chunks/3918.js +0 -1
  136. package/.next/server/chunks/3963.js +0 -1
  137. package/.next/server/chunks/5607.js +0 -1
  138. package/.next/server/chunks/6335.js +0 -1
  139. package/.next/server/chunks/76.js +0 -1
  140. package/.next/server/chunks/7794.js +0 -1
  141. package/.next/server/chunks/839.js +0 -1
  142. package/.next/static/chunks/3155.243c7558a71f0695.js +0 -1
  143. package/.next/static/chunks/3166-e2e2d3255aa5f208.js +0 -1
  144. package/.next/static/chunks/3399.93804fb74f79436c.js +0 -1
  145. package/.next/static/chunks/3836.c0487aea7bd0c6f0.js +0 -1
  146. package/.next/static/chunks/5781.28d03feacead66ad.js +0 -1
  147. package/.next/static/chunks/6355.454b14737c2bf69c.js +0 -1
  148. package/.next/static/chunks/6857.b2c06171638955ea.js +0 -1
  149. package/.next/static/chunks/7191-7badbb9888b79ce9.js +0 -1
  150. package/.next/static/chunks/7481.3c4ad3642e346232.js +0 -1
  151. package/.next/static/chunks/7498-0dc4f9a9ed199d3a.js +0 -1
  152. package/.next/static/chunks/83.ee1fdbe283ac65b6.js +0 -1
  153. package/.next/static/chunks/9173-3a00edf7b695a319.js +0 -1
  154. package/.next/static/chunks/BannerNewsletter.fe0181162c046991.js +0 -1
  155. package/.next/static/chunks/BannerText.681f118e9f149f6c.js +0 -1
  156. package/.next/static/chunks/CartSidebar.55cc31a37ffa6ee6.js +0 -1
  157. package/.next/static/chunks/ProductShelf.b75a7ab8e313ea07.js +0 -1
  158. package/.next/static/chunks/ProductTiles.35cd23ada22f5a96.js +0 -1
  159. package/.next/static/chunks/RegionModal.04b02aafc0836d49.js +0 -1
  160. package/.next/static/chunks/RegionSlider.d063ccee38bdfdb7.js +0 -1
  161. package/.next/static/chunks/Toast.75a18f47eb23b703.js +0 -1
  162. package/.next/static/chunks/UIBannerText.f4167ceafb96cf67.js +0 -1
  163. package/.next/static/chunks/UIToast.a49584c87d3adc17.js +0 -1
  164. package/.next/static/chunks/pages/[slug]/p-73a253032aaf8ce2.js +0 -1
  165. package/.next/static/chunks/pages/_app-df4ba03d82beaf86.js +0 -1
  166. package/.next/static/chunks/webpack-c996443db6f4fa91.js +0 -1
  167. package/.next/static/css/4bd5e2314f697713.css +0 -1
  168. /package/.next/static/{xFgIjKNUaH5r_Yo0uIAnc → 6VSKTBCq7vRe86nXQK6nv}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  import { NextSeo } from 'next-seo'
2
2
  import dynamic from 'next/dynamic'
3
- import { Suspense, lazy, type MouseEvent } from 'react'
3
+ import { Suspense, lazy, useState, type MouseEvent } from 'react'
4
4
 
5
5
  import { useSearch } from '@faststore/sdk'
6
6
  import { useUI } from '@faststore/ui'
@@ -22,6 +22,9 @@ import { useDelayedPagination } from 'src/sdk/search/useDelayedPagination'
22
22
  import { useFilter } from 'src/sdk/search/useFilter'
23
23
  import useScreenResize from 'src/sdk/ui/useScreenResize'
24
24
 
25
+ import styles from '../../sections/ProductGallery/section.module.scss'
26
+ import { useFormattedPrice } from 'src/sdk/product/useFormattedPrice'
27
+
25
28
  const ProductGalleryPage = lazy(() => import('./ProductGalleryPage'))
26
29
  const FilterSkeleton = dynamic(
27
30
  () =>
@@ -60,6 +63,29 @@ export interface ProductGalleryProps {
60
63
  alt: string
61
64
  }
62
65
  }
66
+ productComparison?: {
67
+ enabled?: boolean
68
+ labels?: {
69
+ compareButton: string
70
+ clearSelectionButton: string
71
+ selectionWarning: string
72
+ sidebarComponent?: {
73
+ title: string
74
+ sortLabel: string
75
+ filterLabel: string
76
+ productNameFilterLabel: string
77
+ preferencesLabel: string
78
+ toggleFieldLabel: string
79
+ cartButtonLabel: string
80
+ priceLabel: string
81
+ priceWithTaxLabel: string
82
+ }
83
+ technicalInformation?: {
84
+ title: string
85
+ description: string
86
+ }
87
+ }
88
+ }
63
89
  itemsPerPage?: number
64
90
  loadMorePageButton?: {
65
91
  label?: string
@@ -82,6 +108,7 @@ function ProductGallery({
82
108
  loadMorePageButton,
83
109
  sortBySelector,
84
110
  productCard,
111
+ productComparison,
85
112
  }: ProductGalleryProps) {
86
113
  const {
87
114
  FilterButtonSkeleton,
@@ -92,8 +119,12 @@ function ProductGallery({
92
119
  PrevIcon,
93
120
  ResultsCountSkeleton,
94
121
  SortSkeleton,
122
+ ToggleField,
123
+ ProductComparison,
124
+ ProductComparisonToolbar,
95
125
  __experimentalFilterDesktop: FilterDesktop,
96
126
  __experimentalFilterSlider: FilterSlider,
127
+ __experimentalProductComparisonSidebar: ProductComparisonSidebar,
97
128
  } = useOverrideComponents<'ProductGallery'>()
98
129
 
99
130
  const { openFilter, filter: displayFilter } = useUI()
@@ -102,11 +133,15 @@ function ProductGallery({
102
133
  const data = context?.data
103
134
  const facets = useDelayedFacets(data) ?? []
104
135
  const { next, prev } = useDelayedPagination(totalCount)
105
- const { isDesktop } = useScreenResize()
136
+
137
+ const [showComparisonProducts, setShowComparisonProducts] =
138
+ useState<boolean>(false)
106
139
 
107
140
  useProductsPrefetch(prev ? prev.cursor : null)
108
141
  useProductsPrefetch(next ? next.cursor : null)
109
142
 
143
+ const { isDesktop } = useScreenResize()
144
+
110
145
  const hasFacetsLoaded = Boolean(data?.search?.facets)
111
146
  const hasProductsLoaded = Boolean(data?.search?.products)
112
147
  const initialSelectedFacets =
@@ -125,184 +160,227 @@ function ProductGallery({
125
160
  </h1>
126
161
  </header>
127
162
  )}
128
- <div
129
- data-fs-product-listing-content-grid
130
- data-fs-content="product-gallery"
131
- >
132
- {isDesktop && (
133
- <div data-fs-product-listing-filters>
134
- <FilterSkeleton loading={!hasFacetsLoaded}>
163
+ <ProductComparison.Component>
164
+ <div
165
+ data-fs-product-listing-content-grid
166
+ data-fs-content="product-gallery"
167
+ >
168
+ {isDesktop && (
169
+ <div data-fs-product-listing-filters>
170
+ <FilterSkeleton loading={!hasFacetsLoaded}>
171
+ {hasFacetsLoaded && facets?.length > 0 && (
172
+ <div className="hidden-mobile">
173
+ <FilterDesktop.Component
174
+ {...FilterDesktop.props}
175
+ {...filter}
176
+ title={filterCmsData?.title}
177
+ />
178
+ </div>
179
+ )}
180
+ </FilterSkeleton>
181
+ </div>
182
+ )}
183
+ {!isDesktop && displayFilter && (
184
+ <div data-fs-product-listing-filters>
185
+ <FilterSlider.Component
186
+ {...FilterSlider.props}
187
+ {...filter}
188
+ title={filterCmsData?.title}
189
+ clearButtonLabel={filterCmsData?.mobileOnly?.clearButtonLabel}
190
+ applyButtonLabel={filterCmsData?.mobileOnly?.applyButtonLabel}
191
+ />
192
+ </div>
193
+ )}
194
+ <div data-fs-product-listing-results-count data-count={totalCount}>
195
+ <ResultsCountSkeleton.Component
196
+ data-fs-product-listing-results-count-skeleton
197
+ size={{ width: '100%', height: '1.5rem' }}
198
+ {...ResultsCountSkeleton.props}
199
+ // Dynamic props shouldn't be overridable
200
+ // This decision can be reviewed later if needed
201
+ loading={!hasProductsLoaded}
202
+ >
203
+ <h2 data-testid="total-product-count">
204
+ {totalCount} {totalCountLabel}
205
+ </h2>
206
+ </ResultsCountSkeleton.Component>
207
+ {productComparison?.enabled && (
208
+ <ToggleField.Component
209
+ id="toggle-field-comparison"
210
+ label={productComparison?.labels?.compareButton}
211
+ checked={showComparisonProducts}
212
+ onChange={() =>
213
+ setShowComparisonProducts(!showComparisonProducts)
214
+ }
215
+ {...ToggleField.props}
216
+ />
217
+ )}
218
+ </div>
219
+
220
+ <div data-fs-product-listing-sort>
221
+ <SortSkeleton.Component
222
+ data-fs-product-listing-sort-skeleton
223
+ size={{ width: 'auto', height: '1.5rem' }}
224
+ {...SortSkeleton.props}
225
+ // Dynamic props shouldn't be overridable
226
+ // This decision can be reviewed later if needed
227
+ loading={!hasProductsLoaded}
228
+ >
229
+ <Sort
230
+ label={sortBySelector?.label}
231
+ options={sortBySelector?.options}
232
+ />
233
+ </SortSkeleton.Component>
234
+ <FilterButtonSkeleton.Component
235
+ data-fs-product-listing-filter-button-skeleton
236
+ size={{ width: '6rem', height: '1.5rem' }}
237
+ {...FilterButtonSkeleton.props}
238
+ // Dynamic props shouldn't be overridable
239
+ // This decision can be reviewed later if needed
240
+ loading={!hasFacetsLoaded}
241
+ >
135
242
  {hasFacetsLoaded && facets?.length > 0 && (
136
- <div className="hidden-mobile">
137
- <FilterDesktop.Component
138
- {...FilterDesktop.props}
139
- {...filter}
140
- title={filterCmsData?.title}
141
- />
142
- </div>
243
+ <MobileFilterButton.Component
244
+ variant="tertiary"
245
+ data-testid="open-filter-button"
246
+ icon={
247
+ <FilterIcon.Component
248
+ width={16}
249
+ height={16}
250
+ {...FilterIcon.props}
251
+ name={
252
+ filterCmsData?.mobileOnly?.filterButton?.icon?.icon ??
253
+ FilterIcon.props.name
254
+ }
255
+ aria-label={
256
+ filterCmsData?.mobileOnly?.filterButton?.icon?.alt ??
257
+ FilterIcon.props['aria-label']
258
+ }
259
+ />
260
+ }
261
+ iconPosition="left"
262
+ {...MobileFilterButton.props}
263
+ // Dynamic props shouldn't be overridable
264
+ // This decision can be reviewed later if needed
265
+ onClick={openFilter}
266
+ >
267
+ {filterCmsData?.mobileOnly?.filterButton?.label}
268
+ </MobileFilterButton.Component>
143
269
  )}
144
- </FilterSkeleton>
270
+ </FilterButtonSkeleton.Component>
145
271
  </div>
146
- )}
147
- {!isDesktop && displayFilter && (
148
- <div data-fs-product-listing-filters>
149
- <FilterSlider.Component
150
- {...FilterSlider.props}
151
- {...filter}
152
- title={filterCmsData?.title}
153
- clearButtonLabel={filterCmsData?.mobileOnly?.clearButtonLabel}
154
- applyButtonLabel={filterCmsData?.mobileOnly?.applyButtonLabel}
155
- />
156
- </div>
157
- )}
158
- <div data-fs-product-listing-results-count data-count={totalCount}>
159
- <ResultsCountSkeleton.Component
160
- data-fs-product-listing-results-count-skeleton
161
- size={{ width: '100%', height: '1.5rem' }}
162
- {...ResultsCountSkeleton.props}
163
- // Dynamic props shouldn't be overridable
164
- // This decision can be reviewed later if needed
165
- loading={!hasProductsLoaded}
166
- >
167
- <h2 data-testid="total-product-count">
168
- {totalCount} {totalCountLabel}
169
- </h2>
170
- </ResultsCountSkeleton.Component>
171
- </div>
172
- <div data-fs-product-listing-sort>
173
- <SortSkeleton.Component
174
- data-fs-product-listing-sort-skeleton
175
- size={{ width: 'auto', height: '1.5rem' }}
176
- {...SortSkeleton.props}
177
- // Dynamic props shouldn't be overridable
178
- // This decision can be reviewed later if needed
179
- loading={!hasProductsLoaded}
180
- >
181
- <Sort
182
- label={sortBySelector?.label}
183
- options={sortBySelector?.options}
184
- />
185
- </SortSkeleton.Component>
186
- <FilterButtonSkeleton.Component
187
- data-fs-product-listing-filter-button-skeleton
188
- size={{ width: '6rem', height: '1.5rem' }}
189
- {...FilterButtonSkeleton.props}
190
- // Dynamic props shouldn't be overridable
191
- // This decision can be reviewed later if needed
192
- loading={!hasFacetsLoaded}
193
- >
194
- {hasFacetsLoaded && facets?.length > 0 && (
195
- <MobileFilterButton.Component
196
- variant="tertiary"
197
- data-testid="open-filter-button"
198
- icon={
199
- <FilterIcon.Component
200
- width={16}
201
- height={16}
202
- {...FilterIcon.props}
203
- name={
204
- filterCmsData?.mobileOnly?.filterButton?.icon?.icon ??
205
- FilterIcon.props.name
206
- }
207
- aria-label={
208
- filterCmsData?.mobileOnly?.filterButton?.icon?.alt ??
209
- FilterIcon.props['aria-label']
210
- }
211
- />
212
- }
213
- iconPosition="left"
214
- {...MobileFilterButton.props}
215
- // Dynamic props shouldn't be overridable
216
- // This decision can be reviewed later if needed
217
- onClick={openFilter}
218
- >
219
- {filterCmsData?.mobileOnly?.filterButton?.label}
220
- </MobileFilterButton.Component>
272
+ <div data-fs-product-listing-results>
273
+ {/* Add link to previous page. This helps on SEO */}
274
+ {!!prev && (
275
+ <div data-fs-product-listing-pagination="top">
276
+ <NextSeo
277
+ additionalLinkTags={[{ rel: 'prev', href: prev.link }]}
278
+ />
279
+ <LinkButtonPrev.Component
280
+ rel="prev"
281
+ variant="secondary"
282
+ iconPosition="left"
283
+ icon={
284
+ <PrevIcon.Component
285
+ width={16}
286
+ height={16}
287
+ weight="bold"
288
+ {...PrevIcon.props}
289
+ name={
290
+ previousPageButton?.icon?.icon ?? PrevIcon.props.name
291
+ }
292
+ aria-label={
293
+ previousPageButton?.icon?.alt ??
294
+ previousPageButton?.label ??
295
+ PrevIcon.props['aria-label']
296
+ }
297
+ />
298
+ }
299
+ {...LinkButtonPrev.props}
300
+ // Dynamic props shouldn't be overridable
301
+ // This decision can be reviewed later if needed
302
+ onClick={(e: MouseEvent<HTMLElement>) => {
303
+ e.currentTarget.blur()
304
+ e.preventDefault()
305
+ addPrevPage()
306
+ }}
307
+ href={prev.link}
308
+ >
309
+ {previousPageButton?.label}
310
+ </LinkButtonPrev.Component>
311
+ </div>
221
312
  )}
222
- </FilterButtonSkeleton.Component>
223
- </div>
224
- <div data-fs-product-listing-results>
225
- {/* Add link to previous page. This helps on SEO */}
226
- {prev !== false && (
227
- <div data-fs-product-listing-pagination="top">
228
- <NextSeo
229
- additionalLinkTags={[{ rel: 'prev', href: prev.link }]}
230
- />
231
- <LinkButtonPrev.Component
232
- rel="prev"
233
- variant="secondary"
234
- iconPosition="left"
235
- icon={
236
- <PrevIcon.Component
237
- width={16}
238
- height={16}
239
- weight="bold"
240
- {...PrevIcon.props}
241
- name={previousPageButton?.icon?.icon ?? PrevIcon.props.name}
242
- aria-label={
243
- previousPageButton?.icon?.alt ??
244
- previousPageButton?.label ??
245
- PrevIcon.props['aria-label']
246
- }
313
+ {/* Render ALL products */}
314
+ {hasProductsLoaded ? (
315
+ <Suspense fallback={GalleryPageSkeleton}>
316
+ {pages.map((page) => (
317
+ <ProductGalleryPage
318
+ key={`gallery-page-${page}`}
319
+ page={page}
320
+ title={title}
321
+ productCard={productCard}
322
+ itemsPerPage={itemsPerPage}
323
+ firstPage={pages[0]}
324
+ shouldShowComparison={showComparisonProducts}
325
+ compareLabel={productComparison?.labels?.compareButton}
247
326
  />
248
- }
249
- {...LinkButtonPrev.props}
250
- // Dynamic props shouldn't be overridable
251
- // This decision can be reviewed later if needed
252
- onClick={(e: MouseEvent<HTMLElement>) => {
253
- e.currentTarget.blur()
254
- e.preventDefault()
255
- addPrevPage()
256
- }}
257
- href={prev.link}
258
- >
259
- {previousPageButton?.label}
260
- </LinkButtonPrev.Component>
261
- </div>
262
- )}
263
- {/* Render ALL products */}
264
- {hasProductsLoaded ? (
265
- <Suspense fallback={GalleryPageSkeleton}>
266
- {pages.map((page) => (
267
- <ProductGalleryPage
268
- key={`gallery-page-${page}`}
269
- page={page}
270
- title={title}
271
- productCard={productCard}
272
- itemsPerPage={itemsPerPage}
273
- firstPage={pages[0]}
327
+ ))}
328
+ </Suspense>
329
+ ) : (
330
+ GalleryPageSkeleton
331
+ )}
332
+ {/* Add link to next page. This helps on SEO */}
333
+ {next !== false && (
334
+ <div data-fs-product-listing-pagination="bottom">
335
+ <NextSeo
336
+ additionalLinkTags={[{ rel: 'next', href: next.link }]}
274
337
  />
275
- ))}
276
- </Suspense>
277
- ) : (
278
- GalleryPageSkeleton
279
- )}
280
- {/* Add link to next page. This helps on SEO */}
281
- {next !== false && (
282
- <div data-fs-product-listing-pagination="bottom">
283
- <NextSeo
284
- additionalLinkTags={[{ rel: 'next', href: next.link }]}
285
- />
286
- <LinkButtonNext.Component
287
- testId="show-more"
288
- rel="next"
289
- variant="secondary"
290
- {...LinkButtonNext.props}
291
- // Dynamic props shouldn't be overridable
292
- // This decision can be reviewed later if needed
293
- onClick={(e: MouseEvent<HTMLElement>) => {
294
- e.currentTarget.blur()
295
- e.preventDefault()
296
- addNextPage()
297
- }}
298
- href={next.link}
299
- >
300
- {loadMorePageButton?.label}
301
- </LinkButtonNext.Component>
302
- </div>
303
- )}
338
+ <LinkButtonNext.Component
339
+ testId="show-more"
340
+ rel="next"
341
+ variant="secondary"
342
+ {...LinkButtonNext.props}
343
+ // Dynamic props shouldn't be overridable
344
+ // This decision can be reviewed later if needed
345
+ onClick={(e: MouseEvent<HTMLElement>) => {
346
+ e.currentTarget.blur()
347
+ e.preventDefault()
348
+ addNextPage()
349
+ }}
350
+ href={next.link}
351
+ >
352
+ {loadMorePageButton?.label}
353
+ </LinkButtonNext.Component>
354
+ </div>
355
+ )}
356
+ </div>
304
357
  </div>
305
- </div>
358
+ {showComparisonProducts && (
359
+ <>
360
+ <ProductComparisonSidebar.Component
361
+ direction="rightSide"
362
+ size="partial"
363
+ priceFormatter={useFormattedPrice}
364
+ technicalInformation={{
365
+ title: productComparison?.labels?.technicalInformation?.title,
366
+ description:
367
+ productComparison?.labels?.technicalInformation?.description,
368
+ }}
369
+ overlayProps={{ className: styles.section }}
370
+ {...productComparison.labels.sidebarComponent}
371
+ />
372
+ <ProductComparisonToolbar.Component
373
+ selectionWarningLabel={
374
+ productComparison?.labels?.selectionWarning
375
+ }
376
+ clearSelectionButtonLabel={
377
+ productComparison?.labels?.clearSelectionButton
378
+ }
379
+ compareButtonLabel={productComparison?.labels?.compareButton}
380
+ />
381
+ </>
382
+ )}
383
+ </ProductComparison.Component>
306
384
  </section>
307
385
  )
308
386
  }
@@ -14,6 +14,8 @@ interface Props {
14
14
  >
15
15
  itemsPerPage: number
16
16
  firstPage: number
17
+ shouldShowComparison?: boolean
18
+ compareLabel?: string
17
19
  }
18
20
 
19
21
  function ProductGalleryPage({
@@ -22,6 +24,8 @@ function ProductGalleryPage({
22
24
  productCard,
23
25
  itemsPerPage,
24
26
  firstPage,
27
+ shouldShowComparison,
28
+ compareLabel,
25
29
  }: Props) {
26
30
  const { data } = useGalleryPage(page)
27
31
 
@@ -35,6 +39,8 @@ function ProductGalleryPage({
35
39
  title={title}
36
40
  >
37
41
  <ProductGrid
42
+ shouldShowComparison={shouldShowComparison}
43
+ compareLabel={compareLabel}
38
44
  products={products}
39
45
  page={page}
40
46
  pageSize={itemsPerPage}
@@ -0,0 +1,45 @@
1
+ import { gql } from '@generated'
2
+ import { useQuery } from 'src/sdk/graphql/useQuery'
3
+ import { useSession } from 'src/sdk/session'
4
+ import { useMemo } from 'react'
5
+ import type {
6
+ ClientManyProductsSelectedQueryQuery,
7
+ ClientManyProductsSelectedQueryQueryVariables,
8
+ ClientSearchSuggestionsQueryQuery,
9
+ } from '@generated/graphql'
10
+
11
+ const query = gql(`
12
+ query ClientManyProductsSelectedQuery(
13
+ $productIds: [String!]!
14
+ ) {
15
+ products(productIds: $productIds) {
16
+ ...ProductComparisonFragment_product
17
+ }
18
+ }
19
+ `)
20
+
21
+ export const useProductsSelected = (
22
+ productIds: string[],
23
+ enabled: boolean,
24
+ processResponse: (data: ClientManyProductsSelectedQueryQuery) => void
25
+ ) => {
26
+ const { channel, locale } = useSession()
27
+ const variables = useMemo(() => {
28
+ if (!channel) {
29
+ throw new Error(
30
+ `useProductsSelected: 'channel' from session is an empty string.`
31
+ )
32
+ }
33
+
34
+ return { productIds }
35
+ }, [channel, locale, productIds])
36
+
37
+ return useQuery<
38
+ ClientSearchSuggestionsQueryQuery,
39
+ ClientManyProductsSelectedQueryQueryVariables
40
+ >(query, variables, {
41
+ doNotRun: enabled,
42
+ onSuccess: (data: ClientManyProductsSelectedQueryQuery) =>
43
+ processResponse(data),
44
+ })
45
+ }
@@ -29,6 +29,10 @@ import type {
29
29
  NewsletterFormProps,
30
30
  NewsletterHeaderProps,
31
31
  NewsletterProps,
32
+ ProductComparisonProps,
33
+ ProductComparisonSidebarProps,
34
+ ProductComparisonToolbarProps,
35
+ ProductComparisonTriggerProps,
32
36
  ProductPriceProps,
33
37
  ProductShelfProps,
34
38
  ProductTitleProps,
@@ -36,10 +40,11 @@ import type {
36
40
  RegionBarProps,
37
41
  ShippingSimulationProps,
38
42
  SkeletonProps,
43
+ SkuSelectorProps,
44
+ ToggleFieldProps,
39
45
  SKUMatrixProps,
40
46
  SKUMatrixSidebarProps,
41
47
  SKUMatrixTriggerProps,
42
- SkuSelectorProps,
43
48
  } from '@faststore/ui'
44
49
  import type { PropsWithChildren } from 'react'
45
50
 
@@ -322,10 +327,34 @@ export type SectionsOverrides = {
322
327
  LinkButtonProps,
323
328
  Omit<LinkButtonProps, 'onClick' | 'href'>
324
329
  >
330
+ ProductComparison: ComponentOverrideDefinition<
331
+ ProductComparisonProps,
332
+ ProductComparisonProps
333
+ >
334
+ ProductComparisonSidebar: ComponentOverrideDefinition<
335
+ ProductComparisonSidebarProps,
336
+ ProductComparisonSidebarProps
337
+ >
338
+ ProductComparisonToolbar: ComponentOverrideDefinition<
339
+ ProductComparisonToolbarProps,
340
+ ProductComparisonToolbarProps
341
+ >
342
+ ProductComparisonTrigger: ComponentOverrideDefinition<
343
+ ProductComparisonTriggerProps,
344
+ ProductComparisonTriggerProps
345
+ >
346
+ ToggleField: ComponentOverrideDefinition<
347
+ ToggleFieldProps,
348
+ ToggleFieldProps
349
+ >
325
350
  __experimentalFilterDesktop: ComponentOverrideDefinition<any, any>
326
351
  __experimentalFilterSlider: ComponentOverrideDefinition<any, any>
327
352
  __experimentalProductCard: ComponentOverrideDefinition<any, any>
328
353
  __experimentalEmptyGallery: ComponentOverrideDefinition<any, any>
354
+ __experimentalProductComparisonSidebar: ComponentOverrideDefinition<
355
+ any,
356
+ any
357
+ >
329
358
  }
330
359
  }
331
360
  ProductShelf: {
@@ -68,6 +68,7 @@ const QUERIES = [
68
68
  'collection',
69
69
  'search',
70
70
  'allProducts',
71
+ 'products',
71
72
  'allCollections',
72
73
  'shipping',
73
74
  'redirect',
@@ -1 +0,0 @@
1
- "use strict";exports.id=1333,exports.ids=[1333],exports.modules={2430:(e,i,t)=>{t.d(i,{Z:()=>FilterDeliveryMethodFacet});var r=t(16652),l=t(83339),a=t(70031),c=t(4018),n=t(20997);function FilterDeliveryMethodFacet({item:e,deliveryMethods:i}){let{city:t,postalCode:u}=a.Qf.read(),{openRegionSlider:s}=(0,r.l8)(),o=t?`${(0,c._W)(t)}, ${u}`:u,p={delivery:i?.delivery??"Shipping to","pickup-in-point":i?.pickupInPoint??"Pickup at","pickup-nearby":i?.pickupNearby??"Pickup Nearby","pickup-all":i?.pickupAll?.label??"Pickup Anywhere"};return"delivery"===e.value?(0,n.jsxs)(n.Fragment,{children:[p[e.value],n.jsx(l.Z,{"data-fs-filter-list-item-button":!0,size:"small",onClick:()=>{s(r.gK.changeLocation)},children:o})]}):"pickup-in-point"===e.value?(0,n.jsxs)(n.Fragment,{children:[p[e.value],n.jsx(l.Z,{"data-fs-filter-list-item-button":!0,size:"small",onClick:()=>{s(r.gK.changePickupPoint)},children:e.label})]}):n.jsx(n.Fragment,{children:p[e.value]??e.label})}},70643:(e,i,t)=>{t.d(i,{P:()=>useFormattedPrice});var r=t(16689),l=t(70031);let usePriceFormatter=({decimals:e}={})=>{let{currency:i,locale:t}=(0,l.kP)();return(0,r.useCallback)(r=>Intl.NumberFormat(t,{style:"currency",currency:i.code,minimumFractionDigits:e?2:0}).format(r),[i.code,t,e])},useFormattedPrice=e=>{let i=usePriceFormatter();return(0,r.useMemo)(()=>i(e),[i,e])}}};
@@ -1 +0,0 @@
1
- "use strict";exports.id=2295,exports.ids=[2295],exports.modules={2946:(e,t,a)=>{a.d(t,{Z:()=>DiscountBadge_DiscountBadge});var r=a(16689),n=a.n(r),d=a(40276);let useDiscountPercent=(e,t)=>(0,r.useMemo)(()=>{let a=e-t,r=100*a/e;return Math.round(r)},[t,e]),DiscountBadge_DiscountBadge=({listPrice:e,spotPrice:t,thresholdLow:a=15,thresholdHigh:r=40,size:c,testId:i="fs-discount-badge"})=>{let l=useDiscountPercent(e,t);if(0===l)return n().createElement(n().Fragment,null);let o=l<=a?"low":l<=r?"medium":"high";return n().createElement(d.Z,{"data-fs-discount-badge":!0,"data-fs-discount-badge-variant":o,size:c,"data-testid":i},l,"% off")}},38394:(e,t,a)=>{a.d(t,{Z:()=>c});var r=a(16689),n=a.n(r);let d=(0,r.forwardRef)(function({testId:e="fs-product-card",variant:t="default",bordered:a=!1,outOfStock:r,children:d,...c},i){return n().createElement("article",{ref:i,"data-fs-product-card":r?"out-of-stock":"","data-fs-product-card-variant":t,"data-fs-product-card-bordered":a,"data-testid":e,...c},d)}),c=d},50547:(e,t,a)=>{a.d(t,{Z:()=>p});var r=a(16689),n=a.n(r),d=a(40727),c=a(69088),i=a(31953),l=a(13024),o=a(2614),s=a(2946),u=a(40276),f=a(83339);let m=(0,r.forwardRef)(function({testId:e="fs-product-card-content",title:t,linkProps:a,price:r,outOfStock:m,outOfStockLabel:p="Out of stock",ratingValue:E,showDiscountBadge:g,buttonLabel:Z="Add",onButtonClick:v,children:b,includeTaxes:P=!1,includeTaxesLabel:h="Tax included",sponsored:w=!1,sponsoredLabel:y="Sponsored",...D},S){let k=r?.listPrice?r.listPrice:0,x=r?.value?r.value:0;return n().createElement("section",{ref:S,"data-fs-product-card-content":!0,"data-fs-product-card-badge":g,"data-testid":e,...D},w&&n().createElement("span",{"data-fs-product-card-sponsored-label":!0},y),n().createElement("div",{"data-fs-product-card-heading":!0},n().createElement("h3",{"data-fs-product-card-title":!0},n().createElement(d.Z,{...a,title:t},n().createElement("span",null,t))),!m&&n().createElement(c.Z,{"data-fs-product-card-prices":!0,value:x,listPrice:k,formatter:r?.formatter}),P&&n().createElement(i.Z,{"data-fs-product-card-taxes-label":!0},h),E&&n().createElement(l.Z,{value:E,icon:n().createElement(o.Z,{name:"Star"})})),g&&!m&&n().createElement(s.Z,{listPrice:k,spotPrice:x}),m&&n().createElement(u.Z,null,p),v&&!m&&n().createElement("div",{"data-fs-product-card-actions":!0},n().createElement(f.Z,{variant:"primary",icon:n().createElement(o.Z,{name:"ShoppingCart"}),iconPosition:"left",size:"small",onClick:v},Z)))}),p=m},65167:(e,t,a)=>{a.d(t,{Z:()=>c});var r=a(16689),n=a.n(r);let d=(0,r.forwardRef)(function({testId:e="fs-product-card-image",aspectRatio:t=1,children:a,...r},d){return n().createElement("div",{ref:d,"data-fs-product-card-image":!0,"data-testid":e,style:{"--fs-product-card-image-aspect-ratio":t},...r},a)}),c=d},13024:(e,t,a)=>{a.d(t,{Z:()=>o});var r=a(16689),n=a.n(r),d=a(2614),c=a(37041),i=a(94564);let l=(0,r.forwardRef)(function({children:e,testId:t="fs-rating",length:a=5,value:l=0,icon:o,onChange:s,disabled:u,...f},m){let[p,E]=(0,r.useState)(0),g={"data-fs-rating-icon-outline":!0},Z=n().isValidElement(o)?o:n().createElement(d.Z,{name:"Star"});return n().createElement(i.Z,{ref:m,"data-fs-rating":!0,"data-fs-rating-actionable":"function"==typeof s,"data-testid":t,...f},Array.from({length:a}).map((e,a)=>{let r=a+1;return n().createElement("li",{key:`rating-${a}`,"data-fs-rating-item":r<=(p||l)?"full":r-l>0&&r-l<1?"partial":"empty","data-testid":`${t}-item`},s?n().createElement(c.Z,{"data-fs-rating-button":!0,icon:Z,size:"small","aria-label":"rate",onClick:()=>{s(r)},onMouseEnter:()=>E(r),onMouseLeave:()=>E(l),disabled:u}):n().createElement(n().Fragment,null,n().createElement("div",{"data-fs-rating-icon-wrapper":!0},Z),n().isValidElement(o)?n().cloneElement(o,g):n().createElement(d.Z,{name:"Star","data-fs-rating-icon-outline":!0})))}))}),o=l}};