@graphcommerce/magento-product 3.8.13 → 4.0.2

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.
@@ -3,95 +3,37 @@ import {
3
3
  iconChevronLeft,
4
4
  iconChevronRight,
5
5
  responsiveVal,
6
- SvgImageSimple,
6
+ SvgIcon,
7
7
  useScrollY,
8
- UseStyles,
8
+ extendableComponent,
9
9
  } from '@graphcommerce/next-ui'
10
- import { makeStyles, Theme } from '@material-ui/core'
11
- import clsx from 'clsx'
10
+ import { Box, styled, SxProps, Theme } from '@mui/material'
12
11
  import { m, useTransform } from 'framer-motion'
13
- import React, { PropsWithChildren, useEffect, useRef, useState } from 'react'
12
+ import React, { useEffect, useRef, useState } from 'react'
14
13
 
15
- const useStyles = makeStyles(
16
- (theme: Theme) => ({
17
- wrapper: {
18
- display: 'flex',
19
- justifyContent: 'center',
20
- height: responsiveVal(44, 52),
21
- marginBottom: theme.spacings.sm,
22
- position: 'sticky',
23
- top: theme.page.vertical,
24
- zIndex: 9,
25
- margin: '0 auto',
26
- maxWidth: `calc(100% - 96px - ${theme.spacings.sm} * 2)`,
27
- [theme.breakpoints.down('sm')]: {
28
- textAlign: 'center',
29
- maxWidth: 'unset',
30
- margin: `0 calc(${theme.page.horizontal} * -1)`,
31
- },
32
- [theme.breakpoints.down('xs')]: {
33
- textAlign: 'center',
34
- },
35
- },
36
- container: {
37
- position: 'relative',
38
- maxWidth: '100%',
39
- padding: 6,
40
- paddingLeft: 0,
41
- paddingRight: 0,
42
- [theme.breakpoints.up('md')]: {
43
- background: theme.palette.background.default,
44
- borderRadius: '99em',
45
- },
46
- },
47
- shadow: {
48
- pointerEvents: 'none',
49
- zindex: '-1',
50
- borderRadius: '99em',
51
- position: 'absolute',
52
- height: '100%',
53
- width: '100%',
54
- top: 0,
55
- boxShadow: theme.shadows[6],
56
- [theme.breakpoints.down('sm')]: {
57
- boxShadow: 'none !important',
58
- },
59
- },
60
- containerSticky: {},
61
- scroller: {
62
- paddingLeft: theme.page.horizontal,
63
- paddingRight: theme.page.horizontal,
64
- paddingBottom: 1,
65
- [theme.breakpoints.up('md')]: {
66
- borderRadius: '99em',
67
- paddingLeft: 6,
68
- paddingRight: 6,
69
- },
70
- columnGap: 6,
71
- gridAutoColumns: 'min-content',
72
- },
73
- scrollerSticky: {},
74
- sliderPrev: {
75
- position: 'absolute',
76
- top: 2,
77
- left: 2,
78
- zIndex: 10,
79
- },
80
- sliderNext: {
81
- position: 'absolute',
82
- top: 2,
83
- right: 2,
84
- zIndex: 10,
85
- },
86
- }),
87
- { name: 'ProductListFiltersContainer' },
88
- )
14
+ const MotionDiv = styled(m.div)({})
89
15
 
90
- export type ProductListFiltersContainerProps = PropsWithChildren<UseStyles<typeof useStyles>>
16
+ export type ProductListFiltersContainerProps = { children: React.ReactNode; sx?: SxProps<Theme> }
17
+
18
+ type OwnerState = {
19
+ isSticky: boolean
20
+ }
21
+ const name = 'ProductListFiltersContainer' as const
22
+ const parts = [
23
+ 'wrapper',
24
+ 'container',
25
+ 'shadow',
26
+ 'containerSticky',
27
+ 'scroller',
28
+ 'scrollerSticky',
29
+ 'sliderPrev',
30
+ 'sliderNext',
31
+ ] as const
32
+
33
+ const { withState } = extendableComponent<OwnerState, typeof name, typeof parts>(name, parts)
91
34
 
