@graphcommerce/magento-product 9.1.0-canary.18 → 9.1.0-canary.19
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/CHANGELOG.md +24 -0
- package/components/AddProductsToCart/AddProductsToCartForm.tsx +20 -45
- package/components/AddProductsToCart/useFormAddProductsToCart.ts +16 -1
- package/components/ProductCustomizable/CustomizableAreaOption.tsx +22 -37
- package/components/ProductCustomizable/CustomizableCheckboxOption.tsx +61 -41
- package/components/ProductCustomizable/CustomizableDateOption.graphql +1 -0
- package/components/ProductCustomizable/CustomizableDateOption.tsx +42 -87
- package/components/ProductCustomizable/CustomizableDropDownOption.tsx +13 -5
- package/components/ProductCustomizable/CustomizableFieldOption.tsx +22 -40
- package/components/ProductCustomizable/CustomizableMultipleOption.tsx +54 -35
- package/components/ProductCustomizable/CustomizablePrice.tsx +40 -0
- package/components/ProductCustomizable/CustomizableRadioOption.tsx +56 -36
- package/components/ProductCustomizable/ProductCustomizable.graphql +1 -0
- package/components/ProductCustomizable/ProductCustomizable.tsx +10 -2
- package/components/ProductCustomizable/productCustomizableSelectors.ts +5 -7
- package/components/ProductListItem/ProductDiscountLabel.tsx +1 -1
- package/components/ProductListItem/ProductListItem.tsx +5 -2
- package/components/ProductListItem/ProductNewLabel.tsx +36 -0
- package/components/ProductListItems/renderer.tsx +1 -1
- package/components/ProductPageDescription/ProductPageDescription.tsx +10 -1
- package/components/ProductPageGallery/ProductPageGallery.tsx +22 -19
- package/components/ProductPageGallery/ProductVideo.graphql +1 -0
- package/components/ProductPageGallery/ProductVideo.tsx +169 -0
- package/components/ProductPageMeta/ProductPageMeta.graphql +1 -0
- package/components/ProductPageMeta/ProductPageMeta.tsx +2 -0
- package/components/ProductPagePrice/ProductPagePrice.graphql +1 -10
- package/components/ProductPagePrice/ProductPrice.graphql +12 -0
- package/components/ProductPagePrice/getProductTierPrice.ts +3 -5
- package/components/ProductPagePrice/useCustomizableOptionPrice.ts +38 -29
- package/{Api → graphql/fragments}/ProductListItem.graphql +2 -0
- package/graphql/index.ts +2 -0
- package/index.ts +1 -2
- package/package.json +13 -13
- /package/{Api → graphql/fragments}/ProductPageItem.graphql +0 -0
@@ -1,65 +1,51 @@
|
|
1
|
-
import { TextFieldElement } from '@graphcommerce/ecommerce-ui'
|
1
|
+
import { TextFieldElement, useWatch } from '@graphcommerce/ecommerce-ui'
|
2
2
|
import { Money } from '@graphcommerce/magento-store'
|
3
3
|
import { SectionHeader } from '@graphcommerce/next-ui'
|
4
4
|
import { i18n } from '@lingui/core'
|
5
5
|
import { Box } from '@mui/material'
|
6
6
|
import { useFormAddProductsToCart } from '../AddProductsToCart'
|
7
7
|
import type { OptionTypeRenderer } from './CustomizableAreaOption'
|
8
|
+
import { CustomizablePrice } from './CustomizablePrice'
|
8
9
|
|
9
10
|
export type CustomizableFieldOptionProps = React.ComponentProps<
|
10
11
|
OptionTypeRenderer['CustomizableFieldOption']
|
11
12
|
>
|
12
13
|
|
13
14
|
export function CustomizableFieldOption(props: CustomizableFieldOptionProps) {
|
14
|
-
const { uid, required,
|
15
|
-
const { control
|
15
|
+
const { uid, required, index, title: label, fieldValue, productPrice, currency } = props
|
16
|
+
const { control } = useFormAddProductsToCart()
|
16
17
|
|
18
|
+
const name = `cartItems.${index}.entered_options_record.${uid}` as const
|
17
19
|
if (!fieldValue) return null
|
18
20
|
|
19
21
|
const maxLength = fieldValue.max_characters ?? 0
|
20
22
|
return (
|
21
23
|
<Box>
|
22
|
-
<SectionHeader
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
<SectionHeader
|
25
|
+
labelLeft={
|
26
|
+
<>
|
27
|
+
{label} {required && ' *'}
|
28
|
+
</>
|
29
|
+
}
|
30
|
+
sx={{ mt: 0 }}
|
27
31
|
/>
|
28
32
|
<TextFieldElement
|
29
33
|
sx={{ width: '100%' }}
|
30
34
|
color='primary'
|
31
35
|
multiline
|
32
36
|
control={control}
|
33
|
-
name={
|
37
|
+
name={name}
|
34
38
|
required={Boolean(required)}
|
35
39
|
InputProps={{
|
36
|
-
endAdornment:
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
'&.sizeLarge': { typography: 'h6' },
|
46
|
-
color: getValues(`cartItems.${index}.entered_options.${optionIndex}.value`)
|
47
|
-
? 'text.primary'
|
48
|
-
: 'text.secondary',
|
49
|
-
}}
|
50
|
-
>
|
51
|
-
{/* Change fontFamily so the + is properly outlined */}
|
52
|
-
<span style={{ fontFamily: 'arial', paddingTop: '1px' }}>+{'\u00A0'}</span>
|
53
|
-
<Money
|
54
|
-
value={
|
55
|
-
fieldValue.price_type === 'PERCENT'
|
56
|
-
? productPrice * (fieldValue.price / 100)
|
57
|
-
: fieldValue.price
|
58
|
-
}
|
59
|
-
currency={currency}
|
60
|
-
/>
|
61
|
-
</Box>
|
62
|
-
),
|
40
|
+
endAdornment: (
|
41
|
+
<CustomizablePrice
|
42
|
+
name={name}
|
43
|
+
productPrice={productPrice}
|
44
|
+
currency={currency}
|
45
|
+
price_type={fieldValue.price_type}
|
46
|
+
value={fieldValue.price}
|
47
|
+
/>
|
48
|
+
),
|
63
49
|
}}
|
64
50
|
rules={{
|
65
51
|
maxLength: {
|
@@ -75,10 +61,6 @@ export function CustomizableFieldOption(props: CustomizableFieldOptionProps) {
|
|
75
61
|
maxLength,
|
76
62
|
})
|
77
63
|
}
|
78
|
-
onChange={(data) => {
|
79
|
-
if (!data.currentTarget.value)
|
80
|
-
resetField(`cartItems.${index}.entered_options.${optionIndex}.value`)
|
81
|
-
}}
|
82
64
|
/>
|
83
65
|
</Box>
|
84
66
|
)
|
@@ -6,20 +6,61 @@ import { i18n } from '@lingui/core'
|
|
6
6
|
import { Box } from '@mui/material'
|
7
7
|
import { useFormAddProductsToCart } from '../AddProductsToCart'
|
8
8
|
import type { OptionTypeRenderer } from './CustomizableAreaOption'
|
9
|
+
import type { CustomizableMultipleOptionFragment } from './CustomizableMultipleOption.gql'
|
9
10
|
|
10
11
|
export type CustomizableMultipleOptionProps = React.ComponentProps<
|
11
12
|
OptionTypeRenderer['CustomizableMultipleOption']
|
12
13
|
>
|
13
14
|
|
15
|
+
type MultipleActionCardProps = Pick<CustomizableMultipleOptionProps, 'productPrice' | 'currency'> &
|
16
|
+
Pick<ActionCardProps, 'value' | 'selected'> & {
|
17
|
+
option: NonNullable<NonNullable<CustomizableMultipleOptionFragment['multipleValue']>[number]>
|
18
|
+
}
|
19
|
+
|
20
|
+
function CustomizableMultipleActionCard(props: MultipleActionCardProps) {
|
21
|
+
const { productPrice, currency, option, selected, ...rest } = props
|
22
|
+
const { title, price, price_type } = option
|
23
|
+
|
24
|
+
return (
|
25
|
+
<ActionCard
|
26
|
+
{...rest}
|
27
|
+
selected={selected}
|
28
|
+
title={title}
|
29
|
+
price={
|
30
|
+
price === 0
|
31
|
+
? null
|
32
|
+
: price && (
|
33
|
+
<Box
|
34
|
+
sx={{
|
35
|
+
color: selected ? 'text.primary' : 'text.secondary',
|
36
|
+
}}
|
37
|
+
>
|
38
|
+
<span style={{ fontFamily: 'arial' }}>{'+ '}</span>
|
39
|
+
<Money
|
40
|
+
value={price_type === 'PERCENT' ? productPrice * (price / 100) : price}
|
41
|
+
currency={currency}
|
42
|
+
/>
|
43
|
+
</Box>
|
44
|
+
)
|
45
|
+
}
|
46
|
+
/>
|
47
|
+
)
|
48
|
+
}
|
49
|
+
|
14
50
|
export function CustomizableMultipleOption(props: CustomizableMultipleOptionProps) {
|
15
51
|
const { uid, required, index, title: label, multipleValue, currency, productPrice } = props
|
16
|
-
const { control
|
17
|
-
|
18
|
-
const allSelected = getValues(`cartItems.${index}.customizable_options.${uid}`) || []
|
52
|
+
const { control } = useFormAddProductsToCart()
|
19
53
|
|
20
54
|
return (
|
21
55
|
<Box>
|
22
|
-
<SectionHeader
|
56
|
+
<SectionHeader
|
57
|
+
labelLeft={
|
58
|
+
<>
|
59
|
+
{label} {required && ' *'}
|
60
|
+
</>
|
61
|
+
}
|
62
|
+
sx={{ mt: 0 }}
|
63
|
+
/>
|
23
64
|
<ActionCardListForm
|
24
65
|
sx={(theme) => ({
|
25
66
|
mt: theme.spacings.xxs,
|
@@ -31,37 +72,15 @@ export function CustomizableMultipleOption(props: CustomizableMultipleOptionProp
|
|
31
72
|
: false,
|
32
73
|
}}
|
33
74
|
control={control}
|
34
|
-
render={
|
35
|
-
name={`cartItems.${index}.
|
36
|
-
items={filterNonNullableKeys(multipleValue, ['title']).map(
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
? null
|
44
|
-
: multipleVal.price && (
|
45
|
-
<Box
|
46
|
-
sx={{
|
47
|
-
color: allSelected.includes(multipleVal.uid)
|
48
|
-
? 'text.primary '
|
49
|
-
: 'text.secondary',
|
50
|
-
}}
|
51
|
-
>
|
52
|
-
<span style={{ fontFamily: 'arial' }}>{'+ '}</span>
|
53
|
-
<Money
|
54
|
-
value={
|
55
|
-
multipleVal.price_type === 'PERCENT'
|
56
|
-
? productPrice * (multipleVal.price / 100)
|
57
|
-
: multipleVal.price
|
58
|
-
}
|
59
|
-
currency={currency}
|
60
|
-
/>
|
61
|
-
</Box>
|
62
|
-
),
|
63
|
-
}) satisfies ActionCardProps,
|
64
|
-
)}
|
75
|
+
render={CustomizableMultipleActionCard}
|
76
|
+
name={`cartItems.${index}.selected_options_record.${uid}`}
|
77
|
+
items={filterNonNullableKeys(multipleValue, ['title']).map((multipleVal) => ({
|
78
|
+
productPrice,
|
79
|
+
currency,
|
80
|
+
title: multipleVal.title ?? '',
|
81
|
+
value: multipleVal.uid,
|
82
|
+
option: multipleVal,
|
83
|
+
}))}
|
65
84
|
/>
|
66
85
|
</Box>
|
67
86
|
)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import type { Control, FieldName, FieldPath } from '@graphcommerce/ecommerce-ui'
|
2
|
+
import { useWatch, type FieldValues } from '@graphcommerce/ecommerce-ui'
|
3
|
+
import type { PriceTypeEnum } from '@graphcommerce/graphql-mesh'
|
4
|
+
import { Money, type MoneyFragment } from '@graphcommerce/magento-store'
|
5
|
+
import { Box } from '@mui/material'
|
6
|
+
import { useFormAddProductsToCart, type AddProductsToCartFields } from '../AddProductsToCart'
|
7
|
+
|
8
|
+
type CustomizablePriceProps = {
|
9
|
+
price_type: PriceTypeEnum | null | undefined
|
10
|
+
productPrice: number
|
11
|
+
name: FieldPath<AddProductsToCartFields>
|
12
|
+
} & MoneyFragment
|
13
|
+
|
14
|
+
export function CustomizablePrice(props: CustomizablePriceProps) {
|
15
|
+
const { name, value, currency, price_type, productPrice } = props
|
16
|
+
|
17
|
+
const { control } = useFormAddProductsToCart()
|
18
|
+
const optionValue = !!useWatch({ control, name })
|
19
|
+
|
20
|
+
if (!value) return null
|
21
|
+
|
22
|
+
return (
|
23
|
+
<Box
|
24
|
+
sx={{
|
25
|
+
display: 'flex',
|
26
|
+
typography: 'body1',
|
27
|
+
'&.sizeMedium': { typographty: 'subtitle1' },
|
28
|
+
'&.sizeLarge': { typography: 'h6' },
|
29
|
+
color: optionValue ? 'text.primary' : 'text.secondary',
|
30
|
+
}}
|
31
|
+
>
|
32
|
+
{/* Change fontFamily so the + is properly outlined */}
|
33
|
+
<span style={{ fontFamily: 'arial' }}>+{'\u00A0'}</span>
|
34
|
+
<Money
|
35
|
+
value={price_type === 'PERCENT' ? productPrice * (value / 100) : value}
|
36
|
+
currency={currency}
|
37
|
+
/>
|
38
|
+
</Box>
|
39
|
+
)
|
40
|
+
}
|
@@ -6,63 +6,83 @@ import { i18n } from '@lingui/core'
|
|
6
6
|
import { Box } from '@mui/material'
|
7
7
|
import { useFormAddProductsToCart } from '../AddProductsToCart'
|
8
8
|
import type { OptionTypeRenderer } from './CustomizableAreaOption'
|
9
|
+
import type { CustomizableRadioOptionFragment } from './CustomizableRadioOption.gql'
|
9
10
|
|
10
11
|
export type CustomizableRadioOptionProps = React.ComponentProps<
|
11
12
|
OptionTypeRenderer['CustomizableRadioOption']
|
12
13
|
>
|
13
14
|
|
15
|
+
type RadioActionCardProps = Pick<CustomizableRadioOptionProps, 'productPrice' | 'currency'> &
|
16
|
+
Pick<ActionCardProps, 'value' | 'selected'> & {
|
17
|
+
option: NonNullable<NonNullable<CustomizableRadioOptionFragment['radioValue']>[number]>
|
18
|
+
}
|
19
|
+
|
20
|
+
function CustomizableRadioActionCard(props: RadioActionCardProps) {
|
21
|
+
const { productPrice, currency, value, option, selected, ...rest } = props
|
22
|
+
const { title, price, price_type } = option
|
23
|
+
|
24
|
+
return (
|
25
|
+
<ActionCard
|
26
|
+
{...rest}
|
27
|
+
value={value}
|
28
|
+
title={title}
|
29
|
+
selected={selected}
|
30
|
+
price={
|
31
|
+
price === 0
|
32
|
+
? null
|
33
|
+
: price && (
|
34
|
+
<Box
|
35
|
+
sx={{
|
36
|
+
color: selected ? 'text.primary' : 'text.secondary',
|
37
|
+
}}
|
38
|
+
>
|
39
|
+
{/* Change fontFamily so the + is properly outlined */}
|
40
|
+
<span style={{ fontFamily: 'arial' }}>{'+ '}</span>
|
41
|
+
<Money
|
42
|
+
value={price_type === 'PERCENT' ? productPrice * (price / 100) : price}
|
43
|
+
currency={currency}
|
44
|
+
/>
|
45
|
+
</Box>
|
46
|
+
)
|
47
|
+
}
|
48
|
+
/>
|
49
|
+
)
|
50
|
+
}
|
51
|
+
|
14
52
|
export function CustomizableRadioOption(props: CustomizableRadioOptionProps) {
|
15
53
|
const { uid, required, index, title: label, radioValue, currency, productPrice } = props
|
16
|
-
const { control
|
17
|
-
|
18
|
-
const allSelected = getValues(`cartItems.${index}.customizable_options.${uid}`) || []
|
54
|
+
const { control } = useFormAddProductsToCart()
|
19
55
|
|
20
56
|
return (
|
21
57
|
<Box>
|
22
|
-
<SectionHeader
|
58
|
+
<SectionHeader
|
59
|
+
labelLeft={
|
60
|
+
<>
|
61
|
+
{label} {required && ' *'}
|
62
|
+
</>
|
63
|
+
}
|
64
|
+
sx={{ mt: 0 }}
|
65
|
+
/>
|
23
66
|
<ActionCardListForm
|
24
67
|
sx={(theme) => ({
|
25
68
|
mt: theme.spacings.xxs,
|
26
69
|
})}
|
27
70
|
layout='stack'
|
28
71
|
control={control}
|
29
|
-
render={
|
30
|
-
name={`cartItems.${index}.
|
72
|
+
render={CustomizableRadioActionCard}
|
73
|
+
name={`cartItems.${index}.selected_options_record.${uid}`}
|
31
74
|
rules={{
|
32
75
|
required: required
|
33
76
|
? i18n._(/* i18n*/ 'Please select a value for ‘{label}’', { label })
|
34
77
|
: false,
|
35
78
|
}}
|
36
|
-
items={filterNonNullableKeys(radioValue, ['title']).map(
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
? null
|
44
|
-
: radioVal.price && (
|
45
|
-
<Box
|
46
|
-
sx={{
|
47
|
-
color: allSelected.includes(radioVal.uid)
|
48
|
-
? 'text.primary'
|
49
|
-
: 'text.secondary',
|
50
|
-
}}
|
51
|
-
>
|
52
|
-
{/* Change fontFamily so the + is properly outlined */}
|
53
|
-
<span style={{ fontFamily: 'arial' }}>{'+ '}</span>
|
54
|
-
<Money
|
55
|
-
value={
|
56
|
-
radioVal.price_type === 'PERCENT'
|
57
|
-
? productPrice * (radioVal.price / 100)
|
58
|
-
: radioVal.price
|
59
|
-
}
|
60
|
-
currency={currency}
|
61
|
-
/>
|
62
|
-
</Box>
|
63
|
-
),
|
64
|
-
}) satisfies ActionCardProps,
|
65
|
-
)}
|
79
|
+
items={filterNonNullableKeys(radioValue, ['title']).map((radioVal) => ({
|
80
|
+
productPrice,
|
81
|
+
currency,
|
82
|
+
title: radioVal.title ?? '',
|
83
|
+
value: radioVal.uid,
|
84
|
+
option: radioVal,
|
85
|
+
}))}
|
66
86
|
/>
|
67
87
|
</Box>
|
68
88
|
)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { filterNonNullableKeys, RenderType } from '@graphcommerce/next-ui'
|
2
|
+
import { useMemo } from 'react'
|
2
3
|
import type { AddToCartItemSelector } from '../AddProductsToCart'
|
3
4
|
import type { ProductPagePriceFragment } from '../ProductPagePrice'
|
4
5
|
import type { OptionTypeRenderer } from './CustomizableAreaOption'
|
@@ -42,14 +43,21 @@ export type ProductCustomizableProps = AddToCartItemSelector & {
|
|
42
43
|
export function ProductCustomizable(props: ProductCustomizableProps) {
|
43
44
|
const { product, renderer, index = 0 } = props
|
44
45
|
|
46
|
+
const options = useMemo(
|
47
|
+
() =>
|
48
|
+
filterNonNullableKeys(product.options, ['sort_order']).sort(
|
49
|
+
(a, b) => a.sort_order - b.sort_order,
|
50
|
+
),
|
51
|
+
[product.options],
|
52
|
+
)
|
53
|
+
|
45
54
|
return (
|
46
55
|
<>
|
47
|
-
{
|
56
|
+
{options.map((option) => (
|
48
57
|
<RenderType
|
49
58
|
key={option.uid}
|
50
59
|
renderer={{ ...defaultRenderer, ...renderer }}
|
51
60
|
{...option}
|
52
|
-
optionIndex={option.sort_order + 100}
|
53
61
|
index={index}
|
54
62
|
currency={product.price_range.minimum_price.final_price.currency}
|
55
63
|
productPrice={product.price_range.minimum_price.final_price.value}
|
@@ -24,9 +24,7 @@ export type AnyOption = NonNullable<
|
|
24
24
|
>
|
25
25
|
|
26
26
|
export type OptionValueSelector = {
|
27
|
-
[T in AnyOption as T['__typename']]: (
|
28
|
-
option: T,
|
29
|
-
) => CustomizableProductOptionBase | CustomizableProductOptionBase[]
|
27
|
+
[T in AnyOption as T['__typename']]: (option: AnyOption) => CustomizableProductOptionBase[]
|
30
28
|
}
|
31
29
|
|
32
30
|
type MissingOptionValueSelectors = Omit<
|
@@ -44,12 +42,12 @@ type Selectors = Simplify<
|
|
44
42
|
>
|
45
43
|
|
46
44
|
export const productCustomizableSelectors = {
|
47
|
-
CustomizableAreaOption: (o: CustomizableAreaOptionFragment) => o.areaValue,
|
45
|
+
CustomizableAreaOption: (o: CustomizableAreaOptionFragment) => [o.areaValue],
|
48
46
|
CustomizableCheckboxOption: (o: CustomizableCheckboxOptionFragment) => o.checkboxValue,
|
49
|
-
CustomizableFileOption: (o: CustomizableFileOptionFragment) => o.fileValue,
|
50
|
-
CustomizableDateOption: (o: CustomizableDateOptionFragment) => o.dateValue,
|
47
|
+
CustomizableFileOption: (o: CustomizableFileOptionFragment) => [o.fileValue],
|
48
|
+
CustomizableDateOption: (o: CustomizableDateOptionFragment) => [o.dateValue],
|
51
49
|
CustomizableDropDownOption: (o: CustomizableDropDownOptionFragment) => o.dropdownValue,
|
52
|
-
CustomizableFieldOption: (o: CustomizableFieldOptionFragment) => o.fieldValue,
|
50
|
+
CustomizableFieldOption: (o: CustomizableFieldOptionFragment) => [o.fieldValue],
|
53
51
|
CustomizableMultipleOption: (o: CustomizableMultipleOptionFragment) => o.multipleValue,
|
54
52
|
CustomizableRadioOption: (o: CustomizableRadioOptionFragment) => o.radioValue,
|
55
53
|
}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { PercentFormat } from '@graphcommerce/next-ui'
|
2
2
|
import type { BoxProps } from '@mui/material'
|
3
3
|
import { Box } from '@mui/material'
|
4
|
-
import type { ProductListItemFragment } from '../../
|
4
|
+
import type { ProductListItemFragment } from '../../graphql'
|
5
5
|
|
6
6
|
export type ProductDiscountLabelProps = Pick<ProductListItemFragment, 'price_range'> &
|
7
7
|
Omit<BoxProps, 'children'>
|
@@ -1,9 +1,9 @@
|
|
1
1
|
import type { ImageProps } from '@graphcommerce/image'
|
2
2
|
import { extendableComponent } from '@graphcommerce/next-ui'
|
3
3
|
import type { SxProps, Theme } from '@mui/material'
|
4
|
-
import { Skeleton
|
4
|
+
import { Skeleton } from '@mui/material'
|
5
5
|
import React from 'react'
|
6
|
-
import type { ProductListItemFragment } from '../../
|
6
|
+
import type { ProductListItemFragment } from '../../graphql'
|
7
7
|
import { productLink } from '../../hooks/useProductLink'
|
8
8
|
import { ProductListPrice } from '../ProductListPrice/ProductListPrice'
|
9
9
|
import { ProductDiscountLabel } from './ProductDiscountLabel'
|
@@ -18,6 +18,7 @@ import type { ProductListItemLinkOrDivProps } from './ProductListItemLinkOrDiv'
|
|
18
18
|
import { ProductListItemLinkOrDiv } from './ProductListItemLinkOrDiv'
|
19
19
|
import type { ProductListItemTitleAndPriceProps } from './ProductListItemTitleAndPrice'
|
20
20
|
import { ProductListItemTitleAndPrice } from './ProductListItemTitleAndPrice'
|
21
|
+
import { ProductNewLabel } from './ProductNewLabel'
|
21
22
|
|
22
23
|
const { classes, selectors } = extendableComponent('ProductListItem', [
|
23
24
|
'root',
|
@@ -35,6 +36,7 @@ const { classes, selectors } = extendableComponent('ProductListItem', [
|
|
35
36
|
'placeholder',
|
36
37
|
'image',
|
37
38
|
'discount',
|
39
|
+
'new',
|
38
40
|
] as const)
|
39
41
|
|
40
42
|
type StyleProps = {
|
@@ -123,6 +125,7 @@ export function ProductListItemReal(props: ProductProps) {
|
|
123
125
|
topLeft={
|
124
126
|
<>
|
125
127
|
<ProductDiscountLabel className={classes.discount} price_range={price_range} />
|
128
|
+
<ProductNewLabel className={classes.new} product={props} />
|
126
129
|
{topLeft}
|
127
130
|
</>
|
128
131
|
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import { toDate } from '@graphcommerce/next-ui/Intl/DateTimeFormat/toDate'
|
2
|
+
import { Box, type BoxProps } from '@mui/material'
|
3
|
+
import type { ProductListItemFragment } from '../../graphql'
|
4
|
+
|
5
|
+
export type ProductNewLabelProps = {
|
6
|
+
product: Pick<ProductListItemFragment, 'new_from_date' | 'new_to_date'>
|
7
|
+
} & Omit<BoxProps, 'children'>
|
8
|
+
|
9
|
+
export function ProductNewLabel(props: ProductNewLabelProps) {
|
10
|
+
const { product, ...boxProps } = props
|
11
|
+
const { new_from_date, new_to_date } = product
|
12
|
+
|
13
|
+
const from = toDate(new_from_date)
|
14
|
+
const to = toDate(new_to_date)
|
15
|
+
const now = new Date()
|
16
|
+
if (!from || from > now) return null
|
17
|
+
if (to && to < now) return null
|
18
|
+
|
19
|
+
return (
|
20
|
+
<Box
|
21
|
+
{...boxProps}
|
22
|
+
sx={{
|
23
|
+
typography: 'caption',
|
24
|
+
bgcolor: 'text.primary',
|
25
|
+
fontWeight: 'fontWeightBold',
|
26
|
+
border: 1,
|
27
|
+
borderColor: 'divider',
|
28
|
+
padding: '0px 6px',
|
29
|
+
color: 'background.default',
|
30
|
+
display: 'inline-block',
|
31
|
+
}}
|
32
|
+
>
|
33
|
+
New
|
34
|
+
</Box>
|
35
|
+
)
|
36
|
+
}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import type { TypeRenderer } from '@graphcommerce/next-ui'
|
2
|
-
import type { ProductListItemFragment } from '../../
|
2
|
+
import type { ProductListItemFragment } from '../../graphql'
|
3
3
|
import { ProductListItem } from '../ProductListItem/ProductListItem'
|
4
4
|
|
5
5
|
type SkeletonType = { __typename: 'Skeleton'; uid: string }
|
@@ -8,6 +8,7 @@ import {
|
|
8
8
|
import type { SxProps, Theme } from '@mui/material'
|
9
9
|
import { Box, Typography } from '@mui/material'
|
10
10
|
import type { Variant } from '@mui/material/styles/createTypography'
|
11
|
+
import type { ProductListItemRenderer } from '../ProductListItems/renderer'
|
11
12
|
import { ProductPageName } from '../ProductPageName'
|
12
13
|
import type { ProductPageDescriptionFragment } from './ProductPageDescription.gql'
|
13
14
|
|
@@ -15,6 +16,7 @@ export type ProductPageDescriptionProps = Omit<ColumnTwoWithTopProps, 'top' | 'l
|
|
15
16
|
sx?: SxProps<Theme>
|
16
17
|
fontSize?: 'responsive' | Variant
|
17
18
|
product: ProductPageDescriptionFragment
|
19
|
+
productListRenderer: ProductListItemRenderer
|
18
20
|
}
|
19
21
|
|
20
22
|
const componentName = 'ProductPageDescription'
|
@@ -23,7 +25,14 @@ const parts = ['root', 'description'] as const
|
|
23
25
|
const { classes } = extendableComponent(componentName, parts)
|
24
26
|
|
25
27
|
export function ProductPageDescription(props: ProductPageDescriptionProps) {
|
26
|
-
const {
|
28
|
+
const {
|
29
|
+
product,
|
30
|
+
right,
|
31
|
+
fontSize = 'subtitle1',
|
32
|
+
maxWidth = 'lg',
|
33
|
+
sx = [],
|
34
|
+
productListRenderer,
|
35
|
+
} = props
|
27
36
|
|
28
37
|
return (
|
29
38
|
<LazyHydrate height={500}>
|
@@ -1,6 +1,8 @@
|
|
1
|
+
import type { MotionImageAspectProps } from '@graphcommerce/framer-scroller/components/MotionImageAspect'
|
1
2
|
import type { SidebarGalleryProps, TypeRenderer } from '@graphcommerce/next-ui'
|
2
|
-
import { nonNullable, SidebarGallery } from '@graphcommerce/next-ui'
|
3
|
+
import { filterNonNullableKeys, nonNullable, SidebarGallery } from '@graphcommerce/next-ui'
|
3
4
|
import type { ProductPageGalleryFragment } from './ProductPageGallery.gql'
|
5
|
+
import { ProductVideo, type ProductVideoProps } from './ProductVideo'
|
4
6
|
|
5
7
|
export type ProductPageGalleryRenderers = TypeRenderer<
|
6
8
|
NonNullable<NonNullable<ProductPageGalleryFragment['media_gallery']>[0]>
|
@@ -15,24 +17,25 @@ export function ProductPageGallery(props: ProductPageGalleryProps) {
|
|
15
17
|
const { product, children, aspectRatio: [width, height] = [1532, 1678], ...sidebarProps } = props
|
16
18
|
const { media_gallery } = product
|
17
19
|
|
18
|
-
const images =
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
.
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
20
|
+
const images = filterNonNullableKeys(media_gallery)
|
21
|
+
.filter((p) => p.disabled !== true)
|
22
|
+
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
23
|
+
.map<MotionImageAspectProps<ProductVideoProps>>((item) => {
|
24
|
+
const src = item.url ?? ''
|
25
|
+
const alt = item.label || undefined
|
26
|
+
if (item.__typename === 'ProductImage') return { src, alt, width, height }
|
27
|
+
return {
|
28
|
+
src,
|
29
|
+
alt,
|
30
|
+
width: 1280,
|
31
|
+
height: 720,
|
32
|
+
sx: { objectFit: 'cover' },
|
33
|
+
Additional: ProductVideo,
|
34
|
+
slotProps: {
|
35
|
+
additional: { video: item, width: 1280, height: 720 },
|
36
|
+
},
|
37
|
+
}
|
38
|
+
})
|
36
39
|
|
37
40
|
return (
|
38
41
|
<SidebarGallery
|