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

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 (93) hide show
  1. package/Api/ProductListItem.graphql +1 -2
  2. package/Api/ProductPageItem.graphql +1 -1
  3. package/CHANGELOG.md +276 -84
  4. package/Config.graphqls +13 -0
  5. package/components/AddProductsToCart/AddProductsToCartButton.tsx +17 -4
  6. package/components/AddProductsToCart/AddProductsToCartFab.tsx +7 -2
  7. package/components/AddProductsToCart/AddProductsToCartForm.tsx +31 -29
  8. package/components/AddProductsToCart/AddProductsToCartSnackbar.tsx +14 -63
  9. package/components/AddProductsToCart/AddProductsToCartSnackbarMessage.tsx +84 -0
  10. package/components/AddProductsToCart/UseAddProductsToCartAction.graphql +1 -1
  11. package/components/AddProductsToCart/findAddedItems.ts +1 -4
  12. package/components/AddProductsToCart/index.ts +1 -0
  13. package/components/AddProductsToCart/useAddProductsToCartAction.ts +2 -1
  14. package/components/AddProductsToCart/useFormAddProductsToCart.ts +1 -2
  15. package/components/JsonLdProduct/JsonLdProduct.graphql +1 -1
  16. package/components/JsonLdProduct/ProductPageJsonLd.tsx +1 -1
  17. package/components/ProductAddToCart/ProductAddToCart.tsx +6 -8
  18. package/components/ProductCustomizable/CustomizableCheckboxOption.tsx +3 -4
  19. package/components/ProductCustomizable/CustomizableMultipleOption.tsx +2 -2
  20. package/components/ProductCustomizable/CustomizableRadioOption.tsx +2 -2
  21. package/components/ProductCustomizable/ProductCustomizable.graphql +1 -1
  22. package/components/ProductCustomizable/index.ts +1 -0
  23. package/components/ProductCustomizable/productCustomizableSelectors.ts +59 -0
  24. package/components/ProductFiltersPro/PriceSlider.tsx +1 -2
  25. package/components/ProductFiltersPro/ProductFilterEqualChip.tsx +4 -5
  26. package/components/ProductFiltersPro/ProductFilterEqualSection.tsx +6 -7
  27. package/components/ProductFiltersPro/ProductFilterRangeChip.tsx +1 -1
  28. package/components/ProductFiltersPro/ProductFilterRangeSection.tsx +1 -1
  29. package/components/ProductFiltersPro/ProductFiltersPro.tsx +103 -19
  30. package/components/ProductFiltersPro/ProductFiltersProAggregations.tsx +41 -20
  31. package/components/ProductFiltersPro/ProductFiltersProAllFiltersChip.tsx +6 -10
  32. package/components/ProductFiltersPro/ProductFiltersProAllFiltersSidebar.tsx +18 -8
  33. package/components/ProductFiltersPro/ProductFiltersProCategorySection.tsx +130 -0
  34. package/components/ProductFiltersPro/ProductFiltersProChips.tsx +10 -8
  35. package/components/ProductFiltersPro/ProductFiltersProClearAll.tsx +4 -16
  36. package/components/ProductFiltersPro/ProductFiltersProLayoutSidebar.tsx +15 -7
  37. package/components/ProductFiltersPro/ProductFiltersProLimitChip.tsx +2 -8
  38. package/components/ProductFiltersPro/ProductFiltersProLimitSection.tsx +7 -10
  39. package/components/ProductFiltersPro/ProductFiltersProNoResults.tsx +79 -0
  40. package/components/ProductFiltersPro/ProductFiltersProSortChip.tsx +5 -7
  41. package/components/ProductFiltersPro/ProductFiltersProSortDirectionArrow.tsx +2 -4
  42. package/components/ProductFiltersPro/ProductFiltersProSortSection.tsx +11 -3
  43. package/components/ProductFiltersPro/activeAggregations.ts +5 -9
  44. package/components/ProductFiltersPro/applyAggregationCount.ts +14 -8
  45. package/components/ProductFiltersPro/index.ts +9 -0
  46. package/components/ProductFiltersPro/{useClearAllFiltersHandler.ts → useProductFiltersProClearAllAction.ts} +1 -1
  47. package/components/ProductFiltersPro/useProductFiltersProHasFiltersApplied.ts +21 -0
  48. package/components/ProductFiltersPro/useProductFiltersProSort.tsx +7 -3
  49. package/components/ProductList/ProductList.graphql +8 -5
  50. package/components/ProductListCount/ProductListCount.tsx +3 -1
  51. package/components/ProductListFilters/ProductFilters.graphql +11 -2
  52. package/components/ProductListFilters/ProductListFilters.graphql +1 -1
  53. package/components/ProductListFilters/ProductListFilters.tsx +13 -19
  54. package/components/ProductListFiltersContainer/ProductListFiltersContainer.tsx +2 -4
  55. package/components/ProductListItem/ProductDiscountLabel.tsx +2 -3
  56. package/components/ProductListItem/ProductListItem.tsx +3 -3
  57. package/components/ProductListItem/ProductListItemTitleAndPrice.tsx +18 -15
  58. package/components/ProductListItems/ProductFilterTypes.graphql +8 -0
  59. package/components/ProductListItems/ProductListItemsBase.tsx +71 -30
  60. package/components/ProductListItems/filterTypes.tsx +14 -7
  61. package/components/ProductListItems/filteredProductList.tsx +44 -17
  62. package/components/ProductListItems/getFilterTypes.ts +33 -4
  63. package/components/ProductListItems/productListApplyCategoryDefaults.ts +50 -4
  64. package/components/ProductListItems/renderer.tsx +8 -2
  65. package/components/ProductListPagination/ProductListPagination.tsx +39 -20
  66. package/components/ProductListPrice/ProductListPrice.tsx +9 -4
  67. package/components/ProductListSuggestions/ProductListSuggestions.graphql +5 -0
  68. package/components/ProductListSuggestions/ProductListSuggestions.tsx +42 -0
  69. package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.graphql +3 -0
  70. package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.tsx +3 -0
  71. package/components/ProductPageBreadcrumb/ProductPageBreadcrumbs.tsx +40 -0
  72. package/components/ProductPageBreadcrumb/index.ts +1 -0
  73. package/components/ProductPageDescription/ComplexTextValue.graphql +1 -1
  74. package/components/ProductPageDescription/ProductPageDescription.tsx +1 -1
  75. package/components/ProductPageGallery/ProductImage.graphql +1 -0
  76. package/components/ProductPageGallery/ProductPageGallery.tsx +14 -8
  77. package/components/ProductPagePrice/ProductPagePrice.graphql +0 -6
  78. package/components/ProductPagePrice/ProductPagePrice.tsx +19 -12
  79. package/components/ProductPagePrice/ProductPagePriceTiers.tsx +4 -3
  80. package/components/ProductPagePrice/useCustomizableOptionPrice.ts +11 -53
  81. package/components/ProductShortDescription/ProductShortDescription.tsx +2 -0
  82. package/components/ProductSpecs/ProductSpecs.graphql +21 -1
  83. package/components/ProductSpecs/ProductSpecs.tsx +5 -11
  84. package/components/ProductSpecs/ProductSpecsAggregations.tsx +34 -0
  85. package/components/ProductSpecs/ProductSpecsCustomAttributes.tsx +45 -0
  86. package/components/ProductSpecs/ProductSpecsTypes.graphql +8 -0
  87. package/components/ProductStaticPaths/getProductStaticPaths.ts +1 -1
  88. package/components/ProductWeight/ProductWeight.tsx +12 -9
  89. package/components/index.ts +2 -0
  90. package/hooks/useProductList.ts +148 -0
  91. package/hooks/useProductListLink.ts +6 -3
  92. package/index.ts +1 -0
  93. package/package.json +14 -14
