@faststore/core 3.6.0 → 3.8.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 (184) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +35 -34
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/config.json +3 -3
  5. package/.next/cache/eslint/.cache_1gneedd +1 -1
  6. package/.next/cache/webpack/client-production/0.pack +0 -0
  7. package/.next/cache/webpack/client-production/index.pack +0 -0
  8. package/.next/cache/webpack/server-production/0.pack +0 -0
  9. package/.next/cache/webpack/server-production/index.pack +0 -0
  10. package/.next/next-minimal-server.js.nft.json +1 -1
  11. package/.next/next-server.js.nft.json +1 -1
  12. package/.next/prerender-manifest.js +1 -1
  13. package/.next/prerender-manifest.json +1 -1
  14. package/.next/react-loadable-manifest.json +38 -38
  15. package/.next/routes-manifest.json +1 -1
  16. package/.next/server/chunks/1153.js +1 -1
  17. package/.next/server/chunks/1163.js +2 -2
  18. package/.next/server/chunks/1377.js +1 -1
  19. package/.next/server/chunks/2082.js +3 -3
  20. package/.next/server/chunks/2245.js +1 -1
  21. package/.next/server/chunks/2295.js +1 -1
  22. package/.next/server/chunks/2552.js +1 -1
  23. package/.next/server/chunks/2710.js +1 -1
  24. package/.next/server/chunks/2880.js +1 -1
  25. package/.next/server/chunks/2918.js +2 -2
  26. package/.next/server/chunks/3157.js +1 -1
  27. package/.next/server/chunks/319.js +1 -1
  28. package/.next/server/chunks/3202.js +1 -1
  29. package/.next/server/chunks/371.js +1 -1
  30. package/.next/server/chunks/3716.js +2 -2
  31. package/.next/server/chunks/3779.js +1 -1
  32. package/.next/server/chunks/3922.js +1 -1
  33. package/.next/server/chunks/4012.js +1 -1
  34. package/.next/server/chunks/4222.js +1 -1
  35. package/.next/server/chunks/4358.js +1 -1
  36. package/.next/server/chunks/4451.js +1 -1
  37. package/.next/server/chunks/5110.js +1 -1
  38. package/.next/server/chunks/5156.js +1 -1
  39. package/.next/server/chunks/5284.js +1 -1
  40. package/.next/server/chunks/5342.js +1 -1
  41. package/.next/server/chunks/5380.js +1 -1
  42. package/.next/server/chunks/5430.js +1 -1
  43. package/.next/server/chunks/5476.js +1 -1
  44. package/.next/server/chunks/5484.js +1 -1
  45. package/.next/server/chunks/5671.js +1 -1
  46. package/.next/server/chunks/5754.js +2 -2
  47. package/.next/server/chunks/6198.js +1 -0
  48. package/.next/server/chunks/6335.js +1 -1
  49. package/.next/server/chunks/64.js +1 -1
  50. package/.next/server/chunks/6414.js +1 -1
  51. package/.next/server/chunks/6859.js +3 -3
  52. package/.next/server/chunks/7169.js +1 -1
  53. package/.next/server/chunks/7228.js +1 -1
  54. package/.next/server/chunks/7468.js +1 -1
  55. package/.next/server/chunks/7675.js +1 -1
  56. package/.next/server/chunks/7986.js +1 -1
  57. package/.next/server/chunks/8096.js +1 -1
  58. package/.next/server/chunks/8640.js +1 -1
  59. package/.next/server/chunks/8724.js +1 -1
  60. package/.next/server/chunks/8737.js +1 -1
  61. package/.next/server/chunks/8857.js +1 -1
  62. package/.next/server/chunks/9088.js +1 -1
  63. package/.next/server/chunks/9160.js +1 -1
  64. package/.next/server/chunks/9369.js +1 -1
  65. package/.next/server/chunks/9410.js +1 -1
  66. package/.next/server/chunks/945.js +1 -1
  67. package/.next/server/chunks/9570.js +1 -1
  68. package/.next/server/chunks/9572.js +49 -3
  69. package/.next/server/chunks/983.js +1 -1
  70. package/.next/server/chunks/9844.js +1 -1
  71. package/.next/server/chunks/ButtonSignIn.js +1 -1
  72. package/.next/server/chunks/Dropdown.js +1 -1
  73. package/.next/server/chunks/DropdownButton.js +1 -1
  74. package/.next/server/chunks/DropdownItem.js +1 -1
  75. package/.next/server/chunks/DropdownMenu.js +1 -1
  76. package/.next/server/chunks/FilterSkeleton.js +1 -1
  77. package/.next/server/chunks/ScrollToTopButton.js +1 -1
  78. package/.next/server/chunks/UIBannerText.js +1 -1
  79. package/.next/server/middleware-build-manifest.js +1 -1
  80. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  81. package/.next/server/pages/404.js +1 -1
  82. package/.next/server/pages/404.js.nft.json +1 -1
  83. package/.next/server/pages/500.js +1 -1
  84. package/.next/server/pages/500.js.nft.json +1 -1
  85. package/.next/server/pages/[...slug].js +1 -1
  86. package/.next/server/pages/[...slug].js.nft.json +1 -1
  87. package/.next/server/pages/[slug]/p.js +1 -1
  88. package/.next/server/pages/[slug]/p.js.nft.json +1 -1
  89. package/.next/server/pages/_app.js +1 -1
  90. package/.next/server/pages/_app.js.nft.json +1 -1
  91. package/.next/server/pages/_document.js +1 -1
  92. package/.next/server/pages/_document.js.nft.json +1 -1
  93. package/.next/server/pages/_error.js +1 -1
  94. package/.next/server/pages/_error.js.nft.json +1 -1
  95. package/.next/server/pages/account.js +1 -1
  96. package/.next/server/pages/account.js.nft.json +1 -1
  97. package/.next/server/pages/api/graphql.js +1 -1
  98. package/.next/server/pages/api/graphql.js.nft.json +1 -1
  99. package/.next/server/pages/api/health/live.js +1 -1
  100. package/.next/server/pages/api/health/live.js.nft.json +1 -1
  101. package/.next/server/pages/api/health/ready.js +1 -1
  102. package/.next/server/pages/api/health/ready.js.nft.json +1 -1
  103. package/.next/server/pages/api/preview.js +1 -1
  104. package/.next/server/pages/api/preview.js.nft.json +1 -1
  105. package/.next/server/pages/checkout.js +1 -1
  106. package/.next/server/pages/checkout.js.nft.json +1 -1
  107. package/.next/server/pages/en-US/404.html +1 -1
  108. package/.next/server/pages/en-US/500.html +1 -1
  109. package/.next/server/pages/en-US/account.html +1 -1
  110. package/.next/server/pages/en-US/checkout.html +1 -1
  111. package/.next/server/pages/en-US/login.html +1 -1
  112. package/.next/server/pages/en-US/s.html +1 -1
  113. package/.next/server/pages/en-US.html +1 -1
  114. package/.next/server/pages/index.js +1 -1
  115. package/.next/server/pages/index.js.nft.json +1 -1
  116. package/.next/server/pages/login.js +1 -1
  117. package/.next/server/pages/login.js.nft.json +1 -1
  118. package/.next/server/pages/s.js +1 -1
  119. package/.next/server/pages/s.js.nft.json +1 -1
  120. package/.next/server/pages-manifest.json +1 -1
  121. package/.next/static/chunks/{1153.d7522522b6c917ed.js → 1153.7f616071da309cf5.js} +1 -1
  122. package/.next/static/chunks/{1978.6d1246731da0f1b0.js → 1978.afceeb8879bd2646.js} +1 -1
  123. package/.next/static/chunks/2552.9070ea604ee3c214.js +1 -0
  124. package/.next/static/chunks/2599-67df8c38c483737b.js +1 -0
  125. package/.next/static/chunks/3285.419d379c827c993d.js +1 -0
  126. package/.next/static/chunks/6379-e49fe5643b85d5b5.js +1 -0
  127. package/.next/static/chunks/7498-791ad2ef2c9fc716.js +1 -0
  128. package/.next/static/chunks/7563-e2275f3ee79ddd83.js +1 -0
  129. package/.next/static/chunks/BannerNewsletter.29403e046f34b6c1.js +1 -0
  130. package/.next/static/chunks/BannerText.24939a8013e30e18.js +1 -0
  131. package/.next/static/chunks/CartSidebar.71febd97344a7b4c.js +1 -0
  132. package/.next/static/chunks/ProductShelf.d40d1e693c5a302b.js +1 -0
  133. package/.next/static/chunks/RegionModal.cec6e7d1faeeae2d.js +1 -0
  134. package/.next/static/chunks/Toast.0e3d2c547b67cf60.js +1 -0
  135. package/.next/static/chunks/pages/{404-358f6795222bf991.js → 404-71d5202357fb9e69.js} +1 -1
  136. package/.next/static/chunks/pages/{500-7adc48c3231ccee1.js → 500-75704d5230292280.js} +1 -1
  137. package/.next/static/chunks/pages/{[...slug]-0203b74377537f7d.js → [...slug]-21a03c0e2b98783d.js} +1 -1
  138. package/.next/static/chunks/pages/[slug]/p-fcdc2b26731a7f07.js +1 -0
  139. package/.next/static/chunks/pages/{account-9db0ef5c4174c7dd.js → account-1d2b1635afa59ace.js} +1 -1
  140. package/.next/static/chunks/pages/{checkout-abaa6374ae946641.js → checkout-4b85659485b4500c.js} +1 -1
  141. package/.next/static/chunks/pages/{index-ad532cf8840c5d3f.js → index-2ec096f367505f35.js} +1 -1
  142. package/.next/static/chunks/pages/{login-8d2eb8db226d6363.js → login-363e38c5172ec5a4.js} +1 -1
  143. package/.next/static/chunks/pages/{s-ba5734fbe496d9af.js → s-d74217745109de5d.js} +1 -1
  144. package/.next/static/chunks/webpack-bd90055e2ebc407d.js +1 -0
  145. package/.next/static/css/d92839440872e246.css +1 -0
  146. package/.next/static/er25JH1Tpwm62rGwg_DUa/_buildManifest.js +1 -0
  147. package/.next/trace +98 -98
  148. package/.turbo/turbo-build.log +7 -7
  149. package/.turbo/turbo-lint.log +1 -1
  150. package/.turbo/turbo-test.log +5 -5
  151. package/@generated/gql.ts +18 -2
  152. package/@generated/graphql.ts +157 -0
  153. package/@generated/persisted-documents.json +1 -0
  154. package/@generated/schema.graphql +2 -0
  155. package/CHANGELOG.md +12 -0
  156. package/cms/faststore/sections.json +193 -406
  157. package/package.json +5 -5
  158. package/src/components/cms/RenderSections.tsx +55 -8
  159. package/src/components/cms/ViewportObserver.tsx +3 -1
  160. package/src/components/sections/ProductDetails/DefaultComponents.ts +9 -1
  161. package/src/components/sections/ProductDetails/ProductDetails.tsx +46 -3
  162. package/src/components/sections/ProductDetails/section.module.scss +39 -3
  163. package/src/components/ui/SKUMatrix/SKUMatrixSidebar.tsx +132 -0
  164. package/src/sdk/cart/useBuyButton.ts +41 -20
  165. package/src/sdk/performance/useTTI.ts +35 -0
  166. package/src/sdk/product/useAllVariantProducts.ts +114 -0
  167. package/src/typings/overrides.ts +21 -5
  168. package/.next/server/chunks/463.js +0 -1
  169. package/.next/static/chunks/1550-b2d0f274d1db008a.js +0 -1
  170. package/.next/static/chunks/2552.35321485d927aa08.js +0 -1
  171. package/.next/static/chunks/299.c58d37a55fab85ad.js +0 -1
  172. package/.next/static/chunks/6379-fc541ff2f12fab06.js +0 -1
  173. package/.next/static/chunks/9638-24a86482d4a4b434.js +0 -1
  174. package/.next/static/chunks/BannerNewsletter.96d85bb5fbde1d76.js +0 -1
  175. package/.next/static/chunks/BannerText.f58f1afdd622dd6c.js +0 -1
  176. package/.next/static/chunks/CartSidebar.33f111ff908118cf.js +0 -1
  177. package/.next/static/chunks/ProductShelf.58b9c11d0b9d412c.js +0 -1
  178. package/.next/static/chunks/RegionModal.8d2c6065d8aaf911.js +0 -1
  179. package/.next/static/chunks/Toast.9ad46e15a6444bd6.js +0 -1
  180. package/.next/static/chunks/pages/[slug]/p-b5e429fbf8e96d36.js +0 -1
  181. package/.next/static/chunks/webpack-712cba50a8ffee1b.js +0 -1
  182. package/.next/static/css/9718991cd57978e9.css +0 -1
  183. package/.next/static/hTBj6SJWSWID7EQkobAYG/_buildManifest.js +0 -1
  184. /package/.next/static/{hTBj6SJWSWID7EQkobAYG → er25JH1Tpwm62rGwg_DUa}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/core",
