@graphcommerce/magento-product 8.1.0-canary.3 → 8.1.0-canary.31

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 (60) hide show
  1. package/Api/ProductListItem.graphql +1 -1
  2. package/Api/ProductPageItem.graphql +1 -1
  3. package/CHANGELOG.md +157 -2
  4. package/components/AddProductsToCart/AddProductsToCartButton.tsx +1 -0
  5. package/components/AddProductsToCart/AddProductsToCartForm.tsx +7 -2
  6. package/components/AddProductsToCart/AddProductsToCartSnackbar.tsx +13 -14
  7. package/components/AddProductsToCart/UseAddProductsToCartAction.graphql +1 -1
  8. package/components/AddProductsToCart/findAddedItems.ts +81 -0
  9. package/components/AddProductsToCart/index.ts +3 -0
  10. package/components/AddProductsToCart/useAddProductsToCartAction.ts +6 -3
  11. package/components/AddProductsToCart/useFormAddProductsToCart.ts +1 -2
  12. package/components/JsonLdProduct/JsonLdProduct.graphql +1 -1
  13. package/components/JsonLdProduct/JsonLdProductOffer.graphql +2 -3
  14. package/components/JsonLdProduct/ProductPageJsonLd.tsx +10 -5
  15. package/components/JsonLdProduct/index.ts +1 -0
  16. package/components/ProductAddToCart/ProductAddToCart.tsx +6 -4
  17. package/components/ProductCustomizable/CustomizableAreaOption.tsx +41 -7
  18. package/components/ProductCustomizable/CustomizableDateOption.tsx +60 -7
  19. package/components/ProductCustomizable/CustomizableDropDownOption.tsx +63 -15
  20. package/components/ProductCustomizable/CustomizableFieldOption.tsx +40 -4
  21. package/components/ProductCustomizable/ProductCustomizable.graphql +1 -1
  22. package/components/ProductCustomizable/index.ts +1 -0
  23. package/components/ProductCustomizable/productCustomizableSelectors.ts +59 -0
  24. package/components/ProductFiltersPro/ProductFiltersPro.tsx +49 -12
  25. package/components/ProductFiltersPro/ProductFiltersProAggregations.tsx +14 -0
  26. package/components/ProductFiltersPro/ProductFiltersProAllFiltersChip.tsx +10 -10
  27. package/components/ProductFiltersPro/ProductFiltersProAllFiltersSidebar.tsx +23 -9
  28. package/components/ProductFiltersPro/ProductFiltersProCategorySection.tsx +70 -0
  29. package/components/ProductFiltersPro/ProductFiltersProChips.tsx +10 -8
  30. package/components/ProductFiltersPro/ProductFiltersProLayoutSidebar.tsx +15 -7
  31. package/components/ProductFiltersPro/ProductFiltersProLimitSection.tsx +4 -1
  32. package/components/ProductFiltersPro/ProductFiltersProSortChip.tsx +9 -28
  33. package/components/ProductFiltersPro/ProductFiltersProSortDirectionArrow.tsx +15 -0
  34. package/components/ProductFiltersPro/ProductFiltersProSortSection.tsx +12 -32
  35. package/components/ProductFiltersPro/index.ts +6 -0
  36. package/components/ProductFiltersPro/useProductFiltersProSort.tsx +76 -0
  37. package/components/ProductListFiltersContainer/ProductListFiltersContainer.tsx +2 -4
  38. package/components/ProductListItems/CategoryDefault.graphql +5 -0
  39. package/components/ProductListItems/ProductListItemsBase.tsx +1 -1
  40. package/components/ProductListItems/filterTypes.tsx +1 -1
  41. package/components/ProductListItems/filteredProductList.tsx +1 -1
  42. package/components/ProductListItems/productListApplyCategoryDefaults.ts +28 -0
  43. package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.graphql +3 -0
  44. package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.tsx +8 -3
  45. package/components/ProductPageBreadcrumb/ProductPageBreadcrumbs.tsx +40 -0
  46. package/components/ProductPageBreadcrumb/index.ts +1 -0
  47. package/components/ProductPageDescription/ComplexTextValue.graphql +1 -1
  48. package/components/ProductPageGallery/ProductImage.graphql +1 -0
  49. package/components/ProductPageGallery/ProductPageGallery.tsx +14 -8
  50. package/components/ProductPagePrice/ProductPagePrice.graphql +3 -0
  51. package/components/ProductPagePrice/ProductPagePrice.tsx +11 -4
  52. package/components/ProductPagePrice/useCustomizableOptionPrice.ts +85 -0
  53. package/components/ProductShortDescription/ProductShortDescription.tsx +2 -0
  54. package/components/ProductStaticPaths/getProductStaticPaths.ts +2 -3
  55. package/components/ProductStaticPaths/getSitemapPaths.ts +3 -0
  56. package/components/index.ts +2 -0
  57. package/hooks/useProductListLink.ts +10 -5
  58. package/hooks/useProductListLinkReplace.ts +3 -0
  59. package/package.json +14 -13
  60. package/tsconfig.json +1 -1
