@graphcommerce/magento-product 8.1.0-canary.9 → 9.0.0-canary.55

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 (78) hide show
  1. package/Api/ProductListItem.graphql +1 -2
  2. package/Api/ProductPageItem.graphql +1 -1
  3. package/CHANGELOG.md +113 -0
  4. package/Config.graphqls +13 -0
  5. package/components/AddProductsToCart/AddProductsToCartButton.tsx +3 -2
  6. package/components/AddProductsToCart/AddProductsToCartFab.tsx +2 -2
  7. package/components/AddProductsToCart/AddProductsToCartForm.tsx +31 -28
  8. package/components/AddProductsToCart/AddProductsToCartSnackbar.tsx +25 -16
  9. package/components/AddProductsToCart/UseAddProductsToCartAction.graphql +1 -1
  10. package/components/AddProductsToCart/findAddedItems.ts +1 -4
  11. package/components/AddProductsToCart/useAddProductsToCartAction.ts +2 -1
  12. package/components/AddProductsToCart/useFormAddProductsToCart.ts +1 -2
  13. package/components/JsonLdProduct/JsonLdProduct.graphql +1 -1
  14. package/components/JsonLdProduct/ProductPageJsonLd.tsx +1 -1
  15. package/components/ProductAddToCart/ProductAddToCart.tsx +6 -8
  16. package/components/ProductCustomizable/ProductCustomizable.graphql +1 -1
  17. package/components/ProductCustomizable/index.ts +1 -0
  18. package/components/ProductCustomizable/productCustomizableSelectors.ts +59 -0
  19. package/components/ProductFiltersPro/PriceSlider.tsx +1 -2
  20. package/components/ProductFiltersPro/ProductFilterEqualChip.tsx +1 -1
  21. package/components/ProductFiltersPro/ProductFilterEqualSection.tsx +2 -2
  22. package/components/ProductFiltersPro/ProductFilterRangeChip.tsx +1 -1
  23. package/components/ProductFiltersPro/ProductFilterRangeSection.tsx +1 -1
  24. package/components/ProductFiltersPro/ProductFiltersPro.tsx +103 -19
  25. package/components/ProductFiltersPro/ProductFiltersProAggregations.tsx +31 -18
  26. package/components/ProductFiltersPro/ProductFiltersProAllFiltersChip.tsx +6 -10
  27. package/components/ProductFiltersPro/ProductFiltersProAllFiltersSidebar.tsx +18 -8
  28. package/components/ProductFiltersPro/ProductFiltersProCategorySection.tsx +130 -0
  29. package/components/ProductFiltersPro/ProductFiltersProChips.tsx +10 -8
  30. package/components/ProductFiltersPro/ProductFiltersProClearAll.tsx +4 -16
  31. package/components/ProductFiltersPro/ProductFiltersProLayoutSidebar.tsx +15 -7
  32. package/components/ProductFiltersPro/ProductFiltersProLimitSection.tsx +5 -2
  33. package/components/ProductFiltersPro/ProductFiltersProNoResults.tsx +79 -0
  34. package/components/ProductFiltersPro/ProductFiltersProSortChip.tsx +1 -1
  35. package/components/ProductFiltersPro/ProductFiltersProSortDirectionArrow.tsx +2 -4
  36. package/components/ProductFiltersPro/ProductFiltersProSortSection.tsx +7 -2
  37. package/components/ProductFiltersPro/activeAggregations.ts +5 -9
  38. package/components/ProductFiltersPro/applyAggregationCount.ts +14 -8
  39. package/components/ProductFiltersPro/index.ts +9 -0
  40. package/components/ProductFiltersPro/{useClearAllFiltersHandler.ts → useProductFiltersProClearAllAction.ts} +1 -1
  41. package/components/ProductFiltersPro/useProductFiltersProHasFiltersApplied.ts +21 -0
  42. package/components/ProductFiltersPro/useProductFiltersProSort.tsx +4 -2
  43. package/components/ProductList/ProductList.graphql +8 -5
  44. package/components/ProductListCount/ProductListCount.tsx +3 -1
  45. package/components/ProductListFilters/ProductFilters.graphql +7 -2
  46. package/components/ProductListFilters/ProductListFilters.graphql +1 -1
  47. package/components/ProductListFiltersContainer/ProductListFiltersContainer.tsx +2 -4
  48. package/components/ProductListItem/ProductDiscountLabel.tsx +2 -3
  49. package/components/ProductListItem/ProductListItem.tsx +3 -3
  50. package/components/ProductListItem/ProductListItemTitleAndPrice.tsx +18 -15
  51. package/components/ProductListItems/ProductListItemsBase.tsx +65 -23
  52. package/components/ProductListItems/filterTypes.tsx +14 -5
  53. package/components/ProductListItems/filteredProductList.tsx +23 -0
  54. package/components/ProductListItems/productListApplyCategoryDefaults.ts +44 -4
  55. package/components/ProductListItems/renderer.tsx +8 -2
  56. package/components/ProductListPagination/ProductListPagination.tsx +39 -20
  57. package/components/ProductListPrice/ProductListPrice.tsx +9 -4
  58. package/components/ProductListSuggestions/ProductListSuggestions.graphql +5 -0
  59. package/components/ProductListSuggestions/ProductListSuggestions.tsx +42 -0
  60. package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.graphql +3 -0
  61. package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.tsx +3 -0
  62. package/components/ProductPageBreadcrumb/ProductPageBreadcrumbs.tsx +40 -0
  63. package/components/ProductPageBreadcrumb/index.ts +1 -0
  64. package/components/ProductPageDescription/ComplexTextValue.graphql +1 -1
  65. package/components/ProductPageDescription/ProductPageDescription.tsx +1 -1
  66. package/components/ProductPageGallery/ProductImage.graphql +1 -0
  67. package/components/ProductPageGallery/ProductPageGallery.tsx +14 -8
  68. package/components/ProductPagePrice/ProductPagePrice.graphql +0 -6
  69. package/components/ProductPagePrice/ProductPagePrice.tsx +19 -12
  70. package/components/ProductPagePrice/ProductPagePriceTiers.tsx +4 -3
  71. package/components/ProductPagePrice/useCustomizableOptionPrice.ts +11 -53
  72. package/components/ProductShortDescription/ProductShortDescription.tsx +2 -0
  73. package/components/ProductWeight/ProductWeight.tsx +12 -9
  74. package/components/index.ts +2 -0
  75. package/hooks/useProductList.ts +123 -0
  76. package/hooks/useProductListLink.ts +6 -3
  77. package/index.ts +1 -0
  78. package/package.json +14 -13