3
- "version": "3.6.0",
3
+ "version": "3.8.0",
4
4
  "license": "MIT",
5
5
  "repository": "vtex/faststore",
6
6
  "browserslist": "supports es6-module and not dead",
@@ -43,12 +43,12 @@
43
43
  "@envelop/graphql-jit": "^8.0.3",
44
44
  "@envelop/parser-cache": "^6.0.2",
45
45
  "@envelop/validation-cache": "^6.0.2",
46
- "@faststore/api": "^3.6.0",
47
- "@faststore/components": "^3.5.0",
46
+ "@faststore/api": "^3.7.0",
47
+ "@faststore/components": "^3.7.0",
48
48
  "@faststore/graphql-utils": "^3.5.0",
49
49
  "@faststore/lighthouse": "^3.5.0",
50
50
  "@faststore/sdk": "^3.5.0",
51
- "@faststore/ui": "^3.5.0",
51
+ "@faststore/ui": "^3.7.0",
52
52
  "@graphql-codegen/cli": "5.0.2",
53
53
  "@graphql-codegen/client-preset": "4.2.6",
54
54
  "@graphql-codegen/typescript": "4.0.7",
@@ -128,5 +128,5 @@
128
128
  "node": "18.19.0",
129
129
  "yarn": "1.19.1"
