@graphcommerce/magento-product 8.1.0-canary.26 → 8.1.0-canary.27

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Change Log
2
2
 
3
+ ## 8.1.0-canary.27
4
+
5
+ ### Minor Changes
6
+
7
+ - [#2273](https://github.com/graphcommerce-org/graphcommerce/pull/2273) [`77955c5`](https://github.com/graphcommerce-org/graphcommerce/commit/77955c56ac8633ab1c5e0f3ddb25e3a87236e2bb) - Improve Breadcrumbs on Category and Product pages
8
+ ([@Jessevdpoel](https://github.com/Jessevdpoel))
9
+
3
10
  ## 8.1.0-canary.26
4
11
 
5
12
  ## 8.1.0-canary.25
@@ -13,5 +13,5 @@ export function ProductPageJsonLd<T extends { '@type': string }, P extends JsonL
13
13
  props: ProductPageJsonLdProps<T, P>,
14
14
  ) {
15
15
  const { product, render } = props
16
- return <JsonLd<T> item={render(product)} />
16
+ return <JsonLd<T> item={render(product)} keyVal='product-jsonld' />
17
17
  }
@@ -1,6 +1,6 @@
1
- import { useForm, UseFormProps, UseFormReturn } from '@graphcommerce/ecommerce-ui'
1
+ import { FormAutoSubmit, useForm, UseFormProps, UseFormReturn } from '@graphcommerce/ecommerce-ui'
2
2
  import { useMatchMediaMotionValue, useMemoObject } from '@graphcommerce/next-ui'
3
- import { useEventCallback, useTheme } from '@mui/material'
3
+ import { Theme, useEventCallback, useMediaQuery, useTheme } from '@mui/material'
4
4
  import { m, useTransform } from 'framer-motion'
5
5
  import { useRouter } from 'next/router'
6
6
  import React, { BaseSyntheticEvent, createContext, useContext, useMemo, useRef } from 'react'
@@ -43,12 +43,33 @@ export type FilterFormProviderProps = Omit<
43
43
  > & {
44
44
  children: React.ReactNode
45
45
  params: ProductListParams
46
+ /**
47
+ * Whether the filter should scroll to the products list and whether to submit the form on change.
48
+ */
49
+ autoSubmitMd?: boolean
46
50
  } & DataProps
47
51
 
48
- const isSidebar = import.meta.graphCommerce.productFiltersLayout === 'SIDEBAR'
52
+ function AutoSubmitSidebarDesktop() {
53
+ const { form, submit } = useProductFiltersPro()
54
+
55
+ // We only need to auto-submit when the layout is not sidebar and we're viewing on desktop
56
+ const autoSubmitDisabled = useMediaQuery<Theme>((t) => t.breakpoints.down('md'), {
57
+ defaultMatches: false,
58
+ })
59
+
60
+ return <FormAutoSubmit control={form.control} disabled={autoSubmitDisabled} submit={submit} />
61
+ }
49
62
 
50
63
  export function ProductFiltersPro(props: FilterFormProviderProps) {
51
- const { children, params, aggregations, appliedAggregations, filterTypes, ...formProps } = props
64
+ const {
65
+ children,
66
+ params,
67
+ aggregations,
68
+ appliedAggregations,
69
+ filterTypes,
70
+ autoSubmitMd = false,
71
+ ...formProps
72
+ } = props
52
73
 
53
74
  const defaultValues = useMemoObject(toFilterParams(params))
54
75
  const form = useForm<ProductFilterParams>({ defaultValues, ...formProps })
@@ -58,7 +79,7 @@ export function ProductFiltersPro(props: FilterFormProviderProps) {
58
79
  const theme = useTheme()
59
80
  const isDesktop = useMatchMediaMotionValue('up', 'md')
60
81
  const scrollMarginTop = useTransform(() => (isDesktop.get() ? 0 : theme.appShell.headerHeightSm))
61
- const scroll = useTransform(() => !isSidebar || isDesktop.get())
82
+ const scroll = useTransform(() => !autoSubmitMd || isDesktop.get())
62
83
 
63
84
  const submit = useEventCallback(
64
85
  form.handleSubmit(async (formValues) => {
@@ -87,6 +108,7 @@ export function ProductFiltersPro(props: FilterFormProviderProps) {
87
108
  return (
88
109
  <FilterFormContext.Provider value={filterFormContext}>
89
110
  <m.form ref={ref} noValidate onSubmit={submit} id='products' style={{ scrollMarginTop }} />
111
+ {autoSubmitMd && <AutoSubmitSidebarDesktop />}
90
112
  {children}
91
113
  </FilterFormContext.Provider>
92
114
  )
@@ -1,4 +1,8 @@
1
1
  import { ProductListFiltersFragment } from '../ProductListFilters/ProductListFilters.gql'
2
+ import { ProductFilterEqualChip } from './ProductFilterEqualChip'
3
+ import { ProductFilterEqualSection } from './ProductFilterEqualSection'
4
+ import { ProductFilterRangeChip } from './ProductFilterRangeChip'
5
+ import { ProductFilterRangeSection } from './ProductFilterRangeSection'
2
6
  import { useProductFiltersPro } from './ProductFiltersPro'
3
7
  import { excludeCategory } from './activeAggregations'
4
8
  import { applyAggregationCount } from './applyAggregationCount'
@@ -13,6 +17,16 @@ export type ProductFiltersProAggregationsProps = {
13
17
  renderer?: FilterRenderer
14
18
  }
15
19
 
20
+ export const productFiltersProSectionRenderer = {
21
+ FilterRangeTypeInput: ProductFilterRangeSection,
22
+ FilterEqualTypeInput: ProductFilterEqualSection,
23
+ }
24
+
25
+ export const productFiltersProChipRenderer = {
26
+ FilterEqualTypeInput: ProductFilterEqualChip,
27
+ FilterRangeTypeInput: ProductFilterRangeChip,
28
+ }
29
+
16
30
  export function ProductFiltersProAggregations(props: ProductFiltersProAggregationsProps) {
17
31
  const { renderer } = props
18
32
  const { params, aggregations, appliedAggregations, filterTypes } = useProductFiltersPro()
@@ -1,11 +1,10 @@
1
1
  import { ChipOverlayOrPopper, ChipOverlayOrPopperProps } from '@graphcommerce/next-ui'
2
2
  import { Trans } from '@lingui/react'
3
- import { ProductFilterEqualSection } from './ProductFilterEqualSection'
4
- import { ProductFilterRangeSection } from './ProductFilterRangeSection'
5
3
  import { useProductFiltersPro } from './ProductFiltersPro'
6
4
  import {
7
5
  ProductFiltersProAggregations,
8
6
  ProductFiltersProAggregationsProps,
7
+ productFiltersProSectionRenderer,
9
8
  } from './ProductFiltersProAggregations'
10
9
  import { ProductFiltersProLimitSection } from './ProductFiltersProLimitSection'
11
10
  import {
@@ -23,11 +22,6 @@ export type ProductFiltersProAllFiltersChipProps = ProductFiltersProAggregations
23
22
  'label' | 'selected' | 'selectedLabel' | 'onApply' | 'onReset' | 'onClose' | 'children'
24
23
  >
25
24
 
26
- const defaultRenderer = {
27
- FilterRangeTypeInput: ProductFilterRangeSection,
28
- FilterEqualTypeInput: ProductFilterEqualSection,
29
- }
30
-
31
25
  export function ProductFiltersProAllFiltersChip(props: ProductFiltersProAllFiltersChipProps) {
32
26
  const { sort_fields, total_count, renderer, category, ...rest } = props
33
27
 
@@ -65,7 +59,9 @@ export function ProductFiltersProAllFiltersChip(props: ProductFiltersProAllFilte
65
59
  category={category}
66
60
  />
67
61
  <ProductFiltersProLimitSection />
68
- <ProductFiltersProAggregations renderer={{ ...defaultRenderer, ...renderer }} />
62
+ <ProductFiltersProAggregations
63
+ renderer={{ ...productFiltersProSectionRenderer, ...renderer }}
64
+ />
69
65
  </>
70
66
  )}
71
67
  </ChipOverlayOrPopper>
@@ -4,7 +4,12 @@ import { ProductFilterRangeSection } from './ProductFilterRangeSection'
4
4
  import {
5
5
  ProductFiltersProAggregations,
6
6
  ProductFiltersProAggregationsProps,
7
+ productFiltersProSectionRenderer,
7
8
  } from './ProductFiltersProAggregations'
9
+ import {
10
+ ProductFiltersCategorySectionProps,
11
+ ProductFiltersProCategorySection,
12
+ } from './ProductFiltersProCategorySection'
8
13
  import { ProductFiltersProLimitSection } from './ProductFiltersProLimitSection'
9
14
  import {
10
15
  ProductFiltersProSortSection,
@@ -12,25 +17,30 @@ import {
12
17
  } from './ProductFiltersProSortSection'
13
18
 
14
19
  export type ProductFiltersProAllFiltersSidebarProps = ProductFiltersProAggregationsProps &
15
- ProductFiltersProSortSectionProps & { sx?: SxProps<Theme> }
16
-
17
- const defaultRenderer = {
18
- FilterRangeTypeInput: ProductFilterRangeSection,
19
- FilterEqualTypeInput: ProductFilterEqualSection,
20
- }
20
+ ProductFiltersProSortSectionProps &
21
+ ProductFiltersCategorySectionProps & { sx?: SxProps<Theme> }
21
22
 
23
+ /**
24
+ * @deprecated Not used anymore
25
+ *
26
+ * @param props
27
+ * @returns
28
+ */
22
29
  export function ProductFiltersProAllFiltersSidebar(props: ProductFiltersProAllFiltersSidebarProps) {
23
- const { sort_fields, total_count, renderer, sx = [], category } = props
30
+ const { sort_fields, total_count, renderer, sx = [], category, params } = props
24
31
 
25
32
  return (
26
33
  <Box sx={[{ display: { xs: 'none', md: 'grid' } }, ...(Array.isArray(sx) ? sx : [sx])]}>
34
+ <ProductFiltersProCategorySection category={category} params={params} />
27
35
  <ProductFiltersProSortSection
28
36
  sort_fields={sort_fields}
29
37
  total_count={total_count}
30
38
  category={category}
31
39
  />
32
40
  <ProductFiltersProLimitSection />
33
- <ProductFiltersProAggregations renderer={{ ...defaultRenderer, ...renderer }} />
41
+ <ProductFiltersProAggregations
42
+ renderer={{ ...productFiltersProSectionRenderer, ...renderer }}
43
+ />
34
44
  </Box>
35
45
  )
36
46
  }
@@ -0,0 +1,70 @@
1
+ import { UseCategoryTreeProps, useCategoryTree } from '@graphcommerce/magento-category'
2
+ import {
3
+ ActionCard,
4
+ ActionCardAccordion,
5
+ ActionCardList,
6
+ IconSvg,
7
+ iconChevronLeft,
8
+ responsiveVal,
9
+ } from '@graphcommerce/next-ui'
10
+ import { Trans } from '@lingui/react'
11
+ import { Box, SxProps, Theme } from '@mui/material'
12
+ import { useRouter } from 'next/router'
13
+
14
+ export type ProductFiltersCategorySectionProps = UseCategoryTreeProps & {
15
+ hideTitle?: boolean
16
+ sx?: SxProps<Theme>
17
+ }
18
+
19
+ export function ProductFiltersProCategorySection(props: ProductFiltersCategorySectionProps) {
20
+ const { hideTitle, sx } = props
21
+ const router = useRouter()
22
+ const categoryTree = useCategoryTree(props)
23
+
24
+ if (!categoryTree) return null
25
+
26
+ return (
27
+ <ActionCardAccordion
28
+ sx={[
29
+ hideTitle ? { '& .MuiAccordionSummary-root': { display: 'none' } } : {},
30
+ sx,
31
+ ...(Array.isArray(sx) ? sx : [sx]),
32
+ ]}
33
+ defaultExpanded
34
+ summary={<Trans id='Categories' />}
35
+ details={
36
+ <ActionCardList value='cat' variant='default'>
37
+ {categoryTree.map((item) => (
38
+ <ActionCard
39
+ {...item}
40
+ title={
41
+ item.isBack ? (
42
+ <Box sx={{ display: 'flex', alignItems: 'center' }}>
43
+ <IconSvg src={iconChevronLeft} size='medium' />
44
+ {item.title}
45
+ </Box>
46
+ ) : (
47
+ item.title
48
+ )
49
+ }
50
+ sx={[
51
+ item.isBack ? {} : {},
52
+ {
53
+ '&.sizeSmall': { pl: responsiveVal(8 * item.indent, 12 * item.indent) },
54
+ '&.sizeMedium': { pl: responsiveVal(10 * item.indent, 14 * item.indent) },
55
+ '&.sizeLarge': { pl: responsiveVal(12 * item.indent, 16 * item.indent) },
56
+ '&.sizeResponsive': { pl: responsiveVal(8 * item.indent, 16 * item.indent) },
57
+ },
58
+ ]}
59
+ value={item.href}
60
+ key={item.href}
61
+ selected={item.selected}
62
+ onClick={() => router.push(item.href)}
63
+ />
64
+ ))}
65
+ </ActionCardList>
66
+ }
67
+ right={undefined}
68
+ />
69
+ )
70
+ }
@@ -1,16 +1,18 @@
1
- import { ProductFilterEqualChip } from './ProductFilterEqualChip'
2
- import { ProductFilterRangeChip } from './ProductFilterRangeChip'
3
1
  import {
4
2
  ProductFiltersProAggregations,
5
3
  ProductFiltersProAggregationsProps,
4
+ productFiltersProChipRenderer,
6
5
  } from './ProductFiltersProAggregations'
7
6
 
8
- const defaultRenderer = {
9
- FilterEqualTypeInput: ProductFilterEqualChip,
10
- FilterRangeTypeInput: ProductFilterRangeChip,
11
- }
12
-
7
+ /**
8
+ * @deprecated Not used anymore, use `<ProductFiltersProAggregations renderer={productFiltersProChipRenderer}/>`
9
+ */
13
10
  export function ProductFiltersProFilterChips(props: ProductFiltersProAggregationsProps) {
14
11
  const { renderer } = props
15
- return <ProductFiltersProAggregations {...props} renderer={{ ...defaultRenderer, ...renderer }} />
12
+ return (
13
+ <ProductFiltersProAggregations
14
+ {...props}
15
+ renderer={{ ...productFiltersProChipRenderer, ...renderer }}
16
+ />
17
+ )
16
18
  }
@@ -12,6 +12,7 @@ export type ProductFiltersProLayoutSidebarProps = {
12
12
  count?: React.ReactNode
13
13
  pagination: React.ReactNode
14
14
  header?: React.ReactNode
15
+ children?: React.ReactNode
15
16
  } & Partial<OwnerProps>
16
17
 
17
18
  type OwnerProps = {
@@ -32,6 +33,7 @@ export function ProductFiltersProLayoutSidebar(props: ProductFiltersProLayoutSid
32
33
  sidebarFilters,
33
34
  header,
34
35
  headerPosition = 'before',
36
+ children,
35
37
  } = props
36
38
 
37
39
  const { form, submit } = useProductFiltersPro()
@@ -47,8 +49,6 @@ export function ProductFiltersProLayoutSidebar(props: ProductFiltersProLayoutSid
47
49
 
48
50
  <FormAutoSubmit control={form.control} disabled={autoSubmitDisabled} submit={submit} />
49
51
 
50
- <StickyBelowHeader sx={{ display: { md: 'none' } }}>{horizontalFilters}</StickyBelowHeader>
51
-
52
52
  <Container
53
53
  maxWidth={false}
54
54
  className={classes.content}
@@ -56,12 +56,16 @@ export function ProductFiltersProLayoutSidebar(props: ProductFiltersProLayoutSid
56
56
  display: 'grid',
57
57
  gridTemplate: {
58
58
  xs: `
59
- "beforeContent" auto
60
- "items" auto
61
- "afterContent" auto
59
+ "content" auto
60
+ "horizontalFilters" auto
61
+ "beforeContent" auto
62
+ "items" auto
63
+ "afterContent" auto
62
64
  `,
63
- md: `
64
- "topleft beforeContent" auto
65
+ md: `
66
+ "topleft content" auto
67
+ "sidebar content" auto
68
+ "sidebar beforeContent" auto
65
69
  "sidebar items" min-content
66
70
  "sidebar afterContent" 1fr
67
71
  /300px auto
@@ -87,6 +91,10 @@ export function ProductFiltersProLayoutSidebar(props: ProductFiltersProLayoutSid
87
91
  {sidebarFilters}
88
92
  </Box>
89
93
  )}
94
+ {children && <Box gridArea='content'>{children}</Box>}
95
+ <StickyBelowHeader sx={{ display: { md: 'none', gridArea: 'horizontalFilters' } }}>
96
+ {horizontalFilters}
97
+ </StickyBelowHeader>
90
98
 
91
99
  <Box gridArea='beforeContent' sx={{ mt: { md: 0 } }}>
92
100
  {count}
@@ -11,11 +11,13 @@ import {
11
11
  import { Trans } from '@lingui/react'
12
12
  import { useMemo } from 'react'
13
13
  import { useProductFiltersPro } from './ProductFiltersPro'
14
+ import { SxProps, Theme } from '@mui/material'
14
15
 
15
- export type ProductFiltersProLimitSectionProps = Record<string, unknown>
16
+ export type ProductFiltersProLimitSectionProps = { sx?: SxProps<Theme> }
16
17
 
17
18
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
18
19
  export function ProductFiltersProLimitSection(props: ProductFiltersProLimitSectionProps) {
20
+ const { sx } = props
19
21
  const { form } = useProductFiltersPro()
20
22
  const { control } = form
21
23
  const activePageSize = useWatch({ control, name: 'pageSize' })
@@ -38,6 +40,7 @@ export function ProductFiltersProLimitSection(props: ProductFiltersProLimitSecti
38
40
 
39
41
  return (
40
42
  <ActionCardAccordion
43
+ sx={sx}
41
44
  defaultExpanded={!!activePageSize}
42
45
  summary={<Trans id='Per page' />}
43
46
  details={
@@ -1,16 +1,21 @@
1
1
  import { ActionCard, ActionCardAccordion, ActionCardListForm, Button } from '@graphcommerce/next-ui'
2
2
  import { Trans } from '@lingui/react'
3
+ import { SxProps, Theme } from '@mui/material'
3
4
  import { useProductFiltersPro } from './ProductFiltersPro'
4
5
  import { UseProductFiltersProSortProps, useProductFiltersProSort } from './useProductFiltersProSort'
5
6
 
6
- export type ProductFiltersProSortSectionProps = UseProductFiltersProSortProps
7
+ export type ProductFiltersProSortSectionProps = UseProductFiltersProSortProps & {
8
+ sx?: SxProps<Theme>
9
+ }
7
10
 
8
11
  export function ProductFiltersProSortSection(props: ProductFiltersProSortSectionProps) {
12
+ const { sx } = props
9
13
  const { form } = useProductFiltersPro()
10
14
  const { options, showReset, selected } = useProductFiltersProSort(props)
11
15
 
12
16
  return (
13
17
  <ActionCardAccordion
18
+ sx={sx}
14
19
  defaultExpanded={selected}
15
20
  summary={<Trans id='Sort By' />}
16
21
  details={
@@ -1,11 +1,17 @@
1
1
  export * from './ProductFilterEqualChip'
2
+ export * from './ProductFilterEqualSection'
2
3
  export * from './ProductFilterRangeChip'
4
+ export * from './ProductFilterRangeSection'
3
5
  export * from './ProductFiltersPro'
4
6
  export * from './ProductFiltersProAllFiltersChip'
5
7
  export * from './ProductFiltersProAllFiltersSidebar'
8
+ export * from './ProductFiltersProCategorySection'
6
9
  export * from './ProductFiltersProChips'
7
10
  export * from './ProductFiltersProClearAll'
8
11
  export * from './ProductFiltersProLayoutSidebar'
9
12
  export * from './ProductFiltersProLimitChip'
10
13
  export * from './ProductFiltersProLimitSection'
11
14
  export * from './ProductFiltersProSortChip'
15
+ export * from './ProductFiltersProSortDirectionArrow'
16
+ export * from './ProductFiltersProAggregations'
17
+ export * from './ProductFiltersProSortSection'
@@ -27,7 +27,7 @@ export function useProductFiltersProSort(props: ProductListActionSortProps) {
27
27
  () =>
28
28
  filterNonNullableKeys(sort_fields?.options).map((o) =>
29
29
  !category?.uid && o.value === 'position'
30
- ? { value: 'relevance', label: i18n._('Relevance') }
30
+ ? { value: 'relevance', label: i18n._(/* i18n*/ 'Relevance') }
31
31
  : o,
32
32
  ),
33
33
  [category?.uid, sort_fields?.options],
@@ -41,7 +41,9 @@ export function useProductFiltersProSort(props: ProductListActionSortProps) {
41
41
 
42
42
  const formSort = useWatch({ control, name: 'sort' })
43
43
  const formDirection = useWatch({ control, name: 'dir' })
44
- const showReset = Boolean(formSort !== defaultSortBy || formDirection === 'DESC')
44
+ const showReset =
45
+ (formDirection !== null || formSort !== null) &&
46
+ Boolean(formSort !== defaultSortBy || formDirection === 'DESC')
45
47
  const selected = Boolean(params.sort && (params.sort !== defaultSortBy || params.dir === 'DESC'))
46
48
 
47
49
  const options = useMemo(
@@ -97,7 +97,7 @@ export function ProductListFiltersContainer(props: ProductListFiltersContainerPr
97
97
  top: theme.page.vertical,
98
98
  zIndex: 9,
99
99
  margin: '0 auto',
100
- maxWidth: `calc(100% - 96px - ${theme.spacings.sm} * 2)`,
100
+
101
101
  [theme.breakpoints.down('md')]: {
102
102
  textAlign: 'center',
103
103
  maxWidth: 'unset',
@@ -136,8 +136,7 @@ export function ProductListFiltersContainer(props: ProductListFiltersContainerPr
136
136
  className={classes.scroller}
137
137
  hideScrollbar
138
138
  sx={(theme) => ({
139
- paddingLeft: theme.page.horizontal,
140
- paddingRight: theme.page.horizontal,
139
+ px: theme.page.horizontal,
141
140
  paddingBottom: '1px',
142
141
  [theme.breakpoints.up('md')]: {
143
142
  borderRadius: '99em',
@@ -145,7 +144,6 @@ export function ProductListFiltersContainer(props: ProductListFiltersContainerPr
145
144
  paddingRight: '8px',
146
145
  },
147
146
  py: '5px',
148
-
149
147
  columnGap: '6px',
150
148
  gridAutoColumns: 'min-content',
151
149
  })}
@@ -1,5 +1,8 @@
1
1
  fragment ProductPageBreadcrumb on ProductInterface {
2
+ __typename
3
+ uid
2
4
  name
5
+ url_key
3
6
  categories {
4
7
  uid
5
8
  name
@@ -8,6 +8,9 @@ import { ProductPageBreadcrumbFragment } from './ProductPageBreadcrumb.gql'
8
8
  type ProductPageBreadcrumbsProps = ProductPageBreadcrumbFragment &
9
9
  Omit<BreadcrumbsProps, 'children'>
10
10
 
11
+ /**
12
+ * @deprecated Please use ProductPageBreadcrumbs
13
+ */
11
14
  export function ProductPageBreadcrumb(props: ProductPageBreadcrumbsProps) {
12
15
  const { categories, name, ...breadcrumbProps } = props
13
16
  const prev = usePrevPageRouter()
@@ -0,0 +1,40 @@
1
+ import { usePrevPageRouter } from '@graphcommerce/framer-next-pages'
2
+ import { categoryToBreadcrumbs } from '@graphcommerce/magento-category'
3
+ import { Breadcrumbs } from '@graphcommerce/next-ui'
4
+ import { BreadcrumbsJsonLd } from '@graphcommerce/next-ui/Breadcrumbs/BreadcrumbsJsonLd'
5
+ import { jsonLdBreadcrumb } from '@graphcommerce/next-ui/Breadcrumbs/jsonLdBreadcrumb'
6
+ import { BreadcrumbsProps } from '@mui/material'
7
+ import { useRouter } from 'next/router'
8
+ import { BreadcrumbList } from 'schema-dts'
9
+ import { productPageCategory } from '../ProductPageCategory/productPageCategory'
10
+ import { ProductPageBreadcrumbFragment } from './ProductPageBreadcrumb.gql'
11
+ import { productLink } from '../../hooks/useProductLink'
12
+
13
+ export type ProductPageBreadcrumbsProps = Omit<BreadcrumbsProps, 'children'> & {
14
+ breadcrumbsAmount?: number
15
+ product: ProductPageBreadcrumbFragment
16
+ }
17
+
18
+ export function ProductPageBreadcrumbs(props: ProductPageBreadcrumbsProps) {
19
+ const { product, ...breadcrumbsProps } = props
20
+ const { categories } = product
21
+ const prev = usePrevPageRouter()
22
+ const router = useRouter()
23
+
24
+ const category =
25
+ categories?.find((c) => `/${c?.url_path}` === prev?.asPath) ?? productPageCategory(product)
26
+
27
+ if (!category || !product.name || !product.url_key) return null
28
+
29
+ const breadcrumbs = categoryToBreadcrumbs(category)
30
+
31
+ return (
32
+ <>
33
+ <BreadcrumbsJsonLd<BreadcrumbList>
34
+ breadcrumbs={[...breadcrumbs, { name: product.name, href: productLink(product) }]}
35
+ render={(bc) => ({ '@context': 'https://schema.org', ...jsonLdBreadcrumb(bc, router) })}
36
+ />
37
+ <Breadcrumbs breadcrumbs={breadcrumbs} lastIsLink {...breadcrumbsProps} />
38
+ </>
39
+ )
40
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './ProductPageBreadcrumb.gql'
2
2
  export * from './ProductPageBreadcrumb'
3
+ export * from './ProductPageBreadcrumbs'
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/magento-product",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "8.1.0-canary.26",
5
+ "version": "8.1.0-canary.27",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -18,18 +18,19 @@
18
18
  "typescript": "5.3.3"
19
19
  },
20
20
  "peerDependencies": {
21
- "@graphcommerce/ecommerce-ui": "^8.1.0-canary.26",
22
- "@graphcommerce/eslint-config-pwa": "^8.1.0-canary.26",
23
- "@graphcommerce/framer-next-pages": "^8.1.0-canary.26",
24
- "@graphcommerce/framer-scroller": "^8.1.0-canary.26",
25
- "@graphcommerce/graphql": "^8.1.0-canary.26",
26
- "@graphcommerce/graphql-mesh": "^8.1.0-canary.26",
27
- "@graphcommerce/image": "^8.1.0-canary.26",
28
- "@graphcommerce/magento-cart": "^8.1.0-canary.26",
29
- "@graphcommerce/magento-store": "^8.1.0-canary.26",
30
- "@graphcommerce/next-ui": "^8.1.0-canary.26",
31
- "@graphcommerce/prettier-config-pwa": "^8.1.0-canary.26",
32
- "@graphcommerce/typescript-config-pwa": "^8.1.0-canary.26",
21
+ "@graphcommerce/ecommerce-ui": "^8.1.0-canary.27",
22
+ "@graphcommerce/eslint-config-pwa": "^8.1.0-canary.27",
23
+ "@graphcommerce/framer-next-pages": "^8.1.0-canary.27",
24
+ "@graphcommerce/framer-scroller": "^8.1.0-canary.27",
25
+ "@graphcommerce/graphql": "^8.1.0-canary.27",
26
+ "@graphcommerce/graphql-mesh": "^8.1.0-canary.27",
27
+ "@graphcommerce/image": "^8.1.0-canary.27",
28
+ "@graphcommerce/magento-cart": "^8.1.0-canary.27",
29
+ "@graphcommerce/magento-category": "^8.1.0-canary.27",
30
+ "@graphcommerce/magento-store": "^8.1.0-canary.27",
31
+ "@graphcommerce/next-ui": "^8.1.0-canary.27",
32
+ "@graphcommerce/prettier-config-pwa": "^8.1.0-canary.27",
33
+ "@graphcommerce/typescript-config-pwa": "^8.1.0-canary.27",
33
34
  "@lingui/core": "^4.2.1",
34
35
  "@lingui/macro": "^4.2.1",
35
36
  "@lingui/react": "^4.2.1",