@graphcommerce/magento-product 8.1.0-canary.9 → 9.0.0-canary.55

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 (78) hide show
  1. package/Api/ProductListItem.graphql +1 -2
  2. package/Api/ProductPageItem.graphql +1 -1
  3. package/CHANGELOG.md +113 -0
  4. package/Config.graphqls +13 -0
  5. package/components/AddProductsToCart/AddProductsToCartButton.tsx +3 -2
  6. package/components/AddProductsToCart/AddProductsToCartFab.tsx +2 -2
  7. package/components/AddProductsToCart/AddProductsToCartForm.tsx +31 -28
  8. package/components/AddProductsToCart/AddProductsToCartSnackbar.tsx +25 -16
  9. package/components/AddProductsToCart/UseAddProductsToCartAction.graphql +1 -1
  10. package/components/AddProductsToCart/findAddedItems.ts +1 -4
  11. package/components/AddProductsToCart/useAddProductsToCartAction.ts +2 -1
  12. package/components/AddProductsToCart/useFormAddProductsToCart.ts +1 -2
  13. package/components/JsonLdProduct/JsonLdProduct.graphql +1 -1
  14. package/components/JsonLdProduct/ProductPageJsonLd.tsx +1 -1
  15. package/components/ProductAddToCart/ProductAddToCart.tsx +6 -8
  16. package/components/ProductCustomizable/ProductCustomizable.graphql +1 -1
  17. package/components/ProductCustomizable/index.ts +1 -0
  18. package/components/ProductCustomizable/productCustomizableSelectors.ts +59 -0
  19. package/components/ProductFiltersPro/PriceSlider.tsx +1 -2
  20. package/components/ProductFiltersPro/ProductFilterEqualChip.tsx +1 -1
  21. package/components/ProductFiltersPro/ProductFilterEqualSection.tsx +2 -2
  22. package/components/ProductFiltersPro/ProductFilterRangeChip.tsx +1 -1
  23. package/components/ProductFiltersPro/ProductFilterRangeSection.tsx +1 -1
  24. package/components/ProductFiltersPro/ProductFiltersPro.tsx +103 -19
  25. package/components/ProductFiltersPro/ProductFiltersProAggregations.tsx +31 -18
  26. package/components/ProductFiltersPro/ProductFiltersProAllFiltersChip.tsx +6 -10
  27. package/components/ProductFiltersPro/ProductFiltersProAllFiltersSidebar.tsx +18 -8
  28. package/components/ProductFiltersPro/ProductFiltersProCategorySection.tsx +130 -0
  29. package/components/ProductFiltersPro/ProductFiltersProChips.tsx +10 -8
  30. package/components/ProductFiltersPro/ProductFiltersProClearAll.tsx +4 -16
  31. package/components/ProductFiltersPro/ProductFiltersProLayoutSidebar.tsx +15 -7
  32. package/components/ProductFiltersPro/ProductFiltersProLimitSection.tsx +5 -2
  33. package/components/ProductFiltersPro/ProductFiltersProNoResults.tsx +79 -0
  34. package/components/ProductFiltersPro/ProductFiltersProSortChip.tsx +1 -1
  35. package/components/ProductFiltersPro/ProductFiltersProSortDirectionArrow.tsx +2 -4
  36. package/components/ProductFiltersPro/ProductFiltersProSortSection.tsx +7 -2
  37. package/components/ProductFiltersPro/activeAggregations.ts +5 -9
  38. package/components/ProductFiltersPro/applyAggregationCount.ts +14 -8
  39. package/components/ProductFiltersPro/index.ts +9 -0
  40. package/components/ProductFiltersPro/{useClearAllFiltersHandler.ts → useProductFiltersProClearAllAction.ts} +1 -1
  41. package/components/ProductFiltersPro/useProductFiltersProHasFiltersApplied.ts +21 -0
  42. package/components/ProductFiltersPro/useProductFiltersProSort.tsx +4 -2
  43. package/components/ProductList/ProductList.graphql +8 -5
  44. package/components/ProductListCount/ProductListCount.tsx +3 -1
  45. package/components/ProductListFilters/ProductFilters.graphql +7 -2
  46. package/components/ProductListFilters/ProductListFilters.graphql +1 -1
  47. package/components/ProductListFiltersContainer/ProductListFiltersContainer.tsx +2 -4
  48. package/components/ProductListItem/ProductDiscountLabel.tsx +2 -3
  49. package/components/ProductListItem/ProductListItem.tsx +3 -3
  50. package/components/ProductListItem/ProductListItemTitleAndPrice.tsx +18 -15
  51. package/components/ProductListItems/ProductListItemsBase.tsx +65 -23
  52. package/components/ProductListItems/filterTypes.tsx +14 -5
  53. package/components/ProductListItems/filteredProductList.tsx +23 -0
  54. package/components/ProductListItems/productListApplyCategoryDefaults.ts +44 -4
  55. package/components/ProductListItems/renderer.tsx +8 -2
  56. package/components/ProductListPagination/ProductListPagination.tsx +39 -20
  57. package/components/ProductListPrice/ProductListPrice.tsx +9 -4
  58. package/components/ProductListSuggestions/ProductListSuggestions.graphql +5 -0
  59. package/components/ProductListSuggestions/ProductListSuggestions.tsx +42 -0
  60. package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.graphql +3 -0
  61. package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.tsx +3 -0
  62. package/components/ProductPageBreadcrumb/ProductPageBreadcrumbs.tsx +40 -0
  63. package/components/ProductPageBreadcrumb/index.ts +1 -0
  64. package/components/ProductPageDescription/ComplexTextValue.graphql +1 -1
  65. package/components/ProductPageDescription/ProductPageDescription.tsx +1 -1
  66. package/components/ProductPageGallery/ProductImage.graphql +1 -0
  67. package/components/ProductPageGallery/ProductPageGallery.tsx +14 -8
  68. package/components/ProductPagePrice/ProductPagePrice.graphql +0 -6
  69. package/components/ProductPagePrice/ProductPagePrice.tsx +19 -12
  70. package/components/ProductPagePrice/ProductPagePriceTiers.tsx +4 -3
  71. package/components/ProductPagePrice/useCustomizableOptionPrice.ts +11 -53
  72. package/components/ProductShortDescription/ProductShortDescription.tsx +2 -0
  73. package/components/ProductWeight/ProductWeight.tsx +12 -9
  74. package/components/index.ts +2 -0
  75. package/hooks/useProductList.ts +123 -0
  76. package/hooks/useProductListLink.ts +6 -3
  77. package/index.ts +1 -0
  78. package/package.json +14 -13