130
130
  },
131
- "gitHead": "be4c6d948145860a32be1846d3554de47b4ef970"
131
+ "gitHead": "16615e73214a680a16cf1f92839c8b6ca2c8467e"
132
132
  }
@@ -6,8 +6,10 @@ import {
6
6
  useMemo,
7
7
  } from 'react'
8
8
 
9
+ import { useUI } from '@faststore/ui'
9
10
  import { Section } from '@vtex/client-cms'
10
11
  import dynamic from 'next/dynamic'
12
+ import useTTI from 'src/sdk/performance/useTTI'
11
13
  import SectionBoundary from './SectionBoundary'
12
14
  import ViewportObserver from './ViewportObserver'
13
15
  import COMPONENTS from './global/Components'
@@ -16,6 +18,7 @@ interface Props {
16
18
  components?: Record<string, ComponentType<any>>
17
19
  globalSections?: Array<{ name: string; data: any }>
18
20
  sections?: Array<{ name: string; data: any }>
21
+ isInteractive?: boolean
19
22
  }
20
23
 
21
24
  const SECTIONS_OUT_OF_VIEWPORT = ['CartSidebar', 'RegionModal']
@@ -49,20 +52,47 @@ const useDividedSections = (sections: Section[]) => {
49
52
  export const LazyLoadingSection = ({
50
53
  sectionName,
51
54
  children,
55
+ debug = false,
56
+ isInteractive = false,
52
57
  }: {
53
58
  sectionName: string
54
59
  children: ReactNode
60
+ debug?: boolean
61
+ isInteractive?: boolean
55
62
  }) => {
63
+ const { cart: displayCart, modal: displayModal } = useUI()
56
64
  if (SECTIONS_OUT_OF_VIEWPORT.includes(sectionName)) {
57
- return <>{children}</>
65
+ const shouldLoad =
66
+ isInteractive ||
67
+ (sectionName === 'CartSidebar' && displayCart) ||
68
+ (sectionName === 'RegionModal' && displayModal)
69
+
70
+ if (debug) {
71
+ console.log(
72
+ `section SECTIONS_OUT_OF_VIEWPORT '${sectionName}' shouldLoad:`,
73
+ shouldLoad
74
+ )
75
+ }
76
+
77
+ return shouldLoad ? <>{children}</> : null
58
78
  }
59
79
 
60
80
  return (
61
- <ViewportObserver sectionName={sectionName}>{children}</ViewportObserver>
81
+ <ViewportObserver
82
+ sectionName={sectionName}
83
+ debug={debug}
84
+ isInteractive={isInteractive}
85
+ >
86
+ {children}
87
+ </ViewportObserver>
62
88
  )
63
89
  }
64
90
 
65
- const RenderSectionsBase = ({ sections = [], components }: Props) => {
91
+ const RenderSectionsBase = ({
92
+ sections = [],
93
+ components,
94
+ isInteractive,
95
+ }: Props) => {
66
96
  return (
67
97
  <>
68
98
  {sections.map(({ name, data = {} }, index) => {
@@ -79,7 +109,10 @@ const RenderSectionsBase = ({ sections = [], components }: Props) => {
79
109
 
80
110
  return (
81
111
  <SectionBoundary key={`cms-section-${name}-${index}`} name={name}>
82
- <LazyLoadingSection sectionName={name}>
112
+ <LazyLoadingSection
113
+ sectionName={name}
114
+ isInteractive={isInteractive}
115
+ >
83
116
  <Component {...data} />
84
117
  </LazyLoadingSection>
85
118
  </SectionBoundary>
@@ -99,21 +132,35 @@ function RenderSections({
99
132
  globalSections ?? sections
100
133
  )
101
134
 
135
+ const { isInteractive } = useTTI()
136
+
102
137
  return (
103
138
  <>
104
139
  {firstSections && (
105
- <RenderSectionsBase sections={firstSections} components={components} />
140
+ <RenderSectionsBase
141
+ sections={firstSections}
142
+ components={components}
143
+ isInteractive={isInteractive}
144
+ />
106
145
  )}
107
146
  {sections && sections.length > 0 && (
108
- <RenderSectionsBase sections={sections} components={components} />
147
+ <RenderSectionsBase
148
+ sections={sections}
149
+ components={components}
150
+ isInteractive={isInteractive}
151
+ />
109
152
  )}
110
153
  {children}
111
- <LazyLoadingSection sectionName="Toast">
154
+ <LazyLoadingSection sectionName="Toast" isInteractive={isInteractive}>
112
155
  <Toast />
113
156
  </LazyLoadingSection>
114
157
 
115
158
  {lastSections && (
116
- <RenderSectionsBase sections={lastSections} components={components} />
159
+ <RenderSectionsBase
160
+ sections={lastSections}
161
+ components={components}
162
+ isInteractive={isInteractive}
163
+ />
117
164
  )}
118
165
  </>
119
166
  )
@@ -14,6 +14,7 @@ type ViewportObserverProps = {
14
14
  * Debug/test purposes: enables visual debugging to identify the visibility of the section.
15
15
  */
16
16
  debug?: boolean
17
+ isInteractive?: boolean
17
18
  } & IntersectionObserverInit
18
19
 
19
20
  function ViewportObserver({
@@ -23,6 +24,7 @@ function ViewportObserver({
23
24
  rootMargin,
24
25
  children,
25
26
  debug = false,
27
+ isInteractive = false,
26
28
  }: PropsWithChildren<ViewportObserverProps>) {
27
29
  const [isVisible, setVisible] = useState(false)
28
30
  const ref = useRef<HTMLDivElement | null>(null)
@@ -80,7 +82,7 @@ function ViewportObserver({
80
82
  ></div>
81
83
  )}
82
84
 
83
- {isVisible && children}
85
+ {(isVisible || isInteractive) && children}
84
86
  </>
85
87
  )
86
88
  }
@@ -9,13 +9,17 @@ import {
9
9
  QuantitySelector as UIQuantitySelector,
10
10
  ImageGalleryViewer as UIImageGalleryViewer,
11
11
  ImageGallery as UIImageGallery,
12
+ SKUMatrix as UISKUMatrix,
13
+ SKUMatrixSidebar as UISKUMatrixSidebar,
14
+ SKUMatrixTrigger as UISKUMatrixTrigger,
12
15
  } from '@faststore/ui'
13
16
 
14
17
  import LocalImageGallery from 'src/components/ui/ImageGallery'
15
18
  import LocalShippingSimulation from 'src/components/ui/ShippingSimulation/ShippingSimulation'
16
19
  import { Image } from 'src/components/ui/Image'
17
20
  import LocalNotAvailableButton from 'src/components/product/NotAvailableButton'
18
- import LocalProductDescription from 'src/components/ui/ProductDescription'
21
+ import LocalSKUMatrixSidebar from 'src/components/ui/SKUMatrix/SKUMatrixSidebar'
22
+ import LocalProductDescription from 'src/components/ui/ProductDescription/ProductDescription'
19
23
  import { ProductDetailsSettings as LocalProductDetailsSettings } from 'src/components/ui/ProductDetails'
20
24
 
21
25
  export const ProductDetailsDefaultComponents = {
@@ -29,9 +33,13 @@ export const ProductDetailsDefaultComponents = {
29
33
  ShippingSimulation: UIShippingSimulation,
30
34
  ImageGallery: UIImageGallery,
31
35
  ImageGalleryViewer: UIImageGalleryViewer,
36
+ SKUMatrix: UISKUMatrix,
37
+ SKUMatrixTrigger: UISKUMatrixTrigger,
38
+ SKUMatrixSidebar: UISKUMatrixSidebar,
32
39
  __experimentalImageGalleryImage: Image,
33
40
  __experimentalImageGallery: LocalImageGallery,
34
41
  __experimentalShippingSimulation: LocalShippingSimulation,
42
+ __experimentalSKUMatrixSidebar: LocalSKUMatrixSidebar,
35
43
  __experimentalNotAvailableButton: LocalNotAvailableButton,
36
44
  __experimentalProductDescription: LocalProductDescription,
37
45
  __experimentalProductDetailsSettings: LocalProductDetailsSettings,
@@ -55,6 +55,21 @@ export interface ProductDetailsProps {
55
55
  usePriceWithTaxes?: boolean
56
56
  taxesLabel?: string
57
57
  }
58
+ skuMatrix?: {
59
+ shouldDisplaySKUMatrix?: boolean
60
+ triggerButtonLabel: string
61
+ separatorButtonsText: string
62
+ columns: {
63
+ name: string
64
+ additionalColumns: Array<{ label: string; value: string }>
65
+ availability: {
66
+ label: string
67
+ stockDisplaySettings: 'showAvailability' | 'showStockQuantity'
68
+ }
69
+ price: number
70
+ quantitySelector: number
71
+ }
72
+ }
58
73
  }
59
74
 
60
75
  function ProductDetails({
@@ -74,6 +89,7 @@ function ProductDetails({
74
89
  initiallyExpanded: productDescriptionInitiallyExpanded,
75
90
  displayDescription: shouldDisplayProductDescription,
76
91
  },
92
+ skuMatrix,
77
93
  notAvailableButton: { title: notAvailableButtonTitle },
78
94
  quantitySelector,
79
95
  taxesConfiguration,
@@ -81,17 +97,19 @@ function ProductDetails({
81
97
  const {
82
98
  DiscountBadge,
83
99
  ProductTitle,
100
+ SKUMatrix,
101
+ SKUMatrixTrigger,
84
102
  __experimentalImageGallery: ImageGallery,
85
103
  __experimentalShippingSimulation: ShippingSimulation,
86
104
  __experimentalNotAvailableButton: NotAvailableButton,
87
105
  __experimentalProductDescription: ProductDescription,
88
106
  __experimentalProductDetailsSettings: ProductDetailsSettings,
107
+ __experimentalSKUMatrixSidebar: SKUMatrixSidebar,
89
108
  } = useOverrideComponents<'ProductDetails'>()
90
109
  const { currency } = useSession()
91
110
  const context = usePDP()
92
111
  const { product, isValidating } = context?.data
93
112
  const [quantity, setQuantity] = useState(1)
94
-
95
113
  if (!product) {
96
114
  throw new Error('NotFound')
97
115
  }
@@ -104,7 +122,11 @@ function ProductDetails({
104
122
  brand,
105
123
  isVariantOf,
106
124
  description,
107
- isVariantOf: { name, productGroupID: productId },
125
+ isVariantOf: {
126
+ name,
127
+ productGroupID: productId,
128
+ skuVariants: { slugsMap },
129
+ },
108
130
  image: productImages,
109
131
  offers: {
110
132
  offers: [{ availability, price, listPrice, listPriceWithTaxes, seller }],
@@ -213,6 +235,27 @@ function ProductDetails({
213
235
  isValidating={isValidating}
214
236
  taxesConfiguration={taxesConfiguration}
215
237
  />
238
+
239
+ {skuMatrix?.shouldDisplaySKUMatrix &&
240
+ Object.keys(slugsMap).length > 1 && (
241
+ <>
242
+ <div data-fs-product-details-settings-separator>
243
+ {skuMatrix.separatorButtonsText}
244
+ </div>
245
+
246
+ <SKUMatrix.Component>
247
+ <SKUMatrixTrigger.Component disabled={isValidating}>
248
+ {skuMatrix.triggerButtonLabel}
249
+ </SKUMatrixTrigger.Component>
250
+
251
+ <SKUMatrixSidebar.Component
252
+ formatter={useFormattedPrice}
253
+ columns={skuMatrix.columns}
254
+ overlayProps={{ className: styles.section }}
255
+ />
256
+ </SKUMatrix.Component>
257
+ </>
258
+ )}
216
259
  </section>
217
260
 
218
261
  {!outOfStock && (
@@ -277,7 +320,7 @@ export const fragment = gql(`
277
320
  isVariantOf {
278
321
  name
279
322
  productGroupID
280
- skuVariants {
323
+ skuVariants {
281
324
  activeVariations
282
325
  slugsMap
283
326
  availableVariations
@@ -1,9 +1,13 @@
1
1
  @layer components {
2
2
  .section {
3
3
  // Taxes label
4
- --fs-product-details-taxes-label-color : var(--fs-color-info-text);
5
- --fs-product-details-taxes-text-size : var(--fs-text-size-tiny);
6
- --fs-product-details-taxes-text-weight : var(--fs-text-weight-regular);
4
+ --fs-product-details-taxes-label-color : var(--fs-color-info-text);
5
+ --fs-product-details-taxes-text-size : var(--fs-text-size-tiny);
6
+ --fs-product-details-taxes-text-weight : var(--fs-text-weight-regular);
7
+
8
+ // Separator colors
9
+ --fs-product-details-separator-color : var(--fs-color-neutral-2);
10
+ --fs-product-details-separator-color-text : var(--fs-color-text-light);
7
11
 
8
12
  margin-top: 0;
9
13
 
@@ -29,11 +33,43 @@
29
33
  @import "@faststore/ui/src/components/organisms/ShippingSimulation/styles.scss";
30
34
  @import "@faststore/ui/src/components/organisms/ImageGallery/styles.scss";
31
35
  @import "@faststore/ui/src/components/organisms/ProductDetails/styles.scss";
36
+ @import "@faststore/ui/src/components/organisms/SlideOver/styles.scss";
37
+ @import "@faststore/ui/src/components/organisms/SKUMatrix/styles.scss";
32
38
 
33
39
  [data-fs-product-details-taxes-label] {
34
40
  font-size: var(--fs-product-details-taxes-text-size);
35
41
  font-weight: var(--fs-product-details-taxes-text-weight);
36
42
  color: var(--fs-product-details-taxes-label-color);
37
43
  }
44
+
45
+ [data-fs-product-details-settings-separator] {
46
+ position: relative;
47
+ display: flex;
48
+ align-items: center;
49
+ justify-content: space-between;
50
+ color: var(--fs-product-details-separator-color-text);
51
+
52
+ &::after {
53
+ display: inline-block;
54
+ width: 45%;
55
+ height: 1px;
56
+ content: "";
57
+ background-color: var(--fs-product-details-separator-color);
58
+ }
59
+
60
+ &::before {
61
+ display: inline-block;
62
+ width: 45%;
63
+ height: 1px;
64
+ content: "";
65
+ background-color: var(--fs-product-details-separator-color);
66
+ }
67
+ }
68
+
69
+ [data-fs-sku-matrix] {
70
+ > [data-fs-button] {
71
+ width: 100%;
72
+ }
73
+ }
38
74
  }
39
75
  }
@@ -0,0 +1,132 @@
1
+ import type { SKUMatrixSidebarProps as UISKUMatrixSidebarProps } from '@faststore/ui'
2
+ import {
3
+ SKUMatrixSidebar as UISKUMatrixSidebar,
4
+ useSKUMatrix,
5
+ } from '@faststore/ui'
6
+ import { gql } from '@generated/gql'
7
+ import { useBuyButton } from 'src/sdk/cart/useBuyButton'
8
+ import { usePDP } from 'src/sdk/overrides/PageProvider'
9
+ import { useAllVariantProducts } from 'src/sdk/product/useAllVariantProducts'
10
+
11
+ interface SKUMatrixProps extends UISKUMatrixSidebarProps {}
12
+
13
+ function SKUMatrixSidebar(props: SKUMatrixProps) {
14
+ const {
15
+ data: { product },
16
+ } = usePDP()
17
+
18
+ const { allVariantProducts, isOpen, setAllVariantProducts } = useSKUMatrix()
19
+ const { isValidating } = useAllVariantProducts(
20
+ product.id,
21
+ isOpen,
22
+ setAllVariantProducts
23
+ )
24
+
25
+ const {
26
+ gtin,
27
+ unitMultiplier,
28
+ brand,
29
+ additionalProperty,
30
+ isVariantOf,
31
+ offers: {
32
+ offers: [{ seller }],
33
+ },
34
+ } = product
35
+
36
+ const buyButtonProps = allVariantProducts
37
+ .filter((item) => item.selectedCount)
38
+ .map((item) => {
39
+ const {
40
+ offers: {
41
+ offers: [{ price, priceWithTaxes, listPrice, listPriceWithTaxes }],
42
+ },
43
+ } = item
44
+
45
+ return {
46
+ id: item.id,
47
+ price,
48
+ priceWithTaxes,
49
+ listPrice,
50
+ listPriceWithTaxes,
51
+ seller,
52
+ quantity: item.selectedCount,
53
+ itemOffered: {
54
+ sku: item.id,
55
+ name: item.name,
56
+ gtin,
57
+ image: [item.image],
58
+ brand,
59
+ isVariantOf: {
60
+ ...isVariantOf,
61
+ skuVariants: {
62
+ ...isVariantOf.skuVariants,
63
+ activeVariations: item.specifications,
64
+ },
65
+ },
66
+ additionalProperty,
67
+ unitMultiplier,
68
+ },
69
+ }
70
+ })
71
+
72
+ const buyProps = useBuyButton(buyButtonProps)
73
+
74
+ return (
75
+ <UISKUMatrixSidebar
76
+ buyProps={buyProps}
77
+ title={product.isVariantOf.name ?? ''}
78
+ loading={isValidating}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export const fragment = gql(`
85
+ fragment ProductSKUMatrixSidebarFragment_product on StoreProduct {
86
+ id: productID
87
+ isVariantOf {
88
+ name
89
+ productGroupID
90
+ skuVariants {
91
+ activeVariations
92
+ slugsMap
93
+ availableVariations
94
+ allVariantProducts {
95
+ sku
96
+ name
97
+ image {
98
+ url
99
+ alternateName
100
+ }
101
+ offers {
102
+ highPrice
103
+ lowPrice
104
+ lowPriceWithTaxes
105
+ offerCount
106
+ priceCurrency
107
+ offers {
108
+ listPrice
109
+ listPriceWithTaxes
110
+ sellingPrice
111
+ priceCurrency
112
+ price
113
+ priceWithTaxes
114
+ priceValidUntil
115
+ itemCondition
116
+ availability
117
+ quantity
118
+ }
119
+ }
120
+ additionalProperty {
121
+ propertyID
122
+ value
123
+ name
124
+ valueReference
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ `)
131
+
132
+ export default SKUMatrixSidebar
@@ -8,12 +8,14 @@ import { useUI } from '@faststore/ui'
8
8
  import { useSession } from '../session'
9
9
  import { cartStore } from './index'
10
10
 
11
- export const useBuyButton = (item: CartItem | null) => {
11
+ export const useBuyButton = (item: CartItem | CartItem[] | null) => {
12
12
  const { openCart } = useUI()
13
13
  const {
14
14
  currency: { code },
15
15
  } = useSession()
16
16
 
17
+ const itemIsArray = Array.isArray(item)
18
+
17
19
  const onClick = useCallback(
18
20
  (e: React.MouseEvent<HTMLButtonElement>) => {
19
21
  e.preventDefault()
@@ -22,6 +24,33 @@ export const useBuyButton = (item: CartItem | null) => {
22
24
  return
23
25
  }
24
26
 
27
+ const value = itemIsArray
28
+ ? item.reduce((sum, item) => (sum += item.price * item.quantity), 0)
29
+ : item.price * item.quantity
30
+
31
+ function generatedItem(item: CartItem) {
32
+ return {
33
+ item_id: item.itemOffered.isVariantOf.productGroupID,
34
+ item_name: item.itemOffered.isVariantOf.name,
35
+ item_brand: item.itemOffered.brand.name,
36
+ item_variant: item.itemOffered.sku,
37
+ quantity: item.quantity,
38
+ price: item.price,
39
+ discount: item.listPrice - item.price,
40
+ currency: code as CurrencyCode,
41
+ item_variant_name: item.itemOffered.name,
42
+ product_reference_id: item.itemOffered.gtin,
43
+ }
44
+ }
45
+
46
+ function getItems() {
47
+ if (!itemIsArray) {
48
+ return [generatedItem(item)]
49
+ }
50
+
51
+ return item.map(generatedItem)
52
+ }
53
+
25
54
  import('@faststore/sdk').then(({ sendAnalyticsEvent }) => {
26
55
  sendAnalyticsEvent<AddToCartEvent<AnalyticsItem>>({
27
56
  name: 'add_to_cart',
@@ -29,35 +58,27 @@ export const useBuyButton = (item: CartItem | null) => {
29
58
  currency: code as CurrencyCode,
30
59
  // TODO: In the future, we can explore more robust ways of
31
60
  // calculating the value (gift items, discounts, etc.).
32
- value: item.price * item.quantity,
33
- items: [
34
- {
35
- item_id: item.itemOffered.isVariantOf.productGroupID,
36
- item_name: item.itemOffered.isVariantOf.name,
37
- item_brand: item.itemOffered.brand.name,
38
- item_variant: item.itemOffered.sku,
39
- quantity: item.quantity,
40
- price: item.price,
41
- discount: item.listPrice - item.price,
42
- currency: code as CurrencyCode,
43
- item_variant_name: item.itemOffered.name,
44
- product_reference_id: item.itemOffered.gtin,
45
- },
46
- ],
61
+ value,
62
+ items: getItems(),
47
63
  },
48
64
  })
49
65
  })
50
66
 
51
- cartStore.addItem(item)
67
+ itemIsArray
68
+ ? item.forEach((value) => cartStore.addItem(value))
69
+ : cartStore.addItem(item)
70
+
52
71
  openCart()
53
72
  },
54
- [code, item, openCart]
73
+ [code, item, openCart, itemIsArray]
55
74
  )
56
75
 
57
76
  return {
58
77
  onClick,
59
78
  'data-testid': 'buy-button',
60
- 'data-sku': item?.itemOffered.sku,
61
- 'data-seller': item?.seller.identifier,
79
+ 'data-sku': itemIsArray ? 'sku-matrix-sidebar' : item?.itemOffered.sku,
80
+ 'data-seller': itemIsArray
81
+ ? item[0]?.seller.identifier
82
+ : item?.seller.identifier,
62
83
  }
63
84
  }
@@ -0,0 +1,35 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ const TTI_TIMEOUT = 5000 // 5 seconds without long tasks as a criterion for Time To Interactive - https://web.dev/articles/tti
4
+ export default function useTTI() {
5
+ const [isInteractive, setIsInteractive] = useState(false)
6
+
7
+ useEffect(() => {
8
+ if ('PerformanceObserver' in window) {
9
+ let lastTaskEnd = 0
10
+
11
+ const observer = new PerformanceObserver((list) => {
12
+ for (const entry of list.getEntries()) {
13
+ lastTaskEnd = entry.startTime + entry.duration
14
+ }
15
+ })
16
+
17
+ observer.observe({ type: 'longtask', buffered: true })
18
+
19
+ // Monitoring when TTI might have been reached
20
+ const checkTTI = () => {
21
+ const now = performance.now()
22
+ if (now - lastTaskEnd >= TTI_TIMEOUT) {
23
+ observer.disconnect()
24
+ setIsInteractive(true) // Sets the state to true when TTI is estimated
25
+ } else {
26
+ requestIdleCallback(checkTTI) // Keeps checking while the browser is idle
27
+ }
28
+ }
29
+
30
+ requestIdleCallback(checkTTI)
31
+ }
32
+ }, [])
33
+
34
+ return { isInteractive }
35
+ }