@graphcommerce/magento-product 3.0.1

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 (117) hide show
  1. package/Api/ProductListItem.gql.ts +16 -0
  2. package/Api/ProductListItem.graphql +18 -0
  3. package/Api/ProductPageItem.gql.ts +16 -0
  4. package/Api/ProductPageItem.graphql +11 -0
  5. package/CHANGELOG.md +280 -0
  6. package/_playwright/productURL.fixture.ts +28 -0
  7. package/components/JsonLdProduct/JsonLdProduct.gql.ts +16 -0
  8. package/components/JsonLdProduct/JsonLdProduct.graphql +22 -0
  9. package/components/JsonLdProduct/JsonLdProductOffer.gql.ts +16 -0
  10. package/components/JsonLdProduct/JsonLdProductOffer.graphql +13 -0
  11. package/components/JsonLdProduct/index.tsx +37 -0
  12. package/components/ProductAddToCart/ProductAddToCart.gql.ts +15 -0
  13. package/components/ProductAddToCart/ProductAddToCart.graphql +21 -0
  14. package/components/ProductCustomizable/CustomizableAreaOption.gql.ts +4 -0
  15. package/components/ProductCustomizable/CustomizableAreaOption.graphql +11 -0
  16. package/components/ProductCustomizable/CustomizableCheckboxOption.gql.ts +4 -0
  17. package/components/ProductCustomizable/CustomizableCheckboxOption.graphql +11 -0
  18. package/components/ProductCustomizable/CustomizableDateOption.gql.ts +4 -0
  19. package/components/ProductCustomizable/CustomizableDateOption.graphql +10 -0
  20. package/components/ProductCustomizable/CustomizableDropDownOption.gql.ts +4 -0
  21. package/components/ProductCustomizable/CustomizableDropDownOption.graphql +12 -0
  22. package/components/ProductCustomizable/CustomizableFieldOption.gql.ts +4 -0
  23. package/components/ProductCustomizable/CustomizableFieldOption.graphql +11 -0
  24. package/components/ProductCustomizable/CustomizableFileOption.gql.ts +4 -0
  25. package/components/ProductCustomizable/CustomizableFileOption.graphql +13 -0
  26. package/components/ProductCustomizable/CustomizableMultipleOption.gql.ts +4 -0
  27. package/components/ProductCustomizable/CustomizableMultipleOption.graphql +12 -0
  28. package/components/ProductCustomizable/CustomizableOption.gql.ts +20 -0
  29. package/components/ProductCustomizable/CustomizableOption.graphql +6 -0
  30. package/components/ProductCustomizable/CustomizableRadioOption.gql.ts +4 -0
  31. package/components/ProductCustomizable/CustomizableRadioOption.graphql +12 -0
  32. package/components/ProductCustomizable/ProductCustomizable.gql.ts +14 -0
  33. package/components/ProductCustomizable/ProductCustomizable.graphql +13 -0
  34. package/components/ProductList/ProductList.gql.ts +17 -0
  35. package/components/ProductList/ProductList.graphql +11 -0
  36. package/components/ProductList/ProductListQueryFragment.gql.ts +4 -0
  37. package/components/ProductList/ProductListQueryFragment.graphql +14 -0
  38. package/components/ProductListCount/ProductListCount.gql.ts +4 -0
  39. package/components/ProductListCount/ProductListCount.graphql +3 -0
  40. package/components/ProductListCount/index.tsx +55 -0
  41. package/components/ProductListFilters/FilterCheckboxType.tsx +66 -0
  42. package/components/ProductListFilters/FilterEqualType.tsx +169 -0
  43. package/components/ProductListFilters/FilterRangeType.tsx +170 -0
  44. package/components/ProductListFilters/ProductFiltersQueryFragment.gql.ts +4 -0
  45. package/components/ProductListFilters/ProductFiltersQueryFragment.graphql +5 -0
  46. package/components/ProductListFilters/ProductListFilters.gql.ts +4 -0
  47. package/components/ProductListFilters/ProductListFilters.graphql +12 -0
  48. package/components/ProductListFilters/index.tsx +61 -0
  49. package/components/ProductListFiltersContainer/index.tsx +140 -0
  50. package/components/ProductListItem/index.tsx +223 -0
  51. package/components/ProductListItems/ProductListItems.gql.ts +4 -0
  52. package/components/ProductListItems/ProductListItems.graphql +7 -0
  53. package/components/ProductListItems/ProductListItemsBase.tsx +60 -0
  54. package/components/ProductListItems/ProductListParamsProvider.tsx +20 -0
  55. package/components/ProductListItems/filterTypes.tsx +45 -0
  56. package/components/ProductListItems/filteredProductList.tsx +74 -0
  57. package/components/ProductListItems/getFilterTypes.ts +51 -0
  58. package/components/ProductListItems/index.tsx +6 -0
  59. package/components/ProductListItems/renderer.tsx +16 -0
  60. package/components/ProductListLink/ProductListLink.tsx +50 -0
  61. package/components/ProductListPagination/ProductListPagination.gql.ts +4 -0
  62. package/components/ProductListPagination/ProductListPagination.graphql +6 -0
  63. package/components/ProductListPagination/index.tsx +31 -0
  64. package/components/ProductListPrice/ProductListPrice.gql.ts +4 -0
  65. package/components/ProductListPrice/ProductListPrice.graphql +12 -0
  66. package/components/ProductListPrice/index.tsx +36 -0
  67. package/components/ProductListSort/ProductListSort.gql.ts +4 -0
  68. package/components/ProductListSort/ProductListSort.graphql +10 -0
  69. package/components/ProductListSort/index.tsx +72 -0
  70. package/components/ProductPage/ProductPageQueryFragment.gql.ts +4 -0
  71. package/components/ProductPage/ProductPageQueryFragment.graphql +10 -0
  72. package/components/ProductPageCategory/ProductPageCategory.gql.ts +16 -0
  73. package/components/ProductPageCategory/ProductPageCategory.graphql +8 -0
  74. package/components/ProductPageCategory/index.ts +17 -0
  75. package/components/ProductPageDescription/ProductPageDescription.gql.ts +16 -0
  76. package/components/ProductPageDescription/ProductPageDescription.graphql +9 -0
  77. package/components/ProductPageDescription/index.tsx +54 -0
  78. package/components/ProductPageGallery/ProductImage.gql.ts +4 -0
  79. package/components/ProductPageGallery/ProductImage.graphql +4 -0
  80. package/components/ProductPageGallery/ProductImage.tsx +10 -0
  81. package/components/ProductPageGallery/ProductPageGallery.gql.ts +16 -0
  82. package/components/ProductPageGallery/ProductPageGallery.graphql +10 -0
  83. package/components/ProductPageGallery/ProductVideo.gql.ts +4 -0
  84. package/components/ProductPageGallery/ProductVideo.graphql +10 -0
  85. package/components/ProductPageGallery/ProductVideo.tsx +11 -0
  86. package/components/ProductPageGallery/index.tsx +37 -0
  87. package/components/ProductPageMeta/ProductPageMeta.gql.ts +16 -0
  88. package/components/ProductPageMeta/ProductPageMeta.graphql +7 -0
  89. package/components/ProductPageMeta/index.tsx +17 -0
  90. package/components/ProductPagePrice/ProductPagePrice.gql.ts +16 -0
  91. package/components/ProductPagePrice/ProductPagePrice.graphql +50 -0
  92. package/components/ProductRelated/RelatedProducts.gql.ts +16 -0
  93. package/components/ProductRelated/RelatedProducts.graphql +5 -0
  94. package/components/ProductSidebarDelivery/index.tsx +47 -0
  95. package/components/ProductSpecs/ProductSpecs.gql.ts +4 -0
  96. package/components/ProductSpecs/ProductSpecs.graphql +12 -0
  97. package/components/ProductSpecs/index.tsx +52 -0
  98. package/components/ProductStaticPaths/ProductStaticPaths.gql.ts +13 -0
  99. package/components/ProductStaticPaths/ProductStaticPaths.graphql +12 -0
  100. package/components/ProductStaticPaths/getProductStaticPaths.ts +32 -0
  101. package/components/ProductUpsells/UpsellProducts.gql.ts +16 -0
  102. package/components/ProductUpsells/UpsellProducts.graphql +5 -0
  103. package/components/ProductWeight/ProductWeight.gql.ts +12 -0
  104. package/components/ProductWeight/ProductWeight.graphql +3 -0
  105. package/components/ProductWeight/index.tsx +30 -0
  106. package/components/index.ts +45 -0
  107. package/context/productListParamsContext.ts +7 -0
  108. package/hooks/ProductLink.gql.ts +16 -0
  109. package/hooks/ProductLink.graphql +5 -0
  110. package/hooks/useProductLink.ts +20 -0
  111. package/hooks/useProductListLink.ts +38 -0
  112. package/hooks/useProductListLinkPush.ts +22 -0
  113. package/hooks/useProductListParamsContext.ts +4 -0
  114. package/index.ts +9 -0
  115. package/next-env.d.ts +4 -0
  116. package/package.json +40 -0
  117. package/tsconfig.json +5 -0