@@ -0,0 +1,79 @@
1
+ import { extendableComponent } from '@graphcommerce/next-ui'
2
+ import { Trans } from '@lingui/macro'
3
+ import { Box, Link, SxProps, Theme, Typography } from '@mui/material'
4
+ import { useProductFiltersProClearAllAction } from './useProductFiltersProClearAllAction'
5
+ import { useProductFilterProHasFiltersApplied } from './useProductFiltersProHasFiltersApplied'
6
+
7
+ export type ProductFitlersProNoResultProps = { search?: string | null; sx?: SxProps<Theme> }
8
+
9
+ const name = 'ProductFiltersProNoResults' as const
10
+ const parts = ['root'] as const
11
+ const { classes } = extendableComponent(name, parts)
12
+
13
+ export function ProductFiltersProNoResults(props: ProductFitlersProNoResultProps) {
14
+ const { search, sx = [] } = props
15
+
16
+ const term = search ? `'${search}'` : ''
17
+
18
+ const clearAll = useProductFiltersProClearAllAction()
19
+ const hasFilters = useProductFilterProHasFiltersApplied()
20
+
21
+ return (
22
+ <Box
23
+ className={classes.root}
24
+ sx={[
25
+ (theme) => ({
26
+ marginTop: theme.spacings.md,
27
+ marginBottom: theme.spacings.sm,
28
+ textAlign: 'center',
29
+ }),
30
+ ...(Array.isArray(sx) ? sx : [sx]),
31
+ ]}
32
+ >
33
+ {term ? (
34
+ <>
35
+ <Typography variant='h5' align='center'>
36
+ <Trans>We couldn&apos;t find any results for {term}</Trans>
37
+ </Typography>
38
+ <p>
39
+ {hasFilters ? (
40
+ <Trans>
41
+ Try a different search or{' '}
42
+ <Link
43
+ href='#'
44
+ onClick={(e) => {
45
+ e.preventDefault()
46
+ return clearAll()
47
+ }}
48
+ >
49
+ clear current filters
50
+ </Link>
51
+ </Trans>
52
+ ) : (
53
+ <Trans>Try a different search</Trans>
54
+ )}
55
+ </p>
56
+ </>
57
+ ) : (
58
+ <>
59
+ <Typography variant='h5' align='center'>
60
+ <Trans>We couldn&apos;t find any results</Trans>
61
+ </Typography>
62
+ {hasFilters && (
63
+ <p>
64
+ <Link
65
+ href='#'
66
+ onClick={(e) => {
67
+ e.preventDefault()
68
+ return clearAll()
69
+ }}
70
+ >
71
+ <Trans>Clear current filters</Trans>
72
+ </Link>
73
+ </p>
74
+ )}
75
+ </>
76
+ )}
77
+ </Box>
78
+ )
79
+ }
@@ -15,7 +15,7 @@ export type ProductListActionSortProps = UseProductFiltersProSortProps &
15
15
  >
