2
2
3
3
import { Dialog , Transition } from '@headlessui/react' ;
4
4
import { ShoppingCartIcon } from '@heroicons/react/24/outline' ;
5
+ import LoadingDots from 'components/loading-dots' ;
5
6
import Price from 'components/price' ;
6
7
import { DEFAULT_OPTION } from 'lib/constants' ;
7
- import type { Cart } from 'lib/shopify/types' ;
8
+ import type { Cart , CartItem } from 'lib/shopify/types' ;
8
9
import { createUrl } from 'lib/utils' ;
9
10
import Image from 'next/image' ;
10
11
import Link from 'next/link' ;
11
- import { Fragment , useEffect , useRef , useState } from 'react' ;
12
+ import { Fragment , useEffect , useOptimistic , useRef , useState } from 'react' ;
13
+ import { useFormStatus } from 'react-dom' ;
14
+ import { redirectToCheckout } from './actions' ;
12
15
import CloseCart from './close-cart' ;
13
16
import { DeleteItemButton } from './delete-item-button' ;
14
17
import { EditItemQuantityButton } from './edit-item-quantity-button' ;
@@ -18,8 +21,58 @@ type MerchandiseSearchParams = {
18
21
[ key : string ] : string ;
19
22
} ;
20
23
21
- export default function CartModal ( { cart } : { cart : Cart | undefined } ) {
24
+ type NewState = {
25
+ itemId : string ;
26
+ newQuantity : number ;
27
+ } ;
28
+
29
+ function reducer ( state : Cart | undefined , newState : NewState ) {
30
+ if ( ! state ) {
31
+ return state ;
32
+ }
33
+
34
+ const updatedLines = state . lines . map ( ( item : CartItem ) => {
35
+ if ( item . id === newState . itemId ) {
36
+ const singleItemAmount = Number ( item . cost . totalAmount . amount ) / item . quantity ;
37
+ const newTotalAmount = Number ( item . cost . totalAmount . amount ) + singleItemAmount ;
38
+ return {
39
+ ...item ,
40
+ quantity : newState . newQuantity ,
41
+ cost : {
42
+ ...item . cost ,
43
+ totalAmount : {
44
+ ...item . cost . totalAmount ,
45
+ amount : newTotalAmount . toString ( )
46
+ }
47
+ }
48
+ } ;
49
+ }
50
+ return item ;
51
+ } ) ;
52
+
53
+ const newTotalQuantity = updatedLines . reduce ( ( sum , item ) => sum + item . quantity , 0 ) ;
54
+ const newTotalAmount = updatedLines . reduce (
55
+ ( sum , item ) => sum + Number ( item . cost . totalAmount . amount ) ,
56
+ 0
57
+ ) ;
58
+
59
+ return {
60
+ ...state ,
61
+ lines : updatedLines ,
62
+ totalQuantity : newTotalQuantity ,
63
+ cost : {
64
+ ...state . cost ,
65
+ totalAmount : {
66
+ ...state . cost . totalAmount ,
67
+ amount : newTotalAmount . toString ( )
68
+ }
69
+ }
70
+ } ;
71
+ }
72
+
73
+ export default function CartModal ( { cart : initialCart } : { cart : Cart | undefined } ) {
22
74
const [ isOpen , setIsOpen ] = useState ( false ) ;
75
+ const [ cart , updateCartItem ] = useOptimistic ( initialCart , reducer ) ;
23
76
const quantityRef = useRef ( cart ?. totalQuantity ) ;
24
77
const openCart = ( ) => setIsOpen ( true ) ;
25
78
const closeCart = ( ) => setIsOpen ( false ) ;
@@ -67,7 +120,6 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
67
120
< Dialog . Panel className = "fixed bottom-0 right-0 top-0 flex h-full w-full flex-col border-l border-neutral-200 bg-white/80 p-6 text-black backdrop-blur-xl md:w-[390px] dark:border-neutral-700 dark:bg-black/80 dark:text-white" >
68
121
< div className = "flex items-center justify-between" >
69
122
< p className = "text-lg font-semibold" > My Cart</ p >
70
-
71
123
< button aria-label = "Close cart" onClick = { closeCart } >
72
124
< CloseCart />
73
125
</ button >
@@ -140,11 +192,19 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
140
192
currencyCode = { item . cost . totalAmount . currencyCode }
141
193
/>
142
194
< div className = "ml-auto flex h-9 flex-row items-center rounded-full border border-neutral-200 dark:border-neutral-700" >
143
- < EditItemQuantityButton item = { item } type = "minus" />
195
+ < EditItemQuantityButton
196
+ item = { item }
197
+ type = "minus"
198
+ optimisticUpdate = { updateCartItem }
199
+ />
144
200
< p className = "w-6 text-center" >
145
201
< span className = "w-full text-sm" > { item . quantity } </ span >
146
202
</ p >
147
- < EditItemQuantityButton item = { item } type = "plus" />
203
+ < EditItemQuantityButton
204
+ item = { item }
205
+ type = "plus"
206
+ optimisticUpdate = { updateCartItem }
207
+ />
148
208
</ div >
149
209
</ div >
150
210
</ div >
@@ -174,12 +234,9 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
174
234
/>
175
235
</ div >
176
236
</ div >
177
- < a
178
- href = { cart . checkoutUrl }
179
- className = "block w-full rounded-full bg-blue-600 p-3 text-center text-sm font-medium text-white opacity-90 hover:opacity-100"
180
- >
181
- Proceed to Checkout
182
- </ a >
237
+ < form action = { redirectToCheckout } >
238
+ < CheckoutButton cart = { cart } />
239
+ </ form >
183
240
</ div >
184
241
) }
185
242
</ Dialog . Panel >
@@ -189,3 +246,20 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
189
246
</ >
190
247
) ;
191
248
}
249
+
250
+ function CheckoutButton ( { cart } : { cart : Cart } ) {
251
+ const { pending } = useFormStatus ( ) ;
252
+
253
+ return (
254
+ < >
255
+ < input type = "hidden" name = "url" value = { cart . checkoutUrl } />
256
+ < button
257
+ className = "block w-full rounded-full bg-blue-600 p-3 text-center text-sm font-medium text-white opacity-90 hover:opacity-100"
258
+ type = "submit"
259
+ disabled = { pending }
260
+ >
261
+ { pending ? < LoadingDots className = "bg-white" /> : 'Proceed to Checkout' }
262
+ </ button >
263
+ </ >
264
+ ) ;
265
+ }
0 commit comments