@@ -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 { 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'
@@ -16,23 +16,42 @@ export function ProductListPagination({
16
16
  }: ProductPaginationProps) {
17
17
  if (!page_info || !page_info.total_pages || !page_info.current_page) return null
18
18
 
19
- return (
20
- <Pagination
21
- count={page_info?.total_pages}
22
- page={page_info?.current_page ?? 1}
23
- renderLink={(_, icon, btnProps) => {
24
- const suffix = btnProps.page === 1 ? '' : `#products`
25
- return (
26
- <Link
27
- {...btnProps}
28
- href={`${productListLink({ ...params, currentPage: btnProps.page })}${suffix}`}
29
- color='inherit'
30
- >
31
- {icon}
32
- </Link>
33
- )
34
- }}
35
- {...paginationProps}
36
- />
37
- )
19
+ if (import.meta.graphCommerce.productListPaginationVariant !== 'EXTENDED') {
20
+ return (
21
+ <Pagination
22
+ count={page_info?.total_pages}
23
+ page={page_info?.current_page ?? 1}
24
+ renderLink={(_, icon, btnProps) => {
25
+ const suffix = btnProps.page === 1 ? '' : `#products`
26
+ return (
27
+ <Link
28
+ {...btnProps}
29
+ href={`${productListLink({ ...params, currentPage: btnProps.page })}${suffix}`}
30
+ component={NextLink}
31
+ shallow
32
+ color='inherit'
33
+ >
34
+ {icon}
35
+ </Link>
36
+ )
37
+ }}
38
+ {...paginationProps}
39
+ />
40
+ )
41
+ }
42
+
43
+ if (import.meta.graphCommerce.productListPaginationVariant === 'EXTENDED') {
44
+ return (
45
+ <PaginationExtended
46
+ count={page_info?.total_pages}
47
+ page={page_info?.current_page ?? 1}
48
+ paginationHref={({ page }) =>
49
+ `${productListLink({ ...params, currentPage: page })}${page === 1 ? '' : '#products'}`
50
+ }
51
+ {...paginationProps}
52
+ />
53
+ )
54
+ }
55
+
56
+ return null
38
57
  }
@@ -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
+ }
@@ -1,5 +1,8 @@
1
1
  fragment ProductPageBreadcrumb on ProductInterface {
2
+ __typename
3
+ uid
2
4
  name
5
+ url_key
3
6
  categories {
4
7
  uid
5
8
  name
@@ -8,6 +8,9 @@ import { ProductPageBreadcrumbFragment } from './ProductPageBreadcrumb.gql'
8
8
  type ProductPageBreadcrumbsProps = ProductPageBreadcrumbFragment &
9
9
  Omit<BreadcrumbsProps, 'children'>
10
10
 
11
+ /**
12
+ * @deprecated Please use ProductPageBreadcrumbs
13
+ */
11
14
  export function ProductPageBreadcrumb(props: ProductPageBreadcrumbsProps) {
12
15
  const { categories, name, ...breadcrumbProps } = props
13
16
  const prev = usePrevPageRouter()
@@ -0,0 +1,40 @@
1
+ import { usePrevPageRouter } from '@graphcommerce/framer-next-pages'
2
+ import { categoryToBreadcrumbs } from '@graphcommerce/magento-category'
3
+ import { Breadcrumbs } from '@graphcommerce/next-ui'
4
+ import { BreadcrumbsJsonLd } from '@graphcommerce/next-ui/Breadcrumbs/BreadcrumbsJsonLd'
5
+ import { jsonLdBreadcrumb } from '@graphcommerce/next-ui/Breadcrumbs/jsonLdBreadcrumb'
6
+ import { BreadcrumbsProps } from '@mui/material'
7
+ import { useRouter } from 'next/router'
8
+ import { BreadcrumbList } from 'schema-dts'
9
+ import { productPageCategory } from '../ProductPageCategory/productPageCategory'
10
+ import { ProductPageBreadcrumbFragment } from './ProductPageBreadcrumb.gql'
11
+ import { productLink } from '../../hooks/useProductLink'
12
+
13
+ export type ProductPageBreadcrumbsProps = Omit<BreadcrumbsProps, 'children'> & {
14
+ breadcrumbsAmount?: number
15
+ product: ProductPageBreadcrumbFragment
16
+ }
17
+
18
+ export function ProductPageBreadcrumbs(props: ProductPageBreadcrumbsProps) {
19
+ const { product, ...breadcrumbsProps } = props
20
+ const { categories } = product
21
+ const prev = usePrevPageRouter()
22
+ const router = useRouter()
23
+
24
+ const category =
25
+ categories?.find((c) => `/${c?.url_path}` === prev?.asPath) ?? productPageCategory(product)
26
+
27
+ if (!category || !product.name || !product.url_key) return null
28
+
29
+ const breadcrumbs = categoryToBreadcrumbs(category)
30
+
31
+ return (
32
+ <>
33
+ <BreadcrumbsJsonLd<BreadcrumbList>
34
+ breadcrumbs={[...breadcrumbs, { name: product.name, href: productLink(product) }]}
35
+ render={(bc) => ({ '@context': 'https://schema.org', ...jsonLdBreadcrumb(bc, router) })}
36
+ />
37
+ <Breadcrumbs breadcrumbs={breadcrumbs} lastIsLink {...breadcrumbsProps} />
38
+ </>
39
+ )
40
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './ProductPageBreadcrumb.gql'
2
2
  export * from './ProductPageBreadcrumb'
3
+ export * from './ProductPageBreadcrumbs'
@@ -1,4 +1,4 @@
1
- fragment ComplexTextValue on ComplexTextValue @injectable {
1
+ fragment ComplexTextValue on ComplexTextValue {
2
2
  html
3
3
  __typename
4
4
  }
@@ -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,4 +1,5 @@
1
1
  fragment ProductImage on ProductImage {
2
2
  url
3
3
  label
4
+ disabled
4
5
  }
@@ -22,15 +22,21 @@ export function ProductPageGallery(props: ProductPageGalleryProps) {
22
22
  const images =
23
23
  media_gallery
24
24
  ?.filter(nonNullable)
25
+ .filter((p) => p.disabled !== true)
25
26
  .sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
26
- .map((item) => {
27
- if (item.__typename === 'ProductImage')
28
- return { src: item.url ?? '', alt: item.label || undefined, width, height }
29
- return {
30
- src: '',
31
- alt: `{${item.__typename} not yet supported}`,
32
- }
33
- }) ?? []
27
+ .map((item) =>
28
+ item.__typename === 'ProductImage'
29
+ ? {
30
+ src: item.url ?? '',
31
+ alt: item.label || undefined,
32
+ width,
33
+ height,
34
+ }
35
+ : {
36
+ src: '',
37
+ alt: `{${item.__typename} not yet supported}`,
38
+ },
39
+ ) ?? []
34
40
 
35
41
  return (
36
42
  <SidebarGallery
@@ -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,65 +1,23 @@
1
1
  import { useWatch } from '@graphcommerce/ecommerce-ui'
2
- import { PriceTypeEnum } from '@graphcommerce/graphql-mesh'
3
2
  import { MoneyFragment } from '@graphcommerce/magento-store'
4
3
  import { filterNonNullableKeys, isTypename, nonNullable } from '@graphcommerce/next-ui'
5
- import type { Simplify } from 'type-fest'
6
4
  import { AddToCartItemSelector, useFormAddProductsToCart } from '../AddProductsToCart'
7
- import type { CustomizableAreaOptionFragment } from '../ProductCustomizable/CustomizableAreaOption.gql'
8
- import type { CustomizableCheckboxOptionFragment } from '../ProductCustomizable/CustomizableCheckboxOption.gql'
9
- import type { CustomizableDateOptionFragment } from '../ProductCustomizable/CustomizableDateOption.gql'
10
- import type { CustomizableDropDownOptionFragment } from '../ProductCustomizable/CustomizableDropDownOption.gql'
11
- import type { CustomizableFieldOptionFragment } from '../ProductCustomizable/CustomizableFieldOption.gql'
12
- import type { CustomizableFileOptionFragment } from '../ProductCustomizable/CustomizableFileOption.gql'
13
- import type { CustomizableMultipleOptionFragment } from '../ProductCustomizable/CustomizableMultipleOption.gql'
14
- import { CustomizableRadioOptionFragment } from '../ProductCustomizable/CustomizableRadioOption.gql'
15
- import { ProductCustomizable_SimpleProduct_Fragment } from '../ProductCustomizable/ProductCustomizable.gql'
5
+ import {
6
+ productCustomizableSelectors,
7
+ CustomizableProductOptionBase,
8
+ OptionValueSelector,
9
+ AnyOption,
10
+ SelectorsProp,
11
+ } from '../ProductCustomizable/productCustomizableSelectors'
16
12
  import { ProductPagePriceFragment } from './ProductPagePrice.gql'
17
13
  import { getProductTierPrice } from './getProductTierPrice'
18
14
 
19
- type AnyOption = NonNullable<
20
- NonNullable<ProductCustomizable_SimpleProduct_Fragment['options']>[number]
21
- >
22
- type OptionValueSelector = {
23
- [T in AnyOption as T['__typename']]: (option: T) => Option | Option[]
24
- }
25
-
26
- const defaultSelectors = {
27
- CustomizableAreaOption: (o: CustomizableAreaOptionFragment) => o.areaValue,
28
- CustomizableCheckboxOption: (o: CustomizableCheckboxOptionFragment) => o.checkboxValue,
29
- CustomizableFileOption: (o: CustomizableFileOptionFragment) => o.fileValue,
30
- CustomizableDateOption: (o: CustomizableDateOptionFragment) => o.dateValue,
31
- CustomizableDropDownOption: (o: CustomizableDropDownOptionFragment) => o.dropdownValue,
32
- CustomizableFieldOption: (o: CustomizableFieldOptionFragment) => o.fieldValue,
33
- CustomizableMultipleOption: (o: CustomizableMultipleOptionFragment) => o.multipleValue,
34
- CustomizableRadioOption: (o: CustomizableRadioOptionFragment) => o.radioValue,
35
- }
36
-
37
- type MissingOptionValueSelectors = Omit<OptionValueSelector, keyof typeof defaultSelectors>
38
- type DefinedOptionValueSelectors = Partial<Pick<OptionValueSelector, keyof typeof defaultSelectors>>
39
-
40
- type Selectors = Simplify<
41
- keyof MissingOptionValueSelectors extends never
42
- ? (MissingOptionValueSelectors & DefinedOptionValueSelectors) | undefined
43
- : MissingOptionValueSelectors & DefinedOptionValueSelectors
44
- >
45
-
46
15
  export type UseCustomizableOptionPriceProps = {
47
16
  product: ProductPagePriceFragment
48
17
  } & AddToCartItemSelector &
49
- (keyof MissingOptionValueSelectors extends never
50
- ? { selectors?: Selectors }
51
- : { selectors: Selectors })
52
-
53
- type Option =
54
- | {
55
- price?: number | null | undefined
56
- price_type?: PriceTypeEnum | null | undefined
57
- uid?: string | null | undefined
58
- }
59
- | undefined
60
- | null
18
+ SelectorsProp
61
19
 
62
- function calcOptionPrice(option: Option, product: MoneyFragment) {
20
+ function calcOptionPrice(option: CustomizableProductOptionBase, product: MoneyFragment) {
63
21
  if (!option?.price) return 0
64
22
  switch (option.price_type) {
65
23
  case 'DYNAMIC':
@@ -81,7 +39,7 @@ export function useCustomizableOptionPrice(props: UseCustomizableOptionPriceProp
81
39
  getProductTierPrice(product, cartItem?.quantity) ??
82
40
  product.price_range.minimum_price.final_price
83
41
 
84
- const allSelectors: OptionValueSelector = { ...defaultSelectors, ...selectors }
42
+ const allSelectors: OptionValueSelector = { ...productCustomizableSelectors, ...selectors }
85
43
 
86
44
  if (isTypename(product, ['GroupedProduct'])) return price.value
87
45
  if (!product.options || product.options.length === 0) return price.value
@@ -95,7 +53,7 @@ export function useCustomizableOptionPrice(props: UseCustomizableOptionPriceProp
95
53
 
96
54
  const selector = allSelectors[productOption.__typename] as
97
55
  | undefined
98
- | ((option: AnyOption) => Option | Option[])
56
+ | ((option: AnyOption) => CustomizableProductOptionBase | CustomizableProductOptionBase[])
99
57
  const value = selector ? selector(productOption) : null
100
58
 
101
59
  if (!value) return 0
@@ -13,6 +13,8 @@ export function ProductShortDescription(props: ProductShortDescriptionProps) {
13
13
  const { product, sx = [] } = props
14
14
  const { short_description } = product
15
15
 
16
+ if (!short_description?.html) return null
17
+
16
18
  return (
17
19
  <Typography
18
20
  variant='body1'
@@ -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'