Skip to content

Commit 4b3779a

Browse files
committed
Merge branch 'main' of github.com:lambda-curry/medusa2-starter into training-demo
2 parents f92998a + 0263754 commit 4b3779a

File tree

5 files changed

+546
-209
lines changed

5 files changed

+546
-209
lines changed
Lines changed: 208 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,168 +1,228 @@
1-
import { FC, Fragment, PropsWithChildren, useEffect } from 'react';
2-
import { Dialog, Transition } from '@headlessui/react';
1+
import { FC, Fragment, useCallback, useState, useEffect } from 'react';
2+
import { Dialog, DialogPanel, DialogTitle, DialogBackdrop } from '@headlessui/react';
33
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
4-
import { CartDrawerItem } from './CartDrawerItem';
54
import { formatCartSubtotal, formatPrice } from '@libs/util/prices';
65
import { useCart } from '@app/hooks/useCart';
76
import { IconButton } from '@app/components/common/buttons/IconButton';
87
import { ButtonLink } from '@app/components/common/buttons/ButtonLink';
98
import { Button } from '@app/components/common/buttons/Button';
10-
import { useNavigate } from '@remix-run/react';
9+
import { useNavigate, useFetchers } from '@remix-run/react';
1110
import { useRegion } from '@app/hooks/useRegion';
11+
import { CartDrawerItem } from './CartDrawerItem';
12+
13+
// Cart Drawer Header Component
14+
const CartDrawerHeader: FC<{ itemCount: number; onClose: () => void }> = ({ itemCount, onClose }) => (
15+
<div className="flex items-start justify-between">
16+
<DialogTitle className="text-lg font-bold text-gray-900">
17+
My Cart{' '}
18+
{itemCount > 0 && (
19+
<span className="pl-2">
20+
({itemCount} item{itemCount > 1 ? 's' : ''})
21+
</span>
22+
)}
23+
</DialogTitle>
24+
<div className="ml-3 flex h-7 items-center">
25+
<IconButton icon={XMarkIcon} onClick={onClose} className="-m-2" aria-label="Close panel" />
26+
</div>
27+
</div>
28+
);
29+
30+
// Cart Drawer Empty Component
31+
const CartDrawerEmpty: FC = () => <p className="text-center text-sm text-gray-500">Looks like your cart is empty!</p>;
32+
33+
// Cart Drawer Loading Component
34+
const CartDrawerLoading: FC = () => (
35+
<li className="py-6 list-none">
36+
<div className="flex animate-pulse space-x-4">
37+
<div className="h-24 w-24 rounded-md bg-slate-300" />
38+
<div className="flex h-24 w-full flex-1 flex-col space-y-3 py-1">
39+
<div className="grid grid-cols-3 gap-4">
40+
<div className="col-span-2 h-2 rounded bg-slate-300" />
41+
<div className="col-span-1 h-2 rounded bg-slate-300" />
42+
</div>
43+
<div className="h-2 rounded bg-slate-300" />
44+
<div className="flex-1" />
45+
<div className="grid grid-cols-4 gap-4">
46+
<div className="col-span-1 h-2 rounded bg-slate-300" />
47+
<div className="col-span-2" />
48+
<div className="col-span-1 h-2 rounded bg-slate-300" />
49+
</div>
50+
</div>
51+
</div>
52+
</li>
53+
);
54+
55+
// Cart Drawer Items Component
56+
const CartDrawerItems: FC<{
57+
items: any[];
58+
isRemovingItemId?: string;
59+
currencyCode: string;
60+
}> = ({ items, isRemovingItemId, currencyCode }) => (
61+
<ul className="-my-6 divide-y divide-gray-200 list-none">
62+
{items.map((item) => (
63+
<CartDrawerItem key={item.id} isRemoving={isRemovingItemId === item.id} item={item} currencyCode={currencyCode} />
64+
))}
65+
</ul>
66+
);
67+
68+
// Cart Drawer Content Component
69+
const CartDrawerContent: FC<{
70+
items: any[];
71+
isRemovingItemId?: string;
72+
isAddingItem: boolean;
73+
showEmptyCartMessage: boolean;
74+
isRemovingLastItem: boolean;
75+
currencyCode: string;
76+
}> = ({ items, isRemovingItemId, isAddingItem, showEmptyCartMessage, isRemovingLastItem, currencyCode }) => (
77+
<div className="mt-8">
78+
<div className="flow-root">
79+
{/* Show empty cart message when cart is empty and not loading */}
80+
{(showEmptyCartMessage || isRemovingLastItem) && <CartDrawerEmpty />}
1281

13-
export const CartDrawer: FC<PropsWithChildren> = () => {
82+
{/* Show items when there are items in the cart */}
83+
{items.length > 0 && !isRemovingLastItem && (
84+
<CartDrawerItems items={items} isRemovingItemId={isRemovingItemId} currencyCode={currencyCode} />
85+
)}
86+
87+
{/* Show loading item when adding items but not when removing the last item */}
88+
{isAddingItem && !isRemovingLastItem && <CartDrawerLoading />}
89+
</div>
90+
</div>
91+
);
92+
93+
// Cart Drawer Footer Component
94+
const CartDrawerFooter: FC<{
95+
cart: any;
96+
currencyCode: string;
97+
itemCount: number;
98+
isAddingItem: boolean;
99+
isRemovingLastItem: boolean;
100+
onCheckout: () => void;
101+
onClose: () => void;
102+
}> = ({ cart, currencyCode, itemCount, isAddingItem, isRemovingLastItem, onCheckout, onClose }) => (
103+
<div className="border-t border-gray-200 px-4 py-6 sm:px-6">
104+
<div className="flex justify-between text-base font-bold text-gray-900">
105+
<p>Subtotal</p>
106+
<p>
107+
{cart
108+
? formatCartSubtotal(cart)
109+
: formatPrice(0, {
110+
currency: currencyCode,
111+
})}
112+
</p>
113+
</div>
114+
<p className="mt-0.5 text-sm text-gray-500">Shipping and taxes calculated at checkout.</p>
115+
<div className="mt-6">
116+
<Button
117+
variant="primary"
118+
disabled={itemCount === 0 || isRemovingLastItem}
119+
onClick={onCheckout}
120+
className="h-12 w-full !text-base font-bold"
121+
>
122+
Checkout
123+
</Button>
124+
</div>
125+
<div className="mt-4 flex justify-center text-center text-sm text-gray-500">
126+
<p>
127+
or{' '}
128+
<ButtonLink size="sm" onClick={onClose}>
129+
<div>
130+
Continue Shopping{` `}
131+
<span aria-hidden="true">&rarr;</span>
132+
</div>
133+
</ButtonLink>
134+
</p>
135+
</div>
136+
</div>
137+
);
138+
139+
export const CartDrawer: FC = () => {
14140
const navigate = useNavigate();
15-
const { cart, cartDrawerOpen, toggleCartDrawer, isAddingItem, isRemovingItemId } = useCart();
141+
const {
142+
cart,
143+
cartDrawerOpen,
144+
toggleCartDrawer,
145+
isAddingItem,
146+
isRemovingItemId,
147+
isRemovingLastItem,
148+
showEmptyCartMessage,
149+
} = useCart();
16150
const { region } = useRegion();
17-
let lineItems = cart?.items ?? [];
18-
let lineItemsCount = lineItems.length;
151+
const allFetchers = useFetchers();
152+
153+
// Track if any cart-related fetchers are active
154+
const isCartLoading = allFetchers.some(
155+
(f) =>
156+
(f.state === 'submitting' || f.state === 'loading') &&
157+
(f.formAction?.includes('/api/cart') || f.formData?.get('action') === 'add-to-cart'),
158+
);
159+
160+
// Local state to control the dialog - initialize with cartDrawerOpen
161+
const [isOpen, setIsOpen] = useState(false);
162+
163+
// Sync our local state with the cart drawer state
164+
useEffect(() => {
165+
setIsOpen(cartDrawerOpen === true);
166+
}, [cartDrawerOpen]);
167+
168+
const lineItems = cart?.items ?? [];
19169
const lineItemsTotal = lineItems.reduce((acc, item) => acc + item.quantity, 0);
20-
const isRemovingLastItem = lineItems.length === 1 && isRemovingItemId === lineItems[0].id;
21170

22-
const handleCheckoutClick = () => {
171+
const handleCheckoutClick = useCallback(() => {
23172
navigate('/checkout');
24-
toggleCartDrawer();
25-
};
173+
toggleCartDrawer(false);
174+
}, [navigate, toggleCartDrawer]);
26175

27-
useEffect(() => {
28-
if (lineItemsCount < 1 || isRemovingLastItem) toggleCartDrawer(false);
29-
}, [lineItemsCount]);
176+
const handleClose = useCallback(() => {
177+
toggleCartDrawer(false);
178+
}, [toggleCartDrawer]);
30179

31180
return (
32-
<Transition.Root show={!!cartDrawerOpen} as={Fragment}>
33-
<Dialog as="div" className="relative z-50" onClose={() => toggleCartDrawer(false)}>
34-
<Transition.Child
35-
as={Fragment}
36-
enter="ease-in-out duration-200"
37-
enterFrom="opacity-0"
38-
enterTo="opacity-100"
39-
leave="ease-in-out duration-200"
40-
leaveFrom="opacity-100"
41-
leaveTo="opacity-0"
42-
>
43-
<div className="fixed inset-0 bg-gray-300 bg-opacity-50 backdrop-blur-sm transition-opacity" />
44-
</Transition.Child>
45-
46-
<div className="fixed inset-0 overflow-hidden">
47-
<div className="absolute inset-0 overflow-hidden">
48-
<div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
49-
<Transition.Child
50-
as={Fragment}
51-
enter="transform transition ease-in-out duration-200"
52-
enterFrom="translate-x-full"
53-
enterTo="translate-x-0"
54-
leave="transform transition ease-in-out duration-200"
55-
leaveFrom="translate-x-0"
56-
leaveTo="translate-x-full"
57-
>
58-
<Dialog.Panel className="pointer-events-auto w-screen max-w-md">
59-
<div className="flex h-full flex-col overflow-y-scroll bg-white shadow-xl">
60-
<div className="flex-1 overflow-y-auto px-4 py-6 sm:px-6">
61-
<div className="flex items-start justify-between">
62-
<Dialog.Title className="text-lg font-bold text-gray-900">
63-
My Cart{' '}
64-
{lineItemsTotal > 0 && (
65-
<span className="pl-2">
66-
({lineItemsTotal} item
67-
{lineItemsTotal > 1 ? 's' : ''})
68-
</span>
69-
)}
70-
</Dialog.Title>
71-
<div className="ml-3 flex h-7 items-center">
72-
<IconButton
73-
icon={XMarkIcon}
74-
onClick={() => toggleCartDrawer(false)}
75-
className="-m-2"
76-
aria-label="Close panel"
77-
/>
78-
</div>
79-
</div>
80-
81-
<div className="mt-8">
82-
<div className="flow-root">
83-
{((!isAddingItem && lineItems.length === 0) || isRemovingLastItem) && (
84-
<p className="text-center text-sm text-gray-500">Looks like your cart is empty!</p>
85-
)}
86-
87-
<ul className="-my-6 divide-y divide-gray-200">
88-
{lineItems.map((item) => {
89-
const isRemoving = isRemovingItemId === item.id;
90-
return (
91-
<CartDrawerItem
92-
key={item.id}
93-
isRemoving={isRemoving}
94-
item={item}
95-
currencyCode={region.currency_code}
96-
/>
97-
);
98-
})}
99-
100-
{isAddingItem && (
101-
<li className="py-6">
102-
<div className="flex animate-pulse space-x-4">
103-
<div className="h-24 w-24 rounded-md bg-slate-300" />
104-
<div className="flex h-24 w-full flex-1 flex-col space-y-3 py-1">
105-
<div className="grid grid-cols-3 gap-4">
106-
<div className="col-span-2 h-2 rounded bg-slate-300" />
107-
<div className="col-span-1 h-2 rounded bg-slate-300" />
108-
</div>
109-
<div className="h-2 rounded bg-slate-300" />
110-
<div className="flex-1" />
111-
<div className="grid grid-cols-4 gap-4">
112-
<div className="col-span-1 h-2 rounded bg-slate-300" />
113-
<div className="col-span-2" />
114-
<div className="col-span-1 h-2 rounded bg-slate-300" />
115-
</div>
116-
</div>
117-
</div>
118-
</li>
119-
)}
120-
</ul>
121-
</div>
122-
</div>
123-
</div>
124-
125-
<div className="border-t border-gray-200 px-4 py-6 sm:px-6">
126-
<div className="flex justify-between text-base font-bold text-gray-900">
127-
<p>Subtotal</p>
128-
<p>
129-
{cart
130-
? formatCartSubtotal(cart)
131-
: formatPrice(0, {
132-
currency: region.currency_code,
133-
})}
134-
</p>
135-
</div>
136-
<p className="mt-0.5 text-sm text-gray-500">Shipping and taxes calculated at checkout.</p>
137-
<div className="mt-6">
138-
<Button
139-
variant="primary"
140-
disabled={lineItems.length === 0}
141-
onClick={handleCheckoutClick}
142-
className="h-12 w-full !text-base font-bold"
143-
>
144-
Checkout
145-
</Button>
146-
</div>
147-
<div className="mt-4 flex justify-center text-center text-sm text-gray-500">
148-
<p>
149-
or{' '}
150-
<ButtonLink size="sm" onClick={() => toggleCartDrawer(false)}>
151-
<div>
152-
Continue Shopping{` `}
153-
<span aria-hidden="true">&rarr;</span>
154-
</div>
155-
</ButtonLink>
156-
</p>
157-
</div>
158-
</div>
159-
</div>
160-
</Dialog.Panel>
161-
</Transition.Child>
162-
</div>
181+
<Dialog open={isOpen} onClose={handleClose} className="relative z-50">
182+
{/* Backdrop with transition */}
183+
<DialogBackdrop
184+
transition
185+
className="fixed inset-0 bg-gray-300 bg-opacity-50 backdrop-blur-sm duration-300 ease-out data-[closed]:opacity-0"
186+
/>
187+
188+
<div className="fixed inset-0 overflow-hidden">
189+
<div className="absolute inset-0 overflow-hidden">
190+
<div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
191+
{/* Panel with transition */}
192+
<DialogPanel
193+
transition
194+
className="pointer-events-auto w-screen max-w-md transform duration-500 ease-in-out data-[closed]:translate-x-full"
195+
>
196+
<div className="flex h-full flex-col overflow-y-scroll bg-white shadow-xl">
197+
{/* Content */}
198+
<div className="flex-1 overflow-y-auto px-4 py-6 sm:px-6">
199+
<CartDrawerHeader itemCount={lineItemsTotal} onClose={handleClose} />
200+
201+
<CartDrawerContent
202+
items={lineItems}
203+
isRemovingItemId={isRemovingItemId}
204+
isAddingItem={isAddingItem || isCartLoading}
205+
showEmptyCartMessage={showEmptyCartMessage}
206+
isRemovingLastItem={isRemovingLastItem}
207+
currencyCode={region.currency_code}
208+
/>
209+
</div>
210+
211+
{/* Footer */}
212+
<CartDrawerFooter
213+
cart={cart}
214+
currencyCode={region.currency_code}
215+
itemCount={lineItemsTotal}
216+
isAddingItem={isAddingItem || isCartLoading}
217+
isRemovingLastItem={isRemovingLastItem}
218+
onCheckout={handleCheckoutClick}
219+
onClose={handleClose}
220+
/>
221+
</div>
222+
</DialogPanel>
163223
</div>
164224
</div>
165-
</Dialog>
166-
</Transition.Root>
225+
</div>
226+
</Dialog>
167227
);
168228
};

0 commit comments

Comments
 (0)