92
35
  export default function ProductListFiltersContainer(props: ProductListFiltersContainerProps) {
93
36
  const { children } = props
94
- const classes = useStyles(props)
95
37
  const scrollY = useScrollY()
96
38
 
97
39
  const [isSticky, setIsSticky] = useState<boolean>(false)
@@ -101,6 +43,7 @@ export default function ProductListFiltersContainer(props: ProductListFiltersCon
101
43
  const scrollHalfway = startPosition + spacing
102
44
 
103
45
  const wrapperRef = useRef<HTMLDivElement>(null)
46
+ const classes = withState({ isSticky })
104
47
 
105
48
  // Measure the sizing of the wrapping container
106
49
  useEffect(() => {
@@ -137,24 +80,94 @@ export default function ProductListFiltersContainer(props: ProductListFiltersCon
137
80
  const opacity = useTransform(scrollY, [startPosition, startPosition + spacing], [0, 1])
138
81
 
139
82
  return (
140
- <m.div className={classes.wrapper} ref={wrapperRef}>
83
+ <MotionDiv
84
+ className={classes.wrapper}
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
+ })}
102
+ >
141
103
  <ScrollerProvider scrollSnapAlign='none'>
142
- <ScrollerButton direction='left' className={classes.sliderPrev}>
143
- <SvgImageSimple src={iconChevronLeft} />
104
+ <ScrollerButton
105
+ direction='left'
106
+ className={classes.sliderPrev}
107
+ size='small'
108
+ sx={{ position: 'absolute', top: 2, left: 2, zIndex: 10 }}
109
+ >
110
+ <SvgIcon src={iconChevronLeft} />
144
111
  </ScrollerButton>
145
- <div className={clsx(classes.container, isSticky && classes.containerSticky)}>
112
+ <Box
113
+ className={classes.container}
114
+ sx={(theme) => ({
115
+ position: 'relative',
116
+ maxWidth: '100%',
117
+ padding: '6px',
118
+ paddingLeft: 0,
119
+ paddingRight: 0,
120
+ [theme.breakpoints.up('md')]: {
121
+ background: theme.palette.background.default,
122
+ borderRadius: '99em',
123
+ },
124
+ })}
125
+ >
146
126
  <Scroller
147
- className={clsx(classes.scroller, isSticky && classes.scrollerSticky)}
127
+ className={classes.scroller}
148
128
  hideScrollbar
129
+ sx={(theme) => ({
130
+ paddingLeft: theme.page.horizontal,
131
+ paddingRight: theme.page.horizontal,
132
+ paddingBottom: '1px',
133
+ [theme.breakpoints.up('md')]: {
134
+ borderRadius: '99em',
135
+ paddingLeft: '6px',
136
+ paddingRight: '6px',
137
+ },
138
+ columnGap: '6px',
139
+ gridAutoColumns: 'min-content',
140
+ })}
149
141
  >
150
142
  {children}
151
143
  </Scroller>
152
- <m.div className={classes.shadow} style={{ opacity }} />
153
- </div>
154
- <ScrollerButton direction='right' className={classes.sliderNext}>
155
- <SvgImageSimple src={iconChevronRight} />
144
+ <MotionDiv
145
+ className={classes.shadow}
146
+ style={{ opacity }}
147
+ sx={(theme) => ({
148
+ pointerEvents: 'none',
149
+ zindex: '-1',
150
+ borderRadius: '99em',
151
+ position: 'absolute',
152
+ height: '100%',
153
+ width: '100%',
154
+ top: 0,
155
+ boxShadow: theme.shadows[6],
156
+ [theme.breakpoints.down('md')]: {
157
+ boxShadow: 'none !important',
158
+ },
159
+ })}
160
+ />
161
+ </Box>
162
+ <ScrollerButton
163
+ direction='right'
164
+ className={classes.sliderNext}
165
+ size='small'
166
+ sx={{ position: 'absolute', top: 2, right: 2, zIndex: 10 }}
167
+ >
168
+ <SvgIcon src={iconChevronRight} />
156
169
  </ScrollerButton>
157
170
  </ScrollerProvider>
158
- </m.div>
171
+ </MotionDiv>
159
172
  )
160
173
  }
@@ -1,142 +1,51 @@
1
1
  import { Image, ImageProps } from '@graphcommerce/image'
2
- import { responsiveVal, UseStyles } from '@graphcommerce/next-ui'
3
- import { ButtonBase, makeStyles, Theme, Typography } from '@material-ui/core'
4
- import clsx from 'clsx'
2
+ import { responsiveVal, extendableComponent } from '@graphcommerce/next-ui'
3
+ import { Trans } from '@lingui/macro'
4
+ import { ButtonBase, Typography, Box, styled, SxProps, Theme } from '@mui/material'
5
5
  import PageLink from 'next/link'
6
6
  import { useRouter } from 'next/router'
7
- import React, { PropsWithChildren, useCallback } from 'react'
7
+ import React, { PropsWithChildren, useMemo } from 'react'
8
8
  import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
9
9
  import { useProductLink } from '../../hooks/useProductLink'
10
10
  import ProductListPrice from '../ProductListPrice'
11
11
 
12
- export const useProductListItemStyles = makeStyles(
13
- (theme: Theme) => ({
14
- buttonBase: {
15
- display: 'block',
16
- },
17
- item: {
18
- position: 'relative',
19
- height: '100%',
20
- },
21
- title: {
22
- display: 'inline',
23
- color: theme.palette.text.primary,
24
- overflowWrap: 'break-word',
25
- wordBreak: 'break-all',
26
- maxWidth: '100%',
27
- marginRight: responsiveVal(3, 5),
28
- gridArea: 'title',
29
- fontWeight: theme.typography.fontWeightBold,
30
- },
31
- itemTitleContainer: {
32
- display: 'grid',
33
- gridTemplateColumns: 'unset',
34
- alignItems: 'baseline',
35
- marginTop: theme.spacings.xs,
36
- columnGap: 4,
37
- gridTemplateAreas: `
38
- "title title"
39
- "subtitle price"
40
- `,
41
- justifyContent: 'space-between',
42
- [theme.breakpoints.up('md')]: {
43
- gridTemplateAreas: `"title subtitle price"`,
44
- gridTemplateColumns: 'auto auto 1fr',
45
- },
46
- },
47
- subtitle: {
48
- gridArea: 'subtitle',
49
- },
50
- price: {
51
- gridArea: 'price',
52
- textAlign: 'right',
53
- [theme.breakpoints.up('sm')]: {
54
- justifySelf: 'flex-end',
55
- },
56
- },
57
- overlayItems: {
58
- display: 'grid',
59
- gridTemplateAreas: `
60
- "topLeft topRight"
61
- "bottomLeft bottomRight"
62
- `,
63
- position: 'absolute',
64
- top: 0,
65
- width: '100%',
66
- height: '100%',
67
- gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
68
- gridTemplateRows: 'repeat(2, minmax(0, 1fr))',
69
- padding: responsiveVal(8, 12),
70
- color: theme.palette.text.primary,
71
- },
72
- cellAlignRight: {
73
- justifySelf: 'end',
74
- textAlign: 'right',
75
- },
76
- cellAlignBottom: {
77
- alignSelf: 'flex-end',
78
- },
79
- overlayItem: {
80
- '& div': {
81
- gap: 0,
82
- // whiteSpace: 'nowrap',
83
- },
84
- },
85
- imageContainer: ({ aspectRatio = [4, 3] }: BaseProps) => ({
86
- display: 'block',
87
- height: 0, // https://stackoverflow.com/questions/44770074/css-grid-row-height-safari-bug
88
- position: 'relative',
89
- paddingTop: `calc(100% / ${aspectRatio[0]} * ${aspectRatio[1]})`,
90
- background: theme.palette.background.image, // theme specific,
91
- borderRadius: responsiveVal(theme.shape.borderRadius * 2, theme.shape.borderRadius * 3),
92
- }),
93
- placeholder: {
94
- display: 'flex',
95
- textAlign: 'center',
96
- height: '100%',
97
- justifyContent: 'center',
98
- alignItems: 'center',
99
- color: theme.palette.background.default,
100
- fontWeight: 600,
101
- userSelect: 'none',
102
- },
103
- image: {
104
- objectFit: 'contain',
105
- position: 'absolute',
106
- top: 0,
107
- left: 0,
108
- },
109
- link: {
110
- textDecoration: 'underline',
111
- },
112
- discount: {
113
- ...theme.typography.caption,
114
- background: theme.palette.text.primary,
115
- fontWeight: theme.typography.fontWeightBold,
116
- border: `1px solid ${theme.palette.divider}`,
117
- padding: '0px 6px',
118
- color: theme.palette.background.default,
119
- display: 'inline-block',
120
- },
121
- }),
122
- { name: 'ProductListItem' },
123
- )
12
+ const { classes, selectors } = extendableComponent('ProductListItem', [
13
+ 'root',
14
+ 'item',
15
+ 'title',
16
+ 'titleContainer',
17
+ 'subtitle',
18
+ 'price',
19
+ 'overlayItems',
20
+ 'topLeft',
21
+ 'topRight',
22
+ 'bottomLeft',
23
+ 'bottomRight',
24
+ 'imageContainer',
25
+ 'placeholder',
26
+ 'image',
27
+ 'discount',
28
+ ] as const)
124
29
 
125
30
  export type OverlayAreaKeys = 'topLeft' | 'bottomLeft' | 'topRight' | 'bottomRight'
126
31
 
127
32
  export type OverlayAreas = Partial<Record<OverlayAreaKeys, React.ReactNode>>
128
33
 
34
+ type StyleProps = {
35
+ aspectRatio?: [number, number]
36
+ imageOnly?: boolean
37
+ }
38
+
129
39
  type BaseProps = PropsWithChildren<
130
- {
131
- subTitle?: React.ReactNode
132
- aspectRatio?: [number, number]
133
- imageOnly?: boolean
134
- } & OverlayAreas &
40
+ { subTitle?: React.ReactNode } & StyleProps &
41
+ OverlayAreas &
135
42
  ProductListItemFragment &
136
43
  Pick<ImageProps, 'loading' | 'sizes' | 'dontReportWronglySizedImages'>
137
44
  >
138
45
 
139
- export type ProductListItemProps = BaseProps & UseStyles<typeof useProductListItemStyles>
46
+ export type ProductListItemProps = BaseProps & { sx?: SxProps<Theme> }
47
+
48
+ const StyledImage = styled(Image)({})
140
49
 
141
50
  export default function ProductListItem(props: ProductListItemProps) {
142
51
  const {
@@ -153,61 +62,190 @@ export default function ProductListItem(props: ProductListItemProps) {
153
62
  loading,
154
63
  sizes,
155
64
  dontReportWronglySizedImages,
65
+ aspectRatio = [4, 3],
66
+ sx = [],
156
67
  } = props
157
- const classes = useProductListItemStyles(props)
68
+
158
69
  const productLink = useProductLink(props)
159
70
  const discount = Math.floor(price_range.minimum_price.discount?.percent_off ?? 0)
160
71
  const { locale } = useRouter()
161
72
 
162
- // eslint-disable-next-line react-hooks/exhaustive-deps
163
- const format = useCallback(
164
- new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 1 }).format,
165
- [],
73
+ const formatter = useMemo(
74
+ () => new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 1 }),
75
+ [locale],
166
76
  )
