2026-01-30 17:40:21 +00:00

190 lines
9.6 KiB
TypeScript

import React, { useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BaseButton from '../components/BaseButton';
import BaseIcon from '../components/BaseIcon';
import { getPageTitle, stripePublishableKey } from '../config';
import LayoutGuest from '../layouts/Guest';
import { mdiCart, mdiTrashCan, mdiArrowLeft, mdiChevronRight, mdiLock } from '@mdi/js';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { removeFromCart, updateQuantity } from '../stores/shoppingCartSlice';
import axios from 'axios';
import { loadStripe } from '@stripe/stripe-js';
const stripePromise = loadStripe(stripePublishableKey);
export default function CartPage() {
const dispatch = useAppDispatch();
const cartItems = useAppSelector((state) => state.shoppingCart.items);
const [isCheckingOut, setIsCheckingOut] = useState(false);
const subtotal = cartItems.reduce((acc, item) => acc + item.price * item.quantity, 0);
const shipping = subtotal > 100 ? 0 : 15;
const total = subtotal + shipping;
const handleCheckout = async () => {
setIsCheckingOut(true);
try {
const stripe = await stripePromise;
if (!stripe) throw new Error('Stripe failed to load');
const response = await axios.post('/checkout/create-session', {
items: cartItems,
successUrl: `${window.location.origin}/checkout-success?session_id={CHECKOUT_SESSION_ID}`,
cancelUrl: `${window.location.origin}/checkout-cancel`,
});
const session = response.data;
const result = await stripe.redirectToCheckout({
sessionId: session.id,
});
if (result.error) {
alert(result.error.message);
}
} catch (error) {
console.error('Checkout error:', error);
alert('Something went wrong. Please try again.');
} finally {
setIsCheckingOut(false);
}
};
return (
<div className="bg-gray-50 min-h-screen">
<Head>
<title>{getPageTitle('Shopping Cart')}</title>
</Head>
{/* Navigation */}
<nav className="bg-white border-b border-gray-100 py-4 px-6 flex justify-between items-center sticky top-0 z-50">
<Link href="/" className="flex items-center text-gray-600 hover:text-blue-600 font-medium transition-colors">
<BaseIcon path={mdiArrowLeft} size={20} className="mr-2" /> Continue Shopping
</Link>
<Link href="/" className="text-2xl font-black text-blue-600 tracking-tight">
STORE<span className="text-gray-900">FRONT</span>
</Link>
<div className="w-24"></div> {/* Spacer */}
</nav>
<main className="max-w-7xl mx-auto py-12 px-6">
<h1 className="text-4xl font-extrabold text-gray-900 mb-10">Your Shopping Cart</h1>
{cartItems.length === 0 ? (
<div className="bg-white rounded-3xl p-16 text-center shadow-sm border border-gray-100 max-w-2xl mx-auto">
<div className="bg-blue-50 w-24 h-24 rounded-full flex items-center justify-center mx-auto mb-8 text-blue-600">
<BaseIcon path={mdiCart} size={48} />
</div>
<h2 className="text-3xl font-bold text-gray-900 mb-4">Your cart is empty</h2>
<p className="text-gray-500 mb-10 text-lg">Looks like you haven&apos;t added anything to your cart yet. Browse our products and find something you love!</p>
<BaseButton href="/" label="Start Shopping" color="info" className="px-10 py-4 rounded-2xl font-bold text-lg shadow-lg hover:shadow-xl transition-all" />
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
{/* Items List */}
<div className="lg:col-span-2 space-y-6">
{cartItems.map((item) => (
<div key={item.productId} className="bg-white rounded-3xl p-6 shadow-sm border border-gray-100 flex flex-col sm:flex-row items-center transition-hover hover:shadow-md">
<div className="w-32 h-32 bg-gray-100 rounded-2xl overflow-hidden flex-shrink-0">
<img
src={item.image || "https://images.pexels.com/photos/1350789/pexels-photo-1350789.jpeg?auto=compress&cs=tinysrgb&w=200"}
alt={item.title}
className="w-full h-full object-cover"
/>
</div>
<div className="mt-4 sm:mt-0 sm:ml-8 flex-grow">
<div className="flex justify-between items-start">
<Link href={`/products/${item.productId}`} className="text-xl font-bold text-gray-900 hover:text-blue-600 transition-colors">
{item.title}
</Link>
<button
onClick={() => dispatch(removeFromCart(item.productId))}
className="text-gray-300 hover:text-red-500 transition-colors p-2"
>
<BaseIcon path={mdiTrashCan} size={22} />
</button>
</div>
<p className="text-blue-600 font-bold text-lg mt-1">${item.price}</p>
<div className="flex items-center justify-between mt-6">
<div className="flex items-center bg-gray-50 rounded-xl p-1 border border-gray-100">
<button
onClick={() => dispatch(updateQuantity({ productId: item.productId, quantity: Math.max(1, item.quantity - 1) }))}
className="w-10 h-10 flex items-center justify-center text-gray-500 hover:text-blue-600 hover:bg-white rounded-lg transition-all font-bold"
>
-
</button>
<span className="w-12 text-center font-extrabold text-gray-900 text-lg">{item.quantity}</span>
<button
onClick={() => dispatch(updateQuantity({ productId: item.productId, quantity: item.quantity + 1 }))}
className="w-10 h-10 flex items-center justify-center text-gray-500 hover:text-blue-600 hover:bg-white rounded-lg transition-all font-bold"
>
+
</button>
</div>
<span className="font-black text-gray-900 text-xl">
${(item.price * item.quantity).toFixed(2)}
</span>
</div>
</div>
</div>
))}
</div>
{/* Summary */}
<div className="lg:col-span-1">
<div className="bg-white rounded-[2.5rem] p-10 shadow-sm border border-gray-100 sticky top-28">
<h2 className="text-2xl font-bold text-gray-900 mb-8">Order Summary</h2>
<div className="space-y-5 mb-10">
<div className="flex justify-between text-gray-500">
<span className="font-medium">Subtotal</span>
<span className="font-bold text-gray-900 text-lg">${subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between text-gray-500">
<span className="font-medium">Shipping</span>
<span className="font-bold text-green-600 text-lg">{shipping === 0 ? 'FREE' : `$${shipping.toFixed(2)}`}</span>
</div>
{shipping > 0 && (
<div className="bg-blue-50 p-4 rounded-2xl">
<p className="text-sm text-blue-700 font-medium flex items-center">
<BaseIcon path={mdiChevronRight} size={16} className="mr-1" />
Add <span className="font-bold mx-1">${(100 - subtotal).toFixed(2)}</span> more for Free Shipping
</p>
</div>
)}
<div className="border-t border-gray-100 pt-6 flex justify-between items-center">
<span className="text-xl font-bold text-gray-900">Total</span>
<span className="text-3xl font-black text-blue-600">${total.toFixed(2)}</span>
</div>
</div>
<BaseButton
onClick={handleCheckout}
label={isCheckingOut ? "Processing..." : "Secure Checkout"}
color="info"
className="w-full py-5 rounded-2xl text-xl font-bold shadow-xl hover:shadow-2xl transition-all transform hover:-translate-y-1"
icon={isCheckingOut ? undefined : mdiLock}
disabled={isCheckingOut}
/>
<div className="flex items-center justify-center mt-8 text-gray-400">
<BaseIcon path={mdiLock} size={14} className="mr-2" />
<span className="text-xs uppercase tracking-widest font-bold">Encrypted & Secure</span>
</div>
<div className="flex justify-center mt-4 space-x-4 opacity-30 grayscale">
<img src="https://upload.wikimedia.org/wikipedia/commons/b/b5/PayPal.svg" alt="PayPal" className="h-4" />
<img src="https://upload.wikimedia.org/wikipedia/commons/5/5e/Visa_Inc._logo.svg" alt="Visa" className="h-4" />
<img src="https://upload.wikimedia.org/wikipedia/commons/2/2a/Mastercard-logo.svg" alt="Mastercard" className="h-4" />
</div>
</div>
</div>
</div>
)}
</main>
</div>
);
}
CartPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};