@graphcommerce/magento-product 4.7.2 → 4.8.0
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/CHANGELOG.md +37 -0
- package/components/AddProductsToCart/AddProductsToCartButton.tsx +21 -38
- package/components/AddProductsToCart/AddProductsToCartError.tsx +1 -1
- package/components/AddProductsToCart/AddProductsToCartFab.tsx +18 -0
- package/components/AddProductsToCart/AddProductsToCartForm.tsx +88 -46
- package/components/AddProductsToCart/AddProductsToCartQuantity.tsx +1 -1
- package/components/AddProductsToCart/AddProductsToCartSnackbar.tsx +65 -48
- package/components/AddProductsToCart/index.ts +2 -1
- package/components/AddProductsToCart/useAddProductsToCartAction.ts +40 -0
- package/components/AddProductsToCart/useFormAddProductsToCart.ts +30 -0
- package/components/ProductCustomizable/ProductCustomizable.tsx +11 -5
- package/components/ProductListItem/ProductListItem.tsx +21 -7
- package/components/ProductListItems/ProductListItemsBase.tsx +13 -5
- package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.tsx +22 -24
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 4.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#1679](https://github.com/graphcommerce-org/graphcommerce/pull/1679) [`2b5451395`](https://github.com/graphcommerce-org/graphcommerce/commit/2b5451395dc1173de55d18d08968866e561f90ab) Thanks [@paales](https://github.com/paales)! - Move AddProductsToCartSnackbar to inside AddProductsToCartForm and make AddProductsToCartForm configrable via theme.ts
|
|
8
|
+
|
|
9
|
+
- [#1679](https://github.com/graphcommerce-org/graphcommerce/pull/1679) [`e76df6dc3`](https://github.com/graphcommerce-org/graphcommerce/commit/e76df6dc37c11c793a5d008ba36932d17dc23855) Thanks [@paales](https://github.com/paales)! - Added AddProductsToCartFab for a smaller add to cart button
|
|
10
|
+
|
|
11
|
+
- [#1678](https://github.com/graphcommerce-org/graphcommerce/pull/1678) [`78d7d51cb`](https://github.com/graphcommerce-org/graphcommerce/commit/78d7d51cb1551601d3a4756cd1f2157a49ff93b9) Thanks [@Jessevdpoel](https://github.com/Jessevdpoel)! - Changed styling and forwarded breadcrumbprops
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#1679](https://github.com/graphcommerce-org/graphcommerce/pull/1679) [`c4ed376e2`](https://github.com/graphcommerce-org/graphcommerce/commit/c4ed376e2c72b16b34704d7d1ca69c074de172ba) Thanks [@paales](https://github.com/paales)! - Support passing children to AddProductsToCartButton instead of Add To Cart
|
|
16
|
+
|
|
17
|
+
- Updated dependencies [[`e76df6dc3`](https://github.com/graphcommerce-org/graphcommerce/commit/e76df6dc37c11c793a5d008ba36932d17dc23855), [`0bd9ea582`](https://github.com/graphcommerce-org/graphcommerce/commit/0bd9ea58230dde79c5fe2cdb07e9860151460270)]:
|
|
18
|
+
- @graphcommerce/next-ui@4.29.0
|
|
19
|
+
- @graphcommerce/ecommerce-ui@1.5.5
|
|
20
|
+
- @graphcommerce/framer-scroller@2.1.42
|
|
21
|
+
- @graphcommerce/magento-cart@4.9.1
|
|
22
|
+
- @graphcommerce/magento-store@4.3.3
|
|
23
|
+
|
|
24
|
+
## 4.7.3
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- [#1675](https://github.com/graphcommerce-org/graphcommerce/pull/1675) [`1b1504c9b`](https://github.com/graphcommerce-org/graphcommerce/commit/1b1504c9b0e51f2787bce91e1ff1940f540411d6) Thanks [@paales](https://github.com/paales)! - Added crosssel functionality
|
|
29
|
+
|
|
30
|
+
- Updated dependencies [[`9e630670f`](https://github.com/graphcommerce-org/graphcommerce/commit/9e630670ff6c952ab7b938d890b5509804985cf3), [`cf3518499`](https://github.com/graphcommerce-org/graphcommerce/commit/cf351849999ad6fe73ce2bb258098a7dd301d517), [`81f31d1e5`](https://github.com/graphcommerce-org/graphcommerce/commit/81f31d1e54397368088a4289aaddd29facfceeef), [`2e9fa5984`](https://github.com/graphcommerce-org/graphcommerce/commit/2e9fa5984a07ff14fc1b3a4f62189a26e8e3ecdd), [`adf13069a`](https://github.com/graphcommerce-org/graphcommerce/commit/adf13069af6460c960276b402237371c12fc6dec), [`a8905d263`](https://github.com/graphcommerce-org/graphcommerce/commit/a8905d263273cb9322583d5759a5fdc66eceb8e4), [`1b1504c9b`](https://github.com/graphcommerce-org/graphcommerce/commit/1b1504c9b0e51f2787bce91e1ff1940f540411d6), [`8a34f8081`](https://github.com/graphcommerce-org/graphcommerce/commit/8a34f808186274a6fe1d4f309472f1a9c6d00efd), [`3dde492ad`](https://github.com/graphcommerce-org/graphcommerce/commit/3dde492ad3a49d96481eeb7453fb305d0017b1a5)]:
|
|
31
|
+
- @graphcommerce/next-ui@4.28.1
|
|
32
|
+
- @graphcommerce/graphql@3.5.0
|
|
33
|
+
- @graphcommerce/framer-scroller@2.1.41
|
|
34
|
+
- @graphcommerce/magento-cart@4.9.0
|
|
35
|
+
- @graphcommerce/ecommerce-ui@1.5.4
|
|
36
|
+
- @graphcommerce/magento-store@4.3.2
|
|
37
|
+
- @graphcommerce/framer-next-pages@3.3.2
|
|
38
|
+
- @graphcommerce/image@3.1.10
|
|
39
|
+
|
|
3
40
|
## 4.7.2
|
|
4
41
|
|
|
5
42
|
### Patch Changes
|
|
@@ -1,48 +1,31 @@
|
|
|
1
1
|
import { Button, ButtonProps } from '@graphcommerce/next-ui'
|
|
2
2
|
import { Trans } from '@lingui/react'
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
useAddProductsToCartAction,
|
|
5
|
+
UseAddProductsToCartActionProps,
|
|
6
|
+
} from './useAddProductsToCartAction'
|
|
5
7
|
|
|
6
|
-
export type AddProductsToCartButtonProps =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
| 'onClick'
|
|
20
|
-
| 'loading'
|
|
21
|
-
>
|
|
8
|
+
export type AddProductsToCartButtonProps = UseAddProductsToCartActionProps &
|
|
9
|
+
Pick<
|
|
10
|
+
ButtonProps<'button'>,
|
|
11
|
+
| 'variant'
|
|
12
|
+
| 'color'
|
|
13
|
+
| 'size'
|
|
14
|
+
| 'fullWidth'
|
|
15
|
+
| 'startIcon'
|
|
16
|
+
| 'endIcon'
|
|
17
|
+
| 'onClick'
|
|
18
|
+
| 'sx'
|
|
19
|
+
| 'children'
|
|
20
|
+
>
|
|
22
21
|
|
|
23
22
|
export function AddProductsToCartButton(props: AddProductsToCartButtonProps) {
|
|
24
|
-
const {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const clickHandler: NonNullable<AddProductsToCartButtonProps['onClick']> = useEventCallback(
|
|
28
|
-
(e) => {
|
|
29
|
-
setValue(`cartItems.${index}.sku`, sku)
|
|
30
|
-
onClick?.(e)
|
|
31
|
-
},
|
|
32
|
-
)
|
|
23
|
+
const { children } = props
|
|
24
|
+
const action = useAddProductsToCartAction(props)
|
|
33
25
|
|
|
34
26
|
return (
|
|
35
|
-
<Button
|
|
36
|
-
|
|
37
|
-
color='primary'
|
|
38
|
-
variant='pill'
|
|
39
|
-
size='large'
|
|
40
|
-
{...props}
|
|
41
|
-
disabled={Boolean(formState.errors.cartItems?.[index].sku?.message) || disabled}
|
|
42
|
-
loading={formState.isSubmitting || loading}
|
|
43
|
-
onClick={clickHandler}
|
|
44
|
-
>
|
|
45
|
-
<Trans id='Add to Cart' />
|
|
27
|
+
<Button type='submit' color='primary' variant='pill' size='large' {...props} {...action}>
|
|
28
|
+
{children || <Trans id='Add to Cart' />}
|
|
46
29
|
</Button>
|
|
47
30
|
)
|
|
48
31
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Fab, FabProps, iconShoppingBag } from '@graphcommerce/next-ui'
|
|
2
|
+
import { SxProps, Theme } from '@mui/material'
|
|
3
|
+
import {
|
|
4
|
+
useAddProductsToCartAction,
|
|
5
|
+
UseAddProductsToCartActionProps,
|
|
6
|
+
} from './useAddProductsToCartAction'
|
|
7
|
+
|
|
8
|
+
export type AddProductsToCartFabProps = {
|
|
9
|
+
sx?: SxProps<Theme>
|
|
10
|
+
icon?: FabProps['icon']
|
|
11
|
+
} & Pick<FabProps, 'color' | 'size'> &
|
|
12
|
+
UseAddProductsToCartActionProps
|
|
13
|
+
|
|
14
|
+
export function AddProductsToCartFab(props: AddProductsToCartFabProps) {
|
|
15
|
+
const { icon = iconShoppingBag } = props
|
|
16
|
+
const action = useAddProductsToCartAction(props)
|
|
17
|
+
return <Fab type='submit' {...props} {...action} icon={icon} />
|
|
18
|
+
}
|
|
@@ -1,69 +1,111 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UseFormGraphQlOptions } from '@graphcommerce/ecommerce-ui'
|
|
2
2
|
import { useFormGqlMutationCart } from '@graphcommerce/magento-cart'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { ExtendableComponent } from '@graphcommerce/next-ui'
|
|
4
|
+
import { Box, SxProps, Theme, useThemeProps } from '@mui/material'
|
|
5
|
+
import { useRouter } from 'next/router'
|
|
6
|
+
import { useMemo } from 'react'
|
|
5
7
|
import {
|
|
6
8
|
AddProductsToCartDocument,
|
|
7
9
|
AddProductsToCartMutation,
|
|
8
10
|
AddProductsToCartMutationVariables,
|
|
9
11
|
} from './AddProductsToCart.gql'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export const addProductsToCartContext = createContext(
|
|
17
|
-
undefined as AddProductsToCartContextType | undefined,
|
|
18
|
-
)
|
|
12
|
+
import {
|
|
13
|
+
AddProductsToCartSnackbar,
|
|
14
|
+
AddProductsToCartSnackbarProps,
|
|
15
|
+
} from './AddProductsToCartSnackbar'
|
|
16
|
+
import { AddProductsToCartContext, RedirectType } from './useFormAddProductsToCart'
|
|
19
17
|
|
|
20
18
|
type AddProductsToCartFormProps = {
|
|
19
|
+
// The props are actually used, but are passed through useThemeProps and that breaks react/no-unused-prop-types
|
|
20
|
+
// eslint-disable-next-line react/no-unused-prop-types
|
|
21
21
|
children: React.ReactNode
|
|
22
|
+
// eslint-disable-next-line react/no-unused-prop-types
|
|
22
23
|
sx?: SxProps<Theme>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// eslint-disable-next-line react/no-unused-prop-types
|
|
25
|
+
redirect?: RedirectType
|
|
26
|
+
} & UseFormGraphQlOptions<AddProductsToCartMutation, AddProductsToCartMutationVariables> &
|
|
27
|
+
AddProductsToCartSnackbarProps
|
|
28
|
+
|
|
29
|
+
const name = 'AddProductsToCartForm'
|
|
27
30
|
|
|
31
|
+
/** Expose the component to be exendable in your theme.components */
|
|
32
|
+
declare module '@mui/material/styles/components' {
|
|
33
|
+
interface Components {
|
|
34
|
+
AddProductsToCartForm?: Pick<
|
|
35
|
+
ExtendableComponent<Omit<AddProductsToCartFormProps, 'children'>>,
|
|
36
|
+
'defaultProps'
|
|
37
|
+
>
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Component that handles adding products to the cart. Used on the product page, but can be used for
|
|
43
|
+
* any product listing.
|
|
44
|
+
*
|
|
45
|
+
* Can be configured globally in your theme.ts;
|
|
46
|
+
*
|
|
47
|
+
* - Uses react-hook-form's useForm hook under the hood and exposes the form as a context which can be
|
|
48
|
+
* consumed with `useFormAddProductsToCart` hook.
|
|
49
|
+
* - Cleans up the submitted data.
|
|
50
|
+
* - Redirects the user to the cart/checkout/added page after successful submission.
|
|
51
|
+
*/
|
|
28
52
|
export function AddProductsToCartForm(props: AddProductsToCartFormProps) {
|
|
29
|
-
const {
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
const {
|
|
54
|
+
children,
|
|
55
|
+
redirect = 'cart',
|
|
56
|
+
onComplete,
|
|
57
|
+
sx,
|
|
58
|
+
errorSnackbar,
|
|
59
|
+
successSnackbar,
|
|
60
|
+
...formProps
|
|
61
|
+
} = useThemeProps({ name, props })
|
|
62
|
+
const router = useRouter()
|
|
32
63
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.filter((cartItem) => cartItem.sku)
|
|
38
|
-
.map((cartItem) => ({
|
|
39
|
-
...cartItem,
|
|
40
|
-
selected_options: cartItem.selected_options?.filter(Boolean),
|
|
41
|
-
entered_options: cartItem.entered_options?.filter((option) => option?.value),
|
|
42
|
-
})),
|
|
43
|
-
}),
|
|
64
|
+
const form = useFormGqlMutationCart<
|
|
65
|
+
AddProductsToCartMutation,
|
|
66
|
+
AddProductsToCartMutationVariables
|
|
67
|
+
>(AddProductsToCartDocument, {
|
|
44
68
|
...formProps,
|
|
69
|
+
// We're stripping out incomplete entered options.
|
|
70
|
+
onBeforeSubmit: async (variables) => {
|
|
71
|
+
const variables2 = (await formProps.onBeforeSubmit?.(variables)) ?? variables
|
|
72
|
+
if (variables2 === false) return false
|
|
73
|
+
|
|
74
|
+
const { cartId, cartItems } = variables2
|
|
75
|
+
return {
|
|
76
|
+
cartId,
|
|
77
|
+
cartItems: cartItems
|
|
78
|
+
.filter((cartItem) => cartItem.sku)
|
|
79
|
+
.map((cartItem) => ({
|
|
80
|
+
...cartItem,
|
|
81
|
+
quantity: cartItem.quantity || 1,
|
|
82
|
+
selected_options: cartItem.selected_options?.filter(Boolean),
|
|
83
|
+
entered_options: cartItem.entered_options?.filter((option) => option?.value),
|
|
84
|
+
})),
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
onComplete: async (result, variables) => {
|
|
88
|
+
await onComplete?.(result, variables)
|
|
89
|
+
|
|
90
|
+
if (result.data?.addProductsToCart?.user_errors?.length || result.errors?.length || !redirect)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
if (redirect === 'checkout') await router.push('/checkout')
|
|
94
|
+
if (redirect === 'added') await router.push({ pathname: '/checkout/added' })
|
|
95
|
+
if (redirect === 'cart') await router.push({ pathname: '/cart' })
|
|
96
|
+
},
|
|
45
97
|
})
|
|
46
98
|
|
|
47
99
|
const submit = form.handleSubmit(() => {})
|
|
48
100
|
|
|
49
101
|
return (
|
|
50
|
-
<
|
|
51
|
-
|
|
102
|
+
<AddProductsToCartContext.Provider
|
|
103
|
+
value={useMemo(() => ({ ...form, redirect }), [form, redirect])}
|
|
104
|
+
>
|
|
105
|
+
<Box component='form' onSubmit={submit} noValidate sx={sx} className={name}>
|
|
52
106
|
{children}
|
|
53
107
|
</Box>
|
|
54
|
-
|
|
108
|
+
<AddProductsToCartSnackbar errorSnackbar={errorSnackbar} successSnackbar={successSnackbar} />
|
|
109
|
+
</AddProductsToCartContext.Provider>
|
|
55
110
|
)
|
|
56
111
|
}
|
|
57
|
-
|
|
58
|
-
export function useFormAddProductsToCart(optional: true): AddProductsToCartContextType | undefined
|
|
59
|
-
export function useFormAddProductsToCart(optional?: false): AddProductsToCartContextType
|
|
60
|
-
export function useFormAddProductsToCart(optional = false) {
|
|
61
|
-
const context = useContext(addProductsToCartContext)
|
|
62
|
-
|
|
63
|
-
if (!optional && typeof context === 'undefined') {
|
|
64
|
-
throw Error(
|
|
65
|
-
'useFormAddProductsToCart must be used within a AddProductsToCartForm or provide the optional=true argument',
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
return context
|
|
69
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NumberFieldElement, NumberFieldElementProps } from '@graphcommerce/ecommerce-ui'
|
|
2
|
-
import { useFormAddProductsToCart } from './
|
|
2
|
+
import { useFormAddProductsToCart } from './useFormAddProductsToCart'
|
|
3
3
|
|
|
4
4
|
type AddToCartQuantityProps = Omit<
|
|
5
5
|
NumberFieldElementProps,
|
|
@@ -1,67 +1,84 @@
|
|
|
1
1
|
import { ApolloCartErrorSnackbar } from '@graphcommerce/magento-cart'
|
|
2
2
|
import {
|
|
3
3
|
Button,
|
|
4
|
+
ErrorSnackbar,
|
|
5
|
+
ErrorSnackbarProps,
|
|
6
|
+
filterNonNullableKeys,
|
|
4
7
|
iconChevronRight,
|
|
5
8
|
IconSvg,
|
|
6
9
|
MessageSnackbar,
|
|
7
|
-
|
|
10
|
+
MessageSnackbarProps,
|
|
8
11
|
} from '@graphcommerce/next-ui'
|
|
9
12
|
import { Trans } from '@lingui/react'
|
|
10
13
|
import PageLink from 'next/link'
|
|
11
|
-
import { useFormAddProductsToCart } from './
|
|
14
|
+
import { useFormAddProductsToCart } from './useFormAddProductsToCart'
|
|
15
|
+
|
|
16
|
+
export type AddProductsToCartSnackbarProps = {
|
|
17
|
+
errorSnackbar?: Omit<ErrorSnackbarProps, 'open'>
|
|
18
|
+
successSnackbar?: Omit<MessageSnackbarProps, 'open' | 'action'>
|
|
19
|
+
}
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
export function AddProductsToCartSnackbar(props: AddProductsToCartSnackbarProps) {
|
|
22
|
+
const { errorSnackbar, successSnackbar } = props
|
|
23
|
+
const { formState, error, data, redirect } = useFormAddProductsToCart()
|
|
14
24
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
const showSuccess =
|
|
26
|
+
!formState.isSubmitting &&
|
|
27
|
+
formState.isSubmitSuccessful &&
|
|
28
|
+
!error?.message &&
|
|
29
|
+
!data?.addProductsToCart?.user_errors?.length &&
|
|
30
|
+
!redirect
|
|
18
31
|
|
|
32
|
+
const items = filterNonNullableKeys(data?.addProductsToCart?.cart.items)
|
|
33
|
+
|
|
34
|
+
const showErrorSnackbar = (data?.addProductsToCart?.user_errors?.length ?? 0) > 0
|
|
19
35
|
return (
|
|
20
36
|
<>
|
|
21
|
-
<ApolloCartErrorSnackbar error={error} />
|
|
22
|
-
|
|
23
|
-
<ErrorSnackbar
|
|
24
|
-
variant='pill'
|
|
25
|
-
severity='error'
|
|
26
|
-
open={(data?.addProductsToCart?.user_errors?.length ?? 0) > 0}
|
|
27
|
-
action={
|
|
28
|
-
<Button size='medium' variant='pill' color='secondary'>
|
|
29
|
-
<Trans id='Ok' />
|
|
30
|
-
</Button>
|
|
31
|
-
}
|
|
32
|
-
>
|
|
33
|
-
<>{data?.addProductsToCart?.user_errors?.map((e) => e?.message).join(', ')}</>
|
|
34
|
-
</ErrorSnackbar>
|
|
37
|
+
{error && <ApolloCartErrorSnackbar error={error} />}
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
variant='pill'
|
|
44
|
-
action={
|
|
45
|
-
<PageLink href='/cart' passHref>
|
|
46
|
-
<Button
|
|
47
|
-
id='view-shopping-cart-button'
|
|
48
|
-
size='medium'
|
|
49
|
-
variant='pill'
|
|
50
|
-
color='secondary'
|
|
51
|
-
endIcon={<IconSvg src={iconChevronRight} />}
|
|
52
|
-
sx={{ display: 'flex' }}
|
|
53
|
-
>
|
|
54
|
-
<Trans id='View shopping cart' />
|
|
39
|
+
{showErrorSnackbar && (
|
|
40
|
+
<ErrorSnackbar
|
|
41
|
+
variant='pill'
|
|
42
|
+
severity='error'
|
|
43
|
+
action={
|
|
44
|
+
<Button size='medium' variant='pill' color='secondary'>
|
|
45
|
+
<Trans id='Ok' />
|
|
55
46
|
</Button>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
47
|
+
}
|
|
48
|
+
{...errorSnackbar}
|
|
49
|
+
open={showErrorSnackbar}
|
|
50
|
+
>
|
|
51
|
+
<>{data?.addProductsToCart?.user_errors?.map((e) => e?.message).join(', ')}</>
|
|
52
|
+
</ErrorSnackbar>
|
|
53
|
+
)}
|
|
54
|
+
|
|
55
|
+
{showSuccess && (
|
|
56
|
+
<MessageSnackbar
|
|
57
|
+
variant='pill'
|
|
58
|
+
{...successSnackbar}
|
|
59
|
+
open={showSuccess}
|
|
60
|
+
action={
|
|
61
|
+
<PageLink href='/cart' passHref>
|
|
62
|
+
<Button
|
|
63
|
+
id='view-shopping-cart-button'
|
|
64
|
+
size='medium'
|
|
65
|
+
variant='pill'
|
|
66
|
+
color='secondary'
|
|
67
|
+
endIcon={<IconSvg src={iconChevronRight} />}
|
|
68
|
+
sx={{ display: 'flex' }}
|
|
69
|
+
>
|
|
70
|
+
<Trans id='View shopping cart' />
|
|
71
|
+
</Button>
|
|
72
|
+
</PageLink>
|
|
73
|
+
}
|
|
74
|
+
>
|
|
75
|
+
<Trans
|
|
76
|
+
id='<0>{name}</0> has been added to your shopping cart!'
|
|
77
|
+
components={{ 0: <strong /> }}
|
|
78
|
+
values={{ name: items[items.length - 1]?.product.name }}
|
|
79
|
+
/>
|
|
80
|
+
</MessageSnackbar>
|
|
81
|
+
)}
|
|
65
82
|
</>
|
|
66
83
|
)
|
|
67
84
|
}
|
|
@@ -3,4 +3,5 @@ export * from './AddProductsToCartButton'
|
|
|
3
3
|
export * from './AddProductsToCartError'
|
|
4
4
|
export * from './AddProductsToCartForm'
|
|
5
5
|
export * from './AddProductsToCartQuantity'
|
|
6
|
-
export * from './
|
|
6
|
+
export * from './useFormAddProductsToCart'
|
|
7
|
+
export * from './AddProductsToCartFab'
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEventCallback } from '@mui/material'
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
import { useFormAddProductsToCart } from './useFormAddProductsToCart'
|
|
4
|
+
|
|
5
|
+
export type UseAddProductsToCartActionProps = {
|
|
6
|
+
sku: string | null | undefined
|
|
7
|
+
index?: number
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
loading?: boolean
|
|
10
|
+
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type UseAddProductsToCartActionReturn = {
|
|
14
|
+
disabled: boolean
|
|
15
|
+
loading: boolean
|
|
16
|
+
onClick: React.MouseEventHandler<HTMLButtonElement>
|
|
17
|
+
onMouseDown: React.MouseEventHandler<HTMLButtonElement>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useAddProductsToCartAction(
|
|
21
|
+
props: UseAddProductsToCartActionProps,
|
|
22
|
+
): UseAddProductsToCartActionReturn {
|
|
23
|
+
const { formState, setValue, getValues } = useFormAddProductsToCart()
|
|
24
|
+
const { sku, index = 0, onClick: onClickIncomming, disabled, loading } = props
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
disabled: Boolean(formState.errors.cartItems?.[index].sku?.message || disabled),
|
|
28
|
+
loading: loading || (formState.isSubmitting && getValues(`cartItems.${index}.sku`) === sku),
|
|
29
|
+
onClick: useEventCallback((e) => {
|
|
30
|
+
e.stopPropagation()
|
|
31
|
+
if (formState.isSubmitting) return
|
|
32
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
33
|
+
if (!sku) console.warn(`You must provide a 'sku' to useAddProductsToCartAction`)
|
|
34
|
+
}
|
|
35
|
+
setValue(`cartItems.${index}.sku`, sku ?? '')
|
|
36
|
+
onClickIncomming?.(e)
|
|
37
|
+
}),
|
|
38
|
+
onMouseDown: useEventCallback((e) => e.stopPropagation()),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { UseFormGqlMutationReturn } from '@graphcommerce/ecommerce-ui'
|
|
2
|
+
import { createContext, useContext } from 'react'
|
|
3
|
+
import {
|
|
4
|
+
AddProductsToCartMutation,
|
|
5
|
+
AddProductsToCartMutationVariables,
|
|
6
|
+
} from './AddProductsToCart.gql'
|
|
7
|
+
|
|
8
|
+
export type RedirectType = 'added' | 'cart' | 'checkout' | undefined
|
|
9
|
+
|
|
10
|
+
export type AddProductsToCartContextType = { redirect: RedirectType } & UseFormGqlMutationReturn<
|
|
11
|
+
AddProductsToCartMutation,
|
|
12
|
+
AddProductsToCartMutationVariables
|
|
13
|
+
>
|
|
14
|
+
|
|
15
|
+
export const AddProductsToCartContext = createContext(
|
|
16
|
+
undefined as AddProductsToCartContextType | undefined,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
export function useFormAddProductsToCart(optional: true): AddProductsToCartContextType | undefined
|
|
20
|
+
export function useFormAddProductsToCart(optional?: false): AddProductsToCartContextType
|
|
21
|
+
export function useFormAddProductsToCart(optional = false) {
|
|
22
|
+
const context = useContext(AddProductsToCartContext)
|
|
23
|
+
|
|
24
|
+
if (!optional && typeof context === 'undefined') {
|
|
25
|
+
throw Error(
|
|
26
|
+
'useFormAddProductsToCart must be used within a AddProductsToCartForm or provide the optional=true argument',
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
return context
|
|
30
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SelectElement, TextFieldElement } from '@graphcommerce/ecommerce-ui'
|
|
2
2
|
import { filterNonNullableKeys, RenderType, TypeRenderer } from '@graphcommerce/next-ui'
|
|
3
|
+
import React from 'react'
|
|
3
4
|
import { useFormAddProductsToCart } from '../AddProductsToCart'
|
|
4
5
|
import { ProductCustomizableFragment } from './ProductCustomizable.gql'
|
|
5
6
|
|
|
@@ -10,7 +11,9 @@ type OptionTypeRenderer = TypeRenderer<
|
|
|
10
11
|
}
|
|
11
12
|
>
|
|
12
13
|
|
|
13
|
-
const CustomizableAreaOption
|
|
14
|
+
const CustomizableAreaOption = React.memo<
|
|
15
|
+
React.ComponentProps<OptionTypeRenderer['CustomizableAreaOption']>
|
|
16
|
+
>((props) => {
|
|
14
17
|
const { uid, areaValue, required, optionIndex, index, title } = props
|
|
15
18
|
const maxLength = areaValue?.max_characters ?? undefined
|
|
16
19
|
const { control, register } = useFormAddProductsToCart()
|
|
@@ -34,9 +37,11 @@ const CustomizableAreaOption: OptionTypeRenderer['CustomizableAreaOption'] = (pr
|
|
|
34
37
|
/>
|
|
35
38
|
</>
|
|
36
39
|
)
|
|
37
|
-
}
|
|
40
|
+
})
|
|
38
41
|
|
|
39
|
-
const CustomizableDropDownOption
|
|
42
|
+
const CustomizableDropDownOption = React.memo<
|
|
43
|
+
React.ComponentProps<OptionTypeRenderer['CustomizableDropDownOption']>
|
|
44
|
+
>((props) => {
|
|
40
45
|
const { uid, required, optionIndex, index, title, dropdownValue } = props
|
|
41
46
|
const { control, register } = useFormAddProductsToCart()
|
|
42
47
|
|
|
@@ -52,6 +57,7 @@ const CustomizableDropDownOption: OptionTypeRenderer['CustomizableDropDownOption
|
|
|
52
57
|
name={`cartItems.${index}.entered_options.${optionIndex}.value`}
|
|
53
58
|
label={title}
|
|
54
59
|
required={Boolean(required)}
|
|
60
|
+
defaultValue=''
|
|
55
61
|
options={filterNonNullableKeys(dropdownValue, ['title']).map((option) => ({
|
|
56
62
|
id: option.uid,
|
|
57
63
|
label: option.title,
|
|
@@ -59,7 +65,7 @@ const CustomizableDropDownOption: OptionTypeRenderer['CustomizableDropDownOption
|
|
|
59
65
|
/>
|
|
60
66
|
</>
|
|
61
67
|
)
|
|
62
|
-
}
|
|
68
|
+
})
|
|
63
69
|
|
|
64
70
|
const renderer: OptionTypeRenderer = {
|
|
65
71
|
CustomizableAreaOption,
|
|
@@ -84,7 +90,7 @@ export function ProductCustomizable(props: ProductCustomizableProps) {
|
|
|
84
90
|
key={option.uid}
|
|
85
91
|
renderer={renderer}
|
|
86
92
|
{...option}
|
|
87
|
-
optionIndex={option.sort_order
|
|
93
|
+
optionIndex={option.sort_order + 100}
|
|
88
94
|
index={index}
|
|
89
95
|
/>
|
|
90
96
|
))}
|
|
@@ -6,7 +6,16 @@ import {
|
|
|
6
6
|
breakpointVal,
|
|
7
7
|
} from '@graphcommerce/next-ui'
|
|
8
8
|
import { Trans } from '@lingui/react'
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
ButtonBase,
|
|
11
|
+
Typography,
|
|
12
|
+
Box,
|
|
13
|
+
styled,
|
|
14
|
+
SxProps,
|
|
15
|
+
Theme,
|
|
16
|
+
ButtonBaseProps,
|
|
17
|
+
useEventCallback,
|
|
18
|
+
} from '@mui/material'
|
|
10
19
|
import PageLink from 'next/link'
|
|
11
20
|
import React, { PropsWithChildren } from 'react'
|
|
12
21
|
import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
|
|
@@ -40,16 +49,15 @@ type StyleProps = {
|
|
|
40
49
|
imageOnly?: boolean
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
type BaseProps =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Pick<ImageProps, 'loading' | 'sizes' | 'dontReportWronglySizedImages'>
|
|
48
|
-
>
|
|
52
|
+
type BaseProps = { subTitle?: React.ReactNode; children?: React.ReactNode } & StyleProps &
|
|
53
|
+
OverlayAreas &
|
|
54
|
+
ProductListItemFragment &
|
|
55
|
+
Pick<ImageProps, 'loading' | 'sizes' | 'dontReportWronglySizedImages'>
|
|
49
56
|
|
|
50
57
|
export type ProductListItemProps = BaseProps & {
|
|
51
58
|
sx?: SxProps<Theme>
|
|
52
59
|
titleComponent?: React.ElementType
|
|
60
|
+
onClick?: (event: React.MouseEvent<HTMLAnchorElement>, item: ProductListItemFragment) => void
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
const StyledImage = styled(Image)({})
|
|
@@ -72,8 +80,13 @@ export function ProductListItem(props: ProductListItemProps) {
|
|
|
72
80
|
aspectRatio = [4, 3],
|
|
73
81
|
titleComponent = 'h2',
|
|
74
82
|
sx = [],
|
|
83
|
+
onClick,
|
|
75
84
|
} = props
|
|
76
85
|
|
|
86
|
+
const handleClick = useEventCallback((e: React.MouseEvent<HTMLAnchorElement>) =>
|
|
87
|
+
onClick?.(e, props),
|
|
88
|
+
)
|
|
89
|
+
|
|
77
90
|
const productLink = useProductLink(props)
|
|
78
91
|
const discount = Math.floor(price_range.minimum_price.discount?.percent_off ?? 0)
|
|
79
92
|
|
|
@@ -98,6 +111,7 @@ export function ProductListItem(props: ProductListItemProps) {
|
|
|
98
111
|
...(Array.isArray(sx) ? sx : [sx]),
|
|
99
112
|
]}
|
|
100
113
|
className={classes.root}
|
|
114
|
+
onClick={onClick ? handleClick : undefined}
|
|
101
115
|
>
|
|
102
116
|
<Box
|
|
103
117
|
sx={(theme) => ({
|
|
@@ -12,12 +12,19 @@ export type ProductItemsGridProps = {
|
|
|
12
12
|
renderers: ProductListItemRenderer
|
|
13
13
|
loadingEager?: number
|
|
14
14
|
size?: 'normal' | 'small'
|
|
15
|
-
titleComponent?: React.ElementType
|
|
16
15
|
sx?: BoxProps['sx']
|
|
17
|
-
}
|
|
16
|
+
} & Pick<ProductListItemProps, 'onClick' | 'titleComponent'>
|
|
18
17
|
|
|
19
18
|
export function ProductListItemsBase(props: ProductItemsGridProps) {
|
|
20
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
items,
|
|
21
|
+
sx = [],
|
|
22
|
+
renderers,
|
|
23
|
+
loadingEager = 0,
|
|
24
|
+
size = 'normal',
|
|
25
|
+
titleComponent,
|
|
26
|
+
onClick,
|
|
27
|
+
} = props
|
|
21
28
|
|
|
22
29
|
return (
|
|
23
30
|
<Box
|
|
@@ -41,14 +48,15 @@ export function ProductListItemsBase(props: ProductItemsGridProps) {
|
|
|
41
48
|
<RenderType
|
|
42
49
|
key={item.uid ?? ''}
|
|
43
50
|
renderer={renderers}
|
|
44
|
-
{...item}
|
|
45
|
-
loading={loadingEager > idx ? 'eager' : 'lazy'}
|
|
46
51
|
sizes={
|
|
47
52
|
size === 'small'
|
|
48
53
|
? { 0: '100vw', 354: '50vw', 675: '30vw', 1255: '23vw', 1500: '337px' }
|
|
49
54
|
: { 0: '100vw', 367: '48vw', 994: '30vw', 1590: '23vw', 1920: '443px' }
|
|
50
55
|
}
|
|
56
|
+
{...item}
|
|
57
|
+
loading={loadingEager > idx ? 'eager' : 'lazy'}
|
|
51
58
|
titleComponent={titleComponent}
|
|
59
|
+
onClick={onClick}
|
|
52
60
|
noReport
|
|
53
61
|
/>
|
|
54
62
|
) : null,
|
|
@@ -5,38 +5,36 @@ import PageLink from 'next/link'
|
|
|
5
5
|
import { productPageCategory } from '../ProductPageCategory/productPageCategory'
|
|
6
6
|
import { ProductPageBreadcrumbFragment } from './ProductPageBreadcrumb.gql'
|
|
7
7
|
|
|
8
|
-
type ProductPageBreadcrumbsProps = ProductPageBreadcrumbFragment & BreadcrumbsProps
|
|
8
|
+
type ProductPageBreadcrumbsProps = ProductPageBreadcrumbFragment & Omit<BreadcrumbsProps, 'children'>
|
|
9
9
|
|
|
10
10
|
export function ProductPageBreadcrumb(props: ProductPageBreadcrumbsProps) {
|
|
11
|
-
const { categories, name } = props
|
|
11
|
+
const { categories, name, ...breadcrumbProps } = props
|
|
12
12
|
const prev = usePrevPageRouter()
|
|
13
13
|
|
|
14
14
|
const category =
|
|
15
15
|
categories?.find((c) => `/${c?.url_path}` === prev?.asPath) ?? productPageCategory(props)
|
|
16
16
|
|
|
17
17
|
return (
|
|
18
|
-
<
|
|
19
|
-
<
|
|
20
|
-
<
|
|
21
|
-
<
|
|
22
|
-
<Trans id='Home' />
|
|
23
|
-
</Link>
|
|
24
|
-
</PageLink>
|
|
25
|
-
{category?.breadcrumbs?.map((mapped_category, i) => (
|
|
26
|
-
<Link
|
|
27
|
-
underline='hover'
|
|
28
|
-
key={mapped_category?.category_uid}
|
|
29
|
-
color='inherit'
|
|
30
|
-
href={`/${mapped_category?.category_url_path}`}
|
|
31
|
-
>
|
|
32
|
-
{mapped_category?.category_name}
|
|
33
|
-
</Link>
|
|
34
|
-
))}
|
|
35
|
-
<Link underline='hover' color='inherit' href={`/${category?.url_path}`}>
|
|
36
|
-
{category?.name}
|
|
18
|
+
<Breadcrumbs {...breadcrumbProps}>
|
|
19
|
+
<PageLink href='/' passHref>
|
|
20
|
+
<Link underline='hover' color='inherit'>
|
|
21
|
+
<Trans id='Home' />
|
|
37
22
|
</Link>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
23
|
+
</PageLink>
|
|
24
|
+
{category?.breadcrumbs?.map((mapped_category, i) => (
|
|
25
|
+
<Link
|
|
26
|
+
underline='hover'
|
|
27
|
+
key={mapped_category?.category_uid}
|
|
28
|
+
color='inherit'
|
|
29
|
+
href={`/${mapped_category?.category_url_path}`}
|
|
30
|
+
>
|
|
31
|
+
{mapped_category?.category_name}
|
|
32
|
+
</Link>
|
|
33
|
+
))}
|
|
34
|
+
<Link underline='hover' color='inherit' href={`/${category?.url_path}`}>
|
|
35
|
+
{category?.name}
|
|
36
|
+
</Link>
|
|
37
|
+
<Typography color='text.primary'>{name}</Typography>
|
|
38
|
+
</Breadcrumbs>
|
|
41
39
|
)
|
|
42
40
|
}
|
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": "4.
|
|
5
|
+
"version": "4.8.0",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"prettier": "@graphcommerce/prettier-config-pwa",
|
|
8
8
|
"eslintConfig": {
|
|
@@ -19,15 +19,15 @@
|
|
|
19
19
|
"type-fest": "^2.12.2"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@graphcommerce/ecommerce-ui": "1.5.
|
|
23
|
-
"@graphcommerce/framer-scroller": "2.1.
|
|
24
|
-
"@graphcommerce/framer-next-pages": "3.3.
|
|
25
|
-
"@graphcommerce/graphql": "3.
|
|
22
|
+
"@graphcommerce/ecommerce-ui": "1.5.5",
|
|
23
|
+
"@graphcommerce/framer-scroller": "2.1.42",
|
|
24
|
+
"@graphcommerce/framer-next-pages": "3.3.2",
|
|
25
|
+
"@graphcommerce/graphql": "3.5.0",
|
|
26
26
|
"@graphcommerce/graphql-mesh": "4.2.0",
|
|
27
|
-
"@graphcommerce/image": "3.1.
|
|
28
|
-
"@graphcommerce/magento-cart": "4.
|
|
29
|
-
"@graphcommerce/magento-store": "4.3.
|
|
30
|
-
"@graphcommerce/next-ui": "4.
|
|
27
|
+
"@graphcommerce/image": "3.1.10",
|
|
28
|
+
"@graphcommerce/magento-cart": "4.9.1",
|
|
29
|
+
"@graphcommerce/magento-store": "4.3.3",
|
|
30
|
+
"@graphcommerce/next-ui": "4.29.0",
|
|
31
31
|
"schema-dts": "^1.1.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|