@graphcommerce/magento-product 9.1.0-canary.40 → 9.1.0-canary.42

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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # Change Log
2
2
 
3
+ ## 9.1.0-canary.42
4
+
5
+ ### Patch Changes
6
+
7
+ - [`fe2ca31`](https://github.com/graphcommerce-org/graphcommerce/commit/fe2ca31286628b563f7b490a736d698e170edd65) - Solve issue where ProductListItemReal couldn't be replaced or extended without also replacing ProductListItem ([@paales](https://github.com/paales))
8
+
9
+ ## 9.1.0-canary.41
10
+
11
+ ### Patch Changes
12
+
13
+ - [#2530](https://github.com/graphcommerce-org/graphcommerce/pull/2530) [`2075f33`](https://github.com/graphcommerce-org/graphcommerce/commit/2075f331eec38e894722d8ba4539d865f2db5507) - Add support for `variant=unit` and `variant=total` rendering of `ProductPagePrice` to include the quantity or not. ([@paales](https://github.com/paales))
14
+
15
+ - [#2530](https://github.com/graphcommerce-org/graphcommerce/pull/2530) [`4c60c55`](https://github.com/graphcommerce-org/graphcommerce/commit/4c60c55a0e83a8635fb2e97622cecd981d894970) - Created a ProductPagePriceLowest component that switches when the configurable option changes. ([@paales](https://github.com/paales))
16
+
17
+ - [#2530](https://github.com/graphcommerce-org/graphcommerce/pull/2530) [`5900c8d`](https://github.com/graphcommerce-org/graphcommerce/commit/5900c8d56bc9a3e0e4c2c8e61d5ff219877fd9ec) - Solve issue where the tier price doesn't get divided by the quantity, thus showing the wrong price. ([@paales](https://github.com/paales))
18
+
19
+ - [#2530](https://github.com/graphcommerce-org/graphcommerce/pull/2530) [`f4a20a7`](https://github.com/graphcommerce-org/graphcommerce/commit/f4a20a7bb37701b779dfe7bd3073574eb6c1cab2) - Make sure the product price is updated when the quantity of a product is changed. ([@paales](https://github.com/paales))
20
+
3
21
  ## 9.1.0-canary.40
4
22
 
5
23
  ## 9.1.0-canary.39
@@ -42,7 +42,7 @@ export function findAddedItems(
42
42
  return foundItem
43
43
  })
44
44
  ) {
45
- // console.log("SKU matche, this isn't the configurable")
45
+ // console.log("SKU matched, this isn't the configurable")
46
46
  return false
47
47
  }
48
48
  }
@@ -1,202 +1,13 @@
1
- import type { ImageProps } from '@graphcommerce/image'
2
- import { extendableComponent } from '@graphcommerce/next-ui'
3
- import type { SxProps, Theme } from '@mui/material'
4
- import { Skeleton } from '@mui/material'
5
- import React from 'react'
6
- import type { ProductListItemFragment } from '../../graphql'
7
- import { productLink } from '../../hooks/useProductLink'
8
- import { ProductListPrice } from '../ProductListPrice/ProductListPrice'
9
- import { ProductDiscountLabel } from './ProductDiscountLabel'
10
- import type { ProductListItemImageProps } from './ProductListItemImage'
11
- import { ProductListItemImage, ProductListItemImageSkeleton } from './ProductListItemImage'
12
- import type {
13
- ProductListItemImageAreaKeys,
14
- ProductListsItemImageAreaProps,
15
- } from './ProductListItemImageContainer'
16
- import { ProductImageContainer, ProductListItemImageAreas } from './ProductListItemImageContainer'
17
- import type { ProductListItemLinkOrDivProps } from './ProductListItemLinkOrDiv'
18
- import { ProductListItemLinkOrDiv } from './ProductListItemLinkOrDiv'
19
- import type { ProductListItemTitleAndPriceProps } from './ProductListItemTitleAndPrice'
20
- import { ProductListItemTitleAndPrice } from './ProductListItemTitleAndPrice'
21
- import { ProductNewLabel } from './ProductNewLabel'
1
+ import {
2
+ ProductListItemReal,
3
+ ProductListItemSkeleton,
4
+ type ProductListItemRealProps,
5
+ type ProductListItemSkeletonProps,
6
+ } from './ProductListItemParts'
22
7
 
23
- const { classes } = extendableComponent('ProductListItem', [
24
- 'root',
25
- 'item',
26
- 'title',
27
- 'titleContainer',
28
- 'subtitle',
29
- 'price',
30
- 'overlayItems',
31
- 'topLeft',
32
- 'topRight',
33
- 'bottomLeft',
34
- 'bottomRight',
35
- 'imageContainer',
36
- 'placeholder',
37
- 'image',
38
- 'discount',
39
- 'new',
40
- ] as const)
8
+ export type ProductListItemProps = ProductListItemRealProps | ProductListItemSkeletonProps
41
9
 
42
- type StyleProps = {
43
- imageOnly?: boolean
44
- }
45
-
46
- export type BaseProps = {
47
- imageOnly?: boolean
48
- children?: React.ReactNode
49
- sx?: SxProps<Theme>
50
- onClick?: (
51
- event: React.MouseEvent<HTMLAnchorElement | HTMLDivElement>,
52
- item: ProductListItemFragment,
53
- ) => void
54
- slotProps?: {
55
- root?: Partial<ProductListItemLinkOrDivProps>
56
- image?: Partial<ProductListItemImageProps>
57
- imageAreas?: Partial<ProductListsItemImageAreaProps>
58
- titleAndPrice?: Partial<ProductListItemTitleAndPriceProps>
59
- }
60
- } & StyleProps &
61
- Omit<ProductListItemTitleAndPriceProps, 'title' | 'classes' | 'children'> &
62
- Omit<ProductListItemImageProps, 'classes'> &
63
- Omit<ProductListsItemImageAreaProps, 'classes'> &
64
- Pick<ImageProps, 'loading' | 'sizes' | 'dontReportWronglySizedImages'>
65
-
66
- export type SkeletonProps = BaseProps & { __typename: 'Skeleton' }
67
-
68
- export type ProductProps = BaseProps & ProductListItemFragment
69
-
70
- export type ProductListItemProps = ProductProps | SkeletonProps
71
-
72
- /** @public */
73
- export function ProductListItemReal(props: ProductProps) {
74
- const {
75
- subTitle,
76
- topLeft,
77
- topRight,
78
- bottomLeft,
79
- bottomRight,
80
- small_image,
81
- name,
82
- price_range,
83
- children,
84
- imageOnly = false,
85
- loading,
86
- sizes,
87
- dontReportWronglySizedImages,
88
- aspectRatio = [4, 3],
89
- titleComponent = 'h2',
90
- sx = [],
91
- onClick,
92
- slotProps = {},
93
- } = props
94
-
95
- return (
96
- <ProductListItemLinkOrDiv
97
- href={productLink(props)}
98
- className={classes.root}
99
- onClick={(e: React.MouseEvent<HTMLAnchorElement | HTMLDivElement>) => onClick?.(e, props)}
100
- {...slotProps.root}
101
- sx={[
102
- ...(Array.isArray(sx) ? sx : [sx]),
103
- ...(Array.isArray(slotProps.root?.sx) ? slotProps.root.sx : [slotProps.root?.sx]),
104
- ]}
105
- ref={slotProps.root?.ref as React.Ref<HTMLAnchorElement | HTMLDivElement>}
106
- >
107
- <ProductImageContainer className={classes.imageContainer}>
108
- <ProductListItemImage
109
- classes={classes}
110
- src={small_image?.url}
111
- alt={small_image?.label}
112
- aspectRatio={aspectRatio}
113
- loading={loading}
114
- sizes={sizes}
115
- dontReportWronglySizedImages={dontReportWronglySizedImages}
116
- {...slotProps.image}
117
- />
118
-
119
- {!imageOnly && (
120
- <ProductListItemImageAreas
121
- topRight={topRight}
122
- bottomLeft={bottomLeft}
123
- bottomRight={bottomRight}
124
- classes={classes}
125
- topLeft={
126
- <>
127
- <ProductDiscountLabel className={classes.discount} price_range={price_range} />
128
- <ProductNewLabel className={classes.new} product={props} />
129
- {topLeft}
130
- </>
131
- }
132
- {...slotProps.imageAreas}
133
- />
134
- )}
135
- </ProductImageContainer>
136
-
137
- {!imageOnly && (
138
- <>
139
- <ProductListItemTitleAndPrice
140
- classes={classes}
141
- titleComponent={titleComponent}
142
- title={name}
143
- subTitle={subTitle}
144
- {...slotProps.titleAndPrice}
145
- >
146
- <ProductListPrice {...price_range.minimum_price} />
147
- </ProductListItemTitleAndPrice>
148
- {children}
149
- </>
150
- )}
151
- </ProductListItemLinkOrDiv>
152
- )
153
- }
154
-
155
- /** @public */
156
- export function ProductListItemSkeleton(props: BaseProps) {
157
- const {
158
- children,
159
- imageOnly = false,
160
- aspectRatio,
161
- titleComponent = 'h2',
162
- sx = [],
163
- slotProps = {},
164
- } = props
165
-
166
- return (
167
- <ProductListItemLinkOrDiv
168
- sx={sx}
169
- className={classes.root}
170
- {...slotProps.root}
171
- ref={slotProps.root?.ref as React.Ref<HTMLAnchorElement | HTMLDivElement>}
172
- >
173
- <ProductImageContainer className={classes.imageContainer}>
174
- <ProductListItemImageSkeleton
175
- classes={classes}
176
- aspectRatio={aspectRatio}
177
- {...slotProps.image}
178
- />
179
- </ProductImageContainer>
180
-
181
- {!imageOnly && (
182
- <>
183
- <ProductListItemTitleAndPrice
184
- classes={classes}
185
- titleComponent={titleComponent}
186
- title={<Skeleton variant='text' sx={{ width: '100px' }} />}
187
- subTitle={<Skeleton variant='text' sx={{ width: '20px' }} />}
188
- {...slotProps.titleAndPrice}
189
- >
190
- <Skeleton variant='text' sx={{ width: '20px' }} />
191
- </ProductListItemTitleAndPrice>
192
- {children}
193
- </>
194
- )}
195
- </ProductListItemLinkOrDiv>
196
- )
197
- }
198
-
199
- function isSkeleton(props: ProductListItemProps): props is SkeletonProps {
10
+ function isSkeleton(props: ProductListItemProps): props is ProductListItemSkeletonProps {
200
11
  return props.__typename === 'Skeleton'
201
12
  }
202
13
 
@@ -207,8 +18,3 @@ export function ProductListItem(props: ProductListItemProps) {
207
18
  <ProductListItemReal {...props} />
208
19
  )
209
20
  }
210
-
211
- /** @deprecated */
212
- export type OverlayAreaKeys = ProductListItemImageAreaKeys
213
- /** @deprecated */
214
- export type OverlayAreas = ProductListsItemImageAreaProps
@@ -0,0 +1,192 @@
1
+ import type { ImageProps } from '@graphcommerce/image'
2
+ import { extendableComponent } from '@graphcommerce/next-ui'
3
+ import type { SxProps, Theme } from '@mui/material'
4
+ import { Skeleton } from '@mui/material'
5
+ import React from 'react'
6
+ import type { ProductListItemFragment } from '../../graphql'
7
+ import { productLink } from '../../hooks/useProductLink'
8
+ import { ProductListPrice } from '../ProductListPrice/ProductListPrice'
9
+ import { ProductDiscountLabel } from './ProductDiscountLabel'
10
+ import type { ProductListItemImageProps } from './ProductListItemImage'
11
+ import { ProductListItemImage, ProductListItemImageSkeleton } from './ProductListItemImage'
12
+ import type { ProductListsItemImageAreaProps } from './ProductListItemImageContainer'
13
+ import { ProductImageContainer, ProductListItemImageAreas } from './ProductListItemImageContainer'
14
+ import type { ProductListItemLinkOrDivProps } from './ProductListItemLinkOrDiv'
15
+ import { ProductListItemLinkOrDiv } from './ProductListItemLinkOrDiv'
16
+ import type { ProductListItemTitleAndPriceProps } from './ProductListItemTitleAndPrice'
17
+ import { ProductListItemTitleAndPrice } from './ProductListItemTitleAndPrice'
18
+ import { ProductNewLabel } from './ProductNewLabel'
19
+
20
+ const { classes } = extendableComponent('ProductListItem', [
21
+ 'root',
22
+ 'item',
23
+ 'title',
24
+ 'titleContainer',
25
+ 'subtitle',
26
+ 'price',
27
+ 'overlayItems',
28
+ 'topLeft',
29
+ 'topRight',
30
+ 'bottomLeft',
31
+ 'bottomRight',
32
+ 'imageContainer',
33
+ 'placeholder',
34
+ 'image',
35
+ 'discount',
36
+ 'new',
37
+ ] as const)
38
+
39
+ type StyleProps = {
40
+ imageOnly?: boolean
41
+ }
42
+
43
+ export type BaseProps = {
44
+ imageOnly?: boolean
45
+ children?: React.ReactNode
46
+ sx?: SxProps<Theme>
47
+ onClick?: (
48
+ event: React.MouseEvent<HTMLAnchorElement | HTMLDivElement>,
49
+ item: ProductListItemFragment,
50
+ ) => void
51
+ slotProps?: {
52
+ root?: Partial<ProductListItemLinkOrDivProps>
53
+ image?: Partial<ProductListItemImageProps>
54
+ imageAreas?: Partial<ProductListsItemImageAreaProps>
55
+ titleAndPrice?: Partial<ProductListItemTitleAndPriceProps>
56
+ }
57
+ } & StyleProps &
58
+ Omit<ProductListItemTitleAndPriceProps, 'title' | 'classes' | 'children'> &
59
+ Omit<ProductListItemImageProps, 'classes'> &
60
+ Omit<ProductListsItemImageAreaProps, 'classes'> &
61
+ Pick<ImageProps, 'loading' | 'sizes' | 'dontReportWronglySizedImages'>
62
+
63
+ export type ProductListItemSkeletonProps = BaseProps & { __typename: 'Skeleton' }
64
+
65
+ export type ProductListItemRealProps = BaseProps & ProductListItemFragment
66
+
67
+ /** @public */
68
+ export function ProductListItemReal(props: ProductListItemRealProps) {
69
+ const {
70
+ subTitle,
71
+ topLeft,
72
+ topRight,
73
+ bottomLeft,
74
+ bottomRight,
75
+ small_image,
76
+ name,
77
+ price_range,
78
+ children,
79
+ imageOnly = false,
80
+ loading,
81
+ sizes,
82
+ dontReportWronglySizedImages,
83
+ aspectRatio = [4, 3],
84
+ titleComponent = 'h2',
85
+ sx = [],
86
+ onClick,
87
+ slotProps = {},
88
+ } = props
89
+
90
+ return (
91
+ <ProductListItemLinkOrDiv
92
+ href={productLink(props)}
93
+ className={classes.root}
94
+ onClick={(e: React.MouseEvent<HTMLAnchorElement | HTMLDivElement>) => onClick?.(e, props)}
95
+ {...slotProps.root}
96
+ sx={[
97
+ ...(Array.isArray(sx) ? sx : [sx]),
98
+ ...(Array.isArray(slotProps.root?.sx) ? slotProps.root.sx : [slotProps.root?.sx]),
99
+ ]}
100
+ ref={slotProps.root?.ref as React.Ref<HTMLAnchorElement | HTMLDivElement>}
101
+ >
102
+ <ProductImageContainer className={classes.imageContainer}>
103
+ <ProductListItemImage
104
+ classes={classes}
105
+ src={small_image?.url}
106
+ alt={small_image?.label}
107
+ aspectRatio={aspectRatio}
108
+ loading={loading}
109
+ sizes={sizes}
110
+ dontReportWronglySizedImages={dontReportWronglySizedImages}
111
+ {...slotProps.image}
112
+ />
113
+
114
+ {!imageOnly && (
115
+ <ProductListItemImageAreas
116
+ topRight={topRight}
117
+ bottomLeft={bottomLeft}
118
+ bottomRight={bottomRight}
119
+ classes={classes}
120
+ topLeft={
121
+ <>
122
+ <ProductDiscountLabel className={classes.discount} price_range={price_range} />
123
+ <ProductNewLabel className={classes.new} product={props} />
124
+ {topLeft}
125
+ </>
126
+ }
127
+ {...slotProps.imageAreas}
128
+ />
129
+ )}
130
+ </ProductImageContainer>
131
+
132
+ {!imageOnly && (
133
+ <>
134
+ <ProductListItemTitleAndPrice
135
+ classes={classes}
136
+ titleComponent={titleComponent}
137
+ title={name}
138
+ subTitle={subTitle}
139
+ {...slotProps.titleAndPrice}
140
+ >
141
+ <ProductListPrice {...price_range.minimum_price} />
142
+ </ProductListItemTitleAndPrice>
143
+ {children}
144
+ </>
145
+ )}
146
+ </ProductListItemLinkOrDiv>
147
+ )
148
+ }
149
+
150
+ /** @public */
151
+ export function ProductListItemSkeleton(props: BaseProps) {
152
+ const {
153
+ children,
154
+ imageOnly = false,
155
+ aspectRatio,
156
+ titleComponent = 'h2',
157
+ sx = [],
158
+ slotProps = {},
159
+ } = props
160
+
161
+ return (
162
+ <ProductListItemLinkOrDiv
163
+ sx={sx}
164
+ className={classes.root}
165
+ {...slotProps.root}
166
+ ref={slotProps.root?.ref as React.Ref<HTMLAnchorElement | HTMLDivElement>}
167
+ >
168
+ <ProductImageContainer className={classes.imageContainer}>
169
+ <ProductListItemImageSkeleton
170
+ classes={classes}
171
+ aspectRatio={aspectRatio}
172
+ {...slotProps.image}
173
+ />
174
+ </ProductImageContainer>
175
+
176
+ {!imageOnly && (
177
+ <>
178
+ <ProductListItemTitleAndPrice
179
+ classes={classes}
180
+ titleComponent={titleComponent}
181
+ title={<Skeleton variant='text' sx={{ width: '100px' }} />}
182
+ subTitle={<Skeleton variant='text' sx={{ width: '20px' }} />}
183
+ {...slotProps.titleAndPrice}
184
+ >
185
+ <Skeleton variant='text' sx={{ width: '20px' }} />
186
+ </ProductListItemTitleAndPrice>
187
+ {children}
188
+ </>
189
+ )}
190
+ </ProductListItemLinkOrDiv>
191
+ )
192
+ }
@@ -0,0 +1,6 @@
1
+ export * from './ProductListItem'
2
+ export * from './ProductListItemParts'
3
+ export * from './ProductListItemImage'
4
+ export * from './ProductListItemTitleAndPrice'
5
+ export * from './ProductListItemImageContainer'
6
+ export * from './ProductListItemLinkOrDiv'
@@ -18,23 +18,19 @@ export type ProductListPriceProps = ProductListPriceFragment &
18
18
  Pick<TypographyProps, 'sx'> & {
19
19
  prefix?: React.ReactNode
20
20
  suffix?: React.ReactNode
21
+ asNumber?: boolean
21
22
  }
22
23
 
23
24
  export function ProductListPrice(props: ProductListPriceProps) {
24
- const { regular_price, final_price, sx, prefix, suffix } = props
25
+ const { regular_price, final_price, sx, prefix, suffix, asNumber } = props
25
26
 
26
27
  return (
27
- <Box
28
- className={classes.root}
29
- sx={sxx({ typography: 'body1', display: 'inline-flex', columnGap: '0.3em' }, sx)}
30
- >
28
+ <Box className={classes.root} sx={sxx({ display: 'inline-flex', columnGap: '0.3em' }, sx)}>
31
29
  {prefix && (
32
30
  <PrivateQueryMask
33
31
  component='span'
34
- skeleton={{
35
- variant: 'text',
36
- sx: { width: '3.5em', transform: 'none' },
37
- }}
32
+ sx={{ '&:empty': { display: 'none' } }}
33
+ skeleton={{ variant: 'text', sx: { width: '2em', transform: 'none' } }}
38
34
  className={classes.prefix}
39
35
  >
40
36
  {prefix}
@@ -44,38 +40,27 @@ export function ProductListPrice(props: ProductListPriceProps) {
44
40
  {regular_price.value !== final_price.value && (
45
41
  <PrivateQueryMask
46
42
  component='span'
47
- sx={{
48
- textDecoration: 'line-through',
49
- color: 'text.disabled',
50
- }}
51
- skeleton={{
52
- variant: 'text',
53
- sx: { width: '3.5em', transform: 'none' },
54
- }}
43
+ sx={{ textDecoration: 'line-through', color: 'text.disabled' }}
44
+ skeleton={{ variant: 'text', sx: { width: '3.5em', transform: 'none' } }}
55
45
  className={classes.discountPrice}
56
46
  >
57
- <Money {...regular_price} />
47
+ <Money {...regular_price} asNumber={asNumber} />
58
48
  </PrivateQueryMask>
59
49
  )}
60
50
 
61
51
  <PrivateQueryMask
62
52
  className={classes.finalPrice}
63
53
  component='span'
64
- skeleton={{
65
- variant: 'text',
66
- sx: { width: '3.5em', transform: 'none' },
67
- }}
54
+ skeleton={{ variant: 'text', sx: { width: '3.5em', transform: 'none' } }}
68
55
  >
69
- <Money {...final_price} />
56
+ <Money {...final_price} asNumber={asNumber} />
70
57
  </PrivateQueryMask>
71
58
 
72
59
  {suffix && (
73
60
  <PrivateQueryMask
74
61
  component='span'
75
- skeleton={{
76
- variant: 'text',
77
- sx: { width: '3.5em', transform: 'none' },
78
- }}
62
+ sx={{ '&:empty': { display: 'none' } }}
63
+ skeleton={{ variant: 'text', sx: { width: '2em', transform: 'none' } }}
79
64
  className={classes.suffix}
80
65
  >
81
66
  {suffix}
@@ -15,6 +15,8 @@ export type ProductPagePriceProps = {
15
15
  prefix?: React.ReactNode
16
16
  suffix?: React.ReactNode
17
17
  sx?: SxProps<Theme>
18
+ variant?: 'item' | 'total'
19
+ asNumber?: boolean
18
20
  } & AddToCartItemSelector &
19
21
  UseCustomizableOptionPriceProps
20
22
 
@@ -26,15 +28,18 @@ const { classes } = extendableComponent('ProductPagePrice', [
26
28
  ] as const)
27
29
 
28
30
  export function ProductPagePrice(props: ProductPagePriceProps) {
29
- const { product, index = 0, prefix, suffix, sx } = props
31
+ const { product, index = 0, prefix, suffix, sx, variant = 'item', asNumber } = props
30
32
 
31
33
  const { control } = useFormAddProductsToCart()
32
34
  const quantity = useWatch({ control, name: `cartItems.${index}.quantity` })
33
35
  const price =
34
36
  getProductTierPrice(product, quantity) ?? product.price_range.minimum_price.final_price
35
37
 
36
- const priceValue = useCustomizableOptionPrice(props)
38
+ const priceValue = useCustomizableOptionPrice(props) ?? 0
39
+ const finalPriceValue = variant === 'total' ? priceValue * quantity : priceValue
37
40
  const regularPrice = product.price_range.minimum_price.regular_price
41
+ const regularPriceValue =
42
+ variant === 'total' ? (regularPrice.value ?? 0) * quantity : regularPrice.value
38
43
 
39
44
  return (
40
45
  <Box component='span' sx={sxx({ display: 'inline-flex', columnGap: '0.3em' }, sx)}>
@@ -42,10 +47,8 @@ export function ProductPagePrice(props: ProductPagePriceProps) {
42
47
  <PrivateQueryMask
43
48
  component='span'
44
49
  className={classes.prefix}
45
- skeleton={{
46
- variant: 'text',
47
- sx: { width: '3.5em', transform: 'none' },
48
- }}
50
+ sx={{ '&:empty': { display: 'none' } }}
51
+ skeleton={{ variant: 'text', sx: { width: '3.5em', transform: 'none' } }}
49
52
  >
50
53
  {prefix}
51
54
  </PrivateQueryMask>
@@ -57,7 +60,7 @@ export function ProductPagePrice(props: ProductPagePriceProps) {
57
60
  skeleton={{ variant: 'text', sx: { width: '3.5em', transform: 'none' } }}
58
61
  sx={[{ textDecoration: 'line-through', color: 'text.disabled' }]}
59
62
  >
60
- <Money {...regularPrice} />
63
+ <Money {...regularPrice} value={regularPriceValue} asNumber={asNumber} />
61
64
  </PrivateQueryMask>
62
65
  )}
63
66
  <PrivateQueryMask
@@ -65,16 +68,15 @@ export function ProductPagePrice(props: ProductPagePriceProps) {
65
68
  skeleton={{ variant: 'text', sx: { width: '3.5em', transform: 'none' } }}
66
69
  className={classes.finalPrice}
67
70
  >
68
- <Money {...price} value={priceValue} />
71
+ <Money {...price} value={finalPriceValue} asNumber={asNumber} />
69
72
  </PrivateQueryMask>
73
+
70
74
  {suffix && (
71
75
  <PrivateQueryMask
72
76
  component='span'
73
77
  className={classes.suffix}
74
- skeleton={{
75
- variant: 'text',
76
- sx: { width: '3.5em', transform: 'none' },
77
- }}
78
+ sx={{ '&:empty': { display: 'none' } }}
79
+ skeleton={{ variant: 'text', sx: { width: '3.5em', transform: 'none' } }}
78
80
  >
79
81
  {suffix}
80
82
  </PrivateQueryMask>
@@ -0,0 +1,35 @@
1
+ import { filterNonNullableKeys, sxx } from '@graphcommerce/next-ui'
2
+ import { Trans } from '@lingui/macro'
3
+ import { Box, type SxProps, type Theme } from '@mui/material'
4
+ import { type AddToCartItemSelector } from '../AddProductsToCart'
5
+ import { ProductListPrice } from '../ProductListPrice'
6
+ import { type ProductPagePriceFragment } from './ProductPagePrice.gql'
7
+ import type { UseCustomizableOptionPriceProps } from './useCustomizableOptionPrice'
8
+
9
+ export type ProductPagePriceLowestProps = {
10
+ sx?: SxProps<Theme>
11
+ product: ProductPagePriceFragment
12
+ } & AddToCartItemSelector &
13
+ UseCustomizableOptionPriceProps
14
+
15
+ export function ProductPagePriceLowest(props: ProductPagePriceLowestProps) {
16
+ const { product, sx, index = 0 } = props
17
+ const priceTiers = filterNonNullableKeys(product.price_tiers, ['quantity', 'final_price'])
18
+ const lastTier = priceTiers[priceTiers.length - 1]
19
+
20
+ const lowestTier = (lastTier?.final_price.value ?? 0) / (lastTier?.quantity ?? 1)
21
+ const finalPrice = lowestTier
22
+ ? {
23
+ value: lowestTier,
24
+ currency: product.price_range.minimum_price.final_price.currency,
25
+ }
26
+ : product.price_range.minimum_price.final_price
27
+
28
+ return (
29
+ <Box component='div' sx={sxx({}, sx)}>
30
+ <Trans>
31
+ As low as <ProductListPrice final_price={finalPrice} regular_price={finalPrice} />
32
+ </Trans>
33
+ </Box>
34
+ )
35
+ }
@@ -1,4 +1,5 @@
1
1
  import type { MoneyFragment } from '@graphcommerce/magento-store'
2
+ import { filterNonNullableKeys } from '@graphcommerce/next-ui'
2
3
  import type { ProductPagePriceFragment } from './ProductPagePrice.gql'
3
4
 
4
5
  export function getProductTierPrice(
@@ -8,8 +9,12 @@ export function getProductTierPrice(
8
9
  const { price_tiers } = price
9
10
  let result: MoneyFragment | undefined | null
10
11
 
11
- price_tiers?.forEach((priceTier) => {
12
- if (priceTier?.quantity && quantity >= priceTier?.quantity) result = priceTier?.final_price
12
+ filterNonNullableKeys(price_tiers, ['quantity', 'final_price'])?.forEach((priceTier) => {
13
+ if (quantity >= priceTier.quantity)
14
+ result = {
15
+ value: (priceTier.final_price.value ?? 0) / priceTier.quantity,
16
+ currency: priceTier.final_price.currency,
17
+ }
13
18
  })
14
19
 
15
20
  return result
@@ -1,4 +1,5 @@
1
1
  export * from './ProductPagePrice.gql'
2
2
  export * from './ProductPagePriceTiers'
3
+ export * from './ProductPagePriceLowest'
3
4
  export * from './ProductPagePrice'
4
5
  export * from './getProductTierPrice'
@@ -7,11 +7,7 @@ export * from './ProductList/ProductList.gql'
7
7
  export * from './ProductListCount/ProductListCount'
8
8
  export * from './ProductListFilters'
9
9
  export * from './ProductListFiltersContainer/ProductListFiltersContainer'
10
- export * from './ProductListItem/ProductListItem'
11
- export * from './ProductListItem/ProductListItemImage'
12
- export * from './ProductListItem/ProductListItemTitleAndPrice'
13
- export * from './ProductListItem/ProductListItemImageContainer'
14
- export * from './ProductListItem/ProductListItemLinkOrDiv'
10
+ export * from './ProductListItem'
15
11
  export * from './ProductListItems/filteredProductList'
16
12
  export * from './ProductListItems/filterTypes'
17
13
  export * from './ProductListItems/getFilterTypes'
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/magento-product",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "9.1.0-canary.40",
5
+ "version": "9.1.0-canary.42",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -18,18 +18,18 @@
18
18
  "typescript": "5.7.2"
19
19
  },
20
20
  "peerDependencies": {
21
- "@graphcommerce/ecommerce-ui": "^9.1.0-canary.40",
22
- "@graphcommerce/eslint-config-pwa": "^9.1.0-canary.40",
23
- "@graphcommerce/framer-next-pages": "^9.1.0-canary.40",
24
- "@graphcommerce/framer-scroller": "^9.1.0-canary.40",
25
- "@graphcommerce/graphql": "^9.1.0-canary.40",
26
- "@graphcommerce/graphql-mesh": "^9.1.0-canary.40",
27
- "@graphcommerce/image": "^9.1.0-canary.40",
28
- "@graphcommerce/magento-cart": "^9.1.0-canary.40",
29
- "@graphcommerce/magento-store": "^9.1.0-canary.40",
30
- "@graphcommerce/next-ui": "^9.1.0-canary.40",
31
- "@graphcommerce/prettier-config-pwa": "^9.1.0-canary.40",
32
- "@graphcommerce/typescript-config-pwa": "^9.1.0-canary.40",
21
+ "@graphcommerce/ecommerce-ui": "^9.1.0-canary.42",
22
+ "@graphcommerce/eslint-config-pwa": "^9.1.0-canary.42",
23
+ "@graphcommerce/framer-next-pages": "^9.1.0-canary.42",
24
+ "@graphcommerce/framer-scroller": "^9.1.0-canary.42",
25
+ "@graphcommerce/graphql": "^9.1.0-canary.42",
26
+ "@graphcommerce/graphql-mesh": "^9.1.0-canary.42",
27
+ "@graphcommerce/image": "^9.1.0-canary.42",
28
+ "@graphcommerce/magento-cart": "^9.1.0-canary.42",
29
+ "@graphcommerce/magento-store": "^9.1.0-canary.42",
30
+ "@graphcommerce/next-ui": "^9.1.0-canary.42",
31
+ "@graphcommerce/prettier-config-pwa": "^9.1.0-canary.42",
32
+ "@graphcommerce/typescript-config-pwa": "^9.1.0-canary.42",
33
33
  "@lingui/core": "^4.2.1",
34
34
  "@lingui/macro": "^4.2.1",
35
35
  "@lingui/react": "^4.2.1",