@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.
Files changed (52) hide show
  1. package/Api/ProductListItem.graphql +0 -1
  2. package/CHANGELOG.md +4 -0
  3. package/components/AddProductsToCart/AddProductsToCartButton.tsx +2 -2
  4. package/components/AddProductsToCart/AddProductsToCartFab.tsx +2 -2
  5. package/components/AddProductsToCart/AddProductsToCartForm.tsx +24 -26
  6. package/components/AddProductsToCart/AddProductsToCartSnackbar.tsx +25 -16
  7. package/components/ProductAddToCart/ProductAddToCart.tsx +6 -8
  8. package/components/ProductFiltersPro/PriceSlider.tsx +1 -2
  9. package/components/ProductFiltersPro/ProductFilterEqualChip.tsx +1 -1
  10. package/components/ProductFiltersPro/ProductFilterEqualSection.tsx +2 -2
  11. package/components/ProductFiltersPro/ProductFilterRangeChip.tsx +1 -1
  12. package/components/ProductFiltersPro/ProductFilterRangeSection.tsx +1 -1
  13. package/components/ProductFiltersPro/ProductFiltersPro.tsx +79 -17
  14. package/components/ProductFiltersPro/ProductFiltersProAggregations.tsx +17 -18
  15. package/components/ProductFiltersPro/ProductFiltersProAllFiltersChip.tsx +2 -2
  16. package/components/ProductFiltersPro/ProductFiltersProCategorySection.tsx +99 -39
  17. package/components/ProductFiltersPro/ProductFiltersProClearAll.tsx +4 -16
  18. package/components/ProductFiltersPro/ProductFiltersProLimitSection.tsx +1 -1
  19. package/components/ProductFiltersPro/ProductFiltersProNoResults.tsx +79 -0
  20. package/components/ProductFiltersPro/ProductFiltersProSortChip.tsx +1 -1
  21. package/components/ProductFiltersPro/ProductFiltersProSortSection.tsx +1 -1
  22. package/components/ProductFiltersPro/activeAggregations.ts +5 -9
  23. package/components/ProductFiltersPro/applyAggregationCount.ts +14 -8
  24. package/components/ProductFiltersPro/index.ts +4 -1
  25. package/components/ProductFiltersPro/{useClearAllFiltersHandler.ts → useProductFiltersProClearAllAction.ts} +1 -1
  26. package/components/ProductFiltersPro/useProductFiltersProHasFiltersApplied.ts +21 -0
  27. package/components/ProductList/ProductList.graphql +8 -5
  28. package/components/ProductListCount/ProductListCount.tsx +3 -1
  29. package/components/ProductListFilters/ProductFilters.graphql +7 -2
  30. package/components/ProductListFilters/ProductListFilters.graphql +1 -1
  31. package/components/ProductListItem/ProductDiscountLabel.tsx +2 -3
  32. package/components/ProductListItem/ProductListItem.tsx +3 -3
  33. package/components/ProductListItem/ProductListItemTitleAndPrice.tsx +18 -15
  34. package/components/ProductListItems/ProductListItemsBase.tsx +65 -23
  35. package/components/ProductListItems/filterTypes.tsx +14 -5
  36. package/components/ProductListItems/filteredProductList.tsx +23 -0
  37. package/components/ProductListItems/productListApplyCategoryDefaults.ts +44 -4
  38. package/components/ProductListItems/renderer.tsx +8 -2
  39. package/components/ProductListPagination/ProductListPagination.tsx +3 -1
  40. package/components/ProductListPrice/ProductListPrice.tsx +9 -4
  41. package/components/ProductListSuggestions/ProductListSuggestions.graphql +5 -0
  42. package/components/ProductListSuggestions/ProductListSuggestions.tsx +42 -0
  43. package/components/ProductPageDescription/ProductPageDescription.tsx +1 -1
  44. package/components/ProductPagePrice/ProductPagePrice.graphql +0 -6
  45. package/components/ProductPagePrice/ProductPagePrice.tsx +19 -12
  46. package/components/ProductPagePrice/ProductPagePriceTiers.tsx +4 -3
  47. package/components/ProductWeight/ProductWeight.tsx +12 -9
  48. package/components/index.ts +2 -0
  49. package/hooks/useProductList.ts +123 -0
  50. package/hooks/useProductListLink.ts +6 -3
  51. package/index.ts +1 -0
  52. 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
- (theme) => ({
48
- display: 'grid',
49
- gridColumnGap: theme.spacings.md,
50
- gridRowGap: theme.spacings.md,
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 key={item.uid ?? ''} hydrated={loadingEager > idx ? true : undefined}>
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
- size === 'small'
73
- ? { 0: '100vw', 354: '50vw', 675: '30vw', 1255: '23vw', 1500: '337px' }
74
- : { 0: '100vw', 367: '48vw', 994: '30vw', 1590: '23vw', 1920: '443px' }
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 { StoreConfigQuery } from '@graphcommerce/magento-store'
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 { cloneDeep } from '@graphcommerce/graphql'
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: Promise<CategoryDefaultFragment | null | undefined>,
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, ProductListItemSkeleton } from '../ProductListItem/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: ProductListItemSkeleton,
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, Box } from '@mui/material'
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
- <Box
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
- </Box>
34
+ </InContextMask>
32
35
  )}
33
- <Money {...final_price} />
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,5 @@
1
+ fragment ProductListSuggestions on Products {
2
+ suggestions {
3
+ search
4
+ }
5
+ }
@@ -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}
@@ -13,12 +13,6 @@ fragment ProductPagePrice on ProductInterface {
13
13
  final_price {
14
14
  ...Money
15
15
  }
16
- fixed_product_taxes {
17
- amount {
18
- ...Money
19
- }
20
- label
21
- }
22
16
  }
23
17
  }
24
18
  price_tiers {
@@ -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', ['root', 'discountPrice'] as const)
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
- {product.price_range.minimum_price.regular_price.value !== price.value && (
31
- <Box
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 {...product.price_range.minimum_price.regular_price} />
41
- </Box>
41
+ <Money {...regularPrice} />
42
+ </InContextMask>
42
43
  )}
43
- <Money {...price} value={priceValue} />
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 { Box, SxProps, Theme } from '@mui/material'
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
- <Box sx={sx}>
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
- </Box>
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 { useNumberFormat } from '@graphcommerce/next-ui'
3
+ import { UnitFormat, UnitFormatProps } from '@graphcommerce/next-ui'
4
4
  import { ProductWeightFragment } from './ProductWeight.gql'
5
5
 
6
- export function ProductWeight(props: ProductWeightFragment) {
7
- const { weight } = props
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
- const numberFormatter = useNumberFormat()
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
- {numberFormatter.format(weight)} {unit}
17
- </>
18
+ <UnitFormat unit={unit} {...rest}>
19
+ {product.weight}
20
+ </UnitFormat>
18
21
  )
19
22
  }
@@ -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
+ }