@graphcommerce/magento-product 4.0.4 → 4.1.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 (34) hide show
  1. package/CHANGELOG.md +253 -436
  2. package/components/JsonLdProduct/{index.tsx → jsonLdProduct.tsx} +0 -0
  3. package/components/ProductAddToCart/ProductAddToCart.tsx +21 -6
  4. package/components/ProductListFilters/FilterCheckboxType.tsx +8 -6
  5. package/components/ProductListFilters/FilterEqualType.tsx +2 -2
  6. package/components/ProductListFilters/FilterRangeType.tsx +9 -26
  7. package/components/ProductListFilters/ProductListFilters.tsx +5 -5
  8. package/components/ProductListFiltersContainer/{index.tsx → ProductListFiltersContainer.tsx} +27 -23
  9. package/components/ProductListItem/{index.tsx → ProductListItem.tsx} +2 -2
  10. package/components/ProductListItems/ProductListItems.tsx +6 -0
  11. package/components/ProductListItems/ProductListItemsBase.tsx +2 -2
  12. package/components/ProductListItems/ProductListParamsProvider.tsx +1 -1
  13. package/components/ProductListItems/filterTypes.tsx +1 -1
  14. package/components/ProductListItems/getFilterTypes.ts +1 -2
  15. package/components/ProductListItems/renderer.tsx +2 -4
  16. package/components/ProductListLink/ProductListLink.tsx +35 -35
  17. package/components/ProductListPagination/{index.tsx → ProductListPagination.tsx} +3 -7
  18. package/components/ProductListPrice/{index.tsx → ProductListPrice.tsx} +1 -1
  19. package/components/ProductListSort/{index.tsx → ProductListSort.tsx} +2 -2
  20. package/components/ProductPageCategory/{index.ts → productPageCategory.ts} +1 -1
  21. package/components/ProductPageDescription/ProductPageDescription.tsx +72 -0
  22. package/components/ProductPageGallery/ProductImage.tsx +1 -1
  23. package/components/ProductPageGallery/{index.tsx → ProductPageGallery.tsx} +1 -1
  24. package/components/ProductPageGallery/ProductVideo.tsx +1 -2
  25. package/components/ProductPageMeta/{index.tsx → ProductPageMeta.tsx} +1 -2
  26. package/components/ProductShortDescription/{index.tsx → ProductShortDescription.tsx} +1 -1
  27. package/components/ProductSidebarDelivery/{index.tsx → ProductSidebarDelivery.tsx} +3 -3
  28. package/components/ProductSpecs/{index.tsx → ProductSpecs.tsx} +1 -1
  29. package/components/ProductWeight/{index.tsx → ProductWeight.tsx} +1 -1
  30. package/components/index.ts +16 -26
  31. package/index.ts +0 -2
  32. package/package.json +10 -10
  33. package/components/ProductListItems/index.tsx +0 -6
  34. package/components/ProductPageDescription/index.tsx +0 -63
@@ -6,11 +6,13 @@ import {
6
6
  MessageSnackbar,
7
7
  TextInputNumber,
8
8
  iconChevronRight,
9
- SvgIcon,
9
+ IconSvg,
10
10
  extendableComponent,
11
+ AnimatedRow,
11
12
  } from '@graphcommerce/next-ui'
12
13
  import { Trans } from '@lingui/macro'
13
- import { Divider, Typography, ButtonProps, Box } from '@mui/material'
14
+ import { Divider, Typography, ButtonProps, Box, Alert } from '@mui/material'
15
+ import { AnimatePresence } from 'framer-motion'
14
16
  import PageLink from 'next/link'
15
17
  import React from 'react'
16
18
  import { ProductAddToCartDocument, ProductAddToCartMutationVariables } from './ProductAddToCart.gql'