16
16
 
17
17
  export function ProductFiltersProSortChip(props: ProductListActionSortProps) {
18
- const { sort_fields, chipProps, category, ...rest } = props
18
+ const { sort_fields, total_count, chipProps, category, ...rest } = props
19
19
  const { submit, form } = useProductFiltersPro()
20
20
  const { options, showReset, selected, selectedLabel } = useProductFiltersProSort(props)
21
21
 
@@ -1,7 +1,5 @@
1
1
  import { SortEnum } from '@graphcommerce/graphql-mesh'
2
- import { IconSvg } from '@graphcommerce/next-ui'
3
- import * as IconArrowDown from '@graphcommerce/next-ui/icons/arrow-down.svg'
4
- import * as IconArrowUp from '@graphcommerce/next-ui/icons/arrow-up.svg'
2
+ import { IconSvg, iconArrowDown, iconArrowUp } from '@graphcommerce/next-ui'
5
3
 
6
4
  type Props = {
7
5
  sortDirection: SortEnum | null
@@ -10,7 +8,7 @@ type Props = {
10
8
  export function ProductFiltersProSortDirectionArrow({ sortDirection }: Props) {
11
9
  return (
12
10
  <IconSvg
13
- src={sortDirection === 'ASC' || sortDirection === null ? IconArrowUp : IconArrowDown}
11
+ src={sortDirection === 'ASC' || sortDirection === null ? iconArrowUp : iconArrowDown}
14
12
  sx={{ display: 'flex' }}
15
13
  />
16
14
  )
@@ -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={
@@ -19,7 +24,7 @@ export function ProductFiltersProSortSection(props: ProductFiltersProSortSection
19
24
  name='sort'
20
25
  layout='list'
21
26
  variant='default'
22
- size='medium'
27
+ size='responsive'
23
28
  render={ActionCard}
24
29
  items={options}
25
30
  />
@@ -2,21 +2,17 @@ import { filterNonNullableKeys } from '@graphcommerce/next-ui'
2
2
  import { ProductListFiltersFragment } from '../ProductListFilters/ProductListFilters.gql'
3
3
  import { ProductFilterParams } from '../ProductListItems/filterTypes'
4
4
 
5
- export function excludeCategory(
6
- aggregations: ProductListFiltersFragment['aggregations'],
7
- params: ProductFilterParams,
8
- ) {
9
- return filterNonNullableKeys(aggregations).filter(({ attribute_code }) => {
10
- if (params.search !== null) return true
11
- return attribute_code !== 'category_id' && attribute_code !== 'category_uid'
12
- })
5
+ export function excludeCategory(aggregations: ProductListFiltersFragment['aggregations']) {
6
+ return filterNonNullableKeys(aggregations).filter(
7
+ ({ attribute_code }) => attribute_code !== 'category_id' && attribute_code !== 'category_uid',
8
+ )
13
9
  }
14
10
 
15
11
  export function activeAggregations(
16
12
  aggregations: ProductListFiltersFragment['aggregations'],
17
13
  params: ProductFilterParams,
18
14
  ) {
19
- return excludeCategory(aggregations, params).filter(
15
+ return excludeCategory(aggregations).filter(
20
16
  ({ attribute_code }) =>
21
17
  params.filters[attribute_code]?.from ||
22
18
  params.filters[attribute_code]?.to ||
@@ -27,14 +27,20 @@ export function applyAggregationCount(
27
27
 
28
28
  return {
29
29
  ...aggregation,
30
- options: filterNonNullableKeys(aggregation?.options)?.map((option) => {
31
- if (applied && filterCount === 1) return option
32
- if (applied && filterCount > 1) return { ...option, count: null }
33
- return {
34
- ...option,
35
- count: appliedAggregation?.options?.find((o) => o?.value === option?.value)?.count ?? 0,
36
- }
37
- }),
30
+ options: filterNonNullableKeys(aggregation?.options)
31
+ ?.map((option) => {
32
+ if (applied && filterCount === 1) return option
33
+ if (applied && filterCount > 1) return { ...option, count: null }
34
+ return {
35
+ ...option,
36
+ count: appliedAggregation?.options?.find((o) => o?.value === option?.value)?.count ?? 0,
37
+ }
38
+ })
39
+ .sort((a, b) => {
40
+ if (a.count === 0) return 1
41
+ if (b.count === 0) return -1
42
+ return 0
43
+ }),
38
44
  }
39
45
  })
40
46
  }
@@ -1,11 +1,20 @@
1
1
  export * from './ProductFilterEqualChip'
2
+ export * from './ProductFilterEqualSection'
2
3
  export * from './ProductFilterRangeChip'
4
+ export * from './ProductFilterRangeSection'
3
5
  export * from './ProductFiltersPro'
6
+ export * from './ProductFiltersProAggregations'
4
7
  export * from './ProductFiltersProAllFiltersChip'
5
8
  export * from './ProductFiltersProAllFiltersSidebar'
9
+ export * from './ProductFiltersProCategorySection'
6
10
  export * from './ProductFiltersProChips'
7
11
  export * from './ProductFiltersProClearAll'
8
12
  export * from './ProductFiltersProLayoutSidebar'
9
13
  export * from './ProductFiltersProLimitChip'
10
14
  export * from './ProductFiltersProLimitSection'
11
15
  export * from './ProductFiltersProSortChip'
16
+ export * from './ProductFiltersProSortDirectionArrow'
17
+ export * from './ProductFiltersProSortSection'
18
+ export * from './useProductFiltersProClearAllAction'
19
+ export * from './useProductFiltersProHasFiltersApplied'
20
+ export * from './ProductFiltersProNoResults'
@@ -2,7 +2,7 @@ import { useCallback } from 'react'
2
2
  import { ProductFilterParams } from '../ProductListItems/filterTypes'
3
3
  import { useProductFiltersPro } from './ProductFiltersPro'
4
4
 
5
- export function useClearAllFiltersAction() {
5
+ export function useProductFiltersProClearAllAction() {
6
6
  const { form, submit } = useProductFiltersPro()
7
7
  const { reset, getValues } = form
8
8
 
@@ -0,0 +1,21 @@
1
+ import { useMemo } from 'react'
2
+ import { useProductFiltersPro } from './ProductFiltersPro'
3
+ import { activeAggregations } from './activeAggregations'
4
+ import { applyAggregationCount } from './applyAggregationCount'
5
+
6
+ export function useProductFilterProHasFiltersApplied() {
7
+ const { params, aggregations, appliedAggregations } = useProductFiltersPro()
8
+ const { sort } = params
9
+
10
+ const hasFilters = useMemo(() => {
11
+ const activeFilters = activeAggregations(
12
+ applyAggregationCount(aggregations, appliedAggregations, params),
13
+ params,
14
+ ).map(({ label }) => label)
15
+
16
+ const allFilters = [...activeFilters, sort].filter(Boolean)
17
+ return allFilters.length > 0
18
+ }, [aggregations, appliedAggregations, params, sort])
19
+
20
+ return hasFilters
21
+ }
@@ -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(
@@ -4,6 +4,8 @@ query ProductList(
4
4
  $filters: ProductAttributeFilterInput = {}
5
5
  $sort: ProductAttributeSortInput = {}
6
6
  $search: String = ""
7
+ $context: InContextInput = { loggedIn: false }
8
+ $onlyItems: Boolean = false
7
9
  ) {
8
10
  products(
9
11
  pageSize: $pageSize
@@ -11,11 +13,12 @@ query ProductList(
11
13
  filter: $filters
12
14
  sort: $sort
13
15
  search: $search
14
- ) {
15
- ...ProductListFilters
16
- ...ProductListCount
17
- ...ProductListPagination
18
- ...ProductListSort
16
+ ) @inContext(context: $context) {
17
+ ...ProductListSuggestions @skip(if: $onlyItems)
18
+ ...ProductListFilters @skip(if: $onlyItems)
19
+ ...ProductListCount @skip(if: $onlyItems)
20
+ ...ProductListPagination @skip(if: $onlyItems)
21
+ ...ProductListSort @skip(if: $onlyItems)
19
22
  ...ProductListItems
20
23
  }
21
24
  }
@@ -11,10 +11,11 @@ const { classes, selectors } = extendableComponent('ProductListCount', [
11
11
 
12
12
  export type ProductCountProps = ProductListCountFragment & {
13
13
  sx?: SxProps<Theme>
14
+ children?: React.ReactNode
14
15
  }
15
16
 
16
17
  export function ProductListCount(props: ProductCountProps) {
17
- const { total_count, sx = [] } = props
18
+ const { total_count, children, sx = [] } = props
18
19
 
19
20
  return (
20
21
  <Box
@@ -41,6 +42,7 @@ export function ProductListCount(props: ProductCountProps) {
41
42
  className={classes.count}
42
43
  sx={{ lineHeight: 0 }}
43
44
  >
45
+ {children ? <> {children} </> : null}
44
46
  {total_count === 0 && <Trans id='no products' />}
45
47
  {total_count === 1 && <Trans id='one product' />}
46
48
  {(total_count ?? 0) > 1 && <Trans id='{total_count} products' values={{ total_count }} />}
@@ -1,5 +1,10 @@
1
- query ProductFilters($filters: ProductAttributeFilterInput = {}, $search: String) {
2
- filters: products(filter: $filters, currentPage: 1, pageSize: 1, search: $search) {
1
+ query ProductFilters(
2
+ $filters: ProductAttributeFilterInput = {}
3
+ $search: String
4
+ $context: InContextInput
5
+ ) {
6
+ filters: products(filter: $filters, currentPage: 1, pageSize: 1, search: $search)
7
+ @inContext(context: $context) {
3
8
  ...ProductListFilters
4
9
  }
5
10
  }
@@ -1,5 +1,5 @@
1
1
  fragment ProductListFilters on Products {
2
- aggregations {
2
+ aggregations(filter: { category: { includeDirectChildrenOnly: true } }) {
3
3
  __typename
4
4
  label
5
5
  attribute_code
@@ -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,4 +1,4 @@
1
- import { useNumberFormat } from '@graphcommerce/next-ui'
1
+ import { PercentFormat } from '@graphcommerce/next-ui'
2
2
  import { Box, BoxProps } from '@mui/material'
3
3
  import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
4
4
 
@@ -7,7 +7,6 @@ export type ProductDiscountLabelProps = Pick<ProductListItemFragment, 'price_ran
7
7
 
8
8
  export function ProductDiscountLabel(props: ProductDiscountLabelProps) {
9
9
  const { price_range, ...boxProps } = props
10
- const formatter = useNumberFormat({ style: 'percent', maximumFractionDigits: 1 })
11
10
  const discount = Math.floor(price_range.minimum_price.discount?.percent_off ?? 0)
12
11
 
13
12
  return (
@@ -26,7 +25,7 @@ export function ProductDiscountLabel(props: ProductDiscountLabelProps) {
26
25
  }}
27
26
  {...boxProps}
28
27
  >
29
- {formatter.format(discount / -100)}
28
+ <PercentFormat>{discount / 100}</PercentFormat>
30
29
  </Box>
31
30
  )}
32
31
  </>
@@ -85,8 +85,8 @@ export function ProductListItemReal(props: ProductProps) {
85
85
  onClick,
86
86
  } = props
87
87
 
88
- const handleClick = useEventCallback(
89
- (e: React.MouseEvent<HTMLAnchorElement>) => onClick?.(e, props),
88
+ const handleClick = useEventCallback((e: React.MouseEvent<HTMLAnchorElement>) =>
89
+ onClick?.(e, props),
90
90
  )
91
91
 
92
92
  return (
@@ -140,7 +140,7 @@ export function ProductListItemReal(props: ProductProps) {
140
140
  )
141
141
  }
142
142
 
143
- export function ProductListItemSkeleton(props: SkeletonProps) {
143
+ export function ProductListItemSkeleton(props: BaseProps) {
144
144
  const { children, imageOnly = false, aspectRatio, titleComponent = 'h2', sx = [] } = props
145
145
 
146
146
  return (
@@ -1,4 +1,4 @@
1
- import { Box, Typography } from '@mui/material'
1
+ import { Box, SxProps, Theme, Typography } from '@mui/material'
2
2
  import { productListPrice } from '../ProductListPrice'
3
3
 
4
4
  export type ProductListItemTitleAndPriceProps = {
@@ -7,26 +7,30 @@ export type ProductListItemTitleAndPriceProps = {
7
7
  subTitle?: React.ReactNode
8
8
  children: React.ReactNode
9
9
  classes: { titleContainer: string; title: string; subtitle: string }
10
+ sx?: SxProps<Theme>
10
11
  }
11
12
 
12
13
  export function ProductListItemTitleAndPrice(props: ProductListItemTitleAndPriceProps) {
13
- const { titleComponent = 'h2', classes, children, subTitle, title } = props
14
+ const { titleComponent = 'h2', classes, children, subTitle, title, sx } = props
14
15
 
15
16
  return (
16
17
  <Box
17
18
  className={classes.titleContainer}
18
- sx={(theme) => ({
19
- display: 'grid',
20
- alignItems: 'baseline',
21
- marginTop: theme.spacings.xs,
22
- columnGap: 1,
23
- gridTemplateAreas: {
24
- xs: `"title title" "subtitle price"`,
25
- md: `"title subtitle price"`,
26
- },
27
- gridTemplateColumns: { xs: 'unset', md: 'auto auto 1fr' },
28
- justifyContent: 'space-between',
29
- })}
19
+ sx={[
20
+ (theme) => ({
21
+ display: 'grid',
22
+ alignItems: 'baseline',
23
+ marginTop: theme.spacings.xs,
24
+ columnGap: 1,
25
+ gridTemplateAreas: {
26
+ xs: `"title title" "subtitle price"`,
27
+ md: `"title subtitle price"`,
28
+ },
29
+ gridTemplateColumns: { xs: 'unset', md: 'auto auto 1fr' },
30
+ justifyContent: 'space-between',
31
+ }),
32
+ ...(Array.isArray(sx) ? sx : [sx]),
33
+ ]}
30
34
  >
31
35
  <Typography
32
36
  component={titleComponent}
@@ -37,7 +41,6 @@ export function ProductListItemTitleAndPrice(props: ProductListItemTitleAndPrice
37
41
  overflowWrap: 'break-word',
38
42
  maxWidth: '100%',
39
43
  gridArea: 'title',
40
- fontWeight: 'fontWeightBold',
41
44
  }}
42
45
  className={classes.title}
43
46
  >
@@ -1,5 +1,5 @@
1
1
  import { LazyHydrate, RenderType, extendableComponent, responsiveVal } from '@graphcommerce/next-ui'
2
- import { Box, BoxProps } from '@mui/material'
2
+ import { Box, BoxProps, Breakpoint, Theme, useTheme } from '@mui/material'
3
3
  import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
4
4
  import { AddProductsToCartForm } from '../AddProductsToCart'
5
5
  import { ProductListItemProps } from '../ProductListItem/ProductListItem'
@@ -9,6 +9,27 @@ type ComponentState = {
9
9
  size?: 'normal' | 'small'
10
10
  }
11
11
 
12
+ type ColumnConfig = {
13
+ /**
14
+ * The total width of the grid, this is used to provde the correct values to the image sizes prop so the right image size is loaded.
15
+ *
16
+ * @default "calc(100vw - ${theme.page.horizontal} * 2)"
17
+ */
18
+ totalWidth?: string
19
+ /**
20
+ * Gap between the columns/rows
21
+ *
22
+ * @default theme.spacings.md
23
+ */
24
+ gap?: string
25
+ /**
26
+ * Number of columns
27
+ */
28
+ count: number
29
+ }
30
+
31
+ type ColumnsConfig = Partial<Record<Breakpoint, ColumnConfig>>
32
+
12
33
  export type ProductItemsGridProps = {
13
34
  items?:
14
35
  | Array<(ProductListItemFragment & ProductListItemProps) | null | undefined>
@@ -18,6 +39,7 @@ export type ProductItemsGridProps = {
18
39
  loadingEager?: number
19
40
  title: string
20
41
  sx?: BoxProps['sx']
42
+ columns?: ((theme: Theme) => ColumnsConfig) | ColumnsConfig
21
43
  } & Pick<ProductListItemProps, 'onClick' | 'titleComponent'> &
22
44
  ComponentState
23
45
 
@@ -35,8 +57,28 @@ export function ProductListItemsBase(props: ProductItemsGridProps) {
35
57
  size = 'normal',
36
58
  titleComponent,
37
59
  onClick,
60
+ columns,
38
61
  } = props
39
62
 
63
+ const theme = useTheme()
64
+
65
+ const totalWidth = `calc(100vw - ${theme.page.horizontal} * 2)`
66
+ const gap = theme.spacings.md
67
+
68
+ let columnConfig = typeof columns === 'function' ? columns(theme) : columns
69
+
70
+ if (!columnConfig && size === 'small') {
71
+ columnConfig = {
72
+ xs: { count: 2 },
73
+ md: { count: 3 },
74
+ lg: { count: 4, totalWidth: `${theme.breakpoints.values.xl}px` },
75
+ }
76
+ }
77
+
78
+ if (!columnConfig) {
79
+ columnConfig = { xs: { count: 2 }, md: { count: 3 }, lg: { count: 4 } }
80
+ }
81
+
40
82
  const classes = withState({ size })
41
83
 
42
84
  return (
@@ -44,40 +86,40 @@ export function ProductListItemsBase(props: ProductItemsGridProps) {
44
86
  <Box
45
87
  className={classes.root}
46
88
  sx={[
47
- (theme) => ({
48
- display: 'grid',
49
- gridColumnGap: theme.spacings.md,
50
- gridRowGap: theme.spacings.md,
51
-
52
- '&.sizeSmall': {
53
- gridTemplateColumns: `repeat(auto-fill, minmax(${responsiveVal(150, 280)}, 1fr))`,
54
- },
55
- '&.sizeNormal': {
56
- gridTemplateColumns: {
57
- xs: `repeat(2, 1fr)`,
58
- md: `repeat(3, 1fr)`,
59
- lg: `repeat(4, 1fr)`,
60
- },
89
+ ...Object.entries(columnConfig).map(([key, column]) => ({
90
+ [theme.breakpoints.up(key as Breakpoint)]: {
91
+ gap: column.gap ?? gap,
92
+ // width: totalWidth,
93
+ gridTemplateColumns: `repeat(${column.count}, 1fr)`,
61
94
  },
62
- }),
95
+ })),
96
+ { display: 'grid' },
63
97
  ...(Array.isArray(sx) ? sx : [sx]),
64
98
  ]}
65
99
  >
66
100
  {items?.map((item, idx) =>
67
101
  item ? (
68
- <LazyHydrate key={item.uid ?? ''} hydrated={loadingEager > idx ? true : undefined}>
102
+ <LazyHydrate
103
+ key={item.uid ?? ''}
104
+ hydrated={loadingEager > idx ? true : undefined}
105
+ height={responsiveVal(250, 500)}
106
+ >
69
107
  <RenderType
70
108
  renderer={renderers}
71
- sizes={
72
- size === 'small'
73
- ? { 0: '100vw', 354: '50vw', 675: '30vw', 1255: '23vw', 1500: '337px' }
74
- : { 0: '100vw', 367: '48vw', 994: '30vw', 1590: '23vw', 1920: '443px' }
75
- }
109
+ sizes={Object.fromEntries(
110
+ Object.entries(columnConfig ?? {}).map(([key, column]) => {
111
+ const totalW = column.totalWidth ?? totalWidth
112
+ const columnGap = column.gap ?? gap
113
+ return [
114
+ theme.breakpoints.values[key as Breakpoint],
115
+ `calc((${totalW} - (${columnGap} * ${column.count - 1})) / ${column.count})`,
116
+ ]
117
+ }),
118
+ )}
76
119
  {...item}
77
120
  loading={loadingEager > idx ? 'eager' : 'lazy'}
78
121
  titleComponent={titleComponent}
79
122
  onClick={onClick}
80
- noReport
81
123
  />
82
124
  </LazyHydrate>
83
125
  ) : null,
@@ -43,11 +43,6 @@ export function toFilterParams(params: ProductListParams): ProductFilterParams {
43
43
  }
44
44
  }
45
45
 
46
- export function toProductListParams(params: ProductFilterParams): ProductListParams {
47
- const { sort, dir, ...rest } = params
48
- return { sort: sort ? { [sort]: dir } : {}, ...rest }
49
- }
50
-
51
46
  export type AnyFilterType =
52
47
  | ProductAttributeFilterInput[keyof ProductAttributeFilterInput]
53
48
  | FilterEqualTypeInput
@@ -73,3 +68,17 @@ export function isFilterTypeRange(filter: AnyFilterType): filter is FilterRangeT
73
68
  }
74
69
 
75
70
  export type FilterTypes = Partial<Record<string, string>>
71
+
72
+ export function toProductListParams(params: ProductFilterParams): ProductListParams {
73
+ const { sort, dir, filters, ...rest } = params
74
+
75
+ const newFilers = Object.fromEntries(
76
+ Object.entries(filters).filter(([, value]) => {
77
+ if (isFilterTypeEqual(value)) return Boolean(value.in)
78
+ if (isFilterTypeMatch(value)) return Boolean(value.match)
79
+ if (isFilterTypeRange(value)) return Boolean(value.from || value.to)
80
+ return false
81
+ }),
82
+ )
83
+ return { sort: sort ? { [sort]: dir } : {}, filters: newFilers, ...rest }
84
+ }