148 lines
4.3 KiB
TypeScript
148 lines
4.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
interface CartItem {
|
|
id: string;
|
|
product_slug: string;
|
|
product_name: string;
|
|
variant: string | null;
|
|
quantity: number;
|
|
unit_price: number;
|
|
subtotal: number;
|
|
}
|
|
|
|
interface Cart {
|
|
id: string;
|
|
items: CartItem[];
|
|
item_count: number;
|
|
subtotal: number;
|
|
}
|
|
|
|
export default function CartPage() {
|
|
const [cart, setCart] = useState<Cart | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [checkingOut, setCheckingOut] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const cartId = localStorage.getItem("cart_id");
|
|
if (cartId) {
|
|
fetch(
|
|
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api"}/cart/${cartId}`
|
|
)
|
|
.then((res) => (res.ok ? res.json() : null))
|
|
.then(setCart)
|
|
.finally(() => setLoading(false));
|
|
} else {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const handleCheckout = async () => {
|
|
if (!cart) return;
|
|
setCheckingOut(true);
|
|
|
|
try {
|
|
const res = await fetch(
|
|
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api"}/checkout/session`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
cart_id: cart.id,
|
|
success_url: `${window.location.origin}/checkout/success`,
|
|
cancel_url: `${window.location.origin}/cart`,
|
|
}),
|
|
}
|
|
);
|
|
|
|
if (res.ok) {
|
|
const { checkout_url } = await res.json();
|
|
window.location.href = checkout_url;
|
|
}
|
|
} catch (error) {
|
|
console.error("Checkout error:", error);
|
|
} finally {
|
|
setCheckingOut(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="container mx-auto px-4 py-16 text-center">
|
|
<p>Loading cart...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!cart || cart.items.length === 0) {
|
|
return (
|
|
<div className="container mx-auto px-4 py-16 text-center">
|
|
<h1 className="text-2xl font-bold mb-4">Your Cart</h1>
|
|
<p className="text-muted-foreground mb-8">Your cart is empty.</p>
|
|
<a href="/products" className="text-primary hover:underline">
|
|
Continue Shopping
|
|
</a>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-8">
|
|
<h1 className="text-2xl font-bold mb-8">Your Cart</h1>
|
|
|
|
<div className="grid md:grid-cols-3 gap-8">
|
|
<div className="md:col-span-2 space-y-4">
|
|
{cart.items.map((item) => (
|
|
<div
|
|
key={item.id}
|
|
className="flex items-center gap-4 p-4 border rounded-lg"
|
|
>
|
|
<div className="w-20 h-20 bg-muted rounded" />
|
|
<div className="flex-1">
|
|
<h3 className="font-semibold">{item.product_name}</h3>
|
|
{item.variant && (
|
|
<p className="text-sm text-muted-foreground">
|
|
Variant: {item.variant}
|
|
</p>
|
|
)}
|
|
<p className="text-sm">Qty: {item.quantity}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="font-bold">${item.subtotal.toFixed(2)}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="border rounded-lg p-6 h-fit">
|
|
<h2 className="font-bold mb-4">Order Summary</h2>
|
|
<div className="space-y-2 mb-4">
|
|
<div className="flex justify-between">
|
|
<span>Subtotal</span>
|
|
<span>${cart.subtotal.toFixed(2)}</span>
|
|
</div>
|
|
<div className="flex justify-between text-muted-foreground">
|
|
<span>Shipping</span>
|
|
<span>Calculated at checkout</span>
|
|
</div>
|
|
</div>
|
|
<div className="border-t pt-4 mb-4">
|
|
<div className="flex justify-between font-bold">
|
|
<span>Total</span>
|
|
<span>${cart.subtotal.toFixed(2)}</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={handleCheckout}
|
|
disabled={checkingOut}
|
|
className="w-full bg-primary text-primary-foreground py-3 rounded-md font-medium hover:bg-primary/90 disabled:opacity-50"
|
|
>
|
|
{checkingOut ? "Redirecting..." : "Checkout"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|