@@ -24,7 +26,7 @@ const { classes, selectors } = extendableComponent('ProductAddToCart', [
24
26
 
25
27
  export type AddToCartProps = React.ComponentProps<typeof ProductAddToCart>
26
28
 
27
- export default function ProductAddToCart(
29
+ export function ProductAddToCart(
28
30
  props: Pick<ProductInterface, 'name'> & {
29
31
  variables: Omit<ProductAddToCartMutationVariables, 'cartId'>
30
32
  name: string
@@ -38,7 +40,7 @@ export default function ProductAddToCart(
38
40
  defaultValues: { ...variables },
39
41
  })
40
42
 
41
- const { handleSubmit, formState, error, muiRegister, required } = form
43
+ const { handleSubmit, formState, error, muiRegister, required, data } = form
42
44
  const submitHandler = handleSubmit(() => {})
43
45
 
44
46
  return (
@@ -85,8 +87,21 @@ export default function ProductAddToCart(
85
87
 
86
88
  <ApolloCartErrorAlert error={error} />
87
89
 
90
+ <AnimatePresence initial={false}>
91
+ {data?.addProductsToCart?.user_errors.map((e) => (
92
+ <AnimatedRow key={e?.code}>
93
+ <Alert severity='error'>{e?.message}</Alert>
94
+ </AnimatedRow>
95
+ ))}
96
+ </AnimatePresence>
97
+
88
98
  <MessageSnackbar
89
- open={!formState.isSubmitting && formState.isSubmitSuccessful && !error?.message}
99
+ open={
100
+ !formState.isSubmitting &&
101
+ formState.isSubmitSuccessful &&
102
+ !error?.message &&
103
+ !data?.addProductsToCart?.user_errors?.length
104
+ }
90
105
  variant='pill'
91
106
  action={
92
107
  <PageLink href='/cart' passHref>
@@ -94,7 +109,7 @@ export default function ProductAddToCart(
94
109
  size='medium'
95
110
  variant='pill'
96
111
  color='secondary'
97
- endIcon={<SvgIcon src={iconChevronRight} />}
112
+ endIcon={<IconSvg src={iconChevronRight} />}
98
113
  >
99
114
  <Trans>View shopping cart</Trans>
100
115
  </Button>
@@ -1,16 +1,17 @@
1
1
  import { cloneDeep } from '@graphcommerce/graphql'
2
- import { iconCancelAlt, SvgIcon } from '@graphcommerce/next-ui'
2
+ import { iconCancelAlt, IconSvg } from '@graphcommerce/next-ui'
3
3
  import { Chip, ChipProps, SxProps, Theme } from '@mui/material'
4
4
  import { useProductListLinkReplace } from '../../hooks/useProductListLinkReplace'
5
5
  import { useProductListParamsContext } from '../../hooks/useProductListParamsContext'
6
- import ProductListLink from '../ProductListLink/ProductListLink'
6
+ import { ProductListLink } from '../ProductListLink/ProductListLink'
7
7
  import { FilterIn } from './FilterEqualType'
8
8
  import { ProductListFiltersFragment } from './ProductListFilters.gql'
9
9
 
10
10
  type Filter = NonNullable<NonNullable<ProductListFiltersFragment['aggregations']>[number]>
11
- export type FilterCheckboxTypeProps = Filter & Omit<ChipProps, 'selected'> & { sx?: SxProps<Theme> }
11
+ export type FilterCheckboxTypeProps = Filter &
12
+ Omit<ChipProps<'button'>, 'selected' | 'onDelete' | 'component'> & { sx?: SxProps<Theme> }
12
13
 
13
- export default function FilterCheckboxType(props: FilterCheckboxTypeProps) {
14
+ export function FilterCheckboxType(props: FilterCheckboxTypeProps) {
14
15
  const { attribute_code, count, label, options, ...chipProps } = props
15
16
  const { params } = useProductListParamsContext()
16
17
  const currentFilter = params.filters[attribute_code]
@@ -32,7 +33,8 @@ export default function FilterCheckboxType(props: FilterCheckboxTypeProps) {
32
33
  link={{ scroll: false, replace: true }}
33
34
  >
34
35
  <Chip
35
- color={isActive ? undefined : 'default'}
36
+ component='button'
37
+ color={isActive ? 'primary' : 'default'}
36
38
  onDelete={
37
39
  isActive
38
40
  ? () => {
@@ -45,7 +47,7 @@ export default function FilterCheckboxType(props: FilterCheckboxTypeProps) {
45
47
  }
46
48
  : undefined
47
49
  }
48
- deleteIcon={isActive ? <SvgIcon src={iconCancelAlt} size='small' /> : undefined}
50
+ deleteIcon={isActive ? <IconSvg src={iconCancelAlt} size='small' /> : undefined}
49
51
  label={label}
50
52
  clickable
51
53
  {...chipProps}
@@ -11,7 +11,7 @@ import {
11
11
  import { SetRequired } from 'type-fest'
12
12
  import { useProductListLinkReplace } from '../../hooks/useProductListLinkReplace'
13
13
  import { useProductListParamsContext } from '../../hooks/useProductListParamsContext'
14
- import ProductListLink from '../ProductListLink/ProductListLink'
14
+ import { ProductListLink } from '../ProductListLink/ProductListLink'
15
15
  import { ProductListFiltersFragment } from './ProductListFilters.gql'
16
16
 
17
17
  type OwnerState = {
@@ -44,7 +44,7 @@ type Filter = NonNullable<NonNullable<ProductListFiltersFragment['aggregations']
44
44
 
45
45
  type FilterEqualTypeProps = Filter & Omit<ChipMenuProps, 'selected'>
46
46
 
47
- export default function FilterEqualType(props: FilterEqualTypeProps) {
47
+ export function FilterEqualType(props: FilterEqualTypeProps) {
48
48
  const { attribute_code, count, label, options, __typename, ...chipProps } = props
49
49
  const { params } = useProductListParamsContext()
50
50
  const replaceRoute = useProductListLinkReplace({ scroll: false })
@@ -1,6 +1,7 @@
1
1
  import { cloneDeep, FilterRangeTypeInput } from '@graphcommerce/graphql'
2
2
  import { Money } from '@graphcommerce/magento-store'
3
3
  import { ChipMenu, ChipMenuProps, extendableComponent } from '@graphcommerce/next-ui'
4
+ import { Trans } from '@lingui/macro'
4
5
  import { Box, Slider } from '@mui/material'
5
6
  import React, { useEffect } from 'react'
6
7
  import { useProductListLinkReplace } from '../../hooks/useProductListLinkReplace'
@@ -12,11 +13,9 @@ type FilterRangeTypeProps = NonNullable<
12
13
  > &
13
14
  Omit<ChipMenuProps, 'selected'>
14
15
 
15
- const sliderThumbWidth = 28
16
-
17
16
  const { classes } = extendableComponent('FilterRangeType', ['root', 'container', 'slider'] as const)
18
17
 
19
- export default function FilterRangeType(props: FilterRangeTypeProps) {
18
+ export function FilterRangeType(props: FilterRangeTypeProps) {
20
19
  const { attribute_code, label, options, ...chipProps } = props
21
20
  const { params } = useProductListParamsContext()
22
21
  const replaceRoute = useProductListLinkReplace({ scroll: false })
@@ -79,16 +78,16 @@ export default function FilterRangeType(props: FilterRangeTypeProps) {
79
78
 
80
79
  if (from === min && to !== max)
81
80
  currentLabel = (
82
- <>
83
- {'Below '} <Money round value={Number(currentFilter?.to)} />
84
- </>
81
+ <Trans>
82
+ Below <Money round value={Number(currentFilter?.to)} />
83
+ </Trans>
85
84
  )
86
85
 
87
86
  if (from !== min && to === max)
88
87
  currentLabel = (
89
- <>
90
- {'Above '} <Money round value={Number(currentFilter?.from)} />
91
- </>
88
+ <Trans>
89
+ Above <Money round value={Number(currentFilter?.from)} />
90
+ </Trans>
92
91
  )
93
92
 
94
93
  if (from !== min && to !== max)
@@ -128,6 +127,7 @@ export default function FilterRangeType(props: FilterRangeTypeProps) {
128
127
  <Slider
129
128
  min={min}
130
129
  max={max}
130
+ size='large'
131
131
  aria-labelledby='range-slider'
132
132
  value={value}
133
133
  onChange={(e, newValue) => {
@@ -142,23 +142,6 @@ export default function FilterRangeType(props: FilterRangeTypeProps) {
142
142
  }}
143
143
  valueLabelDisplay='off'
144
144
  className={classes.slider}
145
- sx={(theme) => ({
146
- maxWidth: `calc(100% - ${sliderThumbWidth}px)`,
147
- margin: `${theme.spacings.xxs} auto`,
148
- display: 'block',
149
- paddingBottom: '32px',
150
- '& .MuiSlider-rail': {
151
- height: 4,
152
- borderRadius: '10px',
153
- },
154
- '& .MuiSlider-track': {
155
- height: 4,
156
- },
157
- '& .MuiSlider-thumb': {
158
- width: sliderThumbWidth,
159
- height: sliderThumbWidth,
160
- },
161
- })}
162
145
  />
163
146
  </Box>
164
147
  </ChipMenu>
@@ -1,15 +1,15 @@
1
1
  import { ChipMenuProps } from '@graphcommerce/next-ui'
2
2
  import { FilterTypes } from '../ProductListItems/filterTypes'
3
- import FilterCheckboxType from './FilterCheckboxType'
4
- import FilterEqualType from './FilterEqualType'
5
- import FilterRangeType from './FilterRangeType'
3
+ import { FilterCheckboxType } from './FilterCheckboxType'
4
+ import { FilterEqualType } from './FilterEqualType'
5
+ import { FilterRangeType } from './FilterRangeType'
6
6
  import { ProductListFiltersFragment } from './ProductListFilters.gql'
7
7
 
8
8
  export type ProductFiltersProps = ProductListFiltersFragment & {
9
9
  filterTypes: FilterTypes
10
10
  } & Omit<ChipMenuProps, 'selected' | 'selectedLabel' | 'children' | 'label' | 'onDelete'>
11
11
 
12
- export default function ProductListFilters(props: ProductFiltersProps) {
12
+ export function ProductListFilters(props: ProductFiltersProps) {
13
13
  const { aggregations, filterTypes, ...chipMenuProps } = props
14
14
 
15
15
  return (
@@ -52,7 +52,7 @@ export default function ProductListFilters(props: ProductFiltersProps) {
52
52
  />
53
53
  )
54
54
  }
55
- console.log(
55
+ console.error(
56
56
  'Filter not recognized',
57
57
  aggregation.attribute_code,
58
58
  filterTypes[aggregation.attribute_code],
@@ -3,7 +3,7 @@ import {
3
3
  iconChevronLeft,
4
4
  iconChevronRight,
5
5
  responsiveVal,
6
- SvgIcon,
6
+ IconSvg,
7
7
  useScrollY,
8
8
  extendableComponent,
9
9
  } from '@graphcommerce/next-ui'
@@ -32,8 +32,8 @@ const parts = [
32
32
 
33
33
  const { withState } = extendableComponent<OwnerState, typeof name, typeof parts>(name, parts)
34
34
 
35
- export default function ProductListFiltersContainer(props: ProductListFiltersContainerProps) {
36
- const { children } = props
35
+ export function ProductListFiltersContainer(props: ProductListFiltersContainerProps) {
36
+ const { children, sx = [] } = props
37
37
  const scrollY = useScrollY()
38
38
 
39
39
  const [isSticky, setIsSticky] = useState<boolean>(false)
@@ -83,31 +83,33 @@ export default function ProductListFiltersContainer(props: ProductListFiltersCon
83
83
  <MotionDiv
84
84
  className={classes.wrapper}
85
85
  ref={wrapperRef}
86
- sx={(theme) => ({
87
- display: 'flex',
88
- justifyContent: 'center',
89
- height: responsiveVal(44, 52),
90
- marginBottom: theme.spacings.sm,
91
- position: 'sticky',
92
- top: theme.page.vertical,
93
- zIndex: 9,
94
- margin: '0 auto',
95
- maxWidth: `calc(100% - 96px - ${theme.spacings.sm} * 2)`,
96
- [theme.breakpoints.down('md')]: {
97
- textAlign: 'center',
98
- maxWidth: 'unset',
99
- margin: `0 calc(${theme.page.horizontal} * -1)`,
100
- },
101
- })}
86
+ sx={[
87
+ (theme) => ({
88
+ display: 'flex',
89
+ justifyContent: 'center',
90
+ marginBottom: theme.spacings.sm,
91
+ position: 'sticky',
92
+ top: theme.page.vertical,
93
+ zIndex: 9,
94
+ margin: '0 auto',
95
+ maxWidth: `calc(100% - 96px - ${theme.spacings.sm} * 2)`,
96
+ [theme.breakpoints.down('md')]: {
97
+ textAlign: 'center',
98
+ maxWidth: 'unset',
99
+ margin: `0 calc(${theme.page.horizontal} * -1)`,
100
+ },
101
+ }),
102
+ ...(Array.isArray(sx) ? sx : [sx]),
103
+ ]}
102
104
  >
103
105
  <ScrollerProvider scrollSnapAlign='none'>
104
106
  <ScrollerButton
105
107
  direction='left'
106
108
  className={classes.sliderPrev}
107
109
  size='small'
108
- sx={{ position: 'absolute', top: 2, left: 2, zIndex: 10 }}
110
+ sx={{ position: 'absolute', top: 4, left: 2, zIndex: 10 }}
109
111
  >
110
- <SvgIcon src={iconChevronLeft} />
112
+ <IconSvg src={iconChevronLeft} />
111
113
  </ScrollerButton>
112
114
  <Box
113
115
  className={classes.container}
@@ -121,6 +123,8 @@ export default function ProductListFiltersContainer(props: ProductListFiltersCon
121
123
  background: theme.palette.background.default,
122
124
  borderRadius: '99em',
123
125
  },
126
+ display: 'grid',
127
+ alignItems: 'center',
124
128
  })}
125
129
  >
126
130
  <Scroller
@@ -163,9 +167,9 @@ export default function ProductListFiltersContainer(props: ProductListFiltersCon
163
167
  direction='right'
164
168
  className={classes.sliderNext}
165
169
  size='small'
166
- sx={{ position: 'absolute', top: 2, right: 2, zIndex: 10 }}
170
+ sx={{ position: 'absolute', top: 4, right: 2, zIndex: 10 }}
167
171
  >
168
- <SvgIcon src={iconChevronRight} />
172
+ <IconSvg src={iconChevronRight} />
169
173
  </ScrollerButton>
170
174
  </ScrollerProvider>
171
175
  </MotionDiv>
@@ -7,7 +7,7 @@ import { useRouter } from 'next/router'
7
7
  import React, { PropsWithChildren, useMemo } from 'react'
8
8
  import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
9
9
  import { useProductLink } from '../../hooks/useProductLink'
10
- import ProductListPrice from '../ProductListPrice'
10
+ import { ProductListPrice } from '../ProductListPrice/ProductListPrice'
11
11
 
12
12
  const { classes, selectors } = extendableComponent('ProductListItem', [
13
13
  'root',
@@ -47,7 +47,7 @@ export type ProductListItemProps = BaseProps & { sx?: SxProps<Theme> }
47
47
 
48
48
  const StyledImage = styled(Image)({})
49
49
 
50
- export default function ProductListItem(props: ProductListItemProps) {
50
+ export function ProductListItem(props: ProductListItemProps) {
51
51
  const {
52
52
  subTitle,
53
53
  topLeft,
@@ -0,0 +1,6 @@
1
+ import { ProductListItemsBase, ProductItemsGridProps } from './ProductListItemsBase'
2
+ import { renderer } from './renderer'
3
+
4
+ export function ProductListItems(props: Omit<ProductItemsGridProps, 'renderers'>) {
5
+ return <ProductListItemsBase renderers={renderer} {...props} />
6
+ }
@@ -1,7 +1,7 @@
1
1
  import { RenderType, responsiveVal } from '@graphcommerce/next-ui'
2
2
  import { Box, BoxProps } from '@mui/material'
3
3
  import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
4
- import { ProductListItemProps } from '../ProductListItem'
4
+ import { ProductListItemProps } from '../ProductListItem/ProductListItem'
5
5
  import { ProductListItemRenderer } from './renderer'
6
6
 
7
7
  export type ProductItemsGridProps = {
@@ -15,7 +15,7 @@ export type ProductItemsGridProps = {
15
15
  sx?: BoxProps['sx']
16
16
  }
17
17
 
18
- export default function ProductListItemsBase(props: ProductItemsGridProps) {
18
+ export function ProductListItemsBase(props: ProductItemsGridProps) {
19
19
  const { items, sx = [], renderers, loadingEager = 0, size = 'normal' } = props
20
20
 
21
21
  return (
@@ -2,7 +2,7 @@ import { PropsWithChildren, useState, useEffect, useMemo } from 'react'
2
2
  import { productListParamsContext } from '../../context/productListParamsContext'
3
3
  import { ProductListParams } from './filterTypes'
4
4
 
5
- export default function ProductListParamsProvider({
5
+ export function ProductListParamsProvider({
6
6
  children,
7
7
  value,
8
8
  }: PropsWithChildren<{ value: ProductListParams }>) {
@@ -21,7 +21,7 @@ export type ProductListParams = Exact<{
21
21
 
22
22
  type AnyFilterType = FilterEqualTypeInput | FilterMatchTypeInput | FilterRangeTypeInput | undefined
23
23
 
24
- export function isFilterTypeEqual(filter: AnyFilterType): filter is FilterEqualTypeInput {
24
+ export function isFilterTypeEqual(filter?: unknown): filter is FilterEqualTypeInput {
25
25
  return Boolean(
26
26
  filter && ((filter as FilterEqualTypeInput).eq || (filter as FilterEqualTypeInput).in),
27
27
  )
@@ -1,5 +1,4 @@
1
- import { gql, ApolloClient, NormalizedCacheObject } from '@graphcommerce/graphql'
2
- import { Exact } from '@graphcommerce/graphql'
1
+ import { gql, ApolloClient, NormalizedCacheObject , Exact } from '@graphcommerce/graphql'
3
2
  import { AllFilterInputTypes, FilterTypes } from './filterTypes'
4
3
 
5
4
  const allFilterInputTypes: AllFilterInputTypes[] = [
@@ -1,10 +1,10 @@
1
1
  import { TypeRenderer } from '@graphcommerce/next-ui'
2
2
  import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
3
- import ProductListItem from '../ProductListItem'
3
+ import { ProductListItem } from '../ProductListItem/ProductListItem'
4
4
 
5
5
  export type ProductListItemRenderer = TypeRenderer<ProductListItemFragment>
6
6
 
7
- const renderer: ProductListItemRenderer = {
7
+ export const renderer: ProductListItemRenderer = {
8
8
  SimpleProduct: ProductListItem,
9
9
  ConfigurableProduct: ProductListItem,
10
10
  BundleProduct: ProductListItem,
@@ -12,5 +12,3 @@ const renderer: ProductListItemRenderer = {
12
12
  DownloadableProduct: ProductListItem,
13
13
  GroupedProduct: ProductListItem,
14
14
  }
15
-
16
- export default renderer
@@ -13,41 +13,41 @@ export type ProductListLinkProps = PropsWithChildren<
13
13
  }
14
14
  >
15
15
 
16
- const ProductListLink = React.forwardRef<HTMLAnchorElement, ProductListLinkProps>((props, ref) => {
17
- const { setParams } = useProductListParamsContext()
18
- const {
19
- children,
20
- url,
21
- sort,
22
- currentPage,
23
- pageSize,
24
- filters,
25
- search,
26
- noLink,
27
- link,
28
- ...linkProps
29
- } = props
30
- const newParams = { filters, sort, url, currentPage, pageSize, search }
16
+ export const ProductListLink = React.forwardRef<HTMLAnchorElement, ProductListLinkProps>(
17
+ (props, ref) => {
18
+ const { setParams } = useProductListParamsContext()
19
+ const {
20
+ children,
21
+ url,
22
+ sort,
23
+ currentPage,
24
+ pageSize,
25
+ filters,
26
+ search,
27
+ noLink,
28
+ link,
29
+ ...linkProps
30
+ } = props
31
+ const newParams = { filters, sort, url, currentPage, pageSize, search }
31
32
 
32
- const productListLink = useProductListLink(newParams)
33
- const updateParams = () => setParams(newParams)
33
+ const productListLink = useProductListLink(newParams)
34
+ const updateParams = () => setParams(newParams)
34
35
 
35
- // We're setting nofollow if a custom sort, pageSize, filters or search is set.
36
- let rel: string | undefined
37
- if (Object.keys(sort).length || pageSize || Object.keys(filters).length || search)
38
- rel = 'nofollow'
36
+ // We're setting nofollow if a custom sort, pageSize, filters or search is set.
37
+ let rel: string | undefined
38
+ if (Object.keys(sort).length || pageSize || Object.keys(filters).length || search)
39
+ rel = 'nofollow'
39
40
 
40
- return (
41
- <PageLink href={productListLink} passHref {...link}>
42
- {noLink ? (
43
- children
44
- ) : (
45
- <Link rel={rel} {...linkProps} ref={ref} onClick={updateParams} underline='hover'>
46
- {children}
47
- </Link>
48
- )}
49
- </PageLink>
50
- )
51
- })
52
-
53
- export default ProductListLink
41
+ return (
42
+ <PageLink href={productListLink} passHref {...link}>
43
+ {noLink ? (
44
+ children
45
+ ) : (
46
+ <Link rel={rel} underline='hover' {...linkProps} ref={ref} onClick={updateParams}>
47
+ {children}
48
+ </Link>
49
+ )}
50
+ </PageLink>
51
+ )
52
+ },
53
+ )
@@ -1,17 +1,13 @@
1
1
  import { Pagination } from '@graphcommerce/next-ui'
2
2
  import { PaginationProps } from '@mui/material'
3
- import React from 'react'
4
3
  import { useProductListParamsContext } from '../../hooks/useProductListParamsContext'
5
- import ProductListLink from '../ProductListLink/ProductListLink'
4
+ import { ProductListLink } from '../ProductListLink/ProductListLink'
6
5
  import { ProductListPaginationFragment } from './ProductListPagination.gql'
7
6
 
8
7
  export type ProductPaginationProps = ProductListPaginationFragment &
9
8
  Omit<PaginationProps, 'count' | 'defaultPage' | 'page' | 'renderItem'>
10
9
 
11
- export default function ProductListPagination({
12
- page_info,
13
- ...paginationProps
14
- }: ProductPaginationProps) {
10
+ export function ProductListPagination({ page_info, ...paginationProps }: ProductPaginationProps) {
15
11
  const { params } = useProductListParamsContext()
16
12
 
17
13
  if (!page_info || !page_info.total_pages || !page_info.current_page) return null
@@ -20,7 +16,7 @@ export default function ProductListPagination({
20
16
  <Pagination
21
17
  count={page_info?.total_pages}
22
18
  page={page_info?.current_page ?? 1}
23
- renderLink={(page: number, icon: React.ReactNode, btnProps: any) => (
19
+ renderLink={(_, icon, btnProps) => (
24
20
  <ProductListLink {...btnProps} {...params} currentPage={btnProps.page} color='inherit'>
25
21
  {icon}
26
22
  </ProductListLink>
@@ -10,7 +10,7 @@ const { classes, selectors } = extendableComponent('ProductListPrice', [
10
10
 
11
11
  type ProductListPriceProps = ProductListPriceFragment & Pick<TypographyProps, 'sx'>
12
12
 
13
- export default function ProductListPrice(props: ProductListPriceProps) {
13
+ export function ProductListPrice(props: ProductListPriceProps) {
14
14
  const { regular_price, final_price, sx } = props
15
15
 
16
16
  return (
@@ -5,13 +5,13 @@ import { ListItem, ListItemText } from '@mui/material'
5
5
  import React from 'react'
6
6
  import { useProductListLinkReplace } from '../../hooks/useProductListLinkReplace'
7
7
  import { useProductListParamsContext } from '../../hooks/useProductListParamsContext'
8
- import ProductListLink from '../ProductListLink/ProductListLink'
8
+ import { ProductListLink } from '../ProductListLink/ProductListLink'
9
9
  import { ProductListSortFragment } from './ProductListSort.gql'
10
10
 
11
11
  export type ProductListSortProps = ProductListSortFragment &
12
12
  Omit<ChipMenuProps, 'selected' | 'selectedLabel' | 'children' | 'label' | 'onDelete'>
13
13
 
14
- export default function ProductListSort(props: ProductListSortProps) {
14
+ export function ProductListSort(props: ProductListSortProps) {
15
15
  const { sort_fields, total_count, ...filterMenuProps } = props
16
16
  const { params } = useProductListParamsContext()
17
17
  const replaceRoute = useProductListLinkReplace()
@@ -6,7 +6,7 @@ import { ProductPageCategoryFragment } from './ProductPageCategory.gql'
6
6
  * - Prefers categories that are included in the menu
7
7
  * - Prefers categories that have a longer path than shorter ones.
8
8
  */
9
- export default function productPageCategory(product?: ProductPageCategoryFragment | null) {
9
+ export function productPageCategory(product?: ProductPageCategoryFragment | null) {
10
10
  if (!product?.categories?.length) return undefined
11
11
  return product?.categories?.reduce((carry, value) => {
12
12
  if (!value?.include_in_menu) return carry
@@ -0,0 +1,72 @@
1
+ import {
2
+ ColumnTwoWithTop,
3
+ ColumnTwoWithTopProps,
4
+ extendableComponent,
5
+ responsiveVal,
6
+ } from '@graphcommerce/next-ui'
7
+ import { Box, SxProps, Theme, Typography } from '@mui/material'
8
+ import { Variant } from '@mui/material/styles/createTypography'
9
+ import { ProductPageDescriptionFragment } from './ProductPageDescription.gql'
10
+
11
+ export type ProductPageDescriptionProps = ProductPageDescriptionFragment &
12
+ Omit<ColumnTwoWithTopProps, 'top' | 'left'> & {
13
+ sx?: SxProps<Theme>
14
+ fontSize: 'responsive' | Variant
15
+ }
16
+
17
+ const componentName = 'ProductPageDescription'
18
+ const parts = ['root', 'description'] as const
19
+
20
+ const { classes } = extendableComponent(componentName, parts)
21
+
22
+ export function ProductPageDescription(props: ProductPageDescriptionProps) {
23
+ const { description, name, right, fontSize = 'subtitle1', sx = [] } = props
24
+
25
+ return (
26
+ <ColumnTwoWithTop
27
+ className={classes.root}
28
+ sx={sx}
29
+ top={
30
+ <Typography variant='h1' component='h2'>
31
+ {name}
32
+ </Typography>
33
+ }
34
+ left={
35
+ description && (
36
+ <Box
37
+ className={classes.description}
38
+ // eslint-disable-next-line react/no-danger
39
+ dangerouslySetInnerHTML={{ __html: description.html }}
40
+ sx={[
41
+ {
42
+ '& p:first-of-type': {
43
+ marginTop: 0,
44
+ },
45
+ '& ul': {
46
+ padding: 0,
47
+ margin: 0,
48
+ display: 'inline',
49
+ listStyleType: 'none',
50
+ },
51
+ '& li': {
52
+ display: 'inline',
53
+ },
54
+ },
55
+ fontSize === 'responsive' && {
56
+ '& p, & li': {
57
+ fontSize: responsiveVal(16, 30),
58
+ },
59
+ },
60
+ fontSize !== 'responsive' && {
61
+ '& p, & li': {
62
+ fontSize,
63
+ },
64
+ },
65
+ ]}
66
+ />
67
+ )
68
+ }
69
+ right={right}
70
+ />
71
+ )
72
+ }
@@ -1,7 +1,7 @@
1
1
  import { Image } from '@graphcommerce/image'
2
2
  import { ProductImageFragment } from './ProductImage.gql'
3
3
 
4
- export default function ProductImage(props: ProductImageFragment) {
4
+ export function ProductImage(props: ProductImageFragment) {
5
5
  const { url, label } = props
6
6
 
7
7
  if (!url) return null
@@ -9,7 +9,7 @@ export type ProductPageGalleryRenderers = TypeRenderer<
9
9
  type ProductPageGalleryProps = PropsWithChildren<ProductPageGalleryFragment> &
10
10
  Omit<SidebarGalleryProps, 'sidebar' | 'images'>
11
11
 
12
- export default function ProductPageGallery(props: ProductPageGalleryProps) {
12
+ export function ProductPageGallery(props: ProductPageGalleryProps) {
13
13
  const {
14
14
  media_gallery,
15
15
  children,