@graphcommerce/magento-wishlist 1.0.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/CHANGELOG.md +13 -0
- package/README.md +62 -0
- package/components/ProductWishlistChip/ProductWishlistChip.graphql +4 -0
- package/components/ProductWishlistChip/ProductWishlistChip.tsx +5 -0
- package/components/ProductWishlistChip/ProductWishlistChipBase.tsx +180 -0
- package/components/ProductWishlistChip/ProductWishlistChipDetail.tsx +12 -0
- package/components/ProductWishlistChip/ProductWishlistChipDetailConfigurable.tsx +23 -0
- package/components/WishlistFab/WishlistFab.tsx +91 -0
- package/components/WishlistItem/ProductAddToCart.tsx +135 -0
- package/components/WishlistItem/WishlistItem.graphql +3 -0
- package/components/WishlistItem/WishlistItem.tsx +30 -0
- package/components/WishlistItem/WishlistItemBase.tsx +303 -0
- package/components/WishlistItem/WishlistItemConfigurable.graphql +8 -0
- package/components/WishlistItem/WishlistItemConfigurable.tsx +11 -0
- package/components/WishlistItem/WishlistItemProduct.graphql +11 -0
- package/components/WishlistItems/WishlistItems.graphql +12 -0
- package/components/WishlistItems/WishlistItems.tsx +33 -0
- package/components/WishlistMenuFabItem/WishlistMenuFabItem.tsx +81 -0
- package/components/index.ts +10 -0
- package/hooks/index.ts +3 -0
- package/hooks/useMergeGuestWishlistWithCustomer.tsx +64 -0
- package/hooks/useWishlistEnabled.tsx +9 -0
- package/hooks/useWishlistItems.tsx +53 -0
- package/index.ts +12 -0
- package/package.json +39 -0
- package/queries/AddProductToWishlist.graphql +13 -0
- package/queries/GetGuestWishlistProducts.graphql +14 -0
- package/queries/GetIsInWishlists.graphql +7 -0
- package/queries/GetWishlistProducts.graphql +19 -0
- package/queries/GuestWishlist.graphql +11 -0
- package/queries/GuestWishlist.graphqls +13 -0
- package/queries/RemoveProductFromWishlist.graphql +7 -0
- package/queries/WishlistStoreConfigFragment.graphql +3 -0
- package/queries/WishlistSummaryFragment.graphql +13 -0
- package/tsconfig.json +5 -0
- package/typePolicies.ts +21 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @graphcommerce/magento-wishlist
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- [#1256](https://github.com/graphcommerce-org/graphcommerce/pull/1256) [`669a17a97`](https://github.com/graphcommerce-org/graphcommerce/commit/669a17a973c47c00fed4a649a9da0bfc5670c5da) Thanks [@timhofman](https://github.com/timhofman)! - Wishlist
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`669a17a97`](https://github.com/graphcommerce-org/graphcommerce/commit/669a17a973c47c00fed4a649a9da0bfc5670c5da)]:
|
|
12
|
+
- @graphcommerce/magento-product@4.3.0
|
|
13
|
+
- @graphcommerce/magento-product-configurable@4.1.0
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Wishlist
|
|
2
|
+
|
|
3
|
+
This package enables visitors to manage their wishlist with or without an
|
|
4
|
+
account.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- Manage wishlist without an account
|
|
9
|
+
- Automatically migrate wishlist from guest session to account upon login
|
|
10
|
+
- Add products to wishlist based on SKU (all producttypes)
|
|
11
|
+
- Add products with selected variants, like size and color, to wishlist
|
|
12
|
+
(configurable product)
|
|
13
|
+
- Add products from wishlist to cart (simple, downloadable, virtual)
|
|
14
|
+
- Enable/disable through Magento configuration item
|
|
15
|
+
'magento_wishlist_general_is_enabled'
|
|
16
|
+
|
|
17
|
+
## Additional settings
|
|
18
|
+
|
|
19
|
+
- Enable wishlist for logged in users only (for instance for B2B)
|
|
20
|
+
|
|
21
|
+
Add the following configuration to your env file
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
NEXT_PUBLIC_WISHLIST_HIDE_FOR_GUEST="1"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- Force add products to wishlist (only availble for logged in users)
|
|
28
|
+
|
|
29
|
+
In B2B cases you might want to add multiple configured variants of the same
|
|
30
|
+
product to the wishlist, like a quick order list.
|
|
31
|
+
|
|
32
|
+
Use this setting to force adding products to the wishlist, even when there
|
|
33
|
+
already is a product with the same SKU in the wishlist.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
NEXT_PUBLIC_WISHLIST_IGNORE_PRODUCT_WISHLIST_STATUS="1"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Customizing wishlist styling
|
|
40
|
+
|
|
41
|
+
- The ProductWishlistChip accepts SX props (see ProductWishlistChip and
|
|
42
|
+
ProductWishlistChipDetail)
|
|
43
|
+
- Fancy a different icon? See:
|
|
44
|
+
https://www.graphcommerce.org/docs/framework/icons
|
|
45
|
+
- Styleable through theme provider overMuiCssBaseline.styleOverrides (although
|
|
46
|
+
we do recommend to use the SX approach)
|
|
47
|
+
|
|
48
|
+
## Roadmap
|
|
49
|
+
|
|
50
|
+
- Wishlist overview: add to cart with configurable options (see 'Remarks')
|
|
51
|
+
|
|
52
|
+
## Remarks
|
|
53
|
+
|
|
54
|
+
Magento (<= 2.4.4) throws errors when retrieving product configuration for
|
|
55
|
+
wishlist items. Currently selected products variants are stored, but not yet
|
|
56
|
+
displayed in the wishlist overview
|
|
57
|
+
|
|
58
|
+
Fetching configurable_options throws internal servers due to a mismatch in
|
|
59
|
+
schema and implemented resolvers on Magento's side.
|
|
60
|
+
|
|
61
|
+
Related issue:
|
|
62
|
+
https://github.com/magento/magento2/commit/5c884244cd7f1bd55bb0b908943caaed3e3e762b
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-floating-promises */
|
|
2
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
3
|
+
import { useQuery, useMutation, useApolloClient } from '@graphcommerce/graphql'
|
|
4
|
+
import { CustomerTokenDocument } from '@graphcommerce/magento-customer'
|
|
5
|
+
import { IconSvg, iconHeart, extendableComponent } from '@graphcommerce/next-ui'
|
|
6
|
+
import { t } from '@lingui/macro'
|
|
7
|
+
import { SxProps, Theme, IconButton } from '@mui/material'
|
|
8
|
+
import { useState, useEffect } from 'react'
|
|
9
|
+
import { useWishlistEnabled } from '../../hooks'
|
|
10
|
+
import { AddProductToWishlistDocument } from '../../queries/AddProductToWishlist.gql'
|
|
11
|
+
import { GetIsInWishlistsDocument } from '../../queries/GetIsInWishlists.gql'
|
|
12
|
+
import { GuestWishlistDocument } from '../../queries/GuestWishlist.gql'
|
|
13
|
+
import { RemoveProductFromWishlistDocument } from '../../queries/RemoveProductFromWishlist.gql'
|
|
14
|
+
import { ProductWishlistChipFragment } from './ProductWishlistChip.gql'
|
|
15
|
+
|
|
16
|
+
const hideForGuest = process.env.NEXT_PUBLIC_WISHLIST_HIDE_FOR_GUEST === '1'
|
|
17
|
+
const ignoreProductWishlistStatus =
|
|
18
|
+
process.env.NEXT_PUBLIC_WISHLIST_IGNORE_PRODUCT_WISHLIST_STATUS === '1'
|
|
19
|
+
|
|
20
|
+
export type ProductWishlistChipProps = ProductWishlistChipFragment & { sx?: SxProps<Theme> } & {
|
|
21
|
+
selectedOptions?: string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const name = 'ProductWishlistChipBase' as const
|
|
25
|
+
const parts = ['root', 'wishlistIcon', 'wishlistIconActive', 'wishlistButton'] as const
|
|
26
|
+
const { classes } = extendableComponent(name, parts)
|
|
27
|
+
|
|
28
|
+
export function ProductWishlistChipBase(props: ProductWishlistChipProps) {
|
|
29
|
+
const { sku, selectedOptions = [], sx = [] } = props
|
|
30
|
+
|
|
31
|
+
const [inWishlist, setInWishlist] = useState(false)
|
|
32
|
+
|
|
33
|
+
const { data: token } = useQuery(CustomerTokenDocument)
|
|
34
|
+
const isLoggedIn = token?.customerToken && token?.customerToken.valid
|
|
35
|
+
|
|
36
|
+
const { cache } = useApolloClient()
|
|
37
|
+
|
|
38
|
+
const isWishlistEnabled = useWishlistEnabled()
|
|
39
|
+
|
|
40
|
+
if (!isWishlistEnabled) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const heart = (
|
|
45
|
+
<IconSvg
|
|
46
|
+
src={iconHeart}
|
|
47
|
+
size='medium'
|
|
48
|
+
className={classes.wishlistIcon}
|
|
49
|
+
sx={(theme) => ({ color: theme.palette.primary.main })}
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const activeHeart = (
|
|
54
|
+
<IconSvg
|
|
55
|
+
src={iconHeart}
|
|
56
|
+
size='medium'
|
|
57
|
+
className={classes.wishlistIconActive}
|
|
58
|
+
sx={(theme) => ({ color: theme.palette.primary.main, fill: 'currentcolor' })}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const { data: GetCustomerWishlistData, loading } = useQuery(GetIsInWishlistsDocument, {
|
|
63
|
+
skip: !isLoggedIn,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const { data: guestWishlistData, loading: loadingGuestWishlistData } = useQuery(
|
|
67
|
+
GuestWishlistDocument,
|
|
68
|
+
{
|
|
69
|
+
ssr: false,
|
|
70
|
+
skip: isLoggedIn === true,
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
// Do not display wishlist UI to guests when configured as customer only
|
|
76
|
+
if (hideForGuest && !isLoggedIn) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!sku) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Mark as active when product is available in either customer or guest wishlist
|
|
85
|
+
if (isLoggedIn && !loading) {
|
|
86
|
+
const inWishlistTest =
|
|
87
|
+
GetCustomerWishlistData?.customer?.wishlists[0]?.items_v2?.items.map(
|
|
88
|
+
(item) => item?.product?.sku,
|
|
89
|
+
) || []
|
|
90
|
+
setInWishlist(inWishlistTest.includes(sku))
|
|
91
|
+
} else if (!isLoggedIn) {
|
|
92
|
+
const inWishlistTest = guestWishlistData?.guestWishlist?.items.map((item) => item?.sku) || []
|
|
93
|
+
setInWishlist(inWishlistTest.includes(sku))
|
|
94
|
+
}
|
|
95
|
+
}, [isLoggedIn, sku, loading, GetCustomerWishlistData, guestWishlistData])
|
|
96
|
+
|
|
97
|
+
const [addWishlistItem] = useMutation(AddProductToWishlistDocument)
|
|
98
|
+
const [removeWishlistItem] = useMutation(RemoveProductFromWishlistDocument)
|
|
99
|
+
|
|
100
|
+
const preventAnimationBubble: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
|
101
|
+
e.preventDefault()
|
|
102
|
+
e.stopPropagation()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
|
106
|
+
e.preventDefault()
|
|
107
|
+
|
|
108
|
+
if (!sku) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (isLoggedIn) {
|
|
113
|
+
if (inWishlist && !ignoreProductWishlistStatus) {
|
|
114
|
+
const wishlistItemsInSession =
|
|
115
|
+
GetCustomerWishlistData?.customer?.wishlists[0]?.items_v2?.items || []
|
|
116
|
+
|
|
117
|
+
const item = wishlistItemsInSession.find((element) => element?.product?.sku == sku)
|
|
118
|
+
|
|
119
|
+
if (item?.id) {
|
|
120
|
+
removeWishlistItem({ variables: { wishlistItemId: item.id } })
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
addWishlistItem({
|
|
124
|
+
variables: { input: { sku, quantity: 1, selected_options: selectedOptions } },
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
} else if (inWishlist) {
|
|
128
|
+
cache.modify({
|
|
129
|
+
id: cache.identify({ __typename: 'GuestWishlist' }),
|
|
130
|
+
fields: {
|
|
131
|
+
items(existingItems = []) {
|
|
132
|
+
const items = existingItems.filter((item) => item.sku !== sku)
|
|
133
|
+
return items
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
} else {
|
|
138
|
+
/** Merging of wishlist items is done by policy, see typePolicies.ts */
|
|
139
|
+
cache.writeQuery({
|
|
140
|
+
query: GuestWishlistDocument,
|
|
141
|
+
data: {
|
|
142
|
+
guestWishlist: {
|
|
143
|
+
__typename: 'GuestWishlist',
|
|
144
|
+
items: [
|
|
145
|
+
{
|
|
146
|
+
__typename: 'GuestWishlistItem',
|
|
147
|
+
sku,
|
|
148
|
+
quantity: 1,
|
|
149
|
+
selected_options: selectedOptions,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
broadcast: true,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const button = (
|
|
160
|
+
<IconButton
|
|
161
|
+
key={sku}
|
|
162
|
+
onClick={handleClick}
|
|
163
|
+
onMouseDown={preventAnimationBubble}
|
|
164
|
+
size='small'
|
|
165
|
+
className={classes.wishlistButton}
|
|
166
|
+
sx={[
|
|
167
|
+
(theme) => ({
|
|
168
|
+
padding: theme.spacings.xxs,
|
|
169
|
+
}),
|
|
170
|
+
...(Array.isArray(sx) ? sx : [sx]),
|
|
171
|
+
]}
|
|
172
|
+
title={inWishlist ? t`Remove from wishlist` : t`Add to wishlist`}
|
|
173
|
+
aria-label={inWishlist ? t`Remove from wishlist` : t`Add to wishlist`}
|
|
174
|
+
>
|
|
175
|
+
{inWishlist ? activeHeart : heart}
|
|
176
|
+
</IconButton>
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return !hideForGuest || isLoggedIn ? button : null
|
|
180
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ProductWishlistChipBase, ProductWishlistChipProps } from './ProductWishlistChipBase'
|
|
2
|
+
|
|
3
|
+
export function ProductWishlistChipDetail(props: ProductWishlistChipProps) {
|
|
4
|
+
return (
|
|
5
|
+
<ProductWishlistChipBase
|
|
6
|
+
sx={(theme) => ({
|
|
7
|
+
boxShadow: theme.shadows[6],
|
|
8
|
+
})}
|
|
9
|
+
{...props}
|
|
10
|
+
/>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useConfigurableContext } from '@graphcommerce/magento-product-configurable'
|
|
2
|
+
import { ProductWishlistChipBase, ProductWishlistChipProps } from './ProductWishlistChipBase'
|
|
3
|
+
|
|
4
|
+
export function ProductWishlistChipDetailConfigurable(props: ProductWishlistChipProps) {
|
|
5
|
+
const { sku } = props
|
|
6
|
+
|
|
7
|
+
let selectedOptions: string[] = []
|
|
8
|
+
if (sku) {
|
|
9
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
10
|
+
const context = useConfigurableContext(sku)
|
|
11
|
+
selectedOptions = (Object as any).values(context.selection)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<ProductWishlistChipBase
|
|
16
|
+
sx={(theme) => ({
|
|
17
|
+
boxShadow: theme.shadows[6],
|
|
18
|
+
})}
|
|
19
|
+
selectedOptions={selectedOptions}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useQuery } from '@graphcommerce/graphql'
|
|
2
|
+
import { CustomerTokenDocument } from '@graphcommerce/magento-customer'
|
|
3
|
+
import { iconHeart, DesktopHeaderBadge, IconSvg, extendableComponent } from '@graphcommerce/next-ui'
|
|
4
|
+
import { t } from '@lingui/macro'
|
|
5
|
+
import { Fab, FabProps as FabPropsType, NoSsr, SxProps, Theme } from '@mui/material'
|
|
6
|
+
import PageLink from 'next/link'
|
|
7
|
+
import React, { useEffect } from 'react'
|
|
8
|
+
import { useWishlistEnabled } from '../../hooks'
|
|
9
|
+
import { GetIsInWishlistsDocument } from '../../queries/GetIsInWishlists.gql'
|
|
10
|
+
import { GuestWishlistDocument } from '../../queries/GuestWishlist.gql'
|
|
11
|
+
|
|
12
|
+
type WishlistFabContentProps = {
|
|
13
|
+
icon?: React.ReactNode
|
|
14
|
+
FabProps?: Omit<FabPropsType, 'children'>
|
|
15
|
+
sx?: SxProps<Theme>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const name = 'WishlistFab'
|
|
19
|
+
const parts = ['root'] as const
|
|
20
|
+
const { classes } = extendableComponent(name, parts)
|
|
21
|
+
|
|
22
|
+
const hideForGuest = process.env.NEXT_PUBLIC_WISHLIST_HIDE_FOR_GUEST === '1'
|
|
23
|
+
|
|
24
|
+
function WishlistFabContent(props: WishlistFabContentProps) {
|
|
25
|
+
const { icon, FabProps, sx } = props
|
|
26
|
+
|
|
27
|
+
const { data: token } = useQuery(CustomerTokenDocument)
|
|
28
|
+
const isLoggedIn = token?.customerToken && token?.customerToken.valid
|
|
29
|
+
|
|
30
|
+
const { data: GetCustomerWishlistData, loading } = useQuery(GetIsInWishlistsDocument, {
|
|
31
|
+
skip: !isLoggedIn,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const { data: guestWishlistData, loading: loadingGuestWishlistData } = useQuery(
|
|
35
|
+
GuestWishlistDocument,
|
|
36
|
+
{
|
|
37
|
+
ssr: false,
|
|
38
|
+
skip: isLoggedIn === true,
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
let activeWishlist = false
|
|
43
|
+
if (isLoggedIn) {
|
|
44
|
+
const wishlistItemCount = GetCustomerWishlistData?.customer?.wishlists[0]?.items_count || 0
|
|
45
|
+
activeWishlist = wishlistItemCount > 0
|
|
46
|
+
} else {
|
|
47
|
+
const wishlist = guestWishlistData?.guestWishlist?.items || []
|
|
48
|
+
activeWishlist = wishlist.length > 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const wishlistIcon = icon ?? <IconSvg src={iconHeart} size='large' />
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<PageLink href='/wishlist' passHref>
|
|
55
|
+
<Fab
|
|
56
|
+
color='inherit'
|
|
57
|
+
data-test-id='wishlist-fab'
|
|
58
|
+
aria-label={t`Wishlist`}
|
|
59
|
+
size='large'
|
|
60
|
+
className={classes.root}
|
|
61
|
+
{...FabProps}
|
|
62
|
+
sx={sx}
|
|
63
|
+
>
|
|
64
|
+
{activeWishlist ? (
|
|
65
|
+
<DesktopHeaderBadge color='primary' variant='dot' overlap='circular'>
|
|
66
|
+
{wishlistIcon}
|
|
67
|
+
</DesktopHeaderBadge>
|
|
68
|
+
) : (
|
|
69
|
+
wishlistIcon
|
|
70
|
+
)}
|
|
71
|
+
</Fab>
|
|
72
|
+
</PageLink>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function WishlistFab(props: WishlistFabContentProps) {
|
|
77
|
+
const isWishlistEnabled = useWishlistEnabled()
|
|
78
|
+
|
|
79
|
+
const { data: token } = useQuery(CustomerTokenDocument)
|
|
80
|
+
const isLoggedIn = token?.customerToken && token?.customerToken.valid
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<>
|
|
84
|
+
{isWishlistEnabled && (!hideForGuest || isLoggedIn) && (
|
|
85
|
+
<NoSsr>
|
|
86
|
+
<WishlistFabContent {...props} />
|
|
87
|
+
</NoSsr>
|
|
88
|
+
)}
|
|
89
|
+
</>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { ProductInterface } from '@graphcommerce/graphql-mesh'
|
|
2
|
+
import { ApolloCartErrorAlert, useFormGqlMutationCart } from '@graphcommerce/magento-cart'
|
|
3
|
+
import {
|
|
4
|
+
ProductAddToCartDocument,
|
|
5
|
+
ProductAddToCartMutationVariables,
|
|
6
|
+
} from '@graphcommerce/magento-product'
|
|
7
|
+
import { MoneyProps } from '@graphcommerce/magento-store'
|
|
8
|
+
import {
|
|
9
|
+
Button,
|
|
10
|
+
MessageSnackbar,
|
|
11
|
+
TextInputNumber,
|
|
12
|
+
iconChevronRight,
|
|
13
|
+
IconSvg,
|
|
14
|
+
extendableComponent,
|
|
15
|
+
AnimatedRow,
|
|
16
|
+
} from '@graphcommerce/next-ui'
|
|
17
|
+
import { Trans } from '@lingui/macro'
|
|
18
|
+
import { ButtonProps, Box, Alert } from '@mui/material'
|
|
19
|
+
import { AnimatePresence } from 'framer-motion'
|
|
20
|
+
import PageLink from 'next/link'
|
|
21
|
+
import React from 'react'
|
|
22
|
+
|
|
23
|
+
const { classes, selectors } = extendableComponent('ProductAddToCart', [
|
|
24
|
+
'root',
|
|
25
|
+
'button',
|
|
26
|
+
'price',
|
|
27
|
+
'divider',
|
|
28
|
+
'buttonWrapper',
|
|
29
|
+
] as const)
|
|
30
|
+
|
|
31
|
+
export type AddToCartProps = React.ComponentProps<typeof ProductAddToCart>
|
|
32
|
+
|
|
33
|
+
export function ProductAddToCart(
|
|
34
|
+
props: Pick<ProductInterface, 'name'> & {
|
|
35
|
+
variables: Omit<ProductAddToCartMutationVariables, 'cartId'>
|
|
36
|
+
name: string
|
|
37
|
+
price: MoneyProps
|
|
38
|
+
additionalButtons?: React.ReactNode
|
|
39
|
+
children?: React.ReactNode
|
|
40
|
+
} & Omit<ButtonProps, 'type' | 'name'>,
|
|
41
|
+
) {
|
|
42
|
+
const { name, children, variables, price, sx, additionalButtons, ...buttonProps } = props
|
|
43
|
+
|
|
44
|
+
const form = useFormGqlMutationCart(ProductAddToCartDocument, {
|
|
45
|
+
defaultValues: { ...variables },
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const { handleSubmit, formState, error, muiRegister, required, data } = form
|
|
49
|
+
const submitHandler = handleSubmit(() => {})
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Box component='form' onSubmit={submitHandler} noValidate className={classes.root}>
|
|
53
|
+
<Box
|
|
54
|
+
sx={(theme) => ({
|
|
55
|
+
gridArea: 'itemQuantity',
|
|
56
|
+
})}
|
|
57
|
+
>
|
|
58
|
+
<TextInputNumber
|
|
59
|
+
variant='outlined'
|
|
60
|
+
error={formState.isSubmitted && !!formState.errors.quantity}
|
|
61
|
+
required={required.quantity}
|
|
62
|
+
inputProps={{ min: 1 }}
|
|
63
|
+
{...muiRegister('quantity', { required: required.quantity })}
|
|
64
|
+
helperText={formState.isSubmitted && formState.errors.quantity}
|
|
65
|
+
disabled={formState.isSubmitting}
|
|
66
|
+
size='small'
|
|
67
|
+
sx={(theme) => ({
|
|
68
|
+
alignSelf: 'flex-start',
|
|
69
|
+
})}
|
|
70
|
+
/>
|
|
71
|
+
</Box>
|
|
72
|
+
{children}
|
|
73
|
+
<Box
|
|
74
|
+
className={classes.buttonWrapper}
|
|
75
|
+
sx={(theme) => ({
|
|
76
|
+
gridArea: 'itemCartButton',
|
|
77
|
+
alignSelf: 'flex-start',
|
|
78
|
+
position: 'absolute',
|
|
79
|
+
left: '0',
|
|
80
|
+
bottom: '-35px',
|
|
81
|
+
})}
|
|
82
|
+
>
|
|
83
|
+
<Button
|
|
84
|
+
type='submit'
|
|
85
|
+
className={classes.button}
|
|
86
|
+
loading={formState.isSubmitting}
|
|
87
|
+
color='primary'
|
|
88
|
+
variant='text'
|
|
89
|
+
size='medium'
|
|
90
|
+
{...buttonProps}
|
|
91
|
+
>
|
|
92
|
+
<Trans>Add to Cart</Trans>
|
|
93
|
+
</Button>
|
|
94
|
+
</Box>
|
|
95
|
+
|
|
96
|
+
<ApolloCartErrorAlert error={error} />
|
|
97
|
+
|
|
98
|
+
<AnimatePresence initial={false}>
|
|
99
|
+
{data?.addProductsToCart?.user_errors.map((e) => (
|
|
100
|
+
<AnimatedRow key={e?.code}>
|
|
101
|
+
<Alert severity='error'>{e?.message}</Alert>
|
|
102
|
+
</AnimatedRow>
|
|
103
|
+
))}
|
|
104
|
+
</AnimatePresence>
|
|
105
|
+
|
|
106
|
+
<MessageSnackbar
|
|
107
|
+
open={
|
|
108
|
+
!formState.isSubmitting &&
|
|
109
|
+
formState.isSubmitSuccessful &&
|
|
110
|
+
!error?.message &&
|
|
111
|
+
!data?.addProductsToCart?.user_errors?.length
|
|
112
|
+
}
|
|
113
|
+
variant='pill'
|
|
114
|
+
action={
|
|
115
|
+
<PageLink href='/cart' passHref>
|
|
116
|
+
<Button
|
|
117
|
+
id='view-shopping-cart-button'
|
|
118
|
+
size='medium'
|
|
119
|
+
variant='pill'
|
|
120
|
+
color='secondary'
|
|
121
|
+
endIcon={<IconSvg src={iconChevronRight} />}
|
|
122
|
+
>
|
|
123
|
+
<Trans>View shopping cart</Trans>
|
|
124
|
+
</Button>
|
|
125
|
+
</PageLink>
|
|
126
|
+
}
|
|
127
|
+
>
|
|
128
|
+
<Trans>
|
|
129
|
+
<strong>{name}</strong> has been added to your shopping cart!
|
|
130
|
+
</Trans>
|
|
131
|
+
</MessageSnackbar>
|
|
132
|
+
</Box>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
ProductAddToCart.selectors = selectors
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-floating-promises */
|
|
2
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
3
|
+
import { SxProps, Theme } from '@mui/material'
|
|
4
|
+
|
|
5
|
+
import { PropsWithChildren } from 'react'
|
|
6
|
+
import { ProductAddToCart } from './ProductAddToCart'
|
|
7
|
+
import { WishlistItemBase } from './WishlistItemBase'
|
|
8
|
+
import { WishlistItemProductFragment } from './WishlistItemProduct.gql'
|
|
9
|
+
|
|
10
|
+
type OptionalProductWishlistParent = {
|
|
11
|
+
wishlistItemId?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type WishlistItemProps = PropsWithChildren<WishlistItemProductFragment> & {
|
|
15
|
+
sx?: SxProps<Theme>
|
|
16
|
+
} & OptionalProductWishlistParent
|
|
17
|
+
|
|
18
|
+
export function WishlistItem(props: WishlistItemProps) {
|
|
19
|
+
const { sku, name, price_range } = props
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<WishlistItemBase {...props}>
|
|
23
|
+
<ProductAddToCart
|
|
24
|
+
variables={{ sku: sku ?? '', quantity: 1 }}
|
|
25
|
+
name={name ?? ''}
|
|
26
|
+
price={price_range.minimum_price.regular_price}
|
|
27
|
+
/>
|
|
28
|
+
</WishlistItemBase>
|
|
29
|
+
)
|
|
30
|
+
}
|