@@ -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,53 +1,34 @@
1
- import { useWatch } from '@graphcommerce/ecommerce-ui'
2
- import { useQuery } from '@graphcommerce/graphql'
3
- import { StoreConfigDocument } from '@graphcommerce/magento-store'
4
1
  import {
5
2
  ActionCard,
6
3
  ActionCardListForm,
7
4
  ChipOverlayOrPopper,
8
5
  ChipOverlayOrPopperProps,
9
- filterNonNullableKeys,
10
6
  } from '@graphcommerce/next-ui'
11
7
  import { Trans } from '@lingui/react'
12
- import { useMemo } from 'react'
13
- import { ProductListSortFragment } from '../ProductListSort/ProductListSort.gql'
14
8
  import { useProductFiltersPro } from './ProductFiltersPro'
9
+ import { UseProductFiltersProSortProps, useProductFiltersProSort } from './useProductFiltersProSort'
15
10
 
16
- export type ProductListActionSortProps = ProductListSortFragment &
11
+ export type ProductListActionSortProps = UseProductFiltersProSortProps &
17
12
  Omit<
18
13
  ChipOverlayOrPopperProps,
19
14
  'label' | 'selected' | 'selectedLabel' | 'onApply' | 'onReset' | 'onClose' | 'children'
20
15
  >
21
16
 
