@graphcommerce/magento-recently-viewed-products 7.1.0-canary.41

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @graphcommerce/magento-recently-viewed-products
2
+
3
+ ## 7.1.0-canary.41
4
+
5
+ ### Minor Changes
6
+
7
+ - [#2077](https://github.com/graphcommerce-org/graphcommerce/pull/2077) [`e661106d4`](https://github.com/graphcommerce-org/graphcommerce/commit/e661106d45e51c617533f19b397a812e22b6fc82) - Added recently viewed products hook and render component ([@bramvanderholst](https://github.com/bramvanderholst))
@@ -0,0 +1,20 @@
1
+ """
2
+ Settings for recently viewed products
3
+ """
4
+ input RecentlyViewedProductsConfig {
5
+ """
6
+ Enable/disable recently viewed products
7
+ """
8
+ enabled: Boolean
9
+ """
10
+ Number of recently viewed products to be stored in localStorage
11
+ """
12
+ maxCount: Int
13
+ }
14
+
15
+ extend input GraphCommerceConfig {
16
+ """
17
+ Settings for recently viewed products
18
+ """
19
+ recentlyViewedProducts: RecentlyViewedProductsConfig
20
+ }
package/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # @graphcommerce/magento-recently-viewed-products
2
+
3
+ When visiting a product page, the product SKU is added to a list of recently
4
+ viewed products, stored in the users localStorage.
5
+
6
+ ## Configuration
7
+
8
+ When `configurableVariantForSimple` is enabled in `graphcommerce.config.js`,
9
+ fully configured configurable products will be shown as the selected variant in
10
+ recently viewed products.
@@ -0,0 +1,43 @@
1
+ import { ProductListItemRenderer, ProductScroller } from '@graphcommerce/magento-product'
2
+ import { useInView } from 'framer-motion'
3
+ import { useRef } from 'react'
4
+ import {
5
+ UseRecentlyViewedProductsProps,
6
+ useRecentlyViewedProducts,
7
+ useRecentlyViewedSkus,
8
+ } from '../hooks'
9
+
10
+ export type RecentlyViewedProductsProps = UseRecentlyViewedProductsProps & {
11
+ title?: React.ReactNode
12
+ productListRenderer: ProductListItemRenderer
13
+ loading?: 'lazy' | 'eager'
14
+ }
15
+ export function RecentlyViewedProducts(props: RecentlyViewedProductsProps) {
16
+ const { exclude, title, productListRenderer, loading = 'lazy' } = props
17
+
18
+ const ref = useRef<HTMLDivElement>(null)
19
+ const isInView = useInView(ref, { margin: '300px', once: true })
20
+ const { skus } = useRecentlyViewedSkus({ exclude })
21
+ const productList = useRecentlyViewedProducts({ exclude, skip: !isInView && loading === 'lazy' })
22
+
23
+ if (
24
+ !import.meta.graphCommerce.recentlyViewedProducts?.enabled ||
25
+ (!productList.loading && !skus.length)
26
+ ) {
27
+ return null
28
+ }
29
+
30
+ const loadingProducts = [...Array(skus.length - productList.products.length).keys()].map((i) => ({
31
+ __typename: 'Skeleton' as const,
32
+ uid: i.toString(),
33
+ }))
34
+
35
+ return (
36
+ <ProductScroller
37
+ ref={ref}
38
+ productListRenderer={productListRenderer}
39
+ title={title}
40
+ items={[...loadingProducts, ...productList.products]}
41
+ />
42
+ )
43
+ }
@@ -0,0 +1,10 @@
1
+ query RecentlyViewedProducts {
2
+ recentlyViewedProducts @client {
3
+ __typename
4
+ items {
5
+ __typename
6
+ sku
7
+ parentSku
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,12 @@
1
+ extend type Query {
2
+ recentlyViewedProducts: RecentlyViewedProducts
3
+ }
4
+
5
+ type RecentlyViewedProducts {
6
+ items: [RecentlyViewedProduct!]!
7
+ }
8
+
9
+ type RecentlyViewedProduct {
10
+ sku: String!
11
+ parentSku: String
12
+ }
package/hooks/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './useRecentlyViewedProducts'
2
+ export * from './useRecentlyViewedSkus'
@@ -0,0 +1,44 @@
1
+ import { useQuery } from '@graphcommerce/graphql'
2
+ import { ProductListDocument } from '@graphcommerce/magento-product'
3
+ import { nonNullable } from '@graphcommerce/next-ui'
4
+ import { useRecentlyViewedSkus, UseRecentlyViewedSkusProps } from './useRecentlyViewedSkus'
5
+
6
+ export type UseRecentlyViewedProductsProps = UseRecentlyViewedSkusProps & { skip?: boolean }
7
+ export function useRecentlyViewedProducts(props: UseRecentlyViewedProductsProps) {
8
+ const { exclude, skip = false } = props
9
+ let { skus, loading } = useRecentlyViewedSkus()
10
+
11
+ const productList = useQuery(ProductListDocument, {
12
+ variables: {
13
+ filters: {
14
+ sku: {
15
+ in: skus.map((p) => p.sku).sort(),
16
+ },
17
+ },
18
+ },
19
+ skip: loading || !skus.length || skip,
20
+ })
21
+
22
+ const productData =
23
+ productList.data?.products?.items || productList.previousData?.products?.items || []
24
+
25
+ if (exclude) {
26
+ skus = skus.filter(
27
+ (item) =>
28
+ item?.sku &&
29
+ !exclude.includes(item.sku) &&
30
+ item?.parentSku &&
31
+ !exclude.includes(item.parentSku),
32
+ )
33
+ }
34
+
35
+ // Sort products based on the time they were viewed. Last viewed item should be the first item in the array
36
+ const products = skus
37
+ .map((sku) => productData.find((p) => (p?.sku || '') === sku.sku))
38
+ .filter(nonNullable)
39
+
40
+ return {
41
+ products,
42
+ loading: loading || productList.loading,
43
+ }
44
+ }
@@ -0,0 +1,24 @@
1
+ import { useQuery } from '@graphcommerce/graphql'
2
+ import { RecentlyViewedProductsDocument } from '../graphql/RecentlyViewedProducts.gql'
3
+
4
+ export type UseRecentlyViewedSkusProps = { exclude?: string[] }
5
+
6
+ export function useRecentlyViewedSkus(props: UseRecentlyViewedSkusProps = {}) {
7
+ const { exclude } = props
8
+ const { data, loading, previousData } = useQuery(RecentlyViewedProductsDocument, {
9
+ skip: !import.meta.graphCommerce.recentlyViewedProducts?.enabled,
10
+ })
11
+ let skus = (loading ? previousData : data)?.recentlyViewedProducts?.items || []
12
+
13
+ // Filter out excluded products (current product page)
14
+ if (exclude)
15
+ skus = skus.filter(
16
+ (item) =>
17
+ item?.sku &&
18
+ !exclude.includes(item.sku) &&
19
+ item?.parentSku &&
20
+ !exclude.includes(item.parentSku),
21
+ )
22
+
23
+ return { skus, loading }
24
+ }
package/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './components/RecentlyViewedProducts'
2
+ export * from './hooks'
3
+ export * from './graphql/RecentlyViewedProducts.gql'
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@graphcommerce/magento-recently-viewed-products",
3
+ "homepage": "https://www.graphcommerce.org/",
4
+ "repository": "github:graphcommerce-org/graphcommerce",
5
+ "version": "7.1.0-canary.41",
6
+ "sideEffects": false,
7
+ "prettier": "@graphcommerce/prettier-config-pwa",
8
+ "eslintConfig": {
9
+ "extends": "@graphcommerce/eslint-config-pwa",
10
+ "parserOptions": {
11
+ "project": "./tsconfig.json"
12
+ }
13
+ },
14
+ "peerDependencies": {
15
+ "@graphcommerce/eslint-config-pwa": "7.1.0-canary.41",
16
+ "@graphcommerce/graphql": "7.1.0-canary.41",
17
+ "@graphcommerce/graphql-mesh": "7.1.0-canary.41",
18
+ "@graphcommerce/magento-cart": "7.1.0-canary.41",
19
+ "@graphcommerce/magento-product": "7.1.0-canary.41",
20
+ "@graphcommerce/magento-product-configurable": "7.1.0-canary.41",
21
+ "@graphcommerce/next-config": "7.1.0-canary.41",
22
+ "@graphcommerce/next-ui": "7.1.0-canary.41",
23
+ "@graphcommerce/prettier-config-pwa": "7.1.0-canary.41",
24
+ "@graphcommerce/typescript-config-pwa": "7.1.0-canary.41",
25
+ "@mui/material": "^5.10.16",
26
+ "framer-motion": "^10.0.0",
27
+ "next": "*",
28
+ "react": "^18.2.0",
29
+ "react-dom": "^18.2.0"
30
+ }
31
+ }
@@ -0,0 +1,85 @@
1
+ import { useApolloClient } from '@graphcommerce/graphql'
2
+ import {
3
+ type AddToCartItemSelector,
4
+ type ProductPageMeta,
5
+ ProductPageMetaFragment,
6
+ } from '@graphcommerce/magento-product'
7
+ import { useConfigurableSelectedVariant } from '@graphcommerce/magento-product-configurable/hooks'
8
+ import type { IfConfig, ReactPlugin } from '@graphcommerce/next-config'
9
+ import { useEventCallback } from '@mui/material'
10
+ import { useRouter } from 'next/router'
11
+ import { useEffect } from 'react'
12
+ import { RecentlyViewedProductsDocument } from '../graphql/RecentlyViewedProducts.gql'
13
+
14
+ export const component = 'ProductPageMeta'
15
+ export const exported = '@graphcommerce/magento-product/components/ProductPageMeta/ProductPageMeta'
16
+ export const ifConfig: IfConfig = 'recentlyViewedProducts.enabled'
17
+
18
+ type PluginType = ReactPlugin<typeof ProductPageMeta, AddToCartItemSelector>
19
+
20
+ function ViewHandling(props: { product: ProductPageMetaFragment }) {
21
+ const { product } = props
22
+ const client = useApolloClient()
23
+ const variant = useConfigurableSelectedVariant({ url_key: product?.url_key, index: 0 })
24
+ const { events } = useRouter()
25
+
26
+ const registerView = useEventCallback(async () => {
27
+ const recentlyViewed = await client.query({ query: RecentlyViewedProductsDocument })
28
+ const skus = recentlyViewed.data.recentlyViewedProducts?.items ?? []
29
+
30
+ const isValidVariant =
31
+ (variant?.url_rewrites ?? []).length > 0 &&
32
+ variant?.url_key &&
33
+ import.meta.graphCommerce.configurableVariantForSimple
34
+
35
+ const parentSku = product.sku || ''
36
+ const sku = (isValidVariant ? variant.sku : product.sku) || ''
37
+
38
+ const parentSkuAlreadySet = skus.some(
39
+ (p) => p.sku === product.sku || p.parentSku === product.sku,
40
+ )
41
+ const skuAlreadySet = skus.some((p) => p.sku === sku)
42
+ const skuIsLastItem = skus.length === 0 || skus[skus.length - 1].sku === sku
43
+
44
+ if (skuAlreadySet && skuIsLastItem) return
45
+
46
+ // If SKU already exists in recently viewed products, remove it, so we can re-add it as the first item of the array
47
+ const viewedSkus = [
48
+ ...(skuIsLastItem || parentSkuAlreadySet
49
+ ? skus.filter((p) => p.sku !== parentSku && p.parentSku !== parentSku)
50
+ : skus),
51
+ ]
52
+
53
+ // Limit array
54
+ const items = [
55
+ { __typename: 'RecentlyViewedProduct' as const, parentSku, sku },
56
+ ...viewedSkus,
57
+ ].splice(0, import.meta.graphCommerce.recentlyViewedProducts?.maxCount || 10)
58
+
59
+ client.writeQuery({
60
+ query: RecentlyViewedProductsDocument,
61
+ broadcast: true,
62
+ data: { recentlyViewedProducts: { __typename: 'RecentlyViewedProducts', items } },
63
+ })
64
+ })
65
+
66
+ useEffect(() => {
67
+ events.on('routeChangeStart', registerView)
68
+ return () => events.off('routeChangeStart', registerView)
69
+ }, [events, registerView])
70
+
71
+ return null
72
+ }
73
+
74
+ const RegisterProductAsRecentlyViewed: PluginType = (props) => {
75
+ const { Prev, product } = props
76
+
77
+ return (
78
+ <>
79
+ <ViewHandling product={product} />
80
+ <Prev {...props} />
81
+ </>
82
+ )
83
+ }
84
+
85
+ export const Plugin = RegisterProductAsRecentlyViewed