@faststore/core 3.38.3 → 3.40.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 (83) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +18 -21
  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 +10 -10
  12. package/.next/routes-manifest.json +1 -1
  13. package/.next/server/chunks/41.js +1 -0
  14. package/.next/server/chunks/674.js +1 -1
  15. package/.next/server/chunks/{9380.js → 7348.js} +1 -1
  16. package/.next/server/chunks/8112.js +1 -1
  17. package/.next/server/functions-config-manifest.json +1 -1
  18. package/.next/server/middleware-build-manifest.js +1 -1
  19. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  20. package/.next/server/pages/404.js +1 -1
  21. package/.next/server/pages/404.js.nft.json +1 -1
  22. package/.next/server/pages/500.js +1 -1
  23. package/.next/server/pages/500.js.nft.json +1 -1
  24. package/.next/server/pages/[...slug].js +1 -1
  25. package/.next/server/pages/[...slug].js.nft.json +1 -1
  26. package/.next/server/pages/[slug]/p.js +1 -1
  27. package/.next/server/pages/[slug]/p.js.nft.json +1 -1
  28. package/.next/server/pages/_app.js.nft.json +1 -1
  29. package/.next/server/pages/_document.js.nft.json +1 -1
  30. package/.next/server/pages/_error.js +1 -1
  31. package/.next/server/pages/_error.js.nft.json +1 -1
  32. package/.next/server/pages/account/profile.js +1 -1
  33. package/.next/server/pages/account/profile.js.nft.json +1 -1
  34. package/.next/server/pages/account.js +1 -1
  35. package/.next/server/pages/account.js.nft.json +1 -1
  36. package/.next/server/pages/api/graphql.js +1 -1
  37. package/.next/server/pages/api/graphql.js.nft.json +1 -1
  38. package/.next/server/pages/api/health/live.js.nft.json +1 -1
  39. package/.next/server/pages/api/health/ready.js.nft.json +1 -1
  40. package/.next/server/pages/api/preview.js.nft.json +1 -1
  41. package/.next/server/pages/checkout.js +1 -1
  42. package/.next/server/pages/checkout.js.nft.json +1 -1
  43. package/.next/server/pages/en-US/404.html +2 -2
  44. package/.next/server/pages/en-US/500.html +2 -2
  45. package/.next/server/pages/en-US/checkout.html +2 -2
  46. package/.next/server/pages/en-US/login.html +2 -2
  47. package/.next/server/pages/en-US/s.html +2 -2
  48. package/.next/server/pages/en-US.html +2 -2
  49. package/.next/server/pages/index.js +1 -1
  50. package/.next/server/pages/index.js.nft.json +1 -1
  51. package/.next/server/pages/login.js +1 -1
  52. package/.next/server/pages/login.js.nft.json +1 -1
  53. package/.next/server/pages/s.js +1 -1
  54. package/.next/server/pages/s.js.nft.json +1 -1
  55. package/.next/server/pages-manifest.json +1 -1
  56. package/.next/static/chunks/{1552.b5a073e7ac834965.js → 1552.e7cece605fc2e1cd.js} +1 -1
  57. package/.next/static/chunks/{4349-24e224a3b1307f65.js → 4349-6fc936580b772e67.js} +1 -1
  58. package/.next/static/chunks/{4625-07284599f239c05b.js → 4625-c277826e300c23b7.js} +1 -1
  59. package/.next/static/chunks/4774.907a71a04126d57c.js +1 -0
  60. package/.next/static/chunks/ProductShelf.0b5a71a93bca3148.js +1 -0
  61. package/.next/static/chunks/ProductTiles.a75612c0bcf990d6.js +1 -0
  62. package/.next/static/chunks/pages/account-963cd6d42498ff28.js +1 -0
  63. package/.next/static/chunks/{webpack-827d211260f776ab.js → webpack-071462ed32bcb285.js} +1 -1
  64. package/.next/static/sCq3tiaobInU11Xb5y64T/_buildManifest.js +1 -0
  65. package/.next/trace +109 -108
  66. package/.turbo/turbo-build.log +10 -10
  67. package/.turbo/turbo-test.log +6 -5
  68. package/CHANGELOG.md +12 -0
  69. package/package.json +2 -2
  70. package/src/components/product/ProductGrid/ProductGrid.tsx +67 -40
  71. package/src/components/ui/ProductGallery/ProductGalleryPage.tsx +1 -0
  72. package/src/pages/account/index.tsx +26 -35
  73. package/src/sdk/analytics/hooks/useViewItemListEvent.ts +39 -26
  74. package/src/sdk/product/ProductSentinel.tsx +60 -0
  75. package/src/sdk/product/viewItemListQueue.ts +31 -0
  76. package/src/sdk/search/Sentinel.tsx +3 -29
  77. package/.next/server/chunks/2032.js +0 -1
  78. package/.next/static/D9Rlow6eCZi7BCEu9o7_O/_buildManifest.js +0 -1
  79. package/.next/static/chunks/3143.1aaec7f4a9e83b8d.js +0 -1
  80. package/.next/static/chunks/ProductShelf.e80131dfc337fae6.js +0 -1
  81. package/.next/static/chunks/ProductTiles.413c061cf10578b4.js +0 -1
  82. package/.next/static/chunks/pages/account-32bc9db3fea9b0c4.js +0 -1
  83. /package/.next/static/{D9Rlow6eCZi7BCEu9o7_O → sCq3tiaobInU11Xb5y64T}/_ssgManifest.js +0 -0
@@ -1,23 +1,23 @@
1
1
 
2
- > @faststore/core@3.38.2 prebuild /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@3.39.0 prebuild /home/runner/work/faststore/faststore/packages/core
3
3
  > na run partytown && na run generate