22
17
  export function ProductFiltersProSortChip(props: ProductListActionSortProps) {
23
- const { sort_fields, chipProps, ...rest } = props
24
- const { params, form, submit } = useProductFiltersPro()
25
- const { control } = form
26
- const activeSort = useWatch({ control, name: 'sort' })
27
-
28
- const { data: storeConfigQuery } = useQuery(StoreConfigDocument)
29
- const defaultSort = storeConfigQuery?.storeConfig?.catalog_default_sort_by
30
-
31
- const options = useMemo(
32
- () =>
33
- filterNonNullableKeys(sort_fields?.options, ['value', 'label']).map((option) => ({
34
- ...option,
35
- value: option.value === defaultSort ? null : option.value,
36
- title: option.label,
37
- })),
38
- [defaultSort, sort_fields?.options],
39
- )
18
+ const { sort_fields, chipProps, category, ...rest } = props
19
+ const { submit, form } = useProductFiltersPro()
20
+ const { options, showReset, selected, selectedLabel } = useProductFiltersProSort(props)
40
21
 
41
22
  return (
42
23
  <ChipOverlayOrPopper
43
24
  {...rest}
44
25
  overlayProps={{ sizeSm: 'minimal', sizeMd: 'minimal', ...rest.overlayProps }}
45
26
  label={<Trans id='Sort By' />}
46
- selected={Boolean(params.sort)}
47
- selectedLabel={options.find((option) => option.value === params.sort)?.label}
27
+ selected={selected}
28
+ selectedLabel={selectedLabel}
48
29
  onApply={submit}
49
30
  onReset={
50
- activeSort
31
+ showReset
51
32
  ? () => {
52
33
  form.setValue('sort', null)
53
34
  form.setValue('dir', null)
@@ -60,7 +41,7 @@ export function ProductFiltersProSortChip(props: ProductListActionSortProps) {
60
41
  >
61
42
  {() => (
62
43
  <ActionCardListForm
63
- control={control}
44
+ control={form.control}
64
45
  name='sort'
65
46
  layout='list'
66
47
  variant='default'
@@ -0,0 +1,15 @@
1
+ import { SortEnum } from '@graphcommerce/graphql-mesh'
2
+ import { IconSvg, iconArrowDown, iconArrowUp } from '@graphcommerce/next-ui'
3
+
4
+ type Props = {
5
+ sortDirection: SortEnum | null
6
+ }
7
+
8
+ export function ProductFiltersProSortDirectionArrow({ sortDirection }: Props) {
9
+ return (
10
+ <IconSvg
11
+ src={sortDirection === 'ASC' || sortDirection === null ? iconArrowUp : iconArrowDown}
12
+ sx={{ display: 'flex' }}
13
+ />
14
+ )
15
+ }
@@ -1,46 +1,26 @@
1
- import { useWatch } from '@graphcommerce/ecommerce-ui'
2
- import { useQuery } from '@graphcommerce/graphql'
3
- import { StoreConfigDocument } from '@graphcommerce/magento-store'
4
- import {
5
- ActionCard,
6
- ActionCardAccordion,
7
- ActionCardListForm,
8
- Button,
9
- filterNonNullableKeys,
10
- } from '@graphcommerce/next-ui'
1
+ import { ActionCard, ActionCardAccordion, ActionCardListForm, Button } from '@graphcommerce/next-ui'
11
2
  import { Trans } from '@lingui/react'
12
- import { useMemo } from 'react'
13
- import { ProductListSortFragment } from '../ProductListSort/ProductListSort.gql'
3
+ import { SxProps, Theme } from '@mui/material'
14
4
  import { useProductFiltersPro } from './ProductFiltersPro'
5
+ import { UseProductFiltersProSortProps, useProductFiltersProSort } from './useProductFiltersProSort'
15
6
 
16
- export type ProductFiltersProSortSectionProps = ProductListSortFragment
7
+ export type ProductFiltersProSortSectionProps = UseProductFiltersProSortProps & {
8
+ sx?: SxProps<Theme>
9
+ }
17
10
 
18
11
  export function ProductFiltersProSortSection(props: ProductFiltersProSortSectionProps) {
19
- const { sort_fields } = props
12
+ const { sx } = props
20
13
  const { form } = useProductFiltersPro()
21
- const { control } = form
22
- const activeSort = useWatch({ control, name: 'sort' })
23
-
24
- const { data: storeConfigQuery } = useQuery(StoreConfigDocument)
25
- const defaultSort = storeConfigQuery?.storeConfig?.catalog_default_sort_by
26
-
27
- const options = useMemo(
28
- () =>
29
- filterNonNullableKeys(sort_fields?.options, ['value', 'label']).map((option) => ({
30
- ...option,
31
- value: option.value === defaultSort ? null : option.value,
32
- title: option.label,
33
- })),
34
- [defaultSort, sort_fields?.options],
35
- )
14
+ const { options, showReset, selected } = useProductFiltersProSort(props)
36
15
 
37
16
  return (
38
17
  <ActionCardAccordion
39
- defaultExpanded={!!activeSort}
18
+ sx={sx}
19
+ defaultExpanded={selected}
40
20
  summary={<Trans id='Sort By' />}
41
21
  details={
42
22
  <ActionCardListForm
43
- control={control}
23
+ control={form.control}
44
24
  name='sort'
45
25
  layout='list'
46
26
  variant='default'
@@ -50,7 +30,7 @@ export function ProductFiltersProSortSection(props: ProductFiltersProSortSection
50
30
  />
51
31
  }
52
32
  right={
53
- activeSort ? (
33
+ showReset ? (
54
34
  <Button
55
35
  color='primary'
56
36
  onClick={(e) => {
@@ -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'
@@ -0,0 +1,76 @@
1
+ import { useWatch } from '@graphcommerce/ecommerce-ui'
2
+ import { useQuery } from '@graphcommerce/graphql'
3
+ import { StoreConfigDocument } from '@graphcommerce/magento-store'
4
+ import { filterNonNullableKeys } from '@graphcommerce/next-ui'
5
+ import { i18n } from '@lingui/core'
6
+ import { useMemo } from 'react'
7
+ import { CategoryDefaultFragment } from '../ProductListItems/CategoryDefault.gql'
8
+ import { ProductFilterParams } from '../ProductListItems/filterTypes'
9
+ import { ProductListSortFragment } from '../ProductListSort'
10
+ import { useProductFiltersPro } from './ProductFiltersPro'
11
+ import type { ProductListActionSortProps } from './ProductFiltersProSortChip'
12
+ import { ProductFiltersProSortDirectionArrow } from './ProductFiltersProSortDirectionArrow'
13
+
14
+ const exclude = ['relevance', 'position']
15
+
16
+ export type UseProductFiltersProSortProps = ProductListSortFragment & {
17
+ category?: CategoryDefaultFragment
18
+ }
19
+
20
+ export function useProductFiltersProSort(props: ProductListActionSortProps) {
21
+ const { sort_fields, category } = props
22
+
23
+ const { params, form } = useProductFiltersPro()
24
+ const { control, setValue } = form
25
+
26
+ const sortFields = useMemo(
27
+ () =>
28
+ filterNonNullableKeys(sort_fields?.options).map((o) =>
29
+ !category?.uid && o.value === 'position'
30
+ ? { value: 'relevance', label: i18n._(/* i18n*/ 'Relevance') }
31
+ : o,
32
+ ),
33
+ [category?.uid, sort_fields?.options],
34
+ )
35
+ const availableSortBy = category?.available_sort_by ?? sortFields.map((o) => o.value)
36
+
37
+ const conf = useQuery(StoreConfigDocument).data?.storeConfig
38
+ const defaultSortBy = (
39
+ category ? category.default_sort_by ?? conf?.catalog_default_sort_by ?? 'position' : 'relevance'
40
+ ) as ProductFilterParams['sort']
41
+
42
+ const formSort = useWatch({ control, name: 'sort' })
43
+ const formDirection = useWatch({ control, name: 'dir' })
44
+ const showReset =
45
+ (formDirection !== null || formSort !== null) &&
46
+ Boolean(formSort !== defaultSortBy || formDirection === 'DESC')
47
+ const selected = Boolean(params.sort && (params.sort !== defaultSortBy || params.dir === 'DESC'))
48
+
49
+ const options = useMemo(
50
+ () =>
51
+ sortFields
52
+ .filter((o) => availableSortBy.includes(o.value))
53
+ .map((option) => {
54
+ const value = option.value === defaultSortBy ? null : option.value
55
+ const showSort = formSort === value && !exclude.includes(option.value)
56
+
57
+ return {
58
+ ...option,
59
+ value,
60
+ title: option.label,
61
+ ...(showSort && {
62
+ onClick: () => setValue('dir', formDirection === 'DESC' ? null : 'DESC'),
63
+ price: <ProductFiltersProSortDirectionArrow sortDirection={formDirection} />,
64
+ }),
65
+ }
66
+ }),
67
+ [sortFields, availableSortBy, defaultSortBy, formSort, formDirection, setValue],
68
+ )
69
+
70
+ return {
71
+ options,
72
+ selected,
73
+ showReset,
74
+ selectedLabel: options.find((option) => option.value === params.sort)?.label,
75
+ }
76
+ }
@@ -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
  })}
@@ -0,0 +1,5 @@
1
+ fragment CategoryDefault on CategoryInterface {
2
+ uid
3
+ default_sort_by
4
+ available_sort_by
5
+ }
@@ -1,7 +1,7 @@
1
1
  import { LazyHydrate, RenderType, extendableComponent, responsiveVal } from '@graphcommerce/next-ui'
2
2
  import { Box, BoxProps } from '@mui/material'
3
3
  import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
4
- import { AddProductsToCartForm } from '../../index'
4
+ import { AddProductsToCartForm } from '../AddProductsToCart'
5
5
  import { ProductListItemProps } from '../ProductListItem/ProductListItem'
6
6
  import { ProductListItemRenderer } from './renderer'
7
7
 
@@ -31,7 +31,7 @@ export type ProductFilterParams = {
31
31
 
32
32
  export function toFilterParams(params: ProductListParams): ProductFilterParams {
33
33
  const [sortKey] = Object.keys(params.sort) as [keyof ProductAttributeSortInput]
34
- const dir = params.sort[sortKey] as SortEnum | undefined
34
+ const dir = params.sort[sortKey]?.toUpperCase() as SortEnum | undefined
35
35
 
36
36
  return {
37
37
  ...params,
@@ -35,7 +35,7 @@ export function parseParams(
35
35
  }
36
36
  if (param === 'dir') {
37
37
  const [sortBy] = Object.keys(categoryVariables.sort)
38
- if (sortBy) categoryVariables.sort[sortBy] = value as SortEnum
38
+ if (sortBy) categoryVariables.sort[sortBy] = value?.toUpperCase() as SortEnum
39
39
  return undefined
40
40
  }
41
41
 
@@ -0,0 +1,28 @@
1
+ import { StoreConfigQuery } from '@graphcommerce/magento-store'
2
+ import { CategoryDefaultFragment } from './CategoryDefault.gql'
3
+ import { ProductListParams } from './filterTypes'
4
+ import { cloneDeep } from '@graphcommerce/graphql'
5
+
6
+ export async function productListApplyCategoryDefaults(
7
+ params: ProductListParams | undefined,
8
+ conf: StoreConfigQuery,
9
+ category: Promise<CategoryDefaultFragment | null | undefined>,
10
+ ) {
11
+ if (!params) return params
12
+
13
+ const newParams = cloneDeep(params)
14
+ if (!newParams.pageSize) newParams.pageSize = conf.storeConfig?.grid_per_page ?? 12
15
+
16
+ if (Object.keys(params.sort).length === 0) {
17
+ const categorySort = (await category)?.default_sort_by as keyof ProductListParams['sort']
18
+ const defaultSort = conf.storeConfig?.catalog_default_sort_by as keyof ProductListParams['sort']
19
+ if (categorySort) newParams.sort = { [categorySort]: 'ASC' }
20
+ else if (defaultSort) newParams.sort = { [defaultSort]: 'ASC' }
21
+ }
22
+
23
+ if (!newParams.filters.category_uid?.in?.[0]) {
24
+ newParams.filters.category_uid = { eq: (await category)?.uid }
25
+ }
26
+
27
+ return newParams
28
+ }
@@ -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()
@@ -32,9 +35,11 @@ export function ProductPageBreadcrumb(props: ProductPageBreadcrumbsProps) {
32
35
  {breadcrumb.category_name}
33
36
  </Link>
34
37
  ))}
35
- <Link href={`/${category?.url_path}`} underline='hover' color='inherit'>
36
- {category?.name}
37
- </Link>
38
+ {category && (
39
+ <Link href={`/${category?.url_path}`} underline='hover' color='inherit'>
40
+ {category?.name}
41
+ </Link>
42
+ )}
38
43
  <Typography color='text.primary'>{name}</Typography>
39
44
  </Breadcrumbs>
40
45
  )
@@ -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'
@@ -1,4 +1,4 @@
1
- fragment ComplexTextValue on ComplexTextValue @injectable {
1
+ fragment ComplexTextValue on ComplexTextValue {
2
2
  html
3
3
  __typename
4
4
  }
@@ -1,4 +1,5 @@
1
1
  fragment ProductImage on ProductImage {
2
2
  url
3
3
  label
4
+ disabled
4
5
  }
@@ -22,15 +22,21 @@ export function ProductPageGallery(props: ProductPageGalleryProps) {
22
22
  const images =
23
23
  media_gallery
24
24
  ?.filter(nonNullable)
25
+ .filter((p) => p.disabled !== true)
25
26
  .sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
26
- .map((item) => {
27
- if (item.__typename === 'ProductImage')
28
- return { src: item.url ?? '', alt: item.label || undefined, width, height }
29
- return {
30
- src: '',
31
- alt: `{${item.__typename} not yet supported}`,
32
- }
33
- }) ?? []
27
+ .map((item) =>
28
+ item.__typename === 'ProductImage'
29
+ ? {
30
+ src: item.url ?? '',
31
+ alt: item.label || undefined,
32
+ width,
33
+ height,
34
+ }
35
+ : {
36
+ src: '',
37
+ alt: `{${item.__typename} not yet supported}`,
38
+ },
39
+ ) ?? []
34
40
 
35
41
  return (
36
42
  <SidebarGallery
@@ -1,4 +1,5 @@
1
1
  fragment ProductPagePrice on ProductInterface {
2
+ __typename
2
3
  url_key
3
4
  price_range {
4
5
  minimum_price {
@@ -30,4 +31,6 @@ fragment ProductPagePrice on ProductInterface {
30
31
  }
31
32
  quantity
32
33
  }
34
+
35
+ ...ProductCustomizable
33
36
  }
@@ -1,12 +1,17 @@
1
1
  import { useWatch } from '@graphcommerce/ecommerce-ui'
2
2
  import { Money } from '@graphcommerce/magento-store'
3
+ import { extendableComponent } from '@graphcommerce/next-ui'
4
+ import { Box } from '@mui/material'
3
5
  import { AddToCartItemSelector, useFormAddProductsToCart } from '../AddProductsToCart'
4
6
  import { ProductPagePriceFragment } from './ProductPagePrice.gql'
5
7
  import { getProductTierPrice } from './getProductTierPrice'
6
- import { extendableComponent } from '@graphcommerce/next-ui'
7
- import { Box } from '@mui/material'
8
+ import {
9
+ UseCustomizableOptionPriceProps,
10
+ useCustomizableOptionPrice,
11
+ } from './useCustomizableOptionPrice'
8
12
 
9
- export type ProductPagePriceProps = { product: ProductPagePriceFragment } & AddToCartItemSelector
13
+ export type ProductPagePriceProps = { product: ProductPagePriceFragment } & AddToCartItemSelector &
14
+ UseCustomizableOptionPriceProps
10
15
 
11
16
  const { classes } = extendableComponent('ProductPagePrice', ['root', 'discountPrice'] as const)
12
17
 
@@ -18,6 +23,8 @@ export function ProductPagePrice(props: ProductPagePriceProps) {
18
23
  const price =
19
24
  getProductTierPrice(product, quantity) ?? product.price_range.minimum_price.final_price
20
25
 
26
+ const priceValue = useCustomizableOptionPrice(props)
27
+
21
28
  return (
22
29
  <>
23
30
  {product.price_range.minimum_price.regular_price.value !== price.value && (
@@ -33,7 +40,7 @@ export function ProductPagePrice(props: ProductPagePriceProps) {
33
40
  <Money {...product.price_range.minimum_price.regular_price} />
34
41
  </Box>
35
42
  )}
36
- <Money {...price} />
43
+ <Money {...price} value={priceValue} />
37
44
  </>
38
45
  )
39
46
  }