@graphcommerce/magento-product 8.1.0-canary.5 → 8.1.0-canary.52
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.
- package/Api/ProductListItem.graphql +1 -2
- package/Api/ProductPageItem.graphql +1 -1
- package/CHANGELOG.md +115 -0
- package/Config.graphqls +13 -0
- package/components/AddProductsToCart/AddProductsToCartButton.tsx +3 -2
- package/components/AddProductsToCart/AddProductsToCartFab.tsx +2 -2
- package/components/AddProductsToCart/AddProductsToCartForm.tsx +31 -28
- package/components/AddProductsToCart/AddProductsToCartSnackbar.tsx +25 -16
- package/components/AddProductsToCart/UseAddProductsToCartAction.graphql +1 -1
- package/components/AddProductsToCart/findAddedItems.ts +1 -4
- package/components/AddProductsToCart/useAddProductsToCartAction.ts +2 -1
- package/components/AddProductsToCart/useFormAddProductsToCart.ts +1 -2
- package/components/JsonLdProduct/JsonLdProduct.graphql +1 -1
- package/components/JsonLdProduct/ProductPageJsonLd.tsx +1 -1
- package/components/ProductAddToCart/ProductAddToCart.tsx +6 -8
- package/components/ProductCustomizable/ProductCustomizable.graphql +1 -1
- package/components/ProductCustomizable/index.ts +1 -0
- package/components/ProductCustomizable/productCustomizableSelectors.ts +59 -0
- package/components/ProductFiltersPro/PriceSlider.tsx +1 -2
- package/components/ProductFiltersPro/ProductFilterEqualChip.tsx +1 -1
- package/components/ProductFiltersPro/ProductFilterEqualSection.tsx +2 -2
- package/components/ProductFiltersPro/ProductFilterRangeChip.tsx +1 -1
- package/components/ProductFiltersPro/ProductFilterRangeSection.tsx +1 -1
- package/components/ProductFiltersPro/ProductFiltersPro.tsx +103 -19
- package/components/ProductFiltersPro/ProductFiltersProAggregations.tsx +31 -18
- package/components/ProductFiltersPro/ProductFiltersProAllFiltersChip.tsx +6 -10
- package/components/ProductFiltersPro/ProductFiltersProAllFiltersSidebar.tsx +18 -8
- package/components/ProductFiltersPro/ProductFiltersProCategorySection.tsx +130 -0
- package/components/ProductFiltersPro/ProductFiltersProChips.tsx +10 -8
- package/components/ProductFiltersPro/ProductFiltersProClearAll.tsx +4 -16
- package/components/ProductFiltersPro/ProductFiltersProLayoutSidebar.tsx +15 -7
- package/components/ProductFiltersPro/ProductFiltersProLimitSection.tsx +5 -2
- package/components/ProductFiltersPro/ProductFiltersProNoResults.tsx +79 -0
- package/components/ProductFiltersPro/ProductFiltersProSortChip.tsx +1 -1
- package/components/ProductFiltersPro/ProductFiltersProSortDirectionArrow.tsx +2 -4
- package/components/ProductFiltersPro/ProductFiltersProSortSection.tsx +7 -2
- package/components/ProductFiltersPro/activeAggregations.ts +5 -9
- package/components/ProductFiltersPro/applyAggregationCount.ts +14 -8
- package/components/ProductFiltersPro/index.ts +9 -0
- package/components/ProductFiltersPro/{useClearAllFiltersHandler.ts → useProductFiltersProClearAllAction.ts} +1 -1
- package/components/ProductFiltersPro/useProductFiltersProHasFiltersApplied.ts +21 -0
- package/components/ProductFiltersPro/useProductFiltersProSort.tsx +4 -2
- package/components/ProductList/ProductList.graphql +8 -5
- package/components/ProductListCount/ProductListCount.tsx +3 -1
- package/components/ProductListFilters/ProductFilters.graphql +7 -2
- package/components/ProductListFilters/ProductListFilters.graphql +1 -1
- package/components/ProductListFiltersContainer/ProductListFiltersContainer.tsx +2 -4
- package/components/ProductListItem/ProductDiscountLabel.tsx +2 -3
- package/components/ProductListItem/ProductListItem.tsx +3 -3
- package/components/ProductListItem/ProductListItemTitleAndPrice.tsx +18 -15
- package/components/ProductListItems/ProductListItemsBase.tsx +65 -23
- package/components/ProductListItems/filterTypes.tsx +14 -5
- package/components/ProductListItems/filteredProductList.tsx +23 -0
- package/components/ProductListItems/productListApplyCategoryDefaults.ts +44 -4
- package/components/ProductListItems/renderer.tsx +8 -2
- package/components/ProductListPagination/ProductListPagination.tsx +39 -20
- package/components/ProductListPrice/ProductListPrice.tsx +9 -4
- package/components/ProductListSuggestions/ProductListSuggestions.graphql +5 -0
- package/components/ProductListSuggestions/ProductListSuggestions.tsx +42 -0
- package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.graphql +3 -0
- package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.tsx +3 -0
- package/components/ProductPageBreadcrumb/ProductPageBreadcrumbs.tsx +40 -0
- package/components/ProductPageBreadcrumb/index.ts +1 -0
- package/components/ProductPageDescription/ComplexTextValue.graphql +1 -1
- package/components/ProductPageDescription/ProductPageDescription.tsx +1 -1
- package/components/ProductPageGallery/ProductImage.graphql +1 -0
- package/components/ProductPageGallery/ProductPageGallery.tsx +14 -8
- package/components/ProductPagePrice/ProductPagePrice.graphql +0 -6
- package/components/ProductPagePrice/ProductPagePrice.tsx +19 -12
- package/components/ProductPagePrice/ProductPagePriceTiers.tsx +4 -3
- package/components/ProductPagePrice/useCustomizableOptionPrice.ts +11 -53
- package/components/ProductShortDescription/ProductShortDescription.tsx +2 -0
- package/components/ProductStaticPaths/getProductStaticPaths.ts +2 -3
- package/components/ProductStaticPaths/getSitemapPaths.ts +3 -0
- package/components/ProductWeight/ProductWeight.tsx +12 -9
- package/components/index.ts +2 -0
- package/hooks/useProductList.ts +123 -0
- package/hooks/useProductListLink.ts +6 -3
- package/index.ts +1 -0
- 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'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'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 ?
|
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='
|
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
|
-
|
7
|
-
|
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
|
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)
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
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 =
|
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
|
-
...
|
16
|
-
...
|
17
|
-
...
|
18
|
-
...
|
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(
|
2
|
-
|
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
|
}
|
@@ -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
|
-
|
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
|
-
|
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 {
|
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
|
-
{
|
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
|
-
|
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:
|
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={
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
(
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
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
|
-
|
73
|
-
|
74
|
-
|
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
|
+
}
|