4
4
 
5
5
 
6
- > @faststore/core@3.38.2 partytown /home/runner/work/faststore/faststore/packages/core
6
+ > @faststore/core@3.39.0 partytown /home/runner/work/faststore/faststore/packages/core
7
7
  > partytown copylib ./public/~partytown
8
8
 
9
9
  Partytown lib copied to: /home/runner/work/faststore/faststore/packages/core/public/~partytown
10
10
 
11
- > @faststore/core@3.38.2 generate /home/runner/work/faststore/faststore/packages/core
11
+ > @faststore/core@3.39.0 generate /home/runner/work/faststore/faststore/packages/core
12
12
  > na run generate:schema && na run generate:codegen && na run format:generated
13
13
 
14
14
 
15
- > @faststore/core@3.38.2 generate:schema /home/runner/work/faststore/faststore/packages/core
15
+ > @faststore/core@3.39.0 generate:schema /home/runner/work/faststore/faststore/packages/core
16
16
  > tsx src/server/generator/generateGraphQLSchemaFile.ts
17
17
 
18
18
  Schema GraphQL file generated successfully
19
19
 
20
- > @faststore/core@3.38.2 generate:codegen /home/runner/work/faststore/faststore/packages/core
20
+ > @faststore/core@3.39.0 generate:codegen /home/runner/work/faststore/faststore/packages/core
21
21
  > graphql-codegen
22
22
 
23
23
  [STARTED] Parse Configuration
@@ -37,11 +37,11 @@ Running lifecycle hook "afterStart" scripts...
37
37
  [CLI] Loading Documents
38
38
  [CLI] Generating output
39
39
 
40
- > @faststore/core@3.38.2 format:generated /home/runner/work/faststore/faststore/packages/core
40
+ > @faststore/core@3.39.0 format:generated /home/runner/work/faststore/faststore/packages/core
41
41
  > prettier --write "@generated/**/*.{ts,js,tsx,jsx,json}" --loglevel error
42
42
 
43
43
 
44
- > @faststore/core@3.38.2 build /home/runner/work/faststore/faststore/packages/core
44
+ > @faststore/core@3.39.0 build /home/runner/work/faststore/faststore/packages/core
45
45
  > next build
46
46
 
47
47
  ⚠ No build cache found. Please configure build caching for faster rebuilds. Read more: https://nextjs.org/docs/messages/no-cache
@@ -71,13 +71,13 @@ Route (pages) Size First Load JS
71
71
  ├ └ css/b1806cbafd0c1f81.css 3.06 kB
72
72
  ├ /_app 0 B 92.4 kB
73
73
  ├ ● /[...slug] 2.34 kB 133 kB
74
- ├ ● /[slug]/p 35.5 kB 156 kB
74
+ ├ ● /[slug]/p 35.6 kB 156 kB
75
75
  ├ ├ css/a3ca6a9b63f657be.css 5.75 kB
76
76
  ├ ├ css/62a5153ac7061286.css 6.11 kB
77
77
  ├ └ css/b5bf49598c8f8b66.css 16.1 kB
78
78
  ├ ○ /404 1.48 kB 122 kB
79
79
  ├ ● /500 1.48 kB 122 kB
80
- ├ λ /account 767 B 121 kB
80
+ ├ λ /account 240 B 92.6 kB
81
81
  ├ λ /account/profile 1.34 kB 122 kB
82
82
  ├ └ css/249b9deaabc0d32d.css 2.51 kB
83
83
  ├ λ /api/graphql 0 B 92.4 kB
@@ -91,7 +91,7 @@ Route (pages) Size First Load JS
91
91
  ├ chunks/framework-807b0f81cbc129f0.js 45.4 kB
92
92
  ├ chunks/main-5569625b3fdd6741.js 33.1 kB
93
93
  ├ chunks/pages/_app-012e51403727ae6a.js 10.4 kB
94
- ├ chunks/webpack-827d211260f776ab.js 3.59 kB
94
+ ├ chunks/webpack-071462ed32bcb285.js 3.59 kB
95
95
  └ css/0a57ee6c7a57788c.css 3.49 kB
96
96
 
97
97
  λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
@@ -1,14 +1,15 @@
1
1
 
2
- > @faststore/core@3.38.2 test /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@3.39.0 test /home/runner/work/faststore/faststore/packages/core
3
3
  > jest
4
4
 
5
- PASS test/utils/multipleTemplates.test.ts (23.184 s)
6
- PASS test/server/cms/global.test.ts (24.522 s)
5
+ PASS test/server/cms/global.test.ts (24.343 s)
6
+ PASS test/utils/multipleTemplates.test.ts (25.211 s)
7
7
  PASS test/server/cms/index.test.ts
8
- PASS test/server/index.test.ts (28.509 s)
8
+ PASS test/server/index.test.ts (28.944 s)
9
+ A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
9
10
 
10
11
  Test Suites: 4 passed, 4 total
11
12
  Tests: 22 passed, 22 total
12
13
  Snapshots: 0 total
