227 lines
6.9 KiB
TypeScript
227 lines
6.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useSession } from "next-auth/react";
|
|
import DesktopHeader from "../../components/DesktopHeader";
|
|
import MobileHeader from "../../components/MobileHeader";
|
|
import Footer from "../../components/Footer";
|
|
|
|
type CartItem = {
|
|
id: string;
|
|
name: string;
|
|
unitAmount: number;
|
|
currency: string;
|
|
quantity: number;
|
|
};
|
|
|
|
type Cart = {
|
|
id: string;
|
|
items: CartItem[];
|
|
};
|
|
|
|
export default function CartPage() {
|
|
const [slug, setSlug] = useState("storeshifted");
|
|
const [cart, setCart] = useState<Cart | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [message, setMessage] = useState("");
|
|
const { data: session, status } = useSession();
|
|
const isAuthenticated = status === "authenticated";
|
|
const [addressTo, setAddressTo] = useState({
|
|
name: "",
|
|
street1: "",
|
|
city: "",
|
|
state: "",
|
|
zip: "",
|
|
country: "US",
|
|
phone: "",
|
|
});
|
|
|
|
useEffect(() => {
|
|
const stored = window.localStorage.getItem("connectStoreSlug");
|
|
if (stored) setSlug(stored);
|
|
}, []);
|
|
|
|
async function loadCart(currentSlug: string) {
|
|
setLoading(true);
|
|
setMessage("");
|
|
try {
|
|
const res = await fetch(`/api/cart/get?slug=${currentSlug}`);
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || "Failed to load cart");
|
|
setCart(data.cart);
|
|
} catch (err: any) {
|
|
setMessage(err.message || "Failed to load cart.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadCart(slug);
|
|
}, [slug]);
|
|
|
|
async function updateQty(itemId: string, quantity: number) {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/cart/update", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ itemId, quantity }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || "Failed to update cart");
|
|
await loadCart(slug);
|
|
} catch (err: any) {
|
|
setMessage(err.message || "Failed to update cart.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function removeItem(itemId: string) {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/cart/remove", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ itemId }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || "Failed to remove item");
|
|
await loadCart(slug);
|
|
} catch (err: any) {
|
|
setMessage(err.message || "Failed to remove item.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function checkout() {
|
|
setLoading(true);
|
|
setMessage("");
|
|
try {
|
|
const res = await fetch("/api/cart/checkout", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
slug,
|
|
shippingAddress: addressTo,
|
|
}),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || "Checkout failed");
|
|
window.location.href = data.url;
|
|
} catch (err: any) {
|
|
setMessage(err.message || "Checkout failed.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
const total =
|
|
cart?.items.reduce((sum, item) => sum + item.unitAmount * item.quantity, 0) ?? 0;
|
|
|
|
return (
|
|
<main className="cart-page">
|
|
<DesktopHeader />
|
|
<MobileHeader />
|
|
<section className="section">
|
|
<div className="container cart-page__inner">
|
|
<h1 className="page-title">Cart</h1>
|
|
{isAuthenticated ? (
|
|
<p className="connect-muted">Signed in as {session!.user.email}</p>
|
|
) : (
|
|
<p className="connect-muted">
|
|
Sign in to save your cart across devices. <a href="/login">Sign in</a>
|
|
</p>
|
|
)}
|
|
|
|
{message ? <div className="connect-message">{message}</div> : null}
|
|
|
|
<div className="cart-list">
|
|
{cart?.items.map((item) => (
|
|
<div key={item.id} className="cart-item">
|
|
<div className="cart-item__info">
|
|
<div className="cart-item__name">{item.name}</div>
|
|
<div className="cart-item__price">
|
|
{(item.unitAmount / 100).toFixed(2)} {item.currency.toUpperCase()}
|
|
</div>
|
|
</div>
|
|
<div className="cart-item__actions">
|
|
<input
|
|
type="number"
|
|
min={1}
|
|
value={item.quantity}
|
|
onChange={(e) => updateQty(item.id, Number(e.target.value))}
|
|
/>
|
|
<button className="btn btn--ghost" onClick={() => removeItem(item.id)}>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{!cart?.items.length ? (
|
|
<div className="storefront-muted">Your cart is empty.</div>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="cart-shipping">
|
|
<h2>Shipping</h2>
|
|
<div className="cart-shipping__grid">
|
|
<div className="cart-shipping__card">
|
|
<h3>Ship To</h3>
|
|
<input
|
|
value={addressTo.name}
|
|
onChange={(e) => setAddressTo({ ...addressTo, name: e.target.value })}
|
|
placeholder="Name"
|
|
/>
|
|
<input
|
|
value={addressTo.street1}
|
|
onChange={(e) =>
|
|
setAddressTo({ ...addressTo, street1: e.target.value })
|
|
}
|
|
placeholder="Street"
|
|
/>
|
|
<input
|
|
value={addressTo.city}
|
|
onChange={(e) => setAddressTo({ ...addressTo, city: e.target.value })}
|
|
placeholder="City"
|
|
/>
|
|
<input
|
|
value={addressTo.state}
|
|
onChange={(e) =>
|
|
setAddressTo({ ...addressTo, state: e.target.value })
|
|
}
|
|
placeholder="State/Province"
|
|
/>
|
|
<input
|
|
value={addressTo.zip}
|
|
onChange={(e) => setAddressTo({ ...addressTo, zip: e.target.value })}
|
|
placeholder="Postal Code"
|
|
/>
|
|
<input
|
|
value={addressTo.country}
|
|
onChange={(e) =>
|
|
setAddressTo({ ...addressTo, country: e.target.value })
|
|
}
|
|
placeholder="Country"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="cart-summary">
|
|
<div>
|
|
Total: {(total / 100).toFixed(2)} USD
|
|
</div>
|
|
<button className="btn" onClick={checkout} disabled={loading || !cart?.items.length}>
|
|
Checkout
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<Footer />
|
|
</main>
|
|
);
|
|
}
|