167
77
 
168
78
  return (
169
79
  <PageLink href={productLink} passHref>
170
- <ButtonBase classes={{ root: clsx(classes.buttonBase, classes.item) }} component='a'>
171
- <div className={classes.imageContainer}>
80
+ <ButtonBase
81
+ component='a'
82
+ sx={[
83
+ (theme) => ({
84
+ display: 'block',
85
+ position: 'relative',
86
+ height: '100%',
87
+ borderRadius: responsiveVal(theme.shape.borderRadius * 2, theme.shape.borderRadius * 3),
88
+ }),
89
+ ...(Array.isArray(sx) ? sx : [sx]),
90
+ ]}
91
+ className={classes.root}
92
+ >
93
+ <Box
94
+ sx={(theme) => ({
95
+ display: 'grid',
96
+ bgcolor: 'background.image',
97
+ borderRadius: responsiveVal(theme.shape.borderRadius * 2, theme.shape.borderRadius * 3),
98
+ padding: responsiveVal(8, 12),
99
+ '& > picture': {
100
+ gridArea: `1 / 1 / 3 / 3`,
101
+ margin: `calc(${responsiveVal(8, 12)} * -1)`,
102
+ },
103
+ })}
104
+ className={classes.imageContainer}
105
+ >
172
106
  {small_image ? (
173
- <Image
107
+ <StyledImage
174
108
  layout='fill'
109
+ width={1}
110
+ height={1}
175
111
  sizes={sizes}
176
112
  dontReportWronglySizedImages={dontReportWronglySizedImages}
177
113
  src={small_image.url ?? ''}
178
114
  alt={small_image.label ?? ''}
179
115
  className={classes.image}
180
116
  loading={loading}
117
+ sx={{ objectFit: 'contain', aspectRatio: `${aspectRatio[0] / aspectRatio[1]}` }}
181
118
  />
182
119
  ) : (
183
- <div className={clsx(classes.placeholder, classes.image)}>NO IMAGE</div>
120
+ <Box
121
+ sx={{
122
+ gridArea: `1 / 1 / 3 / 3`,
123
+ typography: 'caption',
124
+ display: 'flex',
125
+ textAlign: 'center',
126
+ height: '100%',
127
+ justifyContent: 'center',
128
+ alignItems: 'center',
129
+ color: 'background.default',
130
+ userSelect: 'none',
131
+ }}
132
+ className={`${classes.placeholder} ${classes.image}`}
133
+ >
134
+ <Trans>No Image</Trans>
135
+ </Box>
184
136
  )}
185
137
 
186
138
  {!imageOnly && (
187
- <div className={classes.overlayItems}>
188
- <div className={classes.overlayItem}>
189
- {discount > 0 && <div className={classes.discount}>{format(discount / -100)}</div>}
139
+ <>
140
+ <Box
141
+ sx={{
142
+ gridArea: `1 / 1 / 2 / 2`,
143
+ zIndex: 1,
144
+ }}
145
+ className={classes.topLeft}
146
+ >
147
+ {discount > 0 && (
148
+ <Box
149
+ className={classes.discount}
150
+ sx={{
151
+ typography: 'caption',
152
+ bgcolor: 'text.primary',
153
+ fontWeight: 'fontWeightBold',
154
+ border: 1,
155
+ borderColor: 'divider',
156
+ padding: '0px 6px',
157
+ color: 'background.default',
158
+ display: 'inline-block',
159
+ }}
160
+ >
161
+ {formatter.format(discount / -100)}
162
+ </Box>
163
+ )}
190
164
  {topLeft}
191
- </div>
192
- <div className={clsx(classes.overlayItem, classes.cellAlignRight)}>{topRight}</div>
193
- <div className={clsx(classes.overlayItem, classes.cellAlignBottom)}>{bottomLeft}</div>
194
- <div className={clsx(classes.cellAlignBottom, classes.cellAlignRight)}>
165
+ </Box>
166
+ <Box
167
+ sx={{
168
+ justifySelf: 'end',
169
+ textAlign: 'right',
170
+ gridArea: `1 / 2 / 2 / 3`,
171
+ zIndex: 1,
172
+ }}
173
+ className={classes.topLeft}
174
+ >
175
+ {topRight}
176
+ </Box>
177
+ <Box
178
+ sx={{
179
+ alignSelf: 'flex-end',
180
+ gridArea: `2 / 1 / 3 / 2`,
181
+ zIndex: 1,
182
+ }}
183
+ className={classes.bottomLeft}
184
+ >
185
+ {bottomLeft}
186
+ </Box>
187
+ <Box
188
+ sx={{
189
+ textAlign: 'right',
190
+ alignSelf: 'flex-end',
191
+ gridArea: `2 / 2 / 3 / 3`,
192
+ zIndex: 1,
193
+ justifySelf: 'end',
194
+ }}
195
+ className={classes.bottomRight}
196
+ >
195
197
  {bottomRight}
196
- </div>
197
- </div>
198
+ </Box>
199
+ </>
198
200
  )}
199
- </div>
201
+ </Box>
200
202
 
201
203
  {!imageOnly && (
202
204
  <>
203
- <div className={classes.itemTitleContainer}>
204
- <Typography component='h2' variant='subtitle1' className={classes.title}>
205
+ <Box
206
+ className={classes.titleContainer}
207
+ sx={(theme) => ({
208
+ display: 'grid',
209
+ alignItems: 'baseline',
210
+ marginTop: theme.spacings.xs,
211
+ columnGap: 1,
212
+ gridTemplateAreas: {
213
+ xs: `"title title" "subtitle price"`,
214
+ md: `"title subtitle price"`,
215
+ },
216
+ gridTemplateColumns: { xs: 'unset', md: 'auto auto 1fr' },
217
+ justifyContent: 'space-between',
218
+ })}
219
+ >
220
+ <Typography
221
+ component='h2'
222
+ variant='subtitle1'
223
+ sx={{
224
+ display: 'inline',
225
+ color: 'text.primary',
226
+ overflowWrap: 'break-word',
227
+ wordBreak: 'break-all',
228
+ maxWidth: '100%',
229
+ gridArea: 'title',
230
+ fontWeight: 'fontWeightBold',
231
+ }}
232
+ className={classes.title}
233
+ >
205
234
  {name}
206
235
  </Typography>
207
- <div className={classes.subtitle}>{subTitle}</div>
236
+ <Box sx={{ gridArea: 'subtitle' }} className={classes.subtitle}>
237
+ {subTitle}
238
+ </Box>
208
239
 
209
- <ProductListPrice {...price_range.minimum_price} classes={{ root: classes.price }} />
210
- </div>
240
+ <ProductListPrice
241
+ {...price_range.minimum_price}
242
+ sx={{
243
+ gridArea: 'price',
244
+ textAlign: 'right',
245
+ justifySelf: { sm: 'flex-end' },
246
+ }}
247
+ />
248
+ </Box>
211
249
  {children}
212
250
  </>
213
251
  )}
@@ -215,3 +253,5 @@ export default function ProductListItem(props: ProductListItemProps) {
215
253
  </PageLink>
216
254
  )
217
255
  }
256
+
257
+ ProductListItem.selectors = { ...selectors, ...ProductListPrice.selectors }