Files
group-2-threaded-project-pe…/web/app/cart/page.js
augmentedpotato 1010d57b79 Stripe Payment
2026-04-09 22:27:03 -06:00

357 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/context/AuthContext";
import { useCart } from "@/context/CartContext";
import { loadStripe } from "@stripe/stripe-js";
import {
Elements,
PaymentElement,
useStripe,
useElements,
} from "@stripe/react-stripe-js";
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || "");
function PaymentForm({ clientSecret, totalAmount, onSuccess, onCancel }) {
const stripe = useStripe();
const elements = useElements();
const [paying, setPaying] = useState(false);
const [payError, setPayError] = useState(null);
async function handlePay(e) {
e.preventDefault();
if (!stripe || !elements) return;
setPaying(true);
setPayError(null);
const { error } = await stripe.confirmPayment({
elements,
confirmParams: { return_url: window.location.origin + "/cart/confirmation" },
redirect: "if_required",
});
if (error) {
setPayError(error.message);
setPaying(false);
}
else {
onSuccess();
}
}
return (
<div className="cart-payment-form">
<h3 className="cart-payment-title">Payment Details</h3>
<p className="cart-payment-total">
Total to pay: <strong>${parseFloat(totalAmount).toFixed(2)}</strong>
</p>
<PaymentElement />
{payError && <p className="cart-error-msg">{payError}</p>}
<div className="cart-payment-actions">
<button
className="cart-pay-btn"
type="button"
onClick={handlePay}
disabled={paying || !stripe}
>
{paying ? "Processing…" : `Pay $${parseFloat(totalAmount).toFixed(2)}`}
</button>
<button className="cart-cancel-btn" type="button" onClick={onCancel}>
Cancel
</button>
</div>
</div>
);
}
export default function CartPage() {
const { user, loading: authLoading } = useAuth();
const {
cart,
cartLoading,
cartError,
selectedStoreId,
updateItem,
removeItem,
clearCart,
applyCoupon,
checkout,
} = useCart();
const router = useRouter();
const [couponInput, setCouponInput] = useState("");
const [couponError, setCouponError] = useState(null);
const [couponLoading, setCouponLoading] = useState(false);
const [checkoutLoading, setCheckoutLoading] = useState(false);
const [checkoutError, setCheckoutError] = useState(null);
const [clientSecret, setClientSecret] = useState(null);
const [checkoutTotal, setCheckoutTotal] = useState(null);
const [confirmed, setConfirmed] = useState(false);
const [localQuantities, setLocalQuantities] = useState({});
useEffect(() => {
if (!authLoading && !user) {
router.push("/login");
}
}, [authLoading, user, router]);
useEffect(() => {
if (cart?.items) {
const map = {};
cart.items.forEach((i) => (map[i.cartItemId] = i.quantity));
setLocalQuantities(map);
}
}, [cart]);
async function handleQuantityChange(cartItemId, newQty) {
if (newQty < 1) {
return;
}
setLocalQuantities((prev) => ({ ...prev, [cartItemId]: newQty }));
try {
await updateItem(cartItemId, newQty);
}
catch {
if (cart?.items) {
const original = cart.items.find((i) => i.cartItemId === cartItemId);
if (original) {
setLocalQuantities((prev) => ({ ...prev, [cartItemId]: original.quantity }));
}
}
}
}
async function handleRemove(cartItemId) {
try {
await removeItem(cartItemId);
}
catch {
}
}
async function handleApplyCoupon() {
if (!couponInput.trim()) return;
setCouponLoading(true);
setCouponError(null);
try {
await applyCoupon(couponInput.trim());
setCouponInput("");
}
catch (err) {
setCouponError(err.message);
}
finally {
setCouponLoading(false);
}
}
async function handleCheckout() {
if (!cart?.items?.length) return;
setCheckoutLoading(true);
setCheckoutError(null);
try {
const result = await checkout("pm_card_visa");
if (result?.clientSecret) {
setClientSecret(result.clientSecret);
setCheckoutTotal(result.totalAmount);
}
else if (result?.status === "succeeded") {
setConfirmed(true);
}
}
catch (err) {
setCheckoutError(err.message);
}
finally {
setCheckoutLoading(false);
}
}
if (authLoading || cartLoading) {
return <main className="cart-page"><p className="cart-status-msg">Loading</p></main>;
}
if (!user) return null;
if (confirmed) {
return (
<main className="cart-page">
<div className="cart-confirmation">
<div className="cart-confirmation-icon"></div>
<h2 className="cart-confirmation-title">Order Confirmed!</h2>
<p className="cart-confirmation-body">
Thank you for your purchase. Your order has been placed successfully.
</p>
<button className="cart-continue-btn" type="button" onClick={() => router.push("/products")}>
Continue Shopping
</button>
</div>
</main>
);
}
if (!selectedStoreId) {
return (
<main className="cart-page">
<p className="cart-status-msg">Please select a store from the navigation bar to view your cart.</p>
</main>
);
}
const items = cart?.items ?? [];
return (
<main className="cart-page">
<h1 className="cart-title">Your Cart</h1>
{cartError && <p className="cart-error-msg">{cartError}</p>}
{items.length === 0 && !cartError && (
<div className="cart-empty">
<p className="cart-empty-msg">Your cart is empty.</p>
<button className="cart-continue-btn" type="button" onClick={() => router.push("/products")}>
Browse Products
</button>
</div>
)}
{items.length > 0 && (
<div className="cart-layout">
<div className="cart-items-section">
{items.map((item) => (
<div key={item.cartItemId} className="cart-item-row">
<img
src={item.imageUrl || "/images/pet-placeholder.png"}
alt={item.prodName}
className="cart-item-img"
onError={(e) => {
e.currentTarget.onerror = null;
e.currentTarget.src = "/images/pet-placeholder.png";
}}
/>
<div className="cart-item-details">
<p className="cart-item-name">{item.prodName}</p>
<p className="cart-item-unit-price">${parseFloat(item.unitPrice).toFixed(2)} each</p>
</div>
<div className="cart-item-qty-controls">
<button
className="cart-qty-btn"
type="button"
onClick={() =>
handleQuantityChange(item.cartItemId, (localQuantities[item.cartItemId] ?? item.quantity) - 1)
}
>
</button>
<span className="cart-qty-val">{localQuantities[item.cartItemId] ?? item.quantity}</span>
<button
className="cart-qty-btn"
type="button"
onClick={() =>
handleQuantityChange(item.cartItemId, (localQuantities[item.cartItemId] ?? item.quantity) + 1)
}
>
+
</button>
</div>
<p className="cart-item-line-total">
${(parseFloat(item.unitPrice) * (localQuantities[item.cartItemId] ?? item.quantity)).toFixed(2)}
</p>
<button
className="cart-item-remove-btn"
type="button"
onClick={() => handleRemove(item.cartItemId)}
>
</button>
</div>
))}
<button className="cart-clear-btn" type="button" onClick={clearCart}>
Clear Cart
</button>
</div>
<aside className="cart-summary">
<h2 className="cart-summary-title">Order Summary</h2>
<div className="cart-summary-row">
<span>Subtotal</span>
<span>${parseFloat(cart.subtotalAmount ?? 0).toFixed(2)}</span>
</div>
{parseFloat(cart.discountAmount ?? 0) > 0 && (
<div className="cart-summary-row cart-summary-discount">
<span>Discount {cart.couponCode && `(${cart.couponCode})`}</span>
<span>${parseFloat(cart.discountAmount).toFixed(2)}</span>
</div>
)}
<div className="cart-summary-row cart-summary-total">
<span>Total</span>
<span>${parseFloat(cart.totalAmount ?? 0).toFixed(2)}</span>
</div>
<div className="cart-coupon-section">
<input
className="cart-coupon-input"
type="text"
placeholder="Coupon code"
value={couponInput}
onChange={(e) => setCouponInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleApplyCoupon()}
/>
<button
className="cart-coupon-btn"
type="button"
onClick={handleApplyCoupon}
disabled={couponLoading}
>
{couponLoading ? "…" : "Apply"}
</button>
{couponError && <p className="cart-coupon-error">{couponError}</p>}
</div>
{checkoutError && <p className="cart-error-msg">{checkoutError}</p>}
{!clientSecret && (
<button
className="cart-checkout-btn"
type="button"
onClick={handleCheckout}
disabled={checkoutLoading || items.length === 0}
>
{checkoutLoading ? "Processing…" : "Checkout"}
</button>
)}
{clientSecret && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<PaymentForm
clientSecret={clientSecret}
totalAmount={checkoutTotal ?? cart.totalAmount}
onSuccess={() => {
setClientSecret(null);
setConfirmed(true);
}}
onCancel={() => setClientSecret(null)}
/>
</Elements>
)}
</aside>
</div>
)}
</main>
);
}