Skip to content
This repository was archived by the owner on Jun 2, 2025. It is now read-only.

Commit 82c5d8a

Browse files
author
Steph Dietz
committed
Merge branch 'add-cart-component'
2 parents 0c0a82a + cd6e9fe commit 82c5d8a

File tree

2 files changed

+256
-2
lines changed

2 files changed

+256
-2
lines changed

frontend/app/components/cart.tsx

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
"use client";
2+
3+
import {
4+
MinusIcon,
5+
PlusIcon,
6+
ShoppingCartIcon,
7+
XMarkIcon,
8+
} from "@heroicons/react/16/solid";
9+
import { useEffect, useRef, useState } from "react";
10+
import clsx from "clsx";
11+
12+
function Price({ amount, currencyCode, className }) {
13+
return (
14+
<span className={className}>
15+
{currencyCode} {amount}
16+
</span>
17+
);
18+
}
19+
20+
function CloseCart() {
21+
return (
22+
<div className="relative flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors dark:border-neutral-700 dark:text-white">
23+
<XMarkIcon
24+
className={clsx("h-6 transition-all ease-in-out hover:scale-110 ")}
25+
/>
26+
</div>
27+
);
28+
}
29+
30+
function OpenCart({ quantity }) {
31+
return (
32+
<div className="relative flex h-11 w-11 items-center justify-center rounded-md border transition-colors border-neutral-700 text-white">
33+
<ShoppingCartIcon className="h-4 transition-all ease-in-out hover:scale-110" />
34+
<div className="flex items-center justify-center flex-none font-bold absolute top-0 bg-indigo-500 text-black w-5 h-5 rounded-full -mt-2 right-0 -mr-2 text-xs text-white">
35+
{quantity > 0 && <span>{quantity}</span>}
36+
</div>
37+
</div>
38+
);
39+
}
40+
41+
function DeleteItemButton({ item }) {
42+
return (
43+
<button
44+
type="submit"
45+
aria-label="Remove cart item"
46+
className="ease flex h-[17px] w-[17px] items-center justify-center rounded-full bg-neutral-500 transition-all duration-200"
47+
>
48+
<XMarkIcon className="hover:text-accent-3 mx-[1px] h-4 w-4 text-white" />
49+
</button>
50+
);
51+
}
52+
53+
function EditItemQuantityButton({ item, type }) {
54+
return (
55+
<button
56+
type="submit"
57+
className={clsx(
58+
"ease flex h-full min-w-[36px] max-w-[36px] flex-none items-center justify-center rounded-full px-2 transition-all duration-200 hover:border-neutral-800 hover:opacity-80",
59+
{
60+
"ml-auto": type === "minus",
61+
}
62+
)}
63+
>
64+
{type === "plus" ? (
65+
<PlusIcon className="h-4 w-4 dark:text-neutral-500" />
66+
) : (
67+
<MinusIcon className="h-4 w-4 dark:text-neutral-500" />
68+
)}
69+
</button>
70+
);
71+
}
72+
73+
export default function CartModal() {
74+
const cart = {
75+
totalQuantity: 3,
76+
lines: [
77+
{
78+
quantity: 1,
79+
merchandise: {
80+
product: {
81+
handle: "product-1",
82+
title: "Product 1",
83+
featuredImage: {
84+
url: "/path/to/image1.jpg",
85+
altText: "Product 1 Image",
86+
},
87+
},
88+
title: "Default Option",
89+
selectedOptions: [{ name: "Size", value: "M" }],
90+
},
91+
cost: {
92+
totalAmount: { amount: "10.00", currencyCode: "USD" },
93+
},
94+
},
95+
{
96+
quantity: 2,
97+
merchandise: {
98+
product: {
99+
handle: "product-2",
100+
title: "Product 2",
101+
featuredImage: {
102+
url: "/path/to/image2.jpg",
103+
altText: "Product 2 Image",
104+
},
105+
},
106+
title: "Default Option",
107+
selectedOptions: [{ name: "Size", value: "L" }],
108+
},
109+
cost: {
110+
totalAmount: { amount: "20.00", currencyCode: "USD" },
111+
},
112+
},
113+
],
114+
cost: {
115+
totalTaxAmount: { amount: "2.00", currencyCode: "USD" },
116+
totalAmount: { amount: "32.00", currencyCode: "USD" },
117+
},
118+
checkoutUrl: "/checkout",
119+
};
120+
121+
const [isOpen, setIsOpen] = useState(false);
122+
const quantityRef = useRef(cart.totalQuantity);
123+
const openCart = () => setIsOpen(true);
124+
const closeCart = () => setIsOpen(false);
125+
126+
useEffect(() => {
127+
if (cart.totalQuantity !== quantityRef.current) {
128+
if (!isOpen) {
129+
setIsOpen(true);
130+
}
131+
quantityRef.current = cart.totalQuantity;
132+
}
133+
}, [isOpen, cart.totalQuantity, quantityRef]);
134+
135+
return (
136+
<>
137+
<button aria-label="Open cart" onClick={openCart}>
138+
<OpenCart quantity={cart.totalQuantity} />
139+
</button>
140+
{isOpen && (
141+
<div className="relative z-50">
142+
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
143+
<div className="fixed bottom-0 right-0 top-0 flex h-full w-full flex-col border-l border-neutral-700 bg-black p-6 text-white md:w-[390px]">
144+
<div className="flex items-center justify-between">
145+
<p className="text-lg font-semibold">My Cart</p>
146+
<button aria-label="Close cart" onClick={closeCart}>
147+
<CloseCart />
148+
</button>
149+
</div>
150+
{cart.lines.length === 0 ? (
151+
<div className="mt-20 flex w-full flex-col items-center justify-center overflow-hidden">
152+
<span className="h-16">🛒</span>
153+
<p className="mt-6 text-center text-2xl font-bold">
154+
Your cart is empty.
155+
</p>
156+
</div>
157+
) : (
158+
<div className="flex h-full flex-col justify-between overflow-hidden p-1">
159+
<ul className="flex-grow overflow-auto py-4">
160+
{cart.lines.map((item, i) => (
161+
<li
162+
key={i}
163+
className="flex w-full flex-col border-b border-neutral-700"
164+
>
165+
<div className="relative flex w-full flex-row justify-between px-1 py-4">
166+
<div className="absolute z-40 -mt-2 ml-[55px]">
167+
<DeleteItemButton item={item} />
168+
</div>
169+
<a
170+
href="#"
171+
onClick={closeCart}
172+
className="z-30 flex flex-row space-x-4"
173+
>
174+
<div className="relative h-16 w-16 cursor-pointer overflow-hidden rounded-md border border-neutral-700 bg-neutral-700">
175+
<img
176+
className="h-full w-full object-cover"
177+
width={64}
178+
height={64}
179+
alt={
180+
item.merchandise.product.featuredImage
181+
.altText || item.merchandise.product.title
182+
}
183+
src={item.merchandise.product.featuredImage.url}
184+
/>
185+
</div>
186+
<div className="flex flex-1 flex-col text-base">
187+
<span className="leading-tight">
188+
{item.merchandise.product.title}
189+
</span>
190+
{item.merchandise.title !== "Default Option" && (
191+
<p className="text-sm text-neutral-300">
192+
{item.merchandise.title}
193+
</p>
194+
)}
195+
</div>
196+
</a>
197+
<div className="flex h-16 flex-col justify-between">
198+
<Price
199+
className="flex justify-end space-y-2 text-right text-sm"
200+
amount={item.cost.totalAmount.amount}
201+
currencyCode={item.cost.totalAmount.currencyCode}
202+
/>
203+
<div className="ml-auto flex h-9 flex-row items-center rounded-full border border-neutral-700">
204+
<EditItemQuantityButton item={item} type="minus" />
205+
<p className="w-6 text-center">
206+
<span className="w-full text-sm">
207+
{item.quantity}
208+
</span>
209+
</p>
210+
<EditItemQuantityButton item={item} type="plus" />
211+
</div>
212+
</div>
213+
</div>
214+
</li>
215+
))}
216+
</ul>
217+
<div className="py-4 text-sm text-neutral-300">
218+
<div className="mb-3 flex items-center justify-between border-b border-neutral-700 pb-1">
219+
<p>Taxes</p>
220+
<Price
221+
className="text-right text-base text-white"
222+
amount={cart.cost.totalTaxAmount.amount}
223+
currencyCode={cart.cost.totalTaxAmount.currencyCode}
224+
/>
225+
</div>
226+
<div className="mb-3 flex items-center justify-between border-b border-neutral-700 pb-1 pt-1">
227+
<p>Shipping</p>
228+
<p className="text-right">Calculated at checkout</p>
229+
</div>
230+
<div className="mb-3 flex items-center justify-between border-b border-neutral-700 pb-1 pt-1">
231+
<p>Total</p>
232+
<Price
233+
className="text-right text-base text-white"
234+
amount={cart.cost.totalAmount.amount}
235+
currencyCode={cart.cost.totalAmount.currencyCode}
236+
/>
237+
</div>
238+
</div>
239+
<a
240+
href={cart.checkoutUrl}
241+
className="block w-full rounded-full bg-indigo-500 p-3 text-center text-sm font-medium text-white opacity-90 hover:opacity-100"
242+
>
243+
Proceed to Checkout
244+
</a>
245+
</div>
246+
)}
247+
</div>
248+
</div>
249+
)}
250+
</>
251+
);
252+
}

frontend/app/components/header.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ShoppingCartIcon } from "@heroicons/react/24/outline";
33
import { Suspense } from "react";
44
import { SearchInputSkeleton } from "./skeletons";
55
import Logo from "./logo";
6+
import Cart from "./cart";
67

78
export default function Header() {
89
return (
@@ -14,9 +15,10 @@ export default function Header() {
1415
<Suspense fallback={<SearchInputSkeleton />}>
1516
<Search />
1617
</Suspense>
17-
<button className="relative flex h-11 w-11 items-center justify-center rounded-md border transition-colors border-neutral-700 text-white">
18+
{/* <button className="relative flex h-11 w-11 items-center justify-center rounded-md border transition-colors border-neutral-700 text-white">
1819
<ShoppingCartIcon className="h-4 transition-all ease-in-out hover:scale-110" />
19-
</button>
20+
</button> */}
21+
<Cart />
2022
</div>
2123
<div className="md:hidden items-center justify-between px-4">
2224
<div className="flex items-center justify-between mb-2">

0 commit comments

Comments
 (0)