@@ -0,0 +1,55 @@
1
+ import { makeStyles, Theme } from '@material-ui/core'
2
+ import { UseStyles, responsiveVal } from '@graphcommerce/next-ui'
3
+
4
+ import { ProductListCountFragment } from './ProductListCount.gql'
5
+
6
+ const useStyles = makeStyles(
7
+ (theme: Theme) => ({
8
+ productListCount: {
9
+ maxWidth: '100%',
10
+ width: responsiveVal(280, 650),
11
+ margin: '0 auto',
12
+ padding: theme.spacings.xs,
13
+ paddingTop: responsiveVal(24, 30),
14
+ paddingBottom: responsiveVal(4, 8),
15
+ position: 'relative',
16
+ textAlign: 'center',
17
+ gridArea: 'count',
18
+ marginBottom: theme.spacings.sm,
19
+ },
20
+ line: {
21
+ background: '#ededed',
22
+ width: '100%',
23
+ height: 1,
24
+ lineHeight: 20,
25
+ },
26
+ count: {
27
+ ...theme.typography.body2,
28
+ margin: '0 auto',
29
+ background: theme.palette.background.default,
30
+ display: 'inline-block',
31
+ padding: `0 ${theme.spacings.xs} 0 ${theme.spacings.xs}`,
32
+ color: theme.palette.primary.mutedText,
33
+ transform: 'translateY(calc(-50% - 1px))',
34
+ },
35
+ }),
36
+ {
37
+ name: 'ProductListCount',
38
+ },
39
+ )
40
+
41
+ export type ProductCountProps = ProductListCountFragment & UseStyles<typeof useStyles>
42
+
43
+ export default function ProductListCount(props: ProductCountProps) {
44
+ const { total_count } = props
45
+ const classes = useStyles(props)
46
+
47
+ return (
48
+ <div className={classes.productListCount}>
49
+ <div className={classes.line} />
50
+ <div className={classes.count}>
51
+ {total_count} product{(total_count ?? 0) > 1 ? 's' : ''}
52
+ </div>
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,66 @@
1
+ import { cloneDeep } from '@apollo/client/utilities'
2
+ import { Chip, ChipProps } from '@material-ui/core'
3
+ import { useChipMenuStyles, SvgImage, iconCloseCircle } from '@graphcommerce/next-ui'
4
+ import clsx from 'clsx'
5
+ import React from 'react'
6
+ import { useProductListLinkPush } from '../../hooks/useProductListLinkPush'
7
+ import { useProductListParamsContext } from '../../hooks/useProductListParamsContext'
8
+ import ProductListLink from '../ProductListLink/ProductListLink'
9
+ import { FilterIn } from './FilterEqualType'
10
+ import { ProductListFiltersFragment } from './ProductListFilters.gql'
11
+
12
+ export type FilterCheckboxTypeProps = NonNullable<
13
+ NonNullable<ProductListFiltersFragment['aggregations']>[0]
14
+ > &
15
+ Omit<ChipProps, 'selected'>
16
+
17
+ export default function FilterCheckboxType(props: FilterCheckboxTypeProps) {
18
+ const { attribute_code, count, label, options, ...chipProps } = props
19
+ const { params } = useProductListParamsContext()
20
+ const classes = useChipMenuStyles(props)
21
+ const currentFilter = params.filters[attribute_code]
22
+ const pushRoute = useProductListLinkPush({ scroll: false })
23
+
24
+ if (!options?.[0]) return null
25
+
26
+ const option = options?.[1]?.value === '1' ? options[1] : options[0]
27
+ const isActive = currentFilter?.in?.includes(option.value)
28
+
29
+ const filter = isActive ? {} : ({ in: [option.value] } as FilterIn)
30
+
31
+ return (
32
+ <ProductListLink
33
+ {...params}
34
+ filters={{ ...params.filters, [attribute_code]: filter }}
35
+ currentPage={undefined}
36
+ noLink
37
+ link={{ scroll: false }}
38
+ >
39
+ <Chip
40
+ variant='outlined'
41
+ color={isActive ? undefined : 'default'}
42
+ onDelete={
43
+ isActive
44
+ ? () => {
45
+ const linkParams = cloneDeep(params)
46
+
47
+ delete linkParams.currentPage
48
+ delete linkParams.filters[attribute_code]
49
+
50
+ pushRoute(linkParams)
51
+ }
52
+ : undefined
53
+ }
54
+ deleteIcon={
55
+ isActive ? (
56
+ <SvgImage src={iconCloseCircle} alt='remove' size='small' loading='eager' />
57
+ ) : undefined
58
+ }
59
+ label={label}
60
+ clickable
61
+ {...chipProps}
62
+ className={clsx(classes.chip, isActive && classes.chipSelected, chipProps.className)}
63
+ />
64
+ </ProductListLink>
65
+ )
66
+ }
@@ -0,0 +1,169 @@
1
+ import { cloneDeep } from '@apollo/client/utilities'
2
+ import {
3
+ Checkbox,
4
+ ListItem,
5
+ ListItemSecondaryAction,
6
+ ListItemText,
7
+ makeStyles,
8
+ Theme,
9
+ } from '@material-ui/core'
10
+ import { FilterEqualTypeInput } from '@graphcommerce/graphql'
11
+ import { ChipMenu, ChipMenuProps, responsiveVal } from '@graphcommerce/next-ui'
12
+ import React from 'react'
13
+ import { SetRequired } from 'type-fest'
14
+ import { useProductListLinkPush } from '../../hooks/useProductListLinkPush'
15
+ import { useProductListParamsContext } from '../../hooks/useProductListParamsContext'
16
+ import ProductListLink from '../ProductListLink/ProductListLink'
17
+ import { ProductListFiltersFragment } from './ProductListFilters.gql'
18
+
19
+ export type FilterIn = SetRequired<Omit<FilterEqualTypeInput, 'eq'>, 'in'>
20
+
21
+ type FilterEqualTypeProps = NonNullable<
22
+ NonNullable<ProductListFiltersFragment['aggregations']>[0]
23
+ > &
24
+ Omit<ChipMenuProps, 'selected'>
25
+
26
+ const useFilterEqualStyles = makeStyles(
27
+ (theme: Theme) => ({
28
+ listItem: {
29
+ padding: `${theme.spacings.xxs} ${theme.spacings.xxs} 0`,
30
+ display: 'block',
31
+ '&:not(:nth-last-of-type(-n+2)) > div': {
32
+ borderBottom: `1px solid ${theme.palette.divider}`,
33
+ },
34
+ },
35
+ listItemInnerContainer: {
36
+ width: '100%',
37
+ paddingTop: responsiveVal(0, 3),
38
+ paddingBottom: theme.spacings.xxs,
39
+ '& > div': {
40
+ display: 'inline-block',
41
+ [theme.breakpoints.down('sm')]: {
42
+ maxWidth: '72%',
43
+ },
44
+ },
45
+ },
46
+ checkbox: {
47
+ padding: 0,
48
+ margin: '3px 0 0 8px',
49
+ float: 'right',
50
+ },
51
+ linkContainer: {
52
+ display: 'grid',
53
+ gridTemplateColumns: 'repeat(2, 1fr)',
54
+ columnGap: responsiveVal(2, 16),
55
+ minWidth: 0,
56
+ [theme.breakpoints.down('sm')]: {
57
+ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
58
+ },
59
+ },
60
+ button: {
61
+ float: 'right',
62
+ marginTop: theme.spacings.xxs,
63
+ marginRight: theme.spacings.xxs,
64
+ textDecoration: 'none',
65
+ },
66
+ resetButton: {
67
+ background: theme.palette.grey['100'],
68
+ marginRight: theme.spacings.xxs,
69
+ },
70
+ filterAmount: {
71
+ color: theme.palette.grey[500],
72
+ marginLeft: 4,
73
+ fontSize: theme.typography.pxToRem(11),
74
+ display: 'inline',
75
+ },
76
+ filterLabel: {
77
+ display: 'inline',
78
+ overflow: 'hidden',
79
+ whiteSpace: 'break-spaces',
80
+ },
81
+ }),
82
+ { name: 'FilterEqual' },
83
+ )
84
+
85
+ export default function FilterEqualType(props: FilterEqualTypeProps) {
86
+ const { attribute_code, count, label, options, ...chipProps } = props
87
+ const { params } = useProductListParamsContext()
88
+ const classes = useFilterEqualStyles()
89
+ const pushRoute = useProductListLinkPush({ scroll: false })
90
+
91
+ const currentFilter: FilterEqualTypeInput = cloneDeep(params.filters[attribute_code]) ?? {
92
+ in: [],
93
+ }
94
+
95
+ const currentLabels =
96
+ options
97
+ ?.filter((option) => option && currentFilter.in?.includes(option.value))
98
+ .map((option) => option && option.label) ?? []
99
+
100
+ const removeFilter = () => {
101
+ const linkParams = cloneDeep(params)
102
+ delete linkParams.filters[attribute_code]
103
+ delete linkParams.currentPage
104
+ pushRoute(linkParams)
105
+ }
106
+
107
+ return (
108
+ <ChipMenu
109
+ variant='outlined'
110
+ {...chipProps}
111
+ label={label}
112
+ selected={currentLabels.length > 0}
113
+ selectedLabel={currentLabels.length > 0 ? currentLabels.join(', ') : undefined}
114
+ onDelete={currentLabels.length > 0 ? removeFilter : undefined}
115
+ >
116
+ <div className={classes.linkContainer}>
117
+ {options?.map((option) => {
118
+ if (!option?.value) return null
119
+ const labelId = `filter-equal-${attribute_code}-${option?.value}`
120
+ const filters = cloneDeep(params.filters)
121
+
122
+ if (currentFilter.in?.includes(option.value)) {
123
+ filters[attribute_code] = {
124
+ ...currentFilter,
125
+ in: currentFilter.in?.filter((v) => v !== option.value),
126
+ }
127
+ } else {
128
+ filters[attribute_code] = {
129
+ ...currentFilter,
130
+ in: [...(currentFilter?.in ?? []), option.value],
131
+ }
132
+ }
133
+
134
+ return (
135
+ <ProductListLink
136
+ {...params}
137
+ filters={filters}
138
+ currentPage={undefined}
139
+ key={option?.value}
140
+ color='inherit'
141
+ >
142
+ <ListItem dense className={classes.listItem}>
143
+ <div className={classes.listItemInnerContainer}>
144
+ <ListItemText
145
+ primary={option?.label}
146
+ classes={{ primary: classes.filterLabel, secondary: classes.filterAmount }}
147
+ secondary={`(${option?.count})`}
148
+ />
149
+ <ListItemSecondaryAction>
150
+ <Checkbox
151
+ edge='start'
152
+ checked={currentFilter.in?.includes(option?.value ?? '')}
153
+ tabIndex={-1}
154
+ size='small'
155
+ color='primary'
156
+ disableRipple
157
+ inputProps={{ 'aria-labelledby': labelId }}
158
+ className={classes.checkbox}
159
+ />
160
+ </ListItemSecondaryAction>
161
+ </div>
162
+ </ListItem>
163
+ </ProductListLink>
164
+ )
165
+ })}
166
+ </div>
167
+ </ChipMenu>
168
+ )
169
+ }
@@ -0,0 +1,170 @@
1
+ import { cloneDeep } from '@apollo/client/utilities'
2
+ import { makeStyles, Mark, Slider, Theme } from '@material-ui/core'
3
+ import { FilterRangeTypeInput } from '@graphcommerce/graphql'
4
+ import { Money } from '@graphcommerce/magento-store'
5
+ import { ChipMenu, ChipMenuProps } from '@graphcommerce/next-ui'
6
+ import React from 'react'
7
+ import { useProductListLinkPush } from '../../hooks/useProductListLinkPush'
8
+ import { useProductListParamsContext } from '../../hooks/useProductListParamsContext'
9
+ import { ProductListFiltersFragment } from './ProductListFilters.gql'
10
+
11
+ type FilterRangeTypeProps = NonNullable<
12
+ NonNullable<ProductListFiltersFragment['aggregations']>[0]
13
+ > &
14
+ Omit<ChipMenuProps, 'selected'>
15
+
16
+ const sliderThumbWidth = 28
17
+ const useFilterRangeType = makeStyles(
18
+ (theme: Theme) => ({
19
+ container: {
20
+ padding: `${theme.spacings.xxs} ${theme.spacings.xxs} !important`,
21
+ width: '100%',
22
+ },
23
+ slider: {
24
+ maxWidth: `calc(100% - ${sliderThumbWidth}px)`,
25
+ margin: `${theme.spacings.xxs} auto`,
26
+ display: 'block',
27
+ paddingBottom: 32,
28
+ '& .MuiSlider-rail': {
29
+ color: theme.palette.secondary.mutedText,
30
+ height: 4,
31
+ borderRadius: 10,
32
+ },
33
+ '& .MuiSlider-track': {
34
+ color: theme.palette.primary.main,
35
+ height: 4,
36
+ },
37
+ '& .MuiSlider-thumb': {
38
+ width: sliderThumbWidth,
39
+ height: sliderThumbWidth,
40
+ marginLeft: `-${sliderThumbWidth * 0.5}px`,
41
+ marginTop: `-${sliderThumbWidth * 0.5}px`,
42
+ background: theme.palette.background.default,
43
+ boxShadow: theme.shadows[4],
44
+ },
45
+ },
46
+ }),
47
+ { name: 'FilterRangeType' },
48
+ )
49
+
50
+ export default function FilterRangeType(props: FilterRangeTypeProps) {
51
+ const { attribute_code, label, options, ...chipProps } = props
52
+ const classes = useFilterRangeType(props)
53
+ const { params } = useProductListParamsContext()
54
+ const pushRoute = useProductListLinkPush({ scroll: false })
55
+
56
+ // eslint-disable-next-line no-case-declarations
57
+ const marks: { [index: number]: Mark } = {}
58
+ const paramValues = params.filters[attribute_code]
59
+
60
+ const [min, maxish] = options
61
+ ?.map((option) => {
62
+ let val = option?.value.replace('*_', '0_') ?? ''
63
+ val = val.replace('_*', '_0')
64
+ const [minVal, maxVal] = val.split('_').map((value) => Number(value))
65
+
66
+ marks[minVal] = { value: minVal, label: minVal }
67
+ marks[maxVal] = { value: maxVal, label: maxVal }
68
+ return [minVal, maxVal]
69
+ })
70
+ .reduce(([prevMin, prevMax], [curMin, curMax]) => [
71
+ Math.min(prevMin, curMin),
72
+ Math.max(curMax, prevMax),
73
+ ]) ?? [0, 0]
74
+
75
+ // eslint-disable-next-line no-case-declarations
76
+ const max = (maxish / (options?.length ?? 2 - 1)) * (options?.length ?? 1)
77
+ marks[max] = { value: max, label: max }
78
+
79
+ const [value, setValue] = React.useState<[number, number]>(
80
+ paramValues ? [Number(paramValues.from), Number(paramValues.to)] : [min, max],
81
+ )
82
+
83
+ const priceFilterUrl = cloneDeep(params)
84
+ delete priceFilterUrl.currentPage
85
+ priceFilterUrl.filters[attribute_code] = {
86
+ from: String(value[0]),
87
+ to: String(value[1]),
88
+ } as FilterRangeTypeInput
89
+
90
+ const resetFilter = () => {
91
+ const linkParams = cloneDeep(params)
92
+
93
+ delete linkParams.currentPage
94
+ delete linkParams.filters[attribute_code]
95
+
96
+ setValue([min, max])
97
+ pushRoute(linkParams)
98
+ }
99
+
100
+ const currentFilter = params.filters[attribute_code] as FilterRangeTypeInput | undefined
101
+
102
+ let currentLabel: React.ReactNode | undefined
103
+
104
+ if (currentFilter) {
105
+ const from = Number(currentFilter?.from ?? 0)
106
+ const to = Number(currentFilter?.to ?? 0)
107
+
108
+ if (from === min && to !== max)
109
+ currentLabel = (
110
+ <>
111
+ {'Below '} <Money round value={Number(currentFilter?.to)} />
112
+ </>
113
+ )
114
+
115
+ if (from !== min && to === max)
116
+ currentLabel = (
117
+ <>
118
+ {'Above '} <Money round value={Number(currentFilter?.from)} />
119
+ </>
120
+ )
121
+
122
+ if (from !== min && to !== max)
123
+ currentLabel = (
124
+ <>
125
+ <Money round value={Number(currentFilter?.from)} />
126
+ {' — '}
127
+ <Money round value={Number(currentFilter.to)} />
128
+ </>
129
+ )
130
+ }
131
+
132
+ return (
133
+ <ChipMenu
134
+ variant='outlined'
135
+ label={label}
136
+ selectedLabel={currentLabel}
137
+ selected={!!currentLabel}
138
+ {...chipProps}
139
+ onDelete={currentLabel ? resetFilter : undefined}
140
+ labelRight={
141
+ <>
142
+ <Money round value={value[0]} />
143
+ {' — '}
144
+ <Money round value={value[1]} />
145
+ </>
146
+ }
147
+ >
148
+ <div className={classes.container}>
149
+ <Slider
150
+ min={min}
151
+ max={max}
152
+ aria-labelledby='range-slider'
153
+ value={value}
154
+ onChange={(e, newValue) => {
155
+ setValue(Array.isArray(newValue) ? [newValue[0], newValue[1]] : [0, 0])
156
+ }}
157
+ onChangeCommitted={(e, newValue) => {
158
+ if (newValue[0] > min || newValue[1] < max) {
159
+ pushRoute({ ...priceFilterUrl })
160
+ } else {
161
+ resetFilter()
162
+ }
163
+ }}
164
+ valueLabelDisplay='off'
165
+ className={classes.slider}
166
+ />
167
+ </div>
168
+ </ChipMenu>
169
+ )
170
+ }
@@ -0,0 +1,4 @@
1
+ /* eslint-disable */
2
+ import * as Types from '@graphcommerce/graphql';
3
+
4
+ export type ProductFiltersQueryFragment = { filters?: Types.Maybe<{ aggregations?: Types.Maybe<Array<Types.Maybe<{ label?: Types.Maybe<string>, count?: Types.Maybe<number>, attribute_code: string, options?: Types.Maybe<Array<Types.Maybe<{ label?: Types.Maybe<string>, value: string, count?: Types.Maybe<number> }>>> }>>> }> };
@@ -0,0 +1,5 @@
1
+ fragment ProductFiltersQueryFragment on Query {
2
+ filters: products(filter: { category_uid: { eq: $categoryUid } }) {
3
+ ...ProductListFilters
4
+ }
5
+ }
@@ -0,0 +1,4 @@
1
+ /* eslint-disable */
2
+ import * as Types from '@graphcommerce/graphql';
3
+
4
+ export type ProductListFiltersFragment = { aggregations?: Types.Maybe<Array<Types.Maybe<{ label?: Types.Maybe<string>, count?: Types.Maybe<number>, attribute_code: string, options?: Types.Maybe<Array<Types.Maybe<{ label?: Types.Maybe<string>, value: string, count?: Types.Maybe<number> }>>> }>>> };
@@ -0,0 +1,12 @@
1
+ fragment ProductListFilters on Products {
2
+ aggregations {
3
+ label
4
+ count
5
+ attribute_code
6
+ options {
7
+ label
8
+ value
9
+ count
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,61 @@
1
+ import { ChipMenuProps } from '@graphcommerce/next-ui'
2
+ import React from 'react'
3
+ import { FilterTypes } from '../ProductListItems/filterTypes'
4
+ import FilterCheckboxType from './FilterCheckboxType'
5
+ import FilterEqualType from './FilterEqualType'
6
+ import FilterRangeType from './FilterRangeType'
7
+ import { ProductListFiltersFragment } from './ProductListFilters.gql'
8
+
9
+ export type ProductFiltersProps = ProductListFiltersFragment & {
10
+ filterTypes: FilterTypes
11
+ } & Omit<ChipMenuProps, 'selected' | 'selectedLabel' | 'children' | 'label' | 'onDelete'>
12
+
13
+ export default function ProductListFilters(props: ProductFiltersProps) {
14
+ const { aggregations, filterTypes, ...chipMenuProps } = props
15
+
16
+ return (
17
+ <>
18
+ {aggregations?.map((aggregation) => {
19
+ if (!aggregation?.attribute_code || aggregation?.attribute_code === 'category_id')
20
+ return null
21
+
22
+ switch (filterTypes[aggregation.attribute_code]) {
23
+ case 'FilterEqualTypeInput':
24
+ if (
25
+ aggregation.options?.[0]?.label === '0' ||
26
+ aggregation.options?.[1]?.label === '0' ||
27
+ aggregation.options?.[0]?.label === '1' ||
28
+ aggregation.options?.[1]?.label === '1'
29
+ ) {
30
+ return (
31
+ <FilterCheckboxType
32
+ key={aggregation.attribute_code}
33
+ {...aggregation}
34
+ {...chipMenuProps}
35
+ />
36
+ )
37
+ }
38
+
39
+ return (
40
+ <FilterEqualType
41
+ key={aggregation.attribute_code}
42
+ {...aggregation}
43
+ {...chipMenuProps}
44
+ />
45
+ )
46
+
47
+ case 'FilterRangeTypeInput':
48
+ return (
49
+ <FilterRangeType
50
+ key={aggregation.attribute_code}
51
+ {...aggregation}
52
+ {...chipMenuProps}
53
+ />
54
+ )
55
+ }
56
+ console.log('Filter not recognized', aggregation)
57
+ return null // `FilterMatchTypeInput not ${aggregation.attribute_code}`
58
+ })}
59
+ </>
60
+ )
61
+ }
@@ -0,0 +1,140 @@
1
+ import { makeStyles, Theme } from '@material-ui/core'
2
+ import { Scroller, ScrollerButton, ScrollerProvider } from '@graphcommerce/framer-scroller'
3
+ import {
4
+ iconChevronLeft,
5
+ iconChevronRight,
6
+ SvgImageSimple,
7
+ UseStyles,
8
+ } from '@graphcommerce/next-ui'
9
+ import clsx from 'clsx'
10
+ import { m, useMotionTemplate, useTransform, useViewportScroll } from 'framer-motion'
11
+ import React, { PropsWithChildren, useEffect, useRef, useState } from 'react'
12
+
13
+ const useStyles = makeStyles(
14
+ (theme: Theme) => ({
15
+ wrapper: {
16
+ display: 'flex',
17
+ justifyContent: 'center',
18
+ height: 44,
19
+ marginBottom: theme.spacings.sm,
20
+ position: 'sticky',
21
+ top: theme.page.vertical,
22
+ zIndex: 9,
23
+ margin: '0 auto',
24
+ maxWidth: `calc(100% - 96px - ${theme.spacings.sm} * 2)`,
25
+ [theme.breakpoints.down('sm')]: {
26
+ textAlign: 'center',
27
+ maxWidth: 'unset',
28
+ margin: `0 calc(${theme.page.horizontal} * -1)`,
29
+ },
30
+ [theme.breakpoints.down('xs')]: {
31
+ textAlign: 'center',
32
+ },
33
+ },
34
+ container: {
35
+ maxWidth: '100%',
36
+ padding: 6,
37
+ [theme.breakpoints.up('md')]: {
38
+ background: '#fff',
39
+ borderRadius: 22,
40
+ // padding: `0 3px`,
41
+ },
42
+ },
43
+ containerSticky: {},
44
+ scroller: {
45
+ display: 'grid',
46
+ gridAutoFlow: 'column',
47
+ borderRadius: 22,
48
+ columnGap: 6,
49
+ },
50
+ scrollerSticky: {},
51
+ sliderPrev: {
52
+ position: 'absolute',
53
+ top: 2,
54
+ left: 2,
55
+ zIndex: 10,
56
+ },
57
+ sliderNext: {
58
+ position: 'absolute',
59
+ top: 2,
60
+ right: 2,
61
+ zIndex: 10,
62
+ },
63
+ }),
64
+ { name: 'ProductListFiltersContainer' },
65
+ )
66
+
67
+ export type ProductListFiltersContainerProps = PropsWithChildren<UseStyles<typeof useStyles>>
68
+
69
+ export default function ProductListFiltersContainer(props: ProductListFiltersContainerProps) {
70
+ const { children } = props
71
+
72
+ const classes = useStyles(props)
73
+ const { scrollY } = useViewportScroll()
74
+ const [isSticky, setIsSticky] = useState<boolean>(false)
75
+ const [startPosition, setStartPosition] = useState(100)
76
+ const [spacing, setSpacing] = useState(20)
77
+ const scrollHalfway = startPosition + spacing
78
+
79
+ const wrapperRef = useRef<HTMLDivElement>(null)
80
+
81
+ // Measure the sizing of the wrapping container
82
+ useEffect(() => {
83
+ const observer = new ResizeObserver(([entry]) => {
84
+ if (window.scrollY > 100) return
85
+ const offset = wrapperRef.current?.offsetTop ?? 0
86
+ const elemHeigh = entry.contentRect.height
87
+ const nextOffset =
88
+ (wrapperRef.current?.parentElement?.nextElementSibling as HTMLElement | null)?.offsetTop ??
89
+ 0
90
+
91
+ setSpacing(nextOffset - elemHeigh - offset + 20)
92
+ setStartPosition(offset)
93
+ })
94
+ if (wrapperRef.current) observer.observe(wrapperRef.current)
95
+ return () => observer.disconnect()
96
+ }, [])
97
+
98
+ useEffect(() => {
99
+ const onCheckStickyChange = (v: number) => {
100
+ if (isSticky && v <= scrollHalfway) {
101
+ setIsSticky(false)
102
+ }
103
+ if (!isSticky && v > scrollHalfway) {
104
+ setIsSticky(true)
105
+ }
106
+ }
107
+ onCheckStickyChange(scrollY.get())
108
+ return scrollY.onChange(onCheckStickyChange)
109
+ }, [isSticky, scrollHalfway, scrollY])
110
+
111
+ const opacity = useTransform(scrollY, [startPosition, startPosition + spacing], [0, 0.08])
112
+ const opacity2 = useTransform(scrollY, [startPosition, startPosition + spacing], [0, 0.1])
113
+ const filter = useMotionTemplate`
114
+ drop-shadow(0 1px 4px rgba(0,0,0,${opacity}))
115
+ drop-shadow(0 4px 10px rgba(0,0,0,${opacity2}))`
116
+
117
+ return (
118
+ <m.div className={classes.wrapper} ref={wrapperRef}>
119
+ <ScrollerProvider scrollSnapAlign='none'>
120
+ <ScrollerButton direction='left' className={classes.sliderPrev}>
121
+ <SvgImageSimple src={iconChevronLeft} />
122
+ </ScrollerButton>
123
+ <m.div
124
+ className={clsx(classes.container, isSticky && classes.containerSticky)}
125
+ style={{ filter }}
126
+ >
127
+ <Scroller
128
+ className={clsx(classes.scroller, isSticky && classes.scrollerSticky)}
129
+ hideScrollbar
130
+ >
131
+ {children}
132
+ </Scroller>
133
+ </m.div>
134
+ <ScrollerButton direction='right' className={classes.sliderNext}>
135
+ <SvgImageSimple src={iconChevronRight} />
136
+ </ScrollerButton>
137
+ </ScrollerProvider>
138
+ </m.div>
139
+ )
140
+ }