@@ -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 { productLink } from '../../hooks/useProductLink'
10
+ import { productPageCategory } from '../ProductPageCategory/productPageCategory'
11
+ import { ProductPageBreadcrumbFragment } from './ProductPageBreadcrumb.gql'
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,5 +1,5 @@
1
1
  fragment ProductSpecs on Products {
2
- aggregations {
2
+ aggregations @skip(if: $useCustomAttributes) {
3
3
  attribute_code
4
4
  count
5
5
  label
@@ -9,4 +9,24 @@ fragment ProductSpecs on Products {
9
9
  value
10
10
  }
11
11
  }
12
+ items {
13
+ __typename
14
+ uid
15
+ ...ProductListItem
16
+ custom_attributesV2(filters: { is_visible_on_front: true }) @include(if: $useCustomAttributes) {
17
+ items {
18
+ code
19
+ __typename
20
+ ... on AttributeValue {
21
+ value
22
+ }
23
+ ... on AttributeSelectedOptions {
24
+ selected_options {
25
+ label
26
+ value
27
+ }
28
+ }
29
+ }
30
+ }
31
+ }
12
32
  }
@@ -1,6 +1,8 @@
1
1
  import { responsiveVal, Row, SectionContainer, extendableComponent } from '@graphcommerce/next-ui'
2
2
  import { Box, SxProps, Theme } from '@mui/material'
3
3
  import { ProductSpecsFragment } from './ProductSpecs.gql'
4
+ import { ProductSpecsAggregations } from './ProductSpecsAggregations'
5
+ import { ProductSpecsCustomAttributes } from './ProductSpecsCustomAttributes'
4
6
 
5
7
  export type ProductSpecsProps = ProductSpecsFragment & {
6
8
  title?: string
@@ -13,7 +15,7 @@ const parts = ['root', 'specs', 'options'] as const
13
15
  const { classes } = extendableComponent(name, parts)
14
16
 
15
17
  export function ProductSpecs(props: ProductSpecsProps) {
16
- const { aggregations, title, children, sx = [] } = props
18
+ const { aggregations, items, title, children, sx = [] } = props
17
19
  const filter = ['price', 'category_id', 'size', 'new', 'sale', 'color']
18
20
  const specs = aggregations?.filter(
19
21
  (attr) => !filter.includes(attr?.attribute_code ?? '') && attr?.options?.[0]?.value !== '0',
@@ -46,16 +48,8 @@ export function ProductSpecs(props: ProductSpecsProps) {
46
48
  },
47
49
  })}
48
50
  >
49
- {specs?.map((aggregation) => (
50
- <li key={aggregation?.attribute_code}>
51
- <div>{aggregation?.label}</div>
52
- <Box className={classes.options} sx={{ display: 'grid', gridAutoFlow: 'row' }}>
53
- {aggregation?.options?.map((option) => (
54
- <span key={option?.value}>{option?.label === '1' ? 'Yes' : option?.label}</span>
55
- ))}
56
- </Box>
57
- </li>
58
- ))}
51
+ {aggregations && <ProductSpecsAggregations aggregations={aggregations} />}
52
+ {items && <ProductSpecsCustomAttributes items={items} />}
59
53
  </Box>
60
54
  {children}
61
55
  </SectionContainer>
@@ -0,0 +1,34 @@
1
+ import { extendableComponent } from '@graphcommerce/next-ui'
2
+ import { Box } from '@mui/material'
3
+ import { ProductSpecsFragment } from './ProductSpecs.gql'
4
+
5
+ const name = 'ProductSpecs' as const
6
+ const parts = ['root', 'specs', 'options'] as const
7
+ const { classes } = extendableComponent(name, parts)
8
+
9
+ export type ProductSpecsAggregationsProps = Pick<ProductSpecsFragment, 'aggregations'>
10
+
11
+ export function ProductSpecsAggregations(props: ProductSpecsAggregationsProps) {
12
+ const { aggregations } = props
13
+ const filter = ['price', 'category_id', 'size', 'new', 'sale', 'color']
14
+ const specs = aggregations?.filter(
15
+ (attr) => !filter.includes(attr?.attribute_code ?? '') && attr?.options?.[0]?.value !== '0',
16
+ )
17
+
18
+ if (specs?.length === 0) return null
19
+
20
+ return (
21
+ <>
22
+ {specs?.map((aggregation) => (
23
+ <li key={aggregation?.attribute_code}>
24
+ <div>{aggregation?.label}</div>
25
+ <Box className={classes.options} sx={{ display: 'grid', gridAutoFlow: 'row' }}>
26
+ {aggregation?.options?.map((option) => (
27
+ <span key={option?.value}>{option?.label === '1' ? 'Yes' : option?.label}</span>
28
+ ))}
29
+ </Box>
30
+ </li>
31
+ ))}
32
+ </>
33
+ )
34
+ }
@@ -0,0 +1,45 @@
1
+ import { useQuery } from '@graphcommerce/graphql'
2
+ import { extendableComponent, ListFormat } from '@graphcommerce/next-ui'
3
+ import { Box } from '@mui/material'
4
+ import { ProductSpecsFragment } from './ProductSpecs.gql'
5
+ import { ProductSpecsTypesDocument } from './ProductSpecsTypes.gql'
6
+
7
+ const name = 'ProductSpecs' as const
8
+ const parts = ['root', 'specs', 'options'] as const
9
+ const { classes } = extendableComponent(name, parts)
10
+
11
+ export type ProductSpecsCustomAttributesProps = Pick<ProductSpecsFragment, 'items'>
12
+
13
+ export function ProductSpecsCustomAttributes(props: ProductSpecsCustomAttributesProps) {
14
+ const { items } = props
15
+
16
+ const specs = items?.[0]?.custom_attributesV2?.items
17
+
18
+ const productSpecsTypes = useQuery(ProductSpecsTypesDocument)
19
+
20
+ if (items?.length === 0) return null
21
+
22
+ return (
23
+ <>
24
+ {specs?.map((item) => (
25
+ <li key={item?.code}>
26
+ <div>
27
+ {productSpecsTypes?.data?.attributesList?.items?.find(
28
+ (type) => type?.code === item?.code,
29
+ )?.label ?? item?.code}
30
+ </div>
31
+ <Box className={classes.options}>
32
+ {item?.__typename === 'AttributeSelectedOptions' && (
33
+ <ListFormat listStyle='long' type='unit'>
34
+ {item?.selected_options?.map((option) => (
35
+ <span key={option?.value}>{option?.label === '1' ? 'Yes' : option?.label}</span>
36
+ ))}
37
+ </ListFormat>
38
+ )}
39
+ {item?.__typename === 'AttributeValue' && <span key={item?.value}>{item.value}</span>}
40
+ </Box>
41
+ </li>
42
+ ))}
43
+ </>
44
+ )
45
+ }
@@ -0,0 +1,8 @@
1
+ query ProductSpecsTypes {
2
+ attributesList(entityType: CATALOG_PRODUCT, filters: { is_visible_on_front: true }) {
3
+ items {
4
+ code
5
+ label
6
+ }
7
+ }
8
+ }
@@ -22,7 +22,7 @@ export async function getProductStaticPaths(
22
22
  const { data } = await query
23
23
  const totalPages = data.products?.page_info?.total_pages ?? 1
24
24
 
25
- if (totalPages > 1 && import.meta.graphCommerce.limitSsg !== true) {
25
+ if (totalPages > 1 && options.limit !== true) {
26
26
  for (let i = 2; i <= totalPages; i++) {
27
27
  pages.push(
28
28
  client.query({
@@ -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,148 @@
1
+ import { debounce } from '@graphcommerce/ecommerce-ui'
2
+ import {
3
+ ApolloClient,
4
+ useQuery,
5
+ useInContextQuery,
6
+ getInContextInput,
7
+ } from '@graphcommerce/graphql'
8
+ import { StoreConfigDocument } from '@graphcommerce/magento-store'
9
+ import { showPageLoadIndicator } from '@graphcommerce/next-ui'
10
+ import { useEventCallback } from '@mui/material'
11
+ import {
12
+ FilterFormProviderProps,
13
+ ProductFiltersDocument,
14
+ ProductFiltersQuery,
15
+ ProductFiltersQueryVariables,
16
+ } from '../components'
17
+ import {
18
+ ProductListDocument,
19
+ ProductListQuery,
20
+ ProductListQueryVariables,
21
+ } from '../components/ProductList/ProductList.gql'
22
+ import { CategoryDefaultFragment } from '../components/ProductListItems/CategoryDefault.gql'
23
+ import { ProductListParams, toProductListParams } from '../components/ProductListItems/filterTypes'
24
+ import { useRouterFilterParams } from '../components/ProductListItems/filteredProductList'
25
+ import {
26
+ productListApplyCategoryDefaults,
27
+ categoryDefaultsToProductListFilters,
28
+ useProductListApplyCategoryDefaults,
29
+ } from '../components/ProductListItems/productListApplyCategoryDefaults'
30
+
31
+ const productListQueries: Array<Promise<any>> = []
32
+
33
+ type Next = Parameters<NonNullable<FilterFormProviderProps['handleSubmit']>>[1]
34
+
35
+ export const prefetchProductList = debounce(
36
+ async (
37
+ variables: ProductListQueryVariables,
38
+ filtersVariables: ProductFiltersQueryVariables,
39
+ next: Next,
40
+ client: ApolloClient<any>,
41
+ shallow: boolean,
42
+ ) => {
43
+ if (!shallow) return next(shallow)
44
+
45
+ showPageLoadIndicator.set(true)
46
+
47
+ const context = getInContextInput(client)
48
+ const productList = client.query({
49
+ query: ProductListDocument,
50
+ variables: { ...variables, context },
51
+ })
52
+
53
+ // const productFilters = client.query({
54
+ // query: ProductFiltersDocument,
55
+ // variables: {
56
+ // ...filtersVariables,
57
+ // context,
58
+ // },
59
+ // })
60
+
61
+ const both = Promise.all([productList])
62
+
63
+ // Push the query to the queue array.
64
+ productListQueries.push(both)
65
+
66
+ // Since we're waiting here the form will be submitting for longer.
67
+ await both
68
+
69
+ const includes = productListQueries.includes(both)
70
+
71
+ // Remove all requests that are before the current request
72
+ const index = productListQueries.indexOf(both)
73
+ if (index > -1) {
74
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
75
+ productListQueries.splice(0, index + 1)
76
+ }
77
+
78
+ if (productListQueries.length === 0) showPageLoadIndicator.set(false)
79
+
80
+ if (includes) {
81
+ // todo: When navigating a category, it should now be a shallow route
82
+
83
+ // If the resolved request is still in the array, it may be rendered (URL may be updated)
84
+ await next(shallow)
85
+ }
86
+
87
+ return undefined
88
+ },
89
+ 200,
90
+ // the maxWait is now set to a somewhat shorter time than the average query time.
91
+ { leading: true, maxWait: 700, trailing: true },
92
+ )
93
+
94
+ /**
95
+ * - Handles shallow routing requests
96
+ * - Handles customer specific product list queries
97
+ */
98
+ export function useProductList<
99
+ T extends ProductListQuery &
100
+ ProductFiltersQuery & {
101
+ params?: ProductListParams
102
+ category?: CategoryDefaultFragment | null | undefined
103
+ },
104
+ >(props: T) {
105
+ const { category } = props
106
+ const { params, shallow } = useRouterFilterParams(props)
107
+ const variables = useProductListApplyCategoryDefaults(params, category)
108
+
109
+ const result = useInContextQuery(ProductListDocument, { variables, skip: !shallow }, props)
110
+ const filters = useInContextQuery(
111
+ ProductFiltersDocument,
112
+ { variables: categoryDefaultsToProductListFilters(variables), skip: !shallow },
113
+ props,
114
+ )
115
+
116
+ const storeConfig = useQuery(StoreConfigDocument).data
117
+
118
+ const handleSubmit: NonNullable<FilterFormProviderProps['handleSubmit']> = useEventCallback(
119
+ async (formValues, next) => {
120
+ if (!storeConfig) return
121
+
122
+ const vars = await productListApplyCategoryDefaults(
123
+ toProductListParams(formValues),
124
+ storeConfig,
125
+ category,
126
+ )
127
+
128
+ const shallowNow =
129
+ JSON.stringify(vars.filters?.category_uid) === JSON.stringify(params?.filters.category_uid)
130
+ await prefetchProductList(
131
+ vars,
132
+ categoryDefaultsToProductListFilters(vars),
133
+ next,
134
+ result.client,
135
+ shallowNow,
136
+ )
137
+ },
138
+ )
139
+
140
+ return {
141
+ ...props,
142
+ filters: filters.data.filters,
143
+ ...result.data,
144
+ params,
145
+ mask: result.mask,
146
+ handleSubmit,
147
+ }
148
+ }