@graphcommerce/magento-product 8.1.0-canary.43 → 8.1.0-canary.45
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 +0 -1
- package/CHANGELOG.md +4 -0
- package/components/AddProductsToCart/AddProductsToCartButton.tsx +2 -2
- package/components/AddProductsToCart/AddProductsToCartFab.tsx +2 -2
- package/components/AddProductsToCart/AddProductsToCartForm.tsx +24 -26
- package/components/AddProductsToCart/AddProductsToCartSnackbar.tsx +25 -16
- package/components/ProductAddToCart/ProductAddToCart.tsx +6 -8
- 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 +79 -17
- package/components/ProductFiltersPro/ProductFiltersProAggregations.tsx +17 -18
- package/components/ProductFiltersPro/ProductFiltersProAllFiltersChip.tsx +2 -2
- package/components/ProductFiltersPro/ProductFiltersProCategorySection.tsx +99 -39
- package/components/ProductFiltersPro/ProductFiltersProClearAll.tsx +4 -16
- package/components/ProductFiltersPro/ProductFiltersProLimitSection.tsx +1 -1
- package/components/ProductFiltersPro/ProductFiltersProNoResults.tsx +79 -0
- package/components/ProductFiltersPro/ProductFiltersProSortChip.tsx +1 -1
- package/components/ProductFiltersPro/ProductFiltersProSortSection.tsx +1 -1
- package/components/ProductFiltersPro/activeAggregations.ts +5 -9
- package/components/ProductFiltersPro/applyAggregationCount.ts +14 -8
- package/components/ProductFiltersPro/index.ts +4 -1
- package/components/ProductFiltersPro/{useClearAllFiltersHandler.ts → useProductFiltersProClearAllAction.ts} +1 -1
- package/components/ProductFiltersPro/useProductFiltersProHasFiltersApplied.ts +21 -0
- 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/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 +3 -1
- package/components/ProductListPrice/ProductListPrice.tsx +9 -4
- package/components/ProductListSuggestions/ProductListSuggestions.graphql +5 -0
- package/components/ProductListSuggestions/ProductListSuggestions.tsx +42 -0
- package/components/ProductPageDescription/ProductPageDescription.tsx +1 -1
- package/components/ProductPagePrice/ProductPagePrice.graphql +0 -6
- package/components/ProductPagePrice/ProductPagePrice.tsx +19 -12
- package/components/ProductPagePrice/ProductPagePriceTiers.tsx +4 -3
- 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 -14
@@ -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
|
+
}
|
@@ -4,7 +4,10 @@ import type {
|
|
4
4
|
FilterRangeTypeInput,
|
5
5
|
SortEnum,
|
6
6
|
} from '@graphcommerce/graphql-mesh'
|
7
|
+
import { useRouter } from 'next/router'
|
7
8
|
import { FilterTypes, ProductListParams } from './filterTypes'
|
9
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
10
|
+
import { equal } from '@wry/equality'
|
8
11
|
|
9
12
|
export function parseParams(
|
10
13
|
url: string,
|
@@ -73,3 +76,23 @@ export function extractUrlQuery(params?: { url: string[] }) {
|
|
73
76
|
if (queryIndex > 0 && !query.length) return [undefined, undefined] as const
|
74
77
|
return [url, query] as const
|
75
78
|
}
|
79
|
+
|
80
|
+
export function useRouterFilterParams(props: {
|
81
|
+
filterTypes?: FilterTypes | undefined
|
82
|
+
params?: ProductListParams
|
83
|
+
}) {
|
84
|
+
const { filterTypes, params } = props
|
85
|
+
const router = useRouter()
|
86
|
+
|
87
|
+
const path = router.asPath.startsWith('/c/') ? router.asPath.slice(3) : router.asPath.slice(1)
|
88
|
+
const [url, query] = extractUrlQuery({ url: path.split('#')[0].split('/') })
|
89
|
+
if (!url || !query || !filterTypes) return { params, shallow: false }
|
90
|
+
|
91
|
+
const searchParam = url.startsWith('search') ? decodeURI(url.split('/')[1] ?? '') : null
|
92
|
+
const clientParams = parseParams(url, query, filterTypes, searchParam)
|
93
|
+
|
94
|
+
if (clientParams && !clientParams?.filters.category_uid && params?.filters.category_uid)
|
95
|
+
clientParams.filters.category_uid = params?.filters.category_uid
|
96
|
+
|
97
|
+
return { params: clientParams, shallow: !equal(params, clientParams) }
|
98
|
+
}
|
@@ -1,13 +1,53 @@
|
|
1
|
-
import {
|
1
|
+
import { cloneDeep, useQuery } from '@graphcommerce/graphql'
|
2
|
+
import { StoreConfigDocument, StoreConfigQuery } from '@graphcommerce/magento-store'
|
2
3
|
import { CategoryDefaultFragment } from './CategoryDefault.gql'
|
3
4
|
import { ProductListParams } from './filterTypes'
|
4
|
-
import {
|
5
|
+
import { ProductListQueryVariables } from '../ProductList/ProductList.gql'
|
5
6
|
|
7
|
+
export function useProductListApplyCategoryDefaults(
|
8
|
+
params: ProductListParams | undefined,
|
9
|
+
category: CategoryDefaultFragment | null | undefined,
|
10
|
+
): ProductListQueryVariables | undefined {
|
11
|
+
const storeConfig = useQuery(StoreConfigDocument)
|
12
|
+
|
13
|
+
if (!params) return params
|
14
|
+
|
15
|
+
const variables = cloneDeep(params)
|
16
|
+
if (!variables.pageSize) variables.pageSize = storeConfig.data?.storeConfig?.grid_per_page ?? 12
|
17
|
+
|
18
|
+
if (Object.keys(params.sort).length === 0) {
|
19
|
+
const categorySort = category?.default_sort_by as keyof ProductListParams['sort']
|
20
|
+
const defaultSort = storeConfig.data?.storeConfig
|
21
|
+
?.catalog_default_sort_by as keyof ProductListParams['sort']
|
22
|
+
if (categorySort) variables.sort = { [categorySort]: 'ASC' }
|
23
|
+
else if (defaultSort) variables.sort = { [defaultSort]: 'ASC' }
|
24
|
+
}
|
25
|
+
|
26
|
+
if (!variables.filters.category_uid?.in?.[0]) {
|
27
|
+
variables.filters.category_uid = { eq: category?.uid }
|
28
|
+
}
|
29
|
+
|
30
|
+
return variables
|
31
|
+
}
|
32
|
+
|
33
|
+
export async function productListApplyCategoryDefaults(
|
34
|
+
params: ProductListParams,
|
35
|
+
conf: StoreConfigQuery,
|
36
|
+
category:
|
37
|
+
| Promise<CategoryDefaultFragment | null | undefined>
|
38
|
+
| CategoryDefaultFragment
|
39
|
+
| null
|
40
|
+
| undefined,
|
41
|
+
): Promise<ProductListQueryVariables>
|
6
42
|
export async function productListApplyCategoryDefaults(
|
7
43
|
params: ProductListParams | undefined,
|
8
44
|
conf: StoreConfigQuery,
|
9
|
-
category:
|
10
|
-
|
45
|
+
category:
|
46
|
+
| Promise<CategoryDefaultFragment | null | undefined>
|
47
|
+
| CategoryDefaultFragment
|
48
|
+
| null
|
49
|
+
| undefined,
|
50
|
+
): Promise<ProductListQueryVariables | undefined> {
|
11
51
|
if (!params) return params
|
12
52
|
|
13
53
|
const newParams = cloneDeep(params)
|
@@ -1,17 +1,23 @@
|
|
1
1
|
import { TypeRenderer } from '@graphcommerce/next-ui'
|
2
2
|
import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
|
3
|
-
import { ProductListItem
|
3
|
+
import { ProductListItem } from '../ProductListItem/ProductListItem'
|
4
4
|
|
5
5
|
type SkeletonType = { __typename: 'Skeleton'; uid: string }
|
6
6
|
export type ProductListItemType = ProductListItemFragment | SkeletonType
|
7
7
|
export type ProductListItemRenderer = TypeRenderer<ProductListItemFragment | SkeletonType>
|
8
8
|
|
9
|
+
/**
|
10
|
+
* @deprecated Please use productListRenderer from the example directory instead.
|
11
|
+
*/
|
9
12
|
export const renderer: ProductListItemRenderer = {
|
10
|
-
Skeleton:
|
13
|
+
Skeleton: ProductListItem,
|
11
14
|
SimpleProduct: ProductListItem,
|
12
15
|
ConfigurableProduct: ProductListItem,
|
13
16
|
BundleProduct: ProductListItem,
|
14
17
|
VirtualProduct: ProductListItem,
|
15
18
|
DownloadableProduct: ProductListItem,
|
16
19
|
GroupedProduct: ProductListItem,
|
20
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
21
|
+
// @ts-ignore GiftCardProduct is only available in Commerce
|
22
|
+
GiftCardProduct: ProductListItem,
|
17
23
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { PaginationExtended, Pagination } from '@graphcommerce/next-ui'
|
1
|
+
import { NextLink, PaginationExtended, Pagination } from '@graphcommerce/next-ui'
|
2
2
|
import { Link, PaginationProps } from '@mui/material'
|
3
3
|
import { productListLink } from '../../hooks/useProductListLink'
|
4
4
|
import { ProductListParams } from '../ProductListItems/filterTypes'
|
@@ -27,6 +27,8 @@ export function ProductListPagination({
|
|
27
27
|
<Link
|
28
28
|
{...btnProps}
|
29
29
|
href={`${productListLink({ ...params, currentPage: btnProps.page })}${suffix}`}
|
30
|
+
component={NextLink}
|
31
|
+
shallow
|
30
32
|
color='inherit'
|
31
33
|
>
|
32
34
|
{icon}
|
@@ -1,10 +1,12 @@
|
|
1
|
+
import { InContextMask } from '@graphcommerce/graphql'
|
1
2
|
import { Money } from '@graphcommerce/magento-store'
|
2
3
|
import { extendableComponent } from '@graphcommerce/next-ui'
|
3
|
-
import { Typography, TypographyProps
|
4
|
+
import { Typography, TypographyProps } from '@mui/material'
|
4
5
|
import { ProductListPriceFragment } from './ProductListPrice.gql'
|
5
6
|
|
6
7
|
export const productListPrice = extendableComponent('ProductListPrice', [
|
7
8
|
'root',
|
9
|
+
'finalPrice',
|
8
10
|
'discountPrice',
|
9
11
|
] as const)
|
10
12
|
|
@@ -18,19 +20,22 @@ export function ProductListPrice(props: ProductListPriceProps) {
|
|
18
20
|
return (
|
19
21
|
<Typography component='div' variant='body1' className={classes.root} sx={sx}>
|
20
22
|
{regular_price.value !== final_price.value && (
|
21
|
-
<
|
23
|
+
<InContextMask
|
22
24
|
component='span'
|
23
25
|
sx={{
|
24
26
|
textDecoration: 'line-through',
|
25
27
|
color: 'text.disabled',
|
26
28
|
marginRight: '8px',
|
27
29
|
}}
|
30
|
+
skeleton={{ width: '3.5em' }}
|
28
31
|
className={classes.discountPrice}
|
29
32
|
>
|
30
33
|
<Money {...regular_price} />
|
31
|
-
</
|
34
|
+
</InContextMask>
|
32
35
|
)}
|
33
|
-
<
|
36
|
+
<InContextMask className={classes.finalPrice} component='span' skeleton={{ width: '3.5em' }}>
|
37
|
+
<Money {...final_price} />
|
38
|
+
</InContextMask>
|
34
39
|
</Typography>
|
35
40
|
)
|
36
41
|
}
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import { ListFormat, filterNonNullableKeys } from '@graphcommerce/next-ui'
|
2
|
+
import { Trans } from '@lingui/macro'
|
3
|
+
import { Box, Link } from '@mui/material'
|
4
|
+
import { productListLinkFromFilter } from '../../hooks/useProductListLink'
|
5
|
+
import { useProductFiltersPro } from '../ProductFiltersPro'
|
6
|
+
import { ProductListSuggestionsFragment } from './ProductListSuggestions.gql'
|
7
|
+
|
8
|
+
type ProductListSuggestionsProps = {
|
9
|
+
products: ProductListSuggestionsFragment
|
10
|
+
}
|
11
|
+
|
12
|
+
export function ProductListSuggestions(props: ProductListSuggestionsProps) {
|
13
|
+
const { products } = props
|
14
|
+
|
15
|
+
const { form, submit, params } = useProductFiltersPro()
|
16
|
+
|
17
|
+
if (!products.suggestions || !products.suggestions.length) return null
|
18
|
+
|
19
|
+
const list = (
|
20
|
+
<ListFormat listStyle='short' type='disjunction'>
|
21
|
+
{filterNonNullableKeys(products.suggestions).map((s) => (
|
22
|
+
<Link
|
23
|
+
key={s.search}
|
24
|
+
href={productListLinkFromFilter({ ...params, search: s.search })}
|
25
|
+
onClick={() => {
|
26
|
+
form.setValue('currentPage', 1)
|
27
|
+
form.setValue('search', s.search)
|
28
|
+
return submit()
|
29
|
+
}}
|
30
|
+
>
|
31
|
+
{s.search}
|
32
|
+
</Link>
|
33
|
+
))}
|
34
|
+
</ListFormat>
|
35
|
+
)
|
36
|
+
|
37
|
+
return (
|
38
|
+
<Box>
|
39
|
+
<Trans>Did you mean: {list}</Trans>
|
40
|
+
</Box>
|
41
|
+
)
|
42
|
+
}
|
@@ -26,7 +26,7 @@ export function ProductPageDescription(props: ProductPageDescriptionProps) {
|
|
26
26
|
const { product, right, fontSize = 'subtitle1', maxWidth = 'lg', sx = [] } = props
|
27
27
|
|
28
28
|
return (
|
29
|
-
<LazyHydrate>
|
29
|
+
<LazyHydrate height={500}>
|
30
30
|
<ColumnTwoWithTop
|
31
31
|
maxWidth={maxWidth}
|
32
32
|
className={classes.root}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { useWatch } from '@graphcommerce/ecommerce-ui'
|
2
|
+
import { InContextMask } from '@graphcommerce/graphql'
|
2
3
|
import { Money } from '@graphcommerce/magento-store'
|
3
4
|
import { extendableComponent } from '@graphcommerce/next-ui'
|
4
|
-
import { Box } from '@mui/material'
|
5
5
|
import { AddToCartItemSelector, useFormAddProductsToCart } from '../AddProductsToCart'
|
6
6
|
import { ProductPagePriceFragment } from './ProductPagePrice.gql'
|
7
7
|
import { getProductTierPrice } from './getProductTierPrice'
|
@@ -13,7 +13,10 @@ import {
|
|
13
13
|
export type ProductPagePriceProps = { product: ProductPagePriceFragment } & AddToCartItemSelector &
|
14
14
|
UseCustomizableOptionPriceProps
|
15
15
|
|
16
|
-
const { classes } = extendableComponent('ProductPagePrice', [
|
16
|
+
const { classes } = extendableComponent('ProductPagePrice', [
|
17
|
+
'finalPrice',
|
18
|
+
'discountPrice',
|
19
|
+
] as const)
|
17
20
|
|
18
21
|
export function ProductPagePrice(props: ProductPagePriceProps) {
|
19
22
|
const { product, index = 0 } = props
|
@@ -24,23 +27,27 @@ export function ProductPagePrice(props: ProductPagePriceProps) {
|
|
24
27
|
getProductTierPrice(product, quantity) ?? product.price_range.minimum_price.final_price
|
25
28
|
|
26
29
|
const priceValue = useCustomizableOptionPrice(props)
|
30
|
+
const regularPrice = product.price_range.minimum_price.regular_price
|
27
31
|
|
28
32
|
return (
|
29
33
|
<>
|
30
|
-
{
|
31
|
-
<
|
34
|
+
{regularPrice.value !== price.value && (
|
35
|
+
<InContextMask
|
32
36
|
component='span'
|
33
|
-
sx={{
|
34
|
-
textDecoration: 'line-through',
|
35
|
-
color: 'text.disabled',
|
36
|
-
marginRight: '8px',
|
37
|
-
}}
|
38
37
|
className={classes.discountPrice}
|
38
|
+
skeleton={{ variant: 'text', sx: { width: '3em', transform: 'none' } }}
|
39
|
+
sx={[{ textDecoration: 'line-through', color: 'text.disabled', marginRight: '8px' }]}
|
39
40
|
>
|
40
|
-
<Money {...
|
41
|
-
</
|
41
|
+
<Money {...regularPrice} />
|
42
|
+
</InContextMask>
|
42
43
|
)}
|
43
|
-
<
|
44
|
+
<InContextMask
|
45
|
+
component='span'
|
46
|
+
skeleton={{ variant: 'text', sx: { width: '3em', transform: 'none' } }}
|
47
|
+
className={classes.finalPrice}
|
48
|
+
>
|
49
|
+
<Money {...price} value={priceValue} />
|
50
|
+
</InContextMask>
|
44
51
|
</>
|
45
52
|
)
|
46
53
|
}
|
@@ -1,7 +1,8 @@
|
|
1
|
+
import { InContextMask } from '@graphcommerce/graphql'
|
1
2
|
import { Money } from '@graphcommerce/magento-store'
|
2
3
|
import { filterNonNullableKeys } from '@graphcommerce/next-ui'
|
3
4
|
import { Trans } from '@lingui/react'
|
4
|
-
import {
|
5
|
+
import { SxProps, Theme } from '@mui/material'
|
5
6
|
import { ProductPagePriceFragment } from './ProductPagePrice.gql'
|
6
7
|
|
7
8
|
export type ProductPagePriceTiersProps = {
|
@@ -21,7 +22,7 @@ export function ProductPagePriceTiers(props: ProductPagePriceTiersProps) {
|
|
21
22
|
if (!priceTiers.length) return null
|
22
23
|
|
23
24
|
return (
|
24
|
-
<
|
25
|
+
<InContextMask sx={sx} variant='rectangular'>
|
25
26
|
{priceTiers.map(({ quantity, final_price, discount }) => (
|
26
27
|
<div key={quantity}>
|
27
28
|
<Trans
|
@@ -31,6 +32,6 @@ export function ProductPagePriceTiers(props: ProductPagePriceTiersProps) {
|
|
31
32
|
/>
|
32
33
|
</div>
|
33
34
|
))}
|
34
|
-
</
|
35
|
+
</InContextMask>
|
35
36
|
)
|
36
37
|
}
|
@@ -1,19 +1,22 @@
|
|
1
1
|
import { useQuery } from '@graphcommerce/graphql'
|
2
2
|
import { StoreConfigDocument } from '@graphcommerce/magento-store'
|
3
|
-
import {
|
3
|
+
import { UnitFormat, UnitFormatProps } from '@graphcommerce/next-ui'
|
4
4
|
import { ProductWeightFragment } from './ProductWeight.gql'
|
5
5
|
|
6
|
-
export
|
7
|
-
|
6
|
+
export type ProductWeightProps = Omit<UnitFormatProps, 'unit'> & { product: ProductWeightFragment }
|
7
|
+
|
8
|
+
export function ProductWeight(props: ProductWeightProps) {
|
9
|
+
const { product, ...rest } = props
|
10
|
+
|
8
11
|
const { data: conf } = useQuery(StoreConfigDocument)
|
9
|
-
const unit = conf?.storeConfig?.weight_unit ?? ''
|
10
12
|
|
11
|
-
|
13
|
+
if (!product.weight) return null
|
14
|
+
|
15
|
+
const unit = conf?.storeConfig?.weight_unit === 'lbs' ? 'pound' : 'kilogram'
|
12
16
|
|
13
|
-
if (!numberFormatter || !weight) return null
|
14
17
|
return (
|
15
|
-
|
16
|
-
{
|
17
|
-
|
18
|
+
<UnitFormat unit={unit} {...rest}>
|
19
|
+
{product.weight}
|
20
|
+
</UnitFormat>
|
18
21
|
)
|
19
22
|
}
|
package/components/index.ts
CHANGED
@@ -41,3 +41,5 @@ export * from './ProductStaticPaths/getSitemapPaths'
|
|
41
41
|
export * from './ProductUpsells/UpsellProducts.gql'
|
42
42
|
export * from './ProductWeight/ProductWeight'
|
43
43
|
export * from './ProductListPrice'
|
44
|
+
export * from './ProductListSuggestions/ProductListSuggestions'
|
45
|
+
export * from './ProductListSuggestions/ProductListSuggestions.gql'
|
@@ -0,0 +1,123 @@
|
|
1
|
+
import { debounce } from '@graphcommerce/ecommerce-ui'
|
2
|
+
import {
|
3
|
+
ApolloQueryResult,
|
4
|
+
ApolloClient,
|
5
|
+
useQuery,
|
6
|
+
useInContextQuery,
|
7
|
+
getInContextInput,
|
8
|
+
} from '@graphcommerce/graphql'
|
9
|
+
import { StoreConfigDocument } from '@graphcommerce/magento-store'
|
10
|
+
import { showPageLoadIndicator } from '@graphcommerce/next-ui'
|
11
|
+
import { useEventCallback } from '@mui/material'
|
12
|
+
import { FilterFormProviderProps, ProductFiltersDocument } from '../components'
|
13
|
+
import {
|
14
|
+
ProductListDocument,
|
15
|
+
ProductListQuery,
|
16
|
+
ProductListQueryVariables,
|
17
|
+
} from '../components/ProductList/ProductList.gql'
|
18
|
+
import { CategoryDefaultFragment } from '../components/ProductListItems/CategoryDefault.gql'
|
19
|
+
import { ProductListParams, toProductListParams } from '../components/ProductListItems/filterTypes'
|
20
|
+
import { useRouterFilterParams } from '../components/ProductListItems/filteredProductList'
|
21
|
+
import {
|
22
|
+
productListApplyCategoryDefaults,
|
23
|
+
useProductListApplyCategoryDefaults,
|
24
|
+
} from '../components/ProductListItems/productListApplyCategoryDefaults'
|
25
|
+
|
26
|
+
const productListQueries: Array<Promise<any>> = []
|
27
|
+
|
28
|
+
type Next = Parameters<NonNullable<FilterFormProviderProps['handleSubmit']>>[1]
|
29
|
+
|
30
|
+
export const prefetchProductList = debounce(
|
31
|
+
async (
|
32
|
+
variables: ProductListQueryVariables,
|
33
|
+
next: Next,
|
34
|
+
client: ApolloClient<any>,
|
35
|
+
shallow: boolean,
|
36
|
+
) => {
|
37
|
+
if (!shallow) return next(shallow)
|
38
|
+
|
39
|
+
showPageLoadIndicator.set(true)
|
40
|
+
|
41
|
+
const context = getInContextInput(client)
|
42
|
+
const productList = client.query({
|
43
|
+
query: ProductListDocument,
|
44
|
+
variables: { ...variables, context },
|
45
|
+
})
|
46
|
+
|
47
|
+
const productFilters = client.query({
|
48
|
+
query: ProductFiltersDocument,
|
49
|
+
variables: {
|
50
|
+
filters: { category_uid: variables.filters?.category_uid },
|
51
|
+
search: variables.search,
|
52
|
+
context,
|
53
|
+
},
|
54
|
+
})
|
55
|
+
|
56
|
+
const both = Promise.all([productList, productFilters])
|
57
|
+
|
58
|
+
// Push the query to the queue array.
|
59
|
+
productListQueries.push(both)
|
60
|
+
|
61
|
+
// Since we're waiting here the form will be submitting for longer.
|
62
|
+
await both
|
63
|
+
|
64
|
+
const includes = productListQueries.includes(both)
|
65
|
+
|
66
|
+
// Remove all requests that are before the current request
|
67
|
+
const index = productListQueries.indexOf(both)
|
68
|
+
if (index > -1) {
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
70
|
+
productListQueries.splice(0, index + 1)
|
71
|
+
}
|
72
|
+
|
73
|
+
if (productListQueries.length === 0) showPageLoadIndicator.set(false)
|
74
|
+
|
75
|
+
if (includes) {
|
76
|
+
// todo: When navigating a category, it should now be a shallow route
|
77
|
+
|
78
|
+
// If the resolved request is still in the array, it may be rendered (URL may be updated)
|
79
|
+
await next(shallow)
|
80
|
+
}
|
81
|
+
|
82
|
+
return undefined
|
83
|
+
},
|
84
|
+
200,
|
85
|
+
// the maxWait is now set to a somewhat shorter time than the average query time.
|
86
|
+
{ leading: true, maxWait: 700, trailing: true },
|
87
|
+
)
|
88
|
+
|
89
|
+
/**
|
90
|
+
* - Handles shallow routing requests
|
91
|
+
* - Handles customer specific product list queries
|
92
|
+
*/
|
93
|
+
export function useProductList<
|
94
|
+
T extends ProductListQuery & {
|
95
|
+
params?: ProductListParams
|
96
|
+
category?: CategoryDefaultFragment | null | undefined
|
97
|
+
},
|
98
|
+
>(props: T) {
|
99
|
+
const { category } = props
|
100
|
+
const { params, shallow } = useRouterFilterParams(props)
|
101
|
+
const variables = useProductListApplyCategoryDefaults(params, category)
|
102
|
+
|
103
|
+
const result = useInContextQuery(ProductListDocument, { variables, skip: !shallow }, props)
|
104
|
+
const storeConfig = useQuery(StoreConfigDocument).data
|
105
|
+
|
106
|
+
const handleSubmit: NonNullable<FilterFormProviderProps['handleSubmit']> = useEventCallback(
|
107
|
+
async (formValues, next) => {
|
108
|
+
if (!storeConfig) return
|
109
|
+
|
110
|
+
const vars = await productListApplyCategoryDefaults(
|
111
|
+
toProductListParams(formValues),
|
112
|
+
storeConfig,
|
113
|
+
category,
|
114
|
+
)
|
115
|
+
|
116
|
+
const shallowNow =
|
117
|
+
JSON.stringify(vars.filters?.category_uid) === JSON.stringify(params?.filters.category_uid)
|
118
|
+
await prefetchProductList(vars, next, result.client, shallowNow)
|
119
|
+
},
|
120
|
+
)
|
121
|
+
|
122
|
+
return { ...props, ...result.data, params, mask: result.mask, handleSubmit }
|
123
|
+
}
|