@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.
- package/Api/ProductListItem.graphql +1 -2
- package/Api/ProductPageItem.graphql +1 -1
- package/CHANGELOG.md +276 -84
- package/Config.graphqls +13 -0
- package/components/AddProductsToCart/AddProductsToCartButton.tsx +17 -4
- package/components/AddProductsToCart/AddProductsToCartFab.tsx +7 -2
- package/components/AddProductsToCart/AddProductsToCartForm.tsx +31 -29
- package/components/AddProductsToCart/AddProductsToCartSnackbar.tsx +14 -63
- package/components/AddProductsToCart/AddProductsToCartSnackbarMessage.tsx +84 -0
- package/components/AddProductsToCart/UseAddProductsToCartAction.graphql +1 -1
- package/components/AddProductsToCart/findAddedItems.ts +1 -4
- package/components/AddProductsToCart/index.ts +1 -0
- package/components/AddProductsToCart/useAddProductsToCartAction.ts +2 -1
- package/components/AddProductsToCart/useFormAddProductsToCart.ts +1 -2
- package/components/JsonLdProduct/JsonLdProduct.graphql +1 -1
- package/components/JsonLdProduct/ProductPageJsonLd.tsx +1 -1
- package/components/ProductAddToCart/ProductAddToCart.tsx +6 -8
- package/components/ProductCustomizable/CustomizableCheckboxOption.tsx +3 -4
- package/components/ProductCustomizable/CustomizableMultipleOption.tsx +2 -2
- package/components/ProductCustomizable/CustomizableRadioOption.tsx +2 -2
- package/components/ProductCustomizable/ProductCustomizable.graphql +1 -1
- package/components/ProductCustomizable/index.ts +1 -0
- package/components/ProductCustomizable/productCustomizableSelectors.ts +59 -0
- package/components/ProductFiltersPro/PriceSlider.tsx +1 -2
- package/components/ProductFiltersPro/ProductFilterEqualChip.tsx +4 -5
- package/components/ProductFiltersPro/ProductFilterEqualSection.tsx +6 -7
- package/components/ProductFiltersPro/ProductFilterRangeChip.tsx +1 -1
- package/components/ProductFiltersPro/ProductFilterRangeSection.tsx +1 -1
- package/components/ProductFiltersPro/ProductFiltersPro.tsx +103 -19
- package/components/ProductFiltersPro/ProductFiltersProAggregations.tsx +41 -20
- package/components/ProductFiltersPro/ProductFiltersProAllFiltersChip.tsx +6 -10
- package/components/ProductFiltersPro/ProductFiltersProAllFiltersSidebar.tsx +18 -8
- package/components/ProductFiltersPro/ProductFiltersProCategorySection.tsx +130 -0
- package/components/ProductFiltersPro/ProductFiltersProChips.tsx +10 -8
- package/components/ProductFiltersPro/ProductFiltersProClearAll.tsx +4 -16
- package/components/ProductFiltersPro/ProductFiltersProLayoutSidebar.tsx +15 -7
- package/components/ProductFiltersPro/ProductFiltersProLimitChip.tsx +2 -8
- package/components/ProductFiltersPro/ProductFiltersProLimitSection.tsx +7 -10
- package/components/ProductFiltersPro/ProductFiltersProNoResults.tsx +79 -0
- package/components/ProductFiltersPro/ProductFiltersProSortChip.tsx +5 -7
- package/components/ProductFiltersPro/ProductFiltersProSortDirectionArrow.tsx +2 -4
- package/components/ProductFiltersPro/ProductFiltersProSortSection.tsx +11 -3
- package/components/ProductFiltersPro/activeAggregations.ts +5 -9
- package/components/ProductFiltersPro/applyAggregationCount.ts +14 -8
- package/components/ProductFiltersPro/index.ts +9 -0
- package/components/ProductFiltersPro/{useClearAllFiltersHandler.ts → useProductFiltersProClearAllAction.ts} +1 -1
- package/components/ProductFiltersPro/useProductFiltersProHasFiltersApplied.ts +21 -0
- package/components/ProductFiltersPro/useProductFiltersProSort.tsx +7 -3
- package/components/ProductList/ProductList.graphql +8 -5
- package/components/ProductListCount/ProductListCount.tsx +3 -1
- package/components/ProductListFilters/ProductFilters.graphql +11 -2
- package/components/ProductListFilters/ProductListFilters.graphql +1 -1
- package/components/ProductListFilters/ProductListFilters.tsx +13 -19
- package/components/ProductListFiltersContainer/ProductListFiltersContainer.tsx +2 -4
- package/components/ProductListItem/ProductDiscountLabel.tsx +2 -3
- package/components/ProductListItem/ProductListItem.tsx +3 -3
- package/components/ProductListItem/ProductListItemTitleAndPrice.tsx +18 -15
- package/components/ProductListItems/ProductFilterTypes.graphql +8 -0
- package/components/ProductListItems/ProductListItemsBase.tsx +71 -30
- package/components/ProductListItems/filterTypes.tsx +14 -7
- package/components/ProductListItems/filteredProductList.tsx +44 -17
- package/components/ProductListItems/getFilterTypes.ts +33 -4
- package/components/ProductListItems/productListApplyCategoryDefaults.ts +50 -4
- package/components/ProductListItems/renderer.tsx +8 -2
- package/components/ProductListPagination/ProductListPagination.tsx +39 -20
- package/components/ProductListPrice/ProductListPrice.tsx +9 -4
- package/components/ProductListSuggestions/ProductListSuggestions.graphql +5 -0
- package/components/ProductListSuggestions/ProductListSuggestions.tsx +42 -0
- package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.graphql +3 -0
- package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.tsx +3 -0
- package/components/ProductPageBreadcrumb/ProductPageBreadcrumbs.tsx +40 -0
- package/components/ProductPageBreadcrumb/index.ts +1 -0
- package/components/ProductPageDescription/ComplexTextValue.graphql +1 -1
- package/components/ProductPageDescription/ProductPageDescription.tsx +1 -1
- package/components/ProductPageGallery/ProductImage.graphql +1 -0
- package/components/ProductPageGallery/ProductPageGallery.tsx +14 -8
- package/components/ProductPagePrice/ProductPagePrice.graphql +0 -6
- package/components/ProductPagePrice/ProductPagePrice.tsx +19 -12
- package/components/ProductPagePrice/ProductPagePriceTiers.tsx +4 -3
- package/components/ProductPagePrice/useCustomizableOptionPrice.ts +11 -53
- package/components/ProductShortDescription/ProductShortDescription.tsx +2 -0
- package/components/ProductSpecs/ProductSpecs.graphql +21 -1
- package/components/ProductSpecs/ProductSpecs.tsx +5 -11
- package/components/ProductSpecs/ProductSpecsAggregations.tsx +34 -0
- package/components/ProductSpecs/ProductSpecsCustomAttributes.tsx +45 -0
- package/components/ProductSpecs/ProductSpecsTypes.graphql +8 -0
- package/components/ProductStaticPaths/getProductStaticPaths.ts +1 -1
- package/components/ProductWeight/ProductWeight.tsx +12 -9
- package/components/index.ts +2 -0
- package/hooks/useProductList.ts +148 -0
- package/hooks/useProductListLink.ts +6 -3
- package/index.ts +1 -0
- 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
|
+
}
|
@@ -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}
|
@@ -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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
@@ -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,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
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
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:
|
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 = { ...
|
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) =>
|
56
|
+
| ((option: AnyOption) => CustomizableProductOptionBase | CustomizableProductOptionBase[])
|
99
57
|
const value = selector ? selector(productOption) : null
|
100
58
|
|
101
59
|
if (!value) return 0
|
@@ -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
|
-
{
|
50
|
-
|
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
|
+
}
|
@@ -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 &&
|
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 {
|
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,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
|
+
}
|