@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,223 @@
1
+ import { Link as MuiLink, makeStyles, Theme, Typography } from '@material-ui/core'
2
+ import { Image, ImageProps } from '@graphcommerce/image'
3
+ import { UseStyles, responsiveVal } from '@graphcommerce/next-ui'
4
+ import clsx from 'clsx'
5
+ import PageLink from 'next/link'
6
+ import { useRouter } from 'next/router'
7
+ import React, { PropsWithChildren, useCallback } from 'react'
8
+ import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
9
+ import { useProductLink } from '../../hooks/useProductLink'
10
+ import ProductListPrice from '../ProductListPrice'
11
+
12
+ export const useProductListItemStyles = makeStyles(
13
+ (theme: Theme) => ({
14
+ item: {
15
+ position: 'relative',
16
+ height: '100%',
17
+ },
18
+ title: {
19
+ display: 'inline',
20
+ color: theme.palette.text.primary,
21
+ overflowWrap: 'break-word',
22
+ wordBreak: 'break-all',
23
+ maxWidth: '100%',
24
+ marginRight: responsiveVal(3, 5),
25
+ gridArea: 'title',
26
+ fontWeight: theme.typography.fontWeightBold,
27
+ },
28
+ itemTitleContainer: {
29
+ display: 'grid',
30
+ gridTemplateColumns: 'unset',
31
+ alignItems: 'baseline',
32
+ marginTop: theme.spacings.xs,
33
+ columnGap: 4,
34
+ gridTemplateAreas: `
35
+ "title title"
36
+ "subtitle price"
37
+ `,
38
+ justifyContent: 'space-between',
39
+ [theme.breakpoints.up('md')]: {
40
+ gridTemplateAreas: `"title subtitle price"`,
41
+ gridTemplateColumns: 'auto auto 1fr',
42
+ },
43
+ },
44
+ subtitle: {
45
+ gridArea: 'subtitle',
46
+ },
47
+ price: {
48
+ gridArea: 'price',
49
+ textAlign: 'right',
50
+ [theme.breakpoints.up('sm')]: {
51
+ justifySelf: 'flex-end',
52
+ },
53
+ },
54
+ overlayItems: {
55
+ display: 'grid',
56
+ gridTemplateAreas: `
57
+ "topLeft topRight"
58
+ "bottomLeft bottomRight"
59
+ `,
60
+ position: 'absolute',
61
+ top: 0,
62
+ width: '100%',
63
+ height: '100%',
64
+ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
65
+ gridTemplateRows: 'repeat(2, minmax(0, 1fr))',
66
+ padding: responsiveVal(8, 12),
67
+ color: theme.palette.text.primary,
68
+ },
69
+ cellAlignRight: {
70
+ justifySelf: 'end',
71
+ textAlign: 'right',
72
+ },
73
+ cellAlignBottom: {
74
+ alignSelf: 'flex-end',
75
+ },
76
+ overlayItem: {
77
+ '& div': {
78
+ gap: 0,
79
+ // whiteSpace: 'nowrap',
80
+ },
81
+ },
82
+ imageContainer: ({ aspectRatio = [4, 3] }: BaseProps) => ({
83
+ display: 'block',
84
+ height: 0, // https://stackoverflow.com/questions/44770074/css-grid-row-height-safari-bug
85
+ position: 'relative',
86
+ paddingTop: `calc(100% / ${aspectRatio[0]} * ${aspectRatio[1]})`,
87
+ background: theme.palette.background.highlight, // thema specifiek
88
+ borderRadius: 2,
89
+ }),
90
+ placeholder: {
91
+ display: 'flex',
92
+ textAlign: 'center',
93
+ height: '100%',
94
+ justifyContent: 'center',
95
+ alignItems: 'center',
96
+ color: theme.palette.background.default,
97
+ fontWeight: 600,
98
+ userSelect: 'none',
99
+ },
100
+ image: {
101
+ objectFit: 'contain',
102
+ position: 'absolute',
103
+ top: 0,
104
+ left: 0,
105
+ },
106
+ link: {
107
+ textDecoration: 'underline',
108
+ },
109
+ discount: {
110
+ ...theme.typography.caption,
111
+ background: theme.palette.text.primary,
112
+ fontWeight: theme.typography.fontWeightBold,
113
+ padding: '0px 6px',
114
+ color: '#fff',
115
+ display: 'inline-block',
116
+ borderRadius: 2,
117
+ },
118
+ }),
119
+ { name: 'ProductListItem' },
120
+ )
121
+
122
+ export type OverlayAreaKeys = 'topLeft' | 'bottomLeft' | 'topRight' | 'bottomRight'
123
+
124
+ export type OverlayAreas = Partial<Record<OverlayAreaKeys, React.ReactNode>>
125
+
126
+ type BaseProps = PropsWithChildren<
127
+ {
128
+ subTitle?: React.ReactNode
129
+ aspectRatio?: [number, number]
130
+ imageOnly?: boolean
131
+ } & OverlayAreas &
132
+ ProductListItemFragment &
133
+ Pick<ImageProps, 'loading' | 'sizes' | 'dontReportWronglySizedImages'>
134
+ >
135
+
136
+ export type ProductListItemProps = BaseProps & UseStyles<typeof useProductListItemStyles>
137
+
138
+ export default function ProductListItem(props: ProductListItemProps) {
139
+ const {
140
+ subTitle,
141
+ topLeft,
142
+ topRight,
143
+ bottomLeft,
144
+ bottomRight,
145
+ small_image,
146
+ name,
147
+ price_range,
148
+ children,
149
+ imageOnly = false,
150
+ loading,
151
+ sizes,
152
+ dontReportWronglySizedImages,
153
+ } = props
154
+ const classes = useProductListItemStyles(props)
155
+ const productLink = useProductLink(props)
156
+ const discount = Math.floor(price_range.minimum_price.discount?.percent_off ?? 0)
157
+ const { locale } = useRouter()
158
+
159
+ // eslint-disable-next-line react-hooks/exhaustive-deps
160
+ const format = useCallback(
161
+ new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 1 }).format,
162
+ [],
163
+ )
164
+
165
+ return (
166
+ <div className={classes.item}>
167
+ <PageLink href={productLink} passHref>
168
+ <MuiLink underline='none'>
169
+ <div className={classes.imageContainer}>
170
+ {small_image ? (
171
+ <Image
172
+ layout='fill'
173
+ sizes={sizes}
174
+ dontReportWronglySizedImages={dontReportWronglySizedImages}
175
+ src={small_image.url ?? ''}
176
+ alt={small_image.label ?? ''}
177
+ className={classes.image}
178
+ loading={loading}
179
+ />
180
+ ) : (
181
+ <div className={clsx(classes.placeholder, classes.image)}>GEEN AFBEELDING</div>
182
+ )}
183
+
184
+ {!imageOnly && (
185
+ <div className={classes.overlayItems}>
186
+ <div className={classes.overlayItem}>
187
+ {discount > 0 && (
188
+ <div className={classes.discount}>{format(discount / -100)}</div>
189
+ )}
190
+ {topLeft}
191
+ </div>
192
+ <div className={clsx(classes.overlayItem, classes.cellAlignRight)}>{topRight}</div>
193
+ <div className={clsx(classes.overlayItem, classes.cellAlignBottom)}>
194
+ {bottomLeft}
195
+ </div>
196
+ <div className={clsx(classes.cellAlignBottom, classes.cellAlignRight)}>
197
+ {bottomRight}
198
+ </div>
199
+ </div>
200
+ )}
201
+ </div>
202
+ </MuiLink>
203
+ </PageLink>
204
+
205
+ {!imageOnly && (
206
+ <>
207
+ <div className={classes.itemTitleContainer}>
208
+ {/* <div> */}
209
+ <Typography component='h2' variant='subtitle1' className={classes.title}>
210
+ {name}
211
+ </Typography>
212
+ {/* </div> */}
213
+ <div className={classes.subtitle}>{subTitle}</div>
214
+
215
+ <ProductListPrice {...price_range.minimum_price} classes={{ root: classes.price }} />
216
+ </div>
217
+
218
+ {children}
219
+ </>
220
+ )}
221
+ </div>
222
+ )
223
+ }
@@ -0,0 +1,4 @@
1
+ /* eslint-disable */
2
+ import * as Types from '@graphcommerce/graphql';
3
+
4
+ export type ProductListItemsFragment = { items?: Types.Maybe<Array<Types.Maybe<{ __typename: 'BundleProduct', uid: string, sku?: Types.Maybe<string>, name?: Types.Maybe<string>, url_key?: Types.Maybe<string>, rating_summary: number, small_image?: Types.Maybe<{ url?: Types.Maybe<string>, label?: Types.Maybe<string> }>, price_range: { maximum_price?: Types.Maybe<{ regular_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> }, discount?: Types.Maybe<{ amount_off?: Types.Maybe<number>, percent_off?: Types.Maybe<number> }>, final_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> } }>, minimum_price: { regular_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> }, discount?: Types.Maybe<{ amount_off?: Types.Maybe<number>, percent_off?: Types.Maybe<number> }>, final_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> } } } } | { __typename: 'ConfigurableProduct', uid: string, sku?: Types.Maybe<string>, name?: Types.Maybe<string>, url_key?: Types.Maybe<string>, rating_summary: number, small_image?: Types.Maybe<{ url?: Types.Maybe<string>, label?: Types.Maybe<string> }>, price_range: { maximum_price?: Types.Maybe<{ regular_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> }, discount?: Types.Maybe<{ amount_off?: Types.Maybe<number>, percent_off?: Types.Maybe<number> }>, final_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> } }>, minimum_price: { regular_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> }, discount?: Types.Maybe<{ amount_off?: Types.Maybe<number>, percent_off?: Types.Maybe<number> }>, final_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> } } }, configurable_options?: Types.Maybe<Array<Types.Maybe<{ attribute_code?: Types.Maybe<string>, uid: string, label?: Types.Maybe<string>, values?: Types.Maybe<Array<Types.Maybe<{ store_label?: Types.Maybe<string>, uid?: Types.Maybe<string>, swatch_data?: Types.Maybe<{ __typename: 'ColorSwatchData', value?: Types.Maybe<string> } | { __typename: 'ImageSwatchData', value?: Types.Maybe<string>, thumbnail?: Types.Maybe<string> } | { __typename: 'TextSwatchData', value?: Types.Maybe<string> }> }>>> }>>>, variants?: Types.Maybe<Array<Types.Maybe<{ attributes?: Types.Maybe<Array<Types.Maybe<{ uid: string, code?: Types.Maybe<string> }>>>, product?: Types.Maybe<{ uid: string, sku?: Types.Maybe<string>, name?: Types.Maybe<string>, small_image?: Types.Maybe<{ label?: Types.Maybe<string>, url?: Types.Maybe<string> }> }> }>>> } | { __typename: 'DownloadableProduct', uid: string, sku?: Types.Maybe<string>, name?: Types.Maybe<string>, url_key?: Types.Maybe<string>, rating_summary: number, small_image?: Types.Maybe<{ url?: Types.Maybe<string>, label?: Types.Maybe<string> }>, price_range: { maximum_price?: Types.Maybe<{ regular_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> }, discount?: Types.Maybe<{ amount_off?: Types.Maybe<number>, percent_off?: Types.Maybe<number> }>, final_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> } }>, minimum_price: { regular_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> }, discount?: Types.Maybe<{ amount_off?: Types.Maybe<number>, percent_off?: Types.Maybe<number> }>, final_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> } } } } | { __typename: 'GroupedProduct', uid: string, sku?: Types.Maybe<string>, name?: Types.Maybe<string>, url_key?: Types.Maybe<string>, rating_summary: number, small_image?: Types.Maybe<{ url?: Types.Maybe<string>, label?: Types.Maybe<string> }>, price_range: { maximum_price?: Types.Maybe<{ regular_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> }, discount?: Types.Maybe<{ amount_off?: Types.Maybe<number>, percent_off?: Types.Maybe<number> }>, final_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> } }>, minimum_price: { regular_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> }, discount?: Types.Maybe<{ amount_off?: Types.Maybe<number>, percent_off?: Types.Maybe<number> }>, final_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> } } } } | { __typename: 'SimpleProduct', uid: string, sku?: Types.Maybe<string>, name?: Types.Maybe<string>, url_key?: Types.Maybe<string>, rating_summary: number, small_image?: Types.Maybe<{ url?: Types.Maybe<string>, label?: Types.Maybe<string> }>, price_range: { maximum_price?: Types.Maybe<{ regular_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> }, discount?: Types.Maybe<{ amount_off?: Types.Maybe<number>, percent_off?: Types.Maybe<number> }>, final_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> } }>, minimum_price: { regular_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> }, discount?: Types.Maybe<{ amount_off?: Types.Maybe<number>, percent_off?: Types.Maybe<number> }>, final_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> } } } } | { __typename: 'VirtualProduct', uid: string, sku?: Types.Maybe<string>, name?: Types.Maybe<string>, url_key?: Types.Maybe<string>, rating_summary: number, small_image?: Types.Maybe<{ url?: Types.Maybe<string>, label?: Types.Maybe<string> }>, price_range: { maximum_price?: Types.Maybe<{ regular_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> }, discount?: Types.Maybe<{ amount_off?: Types.Maybe<number>, percent_off?: Types.Maybe<number> }>, final_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> } }>, minimum_price: { regular_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> }, discount?: Types.Maybe<{ amount_off?: Types.Maybe<number>, percent_off?: Types.Maybe<number> }>, final_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> } } } }>>> };
@@ -0,0 +1,7 @@
1
+ fragment ProductListItems on Products {
2
+ items {
3
+ __typename
4
+ uid
5
+ ...ProductListItem
6
+ }
7
+ }
@@ -0,0 +1,60 @@
1
+ import { Theme, makeStyles } from '@material-ui/core'
2
+ import { Maybe } from '@graphcommerce/graphql'
3
+ import { RenderType, UseStyles, responsiveVal } from '@graphcommerce/next-ui'
4
+ import clsx from 'clsx'
5
+ import React from 'react'
6
+ import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
7
+ import { ProductListItemProps } from '../ProductListItem'
8
+ import { ProductListItemRenderer } from './renderer'
9
+
10
+ export const useStyles = makeStyles(
11
+ (theme: Theme) => ({
12
+ productList: {
13
+ display: 'grid',
14
+ gridColumnGap: theme.spacings.sm,
15
+ gridRowGap: theme.spacings.md,
16
+ gridTemplateColumns: `repeat(auto-fill, minmax(${responsiveVal(150, 360)}, 1fr))`,
17
+ },
18
+ productListsmall: {
19
+ gridTemplateColumns: `repeat(auto-fill, minmax(${responsiveVal(150, 280)}, 1fr))`,
20
+ },
21
+ }),
22
+ { name: 'ProductList' },
23
+ )
24
+
25
+ export type ProductItemsGridProps = {
26
+ items?: Maybe<Array<Maybe<ProductListItemFragment & ProductListItemProps>>>
27
+ renderers: ProductListItemRenderer
28
+ loadingEager?: number
29
+ size?: 'normal' | 'small'
30
+ } & UseStyles<typeof useStyles> &
31
+ JSX.IntrinsicElements['div']
32
+
33
+ export default function ProductListItemsBase(props: ProductItemsGridProps) {
34
+ const { items, renderers, loadingEager = 0, size, ...divProps } = props
35
+ const classes = useStyles(props)
36
+
37
+ return (
38
+ <div
39
+ {...divProps}
40
+ className={clsx(classes.productList, classes[`productList${size}`], divProps.className)}
41
+ >
42
+ {items?.map((item, idx) =>
43
+ item ? (
44
+ <RenderType
45
+ key={item.uid ?? ''}
46
+ renderer={renderers}
47
+ {...item}
48
+ loading={loadingEager > idx ? 'eager' : 'lazy'}
49
+ sizes={
50
+ size === 'small'
51
+ ? { 0: '100vw', 354: '50vw', 675: '30vw', 1255: '23vw', 1500: '337px' }
52
+ : { 0: '100vw', 367: '48vw', 994: '30vw', 1590: '23vw', 1920: '443px' }
53
+ }
54
+ noReport
55
+ />
56
+ ) : null,
57
+ )}
58
+ </div>
59
+ )
60
+ }
@@ -0,0 +1,20 @@
1
+ import { PropsWithChildren, useState, useEffect } from 'react'
2
+ import { productListParamsContext } from '../../context/productListParamsContext'
3
+ import { ProductListParams } from './filterTypes'
4
+
5
+ export default function ProductListParamsProvider({
6
+ children,
7
+ value,
8
+ }: PropsWithChildren<{ value: ProductListParams }>) {
9
+ const [params, setParams] = useState<ProductListParams>(value)
10
+
11
+ useEffect(() => {
12
+ setParams(value)
13
+ }, [value])
14
+
15
+ return (
16
+ <productListParamsContext.Provider value={{ params, setParams }}>
17
+ {children}
18
+ </productListParamsContext.Provider>
19
+ )
20
+ }
@@ -0,0 +1,45 @@
1
+ import {
2
+ Exact,
3
+ Maybe,
4
+ Scalars,
5
+ ProductAttributeFilterInput,
6
+ ProductAttributeSortInput,
7
+ FilterEqualTypeInput,
8
+ FilterMatchTypeInput,
9
+ FilterRangeTypeInput,
10
+ } from '@graphcommerce/graphql'
11
+
12
+ /** This is mainly based on ProductListQueryVariables */
13
+ export type ProductListParams = Exact<{
14
+ pageSize?: Maybe<Scalars['Int']>
15
+ currentPage?: Maybe<Scalars['Int']>
16
+ filters: ProductAttributeFilterInput
17
+ sort: ProductAttributeSortInput
18
+ search?: Maybe<Scalars['String']>
19
+ url: string
20
+ }>
21
+
22
+ type AnyFilterType = FilterEqualTypeInput | FilterMatchTypeInput | FilterRangeTypeInput | undefined
23
+
24
+ export function isFilterTypeEqual(filter: AnyFilterType): filter is FilterEqualTypeInput {
25
+ return Boolean(
26
+ filter && ((filter as FilterEqualTypeInput).eq || (filter as FilterEqualTypeInput).in),
27
+ )
28
+ }
29
+
30
+ export function isFilterTypeMatch(filter: AnyFilterType): filter is FilterMatchTypeInput {
31
+ return Boolean(filter && (filter as FilterMatchTypeInput).match)
32
+ }
33
+
34
+ export function isFilterTypeRange(filter: AnyFilterType): filter is FilterRangeTypeInput {
35
+ return Boolean(
36
+ filter && ((filter as FilterRangeTypeInput).from || (filter as FilterRangeTypeInput).to),
37
+ )
38
+ }
39
+
40
+ export type AllFilterInputTypes =
41
+ | 'FilterEqualTypeInput'
42
+ | 'FilterMatchTypeInput'
43
+ | 'FilterRangeTypeInput'
44
+
45
+ export type FilterTypes = Partial<Record<string, AllFilterInputTypes>>
@@ -0,0 +1,74 @@
1
+ import {
2
+ FilterEqualTypeInput,
3
+ FilterMatchTypeInput,
4
+ FilterRangeTypeInput,
5
+ SortEnum,
6
+ } from '@graphcommerce/graphql'
7
+ import { FilterTypes, ProductListParams } from './filterTypes'
8
+
9
+ export function parseParams(
10
+ url: string,
11
+ query: string[],
12
+ filterTypes: FilterTypes,
13
+ ): ProductListParams | false {
14
+ const categoryVariables: ProductListParams = { url, filters: {}, sort: {} }
15
+
16
+ const typeMap = filterTypes
17
+
18
+ let error = false
19
+ query.reduce<string | undefined>((param, value) => {
20
+ // We parse everything in pairs, every second loop we parse
21
+ if (!param) return value
22
+
23
+ if (param === 'page') {
24
+ categoryVariables.currentPage = Number(value)
25
+ return undefined
26
+ }
27
+ if (param === 'limit') {
28
+ categoryVariables.pageSize = Number(param)
29
+ return undefined
30
+ }
31
+ if (param === 'sort') {
32
+ categoryVariables.sort[value] = 'ASC'
33
+ return undefined
34
+ }
35
+ if (param === 'dir') {
36
+ const [sortBy] = Object.keys(categoryVariables.sort)
37
+ if (sortBy) categoryVariables.sort[sortBy] = value as SortEnum
38
+ return undefined
39
+ }
40
+
41
+ const [from, to] = value.split('-')
42
+ switch (typeMap[param]) {
43
+ case 'FilterMatchTypeInput':
44
+ categoryVariables.filters[param] = { match: value } as FilterMatchTypeInput
45
+ return undefined
46
+ case 'FilterRangeTypeInput':
47
+ categoryVariables.filters[param] = {
48
+ ...(from !== '*' && { from }),
49
+ ...(to !== '*' && { to }),
50
+ } as FilterRangeTypeInput
51
+ return undefined
52
+ case 'FilterEqualTypeInput':
53
+ categoryVariables.filters[param] = { in: value.split(',') } as FilterEqualTypeInput
54
+ return undefined
55
+ }
56
+
57
+ error = true
58
+ return undefined
59
+ }, undefined)
60
+
61
+ return error ? false : categoryVariables
62
+ }
63
+
64
+ export function extractUrlQuery(params?: { url: string[] }) {
65
+ if (!params?.url) return [undefined, undefined] as const
66
+
67
+ const queryIndex = params.url.findIndex((slug) => slug === 'q')
68
+ const qIndex = queryIndex < 0 ? params.url.length : queryIndex
69
+ const url = params.url.slice(0, qIndex).join('/')
70
+ const query = params.url.slice(qIndex + 1)
71
+
72
+ if (queryIndex > 0 && !query.length) return [undefined, undefined] as const
73
+ return [url, query] as const
74
+ }
@@ -0,0 +1,51 @@
1
+ import { gql, ApolloClient, NormalizedCacheObject } from '@apollo/client'
2
+ import { Exact } from '@graphcommerce/graphql'
3
+ import { AllFilterInputTypes, FilterTypes } from './filterTypes'
4
+
5
+ const allFilterInputTypes: AllFilterInputTypes[] = [
6
+ 'FilterEqualTypeInput',
7
+ 'FilterMatchTypeInput',
8
+ 'FilterRangeTypeInput',
9
+ ]
10
+
11
+ type FilterInputTypesQueryVariables = Exact<{ [key: string]: never }>
12
+
13
+ type FilterInputTypesQuery = {
14
+ __type: {
15
+ inputFields: {
16
+ name: string
17
+ type: { name: AllFilterInputTypes }
18
+ }[]
19
+ }
20
+ }
21
+
22
+ const FilterInputTypesDocument = gql`
23
+ query FilterInputTypes {
24
+ __type(name: "ProductAttributeFilterInput") {
25
+ inputFields {
26
+ name
27
+ type {
28
+ name
29
+ }
30
+ }
31
+ }
32
+ }
33
+ `
34
+
35
+ export async function getFilterTypes(
36
+ client: ApolloClient<NormalizedCacheObject>,
37
+ ): Promise<FilterTypes> {
38
+ const filterInputTypes = client.query<FilterInputTypesQuery, FilterInputTypesQueryVariables>({
39
+ query: FilterInputTypesDocument,
40
+ })
41
+
42
+ const typeMap: FilterTypes = {}
43
+
44
+ ;(await filterInputTypes).data?.__type.inputFields.forEach(({ name, type }) => {
45
+ if (!allFilterInputTypes.includes(type.name))
46
+ throw new Error(`filter ${name} with FilterTypeInput ${type.name} not implemented`)
47
+ typeMap[name] = type.name
48
+ })
49
+
50
+ return typeMap
51
+ }
@@ -0,0 +1,6 @@
1
+ import ProductListItemsBase, { ProductItemsGridProps } from './ProductListItemsBase'
2
+ import renderer from './renderer'
3
+
4
+ export default function ProductListItems(props: Omit<ProductItemsGridProps, 'renderers'>) {
5
+ return <ProductListItemsBase renderers={renderer} {...props} />
6
+ }
@@ -0,0 +1,16 @@
1
+ import { TypeRenderer } from '@graphcommerce/next-ui'
2
+ import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
3
+ import ProductListItem from '../ProductListItem'
4
+
5
+ export type ProductListItemRenderer = TypeRenderer<ProductListItemFragment>
6
+
7
+ const renderer: ProductListItemRenderer = {
8
+ SimpleProduct: ProductListItem,
9
+ ConfigurableProduct: ProductListItem,
10
+ BundleProduct: ProductListItem,
11
+ VirtualProduct: ProductListItem,
12
+ DownloadableProduct: ProductListItem,
13
+ GroupedProduct: ProductListItem,
14
+ }
15
+
16
+ export default renderer
@@ -0,0 +1,50 @@
1
+ import { Link, LinkProps } from '@material-ui/core'
2
+ import PageLink, { LinkProps as PageLinkProps } from 'next/link'
3
+ import React, { PropsWithChildren } from 'react'
4
+ import { useProductListLink } from '../../hooks/useProductListLink'
5
+ import { useProductListParamsContext } from '../../hooks/useProductListParamsContext'
6
+ import { ProductListParams } from '../ProductListItems/filterTypes'
7
+
8
+ export type ProductListLinkProps = PropsWithChildren<
9
+ LinkProps &
10
+ ProductListParams & { noLink?: boolean; link?: Omit<PageLinkProps, 'href' | 'passHref'> }
11
+ >
12
+
13
+ const ProductListLink = React.forwardRef<HTMLAnchorElement, ProductListLinkProps>((props, ref) => {
14
+ const { setParams } = useProductListParamsContext()
15
+ const {
16
+ children,
17
+ url,
18
+ sort,
19
+ currentPage,
20
+ pageSize,
21
+ filters,
22
+ search,
23
+ noLink,
24
+ link,
25
+ ...linkProps
26
+ } = props
27
+ const newParams = { filters, sort, url, currentPage, pageSize, search }
28
+
29
+ const productListLink = useProductListLink(newParams)
30
+ const updateParams = () => setParams(newParams)
31
+
32
+ // We're setting nofollow if a custom sort, pageSize, filters or search is set.
33
+ let rel: string | undefined
34
+ if (Object.keys(sort).length || pageSize || Object.keys(filters).length || search)
35
+ rel = 'nofollow'
36
+
37
+ return (
38
+ <PageLink href={productListLink} passHref {...link}>
39
+ {noLink ? (
40
+ children
41
+ ) : (
42
+ <Link rel={rel} {...linkProps} ref={ref} onClick={updateParams}>
43
+ {children}
44
+ </Link>
45
+ )}
46
+ </PageLink>
47
+ )
48
+ })
49
+
50
+ export default ProductListLink
@@ -0,0 +1,4 @@
1
+ /* eslint-disable */
2
+ import * as Types from '@graphcommerce/graphql';
3
+
4
+ export type ProductListPaginationFragment = { page_info?: Types.Maybe<{ current_page?: Types.Maybe<number>, total_pages?: Types.Maybe<number> }> };
@@ -0,0 +1,6 @@
1
+ fragment ProductListPagination on Products {
2
+ page_info {
3
+ current_page
4
+ total_pages
5
+ }
6
+ }
@@ -0,0 +1,31 @@
1
+ import { PaginationProps } from '@material-ui/lab'
2
+ import { Pagination } from '@graphcommerce/next-ui'
3
+ import React from 'react'
4
+ import { useProductListParamsContext } from '../../hooks/useProductListParamsContext'
5
+ import ProductListLink from '../ProductListLink/ProductListLink'
6
+ import { ProductListPaginationFragment } from './ProductListPagination.gql'
7
+
8
+ export type ProductPaginationProps = ProductListPaginationFragment &
9
+ Omit<PaginationProps, 'count' | 'defaultPage' | 'page' | 'renderItem'>
10
+
11
+ export default function ProductListPagination({
12
+ page_info,
13
+ ...paginationProps
14
+ }: ProductPaginationProps) {
15
+ const { params } = useProductListParamsContext()
16
+
17
+ if (!page_info || !page_info.total_pages || !page_info.current_page) return null
18
+
19
+ return (
20
+ <Pagination
21
+ count={page_info?.total_pages}
22
+ page={page_info?.current_page ?? 1}
23
+ renderLink={(page: number, icon: React.ReactNode, btnProps: any) => (
24
+ <ProductListLink {...btnProps} {...params} currentPage={btnProps.page}>
25
+ {icon}
26
+ </ProductListLink>
27
+ )}
28
+ {...paginationProps}
29
+ />
30
+ )
31
+ }
@@ -0,0 +1,4 @@
1
+ /* eslint-disable */
2
+ import * as Types from '@graphcommerce/graphql';
3
+
4
+ export type ProductListPriceFragment = { regular_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> }, discount?: Types.Maybe<{ amount_off?: Types.Maybe<number>, percent_off?: Types.Maybe<number> }>, final_price: { currency?: Types.Maybe<Types.CurrencyEnum>, value?: Types.Maybe<number> } };
@@ -0,0 +1,12 @@
1
+ fragment ProductListPrice on ProductPrice {
2
+ regular_price {
3
+ ...Money
4
+ }
5
+ discount {
6
+ amount_off
7
+ percent_off
8
+ }
9
+ final_price {
10
+ ...Money
11
+ }
12
+ }
@@ -0,0 +1,36 @@
1
+ import { makeStyles, Theme, Typography } from '@material-ui/core'
2
+ import { Money } from '@graphcommerce/magento-store'
3
+ import { UseStyles } from '@graphcommerce/next-ui'
4
+ import React from 'react'
5
+ import { ProductListPriceFragment } from './ProductListPrice.gql'
6
+
7
+ const useStyles = makeStyles(
8
+ (theme: Theme) => ({
9
+ root: {},
10
+ discount: {
11
+ textDecoration: 'line-through',
12
+ color: theme.palette.primary.mutedText,
13
+ display: 'inline',
14
+ marginRight: 8,
15
+ },
16
+ }),
17
+ { name: 'ProductListPrice' },
18
+ )
19
+
20
+ type ProductListPriceProps = ProductListPriceFragment & UseStyles<typeof useStyles>
21
+
22
+ export default function ProductListPrice(props: ProductListPriceProps) {
23
+ const { regular_price, final_price } = props
24
+ const classes = useStyles(props)
25
+
26
+ return (
27
+ <Typography component='div' variant='body1' className={classes.root}>
28
+ {regular_price.value !== final_price.value && (
29
+ <div className={classes.discount}>
30
+ <Money {...regular_price} />
31
+ </div>
32
+ )}
33
+ <Money {...final_price} />
34
+ </Typography>
35
+ )
36
+ }