13
- Time: 29.924 s
14
+ Time: 30.228 s
14
15
  Ran all test suites.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [3.40.0](https://github.com/vtex/faststore/compare/v3.39.0...v3.40.0) (2025-04-11)
7
+
8
+ ### Features
9
+
10
+ - GA `view_item_list` event only when item is in viewport of `ProductGallery` ([#2771](https://github.com/vtex/faststore/issues/2771)) ([16a4efa](https://github.com/vtex/faststore/commit/16a4efa23b7459a4327df0bc994b96c15f44ee5e))
11
+
12
+ # [3.39.0](https://github.com/vtex/faststore/compare/v3.38.3...v3.39.0) (2025-04-11)
13
+
14
+ ### Features
15
+
16
+ - MyAccount redirects using SSR - SFS-2440 ([#2782](https://github.com/vtex/faststore/issues/2782)) ([6f92f41](https://github.com/vtex/faststore/commit/6f92f41e647298734bcc0f503810845c1558fd6f))
17
+
6
18
  ## 3.38.3 (2025-04-11)
7
19
 
8
20
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/core",
3
- "version": "3.38.3",
3
+ "version": "3.40.0",
4
4
  "license": "MIT",
5
5
  "repository": "vtex/faststore",
6
6
  "browserslist": "supports es6-module and not dead",
@@ -105,5 +105,5 @@
105
105
  "ts-jest": "29.1.1",
106
106
  "typescript": "5.3.2"
107
107
  },
108
- "gitHead": "b6d31ee84b18d0c7fbe8e9ed88b89c6d9b516f2e"
108
+ "gitHead": "be8308dc73b997370c8c1aac56682362acfe58c0"
109
109
  }
@@ -10,6 +10,7 @@ import { memo } from 'react'
10
10
  import ViewportObserver from 'src/components/cms/ViewportObserver'
11
11
  import { useOverrideComponents } from 'src/sdk/overrides/OverrideContext'
12
12
  import useScreenResize from 'src/sdk/ui/useScreenResize'
13
+ import ProductSentinel from 'src/sdk/product/ProductSentinel'
13
14
 
14
15
  interface Props {
15
16
  /**
@@ -35,6 +36,10 @@ interface Props {
35
36
  * Determine if the current page is the first page.
36
37
  */
37
38
  firstPage?: number
39
+ /**
40
+ * Title for the `ProductGrid` component that will be send to GA events.
41
+ */
42
+ title?: string
38
43
  }
39
44
 
40
45
  function ProductGrid({
@@ -48,6 +53,7 @@ function ProductGrid({
48
53
  sponsoredLabel,
49
54
  } = {},
50
55
  firstPage,
56
+ title,
51
57
  }: Props) {
52
58
  const { isMobile } = useScreenResize()
53
59
  const { __experimentalProductCard: ProductCard } =
@@ -67,36 +73,19 @@ function ProductGrid({
67
73
  <>
68
74
  {products.slice(0, 2).map(({ node: product }, idx) => (
69
75
  <UIProductGridItem key={`${product.id}`}>
70
- <ProductCard.Component
71
- aspectRatio={aspectRatio}
72
- imgProps={{
73
- width: 150,
74
- height: 150,
75
- sizes: '30vw',
76
- loading: 'eager',
77
- }}
78
- {...ProductCard.props}
79
- bordered={bordered ?? ProductCard.props.bordered}
80
- showDiscountBadge={
81
- showDiscountBadge ?? ProductCard.props.showDiscountBadge
82
- }
76
+ <ProductSentinel
83
77
  product={product}
84
- index={pageSize * page + idx + 1}
85
- taxesConfiguration={taxesConfiguration}
86
- sponsoredLabel={sponsoredLabel}
87
- />
88
- </UIProductGridItem>
89
- ))}
90
- <ViewportObserver sectionName="UIProductGrid-out-viewport">
91
- {products.slice(2).map(({ node: product }, idx) => (
92
- <UIProductGridItem key={`${product.id}`}>
78
+ title={title}
79
+ page={page}
80
+ pageSize={pageSize}
81
+ >
93
82
  <ProductCard.Component
94
83
  aspectRatio={aspectRatio}
95
84
  imgProps={{
96
85
  width: 150,
97
86
  height: 150,
98
87
  sizes: '30vw',
99
- loading: 'lazy',
88
+ loading: 'eager',
100
89
  }}
101
90
  {...ProductCard.props}
102
91
  bordered={bordered ?? ProductCard.props.bordered}
@@ -108,6 +97,37 @@ function ProductGrid({
108
97
  taxesConfiguration={taxesConfiguration}
109
98
  sponsoredLabel={sponsoredLabel}
110
99
  />
100
+ </ProductSentinel>
101
+ </UIProductGridItem>
102
+ ))}
103
+ <ViewportObserver sectionName="UIProductGrid-out-viewport">
104
+ {products.slice(2).map(({ node: product }, idx) => (
105
+ <UIProductGridItem key={`${product.id}`}>
106
+ <ProductSentinel
107
+ product={product}
108
+ title={title}
109
+ page={page}
110
+ pageSize={pageSize}
111
+ >
112
+ <ProductCard.Component
113
+ aspectRatio={aspectRatio}
114
+ imgProps={{
115
+ width: 150,
116
+ height: 150,
117
+ sizes: '30vw',
118
+ loading: 'lazy',
119
+ }}
120
+ {...ProductCard.props}
121
+ bordered={bordered ?? ProductCard.props.bordered}
122
+ showDiscountBadge={
123
+ showDiscountBadge ?? ProductCard.props.showDiscountBadge
124
+ }
125
+ product={product}
126
+ index={pageSize * page + idx + 1}
127
+ taxesConfiguration={taxesConfiguration}
128
+ sponsoredLabel={sponsoredLabel}
129
+ />
130
+ </ProductSentinel>
111
131
  </UIProductGridItem>
112
132
  ))}
113
133
  </ViewportObserver>
@@ -116,24 +136,31 @@ function ProductGrid({
116
136
  <>
117
137
  {products.map(({ node: product }, idx) => (
118
138
  <UIProductGridItem key={`${product.id}`}>
119
- <ProductCard.Component
120
- aspectRatio={aspectRatio}
121
- imgProps={{
122
- width: 150,
123
- height: 150,
124
- sizes: '30vw',
125
- loading: idx === 0 ? 'eager' : 'lazy',
126
- }}
127
- {...ProductCard.props}
128
- bordered={bordered ?? ProductCard.props.bordered}
129
- showDiscountBadge={
130
- showDiscountBadge ?? ProductCard.props.showDiscountBadge
131
- }
139
+ <ProductSentinel
132
140
  product={product}
133
- index={pageSize * page + idx + 1}
134
- taxesConfiguration={taxesConfiguration}
135
- sponsoredLabel={sponsoredLabel}
136
- />
141
+ title={title}
142
+ page={page}
143
+ pageSize={pageSize}
144
+ >
145
+ <ProductCard.Component
146
+ aspectRatio={aspectRatio}
147
+ imgProps={{
148
+ width: 150,
149
+ height: 150,
150
+ sizes: '30vw',
151
+ loading: idx === 0 ? 'eager' : 'lazy',
152
+ }}
153
+ {...ProductCard.props}
154
+ bordered={bordered ?? ProductCard.props.bordered}
155
+ showDiscountBadge={
156
+ showDiscountBadge ?? ProductCard.props.showDiscountBadge
157
+ }
158
+ product={product}
159
+ index={pageSize * page + idx + 1}
160
+ taxesConfiguration={taxesConfiguration}
161
+ sponsoredLabel={sponsoredLabel}
162
+ />
163
+ </ProductSentinel>
137
164
  </UIProductGridItem>
138
165
  ))}
139
166
  </>
@@ -40,6 +40,7 @@ function ProductGalleryPage({
40
40
  pageSize={itemsPerPage}
41
41
  productCard={productCard}
42
42
  firstPage={firstPage}
43
+ title={title}
43
44
  />
44
45
  </Sentinel>
45
46
  )
@@ -1,44 +1,35 @@
1
- import { NextSeo } from 'next-seo'
2
- import type { ComponentType } from 'react'
3
- import { useEffect } from 'react'
4
- import RenderSections from 'src/components/cms/RenderSections'
5
- import { default as GLOBAL_COMPONENTS } from 'src/components/cms/global/Components'
6
- import CUSTOM_COMPONENTS from 'src/customizations/src/components'
7
1
  import storeConfig from 'discovery.config'
8
- import {
9
- getServerSideProps,
10
- type MyAccountProps,
11
- } from 'src/experimental/myAccountSeverSideProps'
12
- import { useRouter } from 'next/router'
13
2
 
14
- /* A list of components that can be used in the CMS. */
15
- const COMPONENTS: Record<string, ComponentType<any>> = {
16
- ...GLOBAL_COMPONENTS,
17
- ...CUSTOM_COMPONENTS,
3
+ import type { GetServerSideProps, NextPage } from 'next'
4
+
5
+ const MyAccountRedirectPage: NextPage = () => {
6
+ return null
18
7
  }
19
8
 
20
- function Page({ globalSections }: MyAccountProps) {
21
- const router = useRouter()
22
- useEffect(() => {
23
- if (storeConfig.experimental.enableFaststoreMyAccount) {
24
- router.push('/account/profile') // current default path in my account
25
- } else {
26
- window.location.href = `${storeConfig.accountUrl}${window.location.search}`
9
+ export const getServerSideProps: GetServerSideProps = async ({ query }) => {
10
+ if (storeConfig.experimental.enableFaststoreMyAccount) {
11
+ return {
12
+ redirect: {
13
+ destination: '/account/profile',
14
+ permanent: false,
15
+ },
27
16
  }
28
- }, [])
17
+ }
29
18
 
30
- return (
31
- <RenderSections
32
- globalSections={globalSections.sections}
33
- components={COMPONENTS}
34
- >
35
- <NextSeo noindex nofollow />
19
+ const searchParams = new URLSearchParams()
36
20
 
37
- <div>loading...</div>
38
- </RenderSections>
39
- )
40
- }
21
+ for (const key in query) {
22
+ const value = query[key]
23
+ const values = Array.isArray(value) ? value : [value]
24
+ values.forEach((v) => v && searchParams.append(key, v))
25
+ }
41
26
 
42
- export { getServerSideProps }
27
+ return {
28
+ redirect: {
29
+ destination: `${storeConfig.accountUrl}?${searchParams.toString()}`,
30
+ permanent: false,
31
+ },
32
+ }
33
+ }
43
34
 
44
- export default Page
35
+ export default MyAccountRedirectPage
@@ -1,4 +1,4 @@
1
- import { useCallback } from 'react'
1
+ import { useCallback, useRef } from 'react'
2
2
  import type { CurrencyCode, ViewItemListEvent } from '@faststore/sdk'
3
3
 
4
4
  import { useSession } from 'src/sdk/session'
@@ -7,12 +7,16 @@ import type { ProductSummary_ProductFragment } from '@generated/graphql'
7
7
  import type { AnalyticsItem } from '../types'
8
8
 
9
9
  type Props = {
10
- products: Array<{ node: ProductSummary_ProductFragment }>
10
+ products?: Array<{ node: ProductSummary_ProductFragment }>
11
11
  title: string
12
12
  page: number
13
13
  pageSize: number
14
14
  }
15
15
 
16
+ type ProductsFallbackProps = {
17
+ products?: Array<{ node: ProductSummary_ProductFragment }>
18
+ }
19
+
16
20
  export const useViewItemListEvent = ({
17
21
  products,
18
22
  title,
@@ -23,31 +27,40 @@ export const useViewItemListEvent = ({
23
27
  currency: { code },
24
28
  } = useSession()
25
29
 
26
- const sendViewItemListEvent = useCallback(() => {
27
- import('@faststore/sdk').then(({ sendAnalyticsEvent }) => {
28
- sendAnalyticsEvent<ViewItemListEvent<AnalyticsItem>>({
29
- name: 'view_item_list',
30
- params: {
31
- item_list_name: title,
32
- item_list_id: title,
33
- items: products.map(({ node: product }, index) => ({
34
- item_id: product.isVariantOf.productGroupID,
35
- item_name: product.isVariantOf.name,
36
- item_brand: product.brand.name,
37
- item_variant: product.sku,
38
- price: product.offers.offers[0].price,
39
- index: page * pageSize + index + 1,
40
- discount:
41
- product.offers.offers[0].listPrice -
42
- product.offers.offers[0].price,
43
- currency: code as CurrencyCode,
44
- item_variant_name: product.name,
45
- product_reference_id: product.gtin,
46
- })),
47
- },
30
+ const sendViewItemListEvent = useCallback(
31
+ (productsFallback?: ProductsFallbackProps) => {
32
+ const items = products?.length ? products : productsFallback?.products
33
+
34
+ if (!items || items.length === 0) {
35
+ return
36
+ }
37
+
38
+ import('@faststore/sdk').then(({ sendAnalyticsEvent }) => {
39
+ sendAnalyticsEvent<ViewItemListEvent<AnalyticsItem>>({
40
+ name: 'view_item_list',
41
+ params: {
42
+ item_list_name: title,
43
+ item_list_id: title,
44
+ items: items.map(({ node: product }, index) => ({
45
+ item_id: product.isVariantOf.productGroupID,
46
+ item_name: product.isVariantOf.name,
47
+ item_brand: product.brand.name,
48
+ item_variant: product.sku,
49
+ price: product.offers.offers[0].price,
50
+ index: page * pageSize + index + 1,
51
+ discount:
52
+ product.offers.offers[0].listPrice -
53
+ product.offers.offers[0].price,
54
+ currency: code as CurrencyCode,
55
+ item_variant_name: product.name,
56
+ product_reference_id: product.gtin,
57
+ })),
58
+ },
59
+ })
48
60
  })
49
- })
50
- }, [code, products, title, page, pageSize])
61
+ },
62
+ [code, products, title, page, pageSize]
63
+ )
51
64
 
52
65
  return { sendViewItemListEvent }
53
66
  }
@@ -0,0 +1,60 @@
1
+ import { useEffect, type PropsWithChildren } from 'react'
2
+ import { useInView } from 'react-intersection-observer'
3
+
4
+ import type { ProductSummary_ProductFragment } from '@generated/graphql'
5
+
6
+ import { useViewItemListEvent } from '../analytics/hooks/useViewItemListEvent'
7
+ import { queueViewItemList } from './viewItemListQueue'
8
+
9
+ interface ProductSentinelProps {
10
+ /**
11
+ * Title for the `ProductSentinel` component.
12
+ */
13
+ title: string
14
+ /**
15
+ * Product listed on the grid.
16
+ */
17
+ product: ProductSummary_ProductFragment
18
+ /**
19
+ * The page's number that the Product is being rendered.
20
+ */
21
+ page: number
22
+ /**
23
+ * Quantity of products listed by page.
24
+ */
25
+ pageSize: number
26
+ }
27
+
28
+ /**
29
+ * Use this component to add a boundary between Products to send the right view_item_list event to GA in ProductGallery
30
+ */
31
+ export function ProductSentinel({
32
+ product,
33
+ title,
34
+ page,
35
+ pageSize,
36
+ children,
37
+ }: PropsWithChildren<ProductSentinelProps>) {
38
+ const { ref, inView } = useInView()
39
+
40
+ const { sendViewItemListEvent } = useViewItemListEvent({
41
+ title,
42
+ page,
43
+ pageSize,
44
+ })
45
+
46
+ useEffect(() => {
47
+ if (inView) {
48
+ queueViewItemList({ product, sendViewItemListEvent })
49
+ }
50
+ }, [inView, product])
51
+
52
+ return (
53
+ // minHeight to avoid the layout shift when the sentinel is not in view, '15.225rem' is the min value from the Product Card
54
+ <div ref={ref} style={{ minHeight: '15.225rem' }}>
55
+ {children}
56
+ </div>
57
+ )
58
+ }
59
+
60
+ export default ProductSentinel
@@ -0,0 +1,31 @@
1
+ import type { ProductSummary_ProductFragment } from '@generated/graphql'
2
+
3
+ const sentItems = new Set<string>()
4
+ let pendingItems: { node: ProductSummary_ProductFragment }[] = []
5
+ let debounceTimeout: NodeJS.Timeout | null = null
6
+
7
+ type QueueViewItemListProps = {
8
+ product: ProductSummary_ProductFragment
9
+ sendViewItemListEvent: (props: {
10
+ products: Array<{ node: ProductSummary_ProductFragment }>
11
+ }) => void
12
+ }
13
+
14
+ export function queueViewItemList({
15
+ product,
16
+ sendViewItemListEvent,
17
+ }: QueueViewItemListProps) {
18
+ if (sentItems.has(product.id)) return
19
+
20
+ sentItems.add(product.id)
21
+ pendingItems.push({ node: product })
22
+
23
+ if (debounceTimeout) clearTimeout(debounceTimeout)
24
+
25
+ debounceTimeout = setTimeout(() => {
26
+ if (pendingItems.length > 0) {
27
+ sendViewItemListEvent({ products: pendingItems })
28
+ pendingItems = []
29
+ }
30
+ }, 300)
31
+ }
@@ -1,5 +1,5 @@
1
1
  import { useSearch } from '@faststore/sdk'
2
- import { useEffect, useRef, type PropsWithChildren } from 'react'
2
+ import { useEffect, type PropsWithChildren } from 'react'
3
3
  import { useInView } from 'react-intersection-observer'
4
4
  import type { NextRouter } from 'next/router'
5
5
  import { useRouter } from 'next/router'
@@ -7,7 +7,6 @@ import { useRouter } from 'next/router'
7
7
  import type { ProductSummary_ProductFragment } from '@generated/graphql'
8
8
 
9
9
  import useScreenResize from 'src/sdk/ui/useScreenResize'
10
- import { useViewItemListEvent } from '../analytics/hooks/useViewItemListEvent'
11
10
 
12
11
  interface SentinelProps {
13
12
  page: number
@@ -39,24 +38,11 @@ const replacePagination = (page: string, router: NextRouter) => {
39
38
  * Also, this component's name is kind of curious. Wikipedia calls is Page Break(https://en.wikipedia.org/wiki/Page_break)
40
39
  * however all codes I've seen online use Sentinel
41
40
  */
42
- function Sentinel({
43
- page,
44
- pageSize,
45
- products,
46
- title,
47
- children,
48
- }: PropsWithChildren<SentinelProps>) {
41
+ function Sentinel({ page, children }: PropsWithChildren<SentinelProps>) {
49
42
  const router = useRouter()
50
43
  const { pages } = useSearch()
51
44
  const { isMobile } = useScreenResize()
52
45
  const { ref, inView } = useInView({ threshold: isMobile ? 0.3 : 0.7 })
53
- const { sendViewItemListEvent } = useViewItemListEvent({
54
- products,
55
- title,
56
- page,
57
- pageSize,
58
- })
59
- const viewedRef = useRef(false)
60
46
 
61
47
  useEffect(() => {
62
48
  // Only replace pagination state when infinite scroll
@@ -64,19 +50,7 @@ function Sentinel({
64
50
  if (inView && pages.length > 1) {
65
51
  replacePagination(page.toString(), router)
66
52
  }
67
-
68
- if (inView && !viewedRef.current && products.length) {
69
- sendViewItemListEvent()
70
- viewedRef.current = true
71
- }
72
- }, [
73
- pages.length,
74
- inView,
75
- page,
76
- router,
77
- sendViewItemListEvent,
78
- products.length,
79
- ])
53
+ }, [pages.length, inView, page])
80
54
 
81
55
  return <div ref={ref}>{children}</div>
82
56
  }
@@ -1 +0,0 @@
1
- "use strict";exports.id=2032,exports.ids=[2032],exports.modules={52032:(e,i,t)=>{t.d(i,{m:()=>useViewItemListEvent});var r=t(16689),s=t(84355);let useViewItemListEvent=({products:e,title:i,page:n,pageSize:a})=>{let{currency:{code:m}}=(0,s.kP)(),o=(0,r.useCallback)(()=>{Promise.all([t.e(2880),t.e(1153)]).then(t.bind(t,81153)).then(({sendAnalyticsEvent:t})=>{t({name:"view_item_list",params:{item_list_name:i,item_list_id:i,items:e.map(({node:e},i)=>({item_id:e.isVariantOf.productGroupID,item_name:e.isVariantOf.name,item_brand:e.brand.name,item_variant:e.sku,price:e.offers.offers[0].price,index:n*a+i+1,discount:e.offers.offers[0].listPrice-e.offers.offers[0].price,currency:m,item_variant_name:e.name,product_reference_id:e.gtin}))}})})},[m,e,i,n,a]);return{sendViewItemListEvent:o}}}};
@@ -1 +0,0 @@
1
- self.__BUILD_MANIFEST=function(s,c,a,t,e,i){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":[s,c,a,"static/css/b1806cbafd0c1f81.css","static/chunks/pages/index-d36123e232b889d8.js"],"/404":[s,c,a,t,"static/chunks/pages/404-8e61ea158e7b314c.js"],"/500":[s,c,a,t,"static/chunks/pages/500-68f51177e64de6e7.js"],"/_error":["static/chunks/pages/_error-f432276d5ac8f150.js"],"/account":[s,c,a,"static/chunks/pages/account-32bc9db3fea9b0c4.js"],"/account/profile":[s,c,a,"static/css/249b9deaabc0d32d.css","static/chunks/pages/account/profile-1fdb500de8f12b3a.js"],"/checkout":[s,c,a,"static/chunks/pages/checkout-0dfe0dc9cee443ba.js"],"/login":[s,c,a,t,"static/chunks/pages/login-b59ea5561defc0d4.js"],"/s":[s,c,a,e,i,t,"static/chunks/pages/s-ffb46af7067b79b1.js"],"/[slug]/p":[s,"static/chunks/5259-90f7ef92cf24e49a.js",c,a,"static/chunks/7498-49bf89838314b503.js","static/css/a3ca6a9b63f657be.css","static/css/62a5153ac7061286.css","static/chunks/4349-24e224a3b1307f65.js","static/css/b5bf49598c8f8b66.css","static/chunks/pages/[slug]/p-2e02254149cef33d.js"],"/[...slug]":[s,c,a,e,i,"static/chunks/pages/[...slug]-f4fd6c8d7dc53f8f.js"],sortedPages:["/","/404","/500","/_app","/_error","/account","/account/profile","/checkout","/login","/s","/[slug]/p","/[...slug]"]}}("static/chunks/6167-ecb49640dcb9d566.js","static/css/b0c0e0632c5d7f52.css","static/chunks/6335-2bf379c0870c440e.js","static/css/2980acad3f8e1028.css","static/css/2841bab51b99dd53.css","static/chunks/4625-07284599f239c05b.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
@@ -1 +0,0 @@
1
- "use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[3143],{3143:function(e,r,t){t.r(r),t.d(r,{default:function(){return w}});var n=t(4424),i=t(9115),o=t(1347),s=t(3125),a=t(4194),c=t(143),d=t(6220),u=t(6259),l=t(1549);function ownKeys(e,r){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);r&&(n=n.filter(function(r){return Object.getOwnPropertyDescriptor(e,r).enumerable})),t.push.apply(t,n)}return t}function _objectSpread(e){for(var r=1;r<arguments.length;r++){var t=null!=arguments[r]?arguments[r]:{};r%2?ownKeys(Object(t),!0).forEach(function(r){(0,n.Z)(e,r,t[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(t)):ownKeys(Object(t)).forEach(function(r){Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(t,r))})}return e}var p=(0,a.memo)(function(e){var{products:r,page:t,pageSize:n,productCard:{showDiscountBadge:a,bordered:p,taxesConfiguration:h,sponsoredLabel:f}={},firstPage:v}=e,{isMobile:g}=(0,u.Z)(),{__experimentalProductCard:b}=(0,d.r3)();return(0,l.jsx)(s.Z,{aspectRatio:1,loading:0===r.length,children:(0,l.jsx)(i.Z,{children:g&&v===t?(0,l.jsxs)(l.Fragment,{children:[r.slice(0,2).map((e,r)=>{var{node:i}=e;return(0,l.jsx)(o.Z,{children:(0,l.jsx)(b.Component,_objectSpread(_objectSpread({aspectRatio:1,imgProps:{width:150,height:150,sizes:"30vw",loading:"eager"}},b.props),{},{bordered:null!=p?p:b.props.bordered,showDiscountBadge:null!=a?a:b.props.showDiscountBadge,product:i,index:n*t+r+1,taxesConfiguration:h,sponsoredLabel:f}))},"".concat(i.id))}),(0,l.jsx)(c.Z,{sectionName:"UIProductGrid-out-viewport",children:r.slice(2).map((e,r)=>{var{node:i}=e;return(0,l.jsx)(o.Z,{children:(0,l.jsx)(b.Component,_objectSpread(_objectSpread({aspectRatio:1,imgProps:{width:150,height:150,sizes:"30vw",loading:"lazy"}},b.props),{},{bordered:null!=p?p:b.props.bordered,showDiscountBadge:null!=a?a:b.props.showDiscountBadge,product:i,index:n*t+r+1,taxesConfiguration:h,sponsoredLabel:f}))},"".concat(i.id))})})]}):(0,l.jsx)(l.Fragment,{children:r.map((e,r)=>{var{node:i}=e;return(0,l.jsx)(o.Z,{children:(0,l.jsx)(b.Component,_objectSpread(_objectSpread({aspectRatio:1,imgProps:{width:150,height:150,sizes:"30vw",loading:0===r?"eager":"lazy"}},b.props),{},{bordered:null!=p?p:b.props.bordered,showDiscountBadge:null!=a?a:b.props.showDiscountBadge,product:i,index:n*t+r+1,taxesConfiguration:h,sponsoredLabel:f}))},"".concat(i.id))})})})})}),h=t(9029),f=t(6410),v=t(4444),g=t(2032),replacePagination=(e,r)=>{var t=new URL(window.location.href);t.searchParams.get("page")!==e&&(t.searchParams.set("page",e),r.replace(t,void 0,{shallow:!0,scroll:!1}))},search_Sentinel=function(e){var{page:r,pageSize:t,products:n,title:i,children:o}=e,s=(0,v.useRouter)(),{pages:c}=(0,h.R)(),{isMobile:d}=(0,u.Z)(),{ref:p,inView:b}=(0,f.YD)({threshold:d?.3:.7}),{sendViewItemListEvent:w}=(0,g.m)({products:n,title:i,page:r,pageSize:t}),y=(0,a.useRef)(!1);return(0,a.useEffect)(()=>{b&&c.length>1&&replacePagination(r.toString(),s),b&&!y.current&&n.length&&(w(),y.current=!0)},[c.length,b,r,s,w,n.length]),(0,l.jsx)("div",{ref:p,children:o})},b=t(2125),w=(0,a.memo)(function(e){var r,t,n,{page:i,title:o,productCard:s,itemsPerPage:a,firstPage:c}=e,{data:d}=(0,b.__)(i),u=null!==(r=null==d?void 0:null===(t=d.search)||void 0===t?void 0:null===(n=t.products)||void 0===n?void 0:n.edges)&&void 0!==r?r:[];return(0,l.jsx)(search_Sentinel,{products:u,page:i,pageSize:a,title:o,children:(0,l.jsx)(p,{products:u,page:i,pageSize:a,productCard:s,firstPage:c})})})},2032:function(e,r,t){t.d(r,{m:function(){return useViewItemListEvent}});var n=t(4194),i=t(4355),useViewItemListEvent=e=>{var{products:r,title:o,page:s,pageSize:a}=e,{currency:{code:c}}=(0,i.kP)();return{sendViewItemListEvent:(0,n.useCallback)(()=>{t.e(7195).then(t.bind(t,1153)).then(e=>{var{sendAnalyticsEvent:t}=e;t({name:"view_item_list",params:{item_list_name:o,item_list_id:o,items:r.map((e,r)=>{var{node:t}=e;return{item_id:t.isVariantOf.productGroupID,item_name:t.isVariantOf.name,item_brand:t.brand.name,item_variant:t.sku,price:t.offers.offers[0].price,index:s*a+r+1,discount:t.offers.offers[0].listPrice-t.offers.offers[0].price,currency:c,item_variant_name:t.name,product_reference_id:t.gtin}})}})})},[c,r,o,s,a])}}},6410:function(e,r,t){t.d(r,{YD:function(){return useInView},df:function(){return d}});var n=t(4194);function _extends(){return(_extends=Object.assign||function(e){for(var r=1;r<arguments.length;r++){var t=arguments[r];for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])}return e}).apply(this,arguments)}function _setPrototypeOf(e,r){return(_setPrototypeOf=Object.setPrototypeOf||function(e,r){return e.__proto__=r,e})(e,r)}var i=new Map,o=new WeakMap,s=0,a=void 0;function observe(e,r,t,n){if(void 0===t&&(t={}),void 0===n&&(n=a),void 0===window.IntersectionObserver&&void 0!==n){var c=e.getBoundingClientRect();return r(n,{isIntersecting:n,target:e,intersectionRatio:"number"==typeof t.threshold?t.threshold:0,time:0,boundingClientRect:c,intersectionRect:c,rootBounds:c}),function(){}}var d=function(e){var r=Object.keys(e).sort().filter(function(r){return void 0!==e[r]}).map(function(r){var t;return r+"_"+("root"===r?(t=e.root)?(o.has(t)||(s+=1,o.set(t,s.toString())),o.get(t)):"0":e[r])}).toString(),t=i.get(r);if(!t){var n,a=new Map,c=new IntersectionObserver(function(r){r.forEach(function(r){var t,i=r.isIntersecting&&n.some(function(e){return r.intersectionRatio>=e});e.trackVisibility&&void 0===r.isVisible&&(r.isVisible=i),null==(t=a.get(r.target))||t.forEach(function(e){e(i,r)})})},e);n=c.thresholds||(Array.isArray(e.threshold)?e.threshold:[e.threshold||0]),t={id:r,observer:c,elements:a},i.set(r,t)}return t}(t),u=d.id,l=d.observer,p=d.elements,h=p.get(e)||[];return p.has(e)||p.set(e,h),h.push(r),l.observe(e),function(){h.splice(h.indexOf(r),1),0===h.length&&(p.delete(e),l.unobserve(e)),0===p.size&&(l.disconnect(),i.delete(u))}}var c=["children","as","triggerOnce","threshold","root","rootMargin","onChange","skip","trackVisibility","delay","initialInView","fallbackInView"];function isPlainChildren(e){return"function"!=typeof e.children}var d=function(e){function InView(r){var t;return(t=e.call(this,r)||this).node=null,t._unobserveCb=null,t.handleNode=function(e){!t.node||(t.unobserve(),e||t.props.triggerOnce||t.props.skip||t.setState({inView:!!t.props.initialInView,entry:void 0})),t.node=e||null,t.observeNode()},t.handleChange=function(e,r){e&&t.props.triggerOnce&&t.unobserve(),isPlainChildren(t.props)||t.setState({inView:e,entry:r}),t.props.onChange&&t.props.onChange(e,r)},t.state={inView:!!r.initialInView,entry:void 0},t}InView.prototype=Object.create(e.prototype),InView.prototype.constructor=InView,_setPrototypeOf(InView,e);var r=InView.prototype;return r.componentDidUpdate=function(e){(e.rootMargin!==this.props.rootMargin||e.root!==this.props.root||e.threshold!==this.props.threshold||e.skip!==this.props.skip||e.trackVisibility!==this.props.trackVisibility||e.delay!==this.props.delay)&&(this.unobserve(),this.observeNode())},r.componentWillUnmount=function(){this.unobserve(),this.node=null},r.observeNode=function(){if(this.node&&!this.props.skip){var e=this.props,r=e.threshold,t=e.root,n=e.rootMargin,i=e.trackVisibility,o=e.delay,s=e.fallbackInView;this._unobserveCb=observe(this.node,this.handleChange,{threshold:r,root:t,rootMargin:n,trackVisibility:i,delay:o},s)}},r.unobserve=function(){this._unobserveCb&&(this._unobserveCb(),this._unobserveCb=null)},r.render=function(){if(!isPlainChildren(this.props)){var e=this.state,r=e.inView,t=e.entry;return this.props.children({inView:r,entry:t,ref:this.handleNode})}var i=this.props,o=i.children,s=i.as,a=function(e,r){if(null==e)return{};var t,n,i={},o=Object.keys(e);for(n=0;n<o.length;n++)t=o[n],r.indexOf(t)>=0||(i[t]=e[t]);return i}(i,c);return n.createElement(s||"div",_extends({ref:this.handleNode},a),o)},InView}(n.Component);function useInView(e){var r=void 0===e?{}:e,t=r.threshold,i=r.delay,o=r.trackVisibility,s=r.rootMargin,a=r.root,c=r.triggerOnce,d=r.skip,u=r.initialInView,l=r.fallbackInView,p=n.useRef(),h=n.useState({inView:!!u}),f=h[0],v=h[1],g=n.useCallback(function(e){void 0!==p.current&&(p.current(),p.current=void 0),!d&&e&&(p.current=observe(e,function(e,r){v({inView:e,entry:r}),r.isIntersecting&&c&&p.current&&(p.current(),p.current=void 0)},{root:a,rootMargin:s,threshold:t,trackVisibility:o,delay:i},l))},[Array.isArray(t)?t.toString():t,a,s,c,d,o,l,i]);(0,n.useEffect)(function(){p.current||!f.entry||c||d||v({inView:!!u})});var b=[g,f.inView,f.entry];return b.ref=b[0],b.inView=b[1],b.entry=b[2],b}d.displayName="InView",d.defaultProps={threshold:0,triggerOnce:!1,initialInView:!1}},9115:function(e,r,t){var n=t(4194);let i=(0,n.forwardRef)(function({testId:e="fs-product-grid",children:r,...t},i){return n.createElement("ul",{ref:i,"data-fs-product-grid":!0,...t},r)});r.Z=i},1347:function(e,r,t){var n=t(4194);let i=(0,n.forwardRef)(function({testId:e="fs-product-grid-item",children:r,...t},i){return n.createElement("li",{ref:i,"data-fs-product-grid-item":!0,...t},r)});r.Z=i}}]);