Initial commit

This commit is contained in:
2026-09-16 17:36:46 +05:30
commit c9f429d123
117 changed files with 14145 additions and 0 deletions

View File

@@ -0,0 +1,194 @@
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useNavigate } from 'react-router-dom';
import { Trash2, Plus, Minus, ArrowRight, ShieldCheck, ShoppingBag, Truck } from 'lucide-react';
import { fetchCart, updateCartItemAsync, removeCartItemAsync, applyCouponAsync } from '../../store/slices/cartSlice';
import { showToast } from '../../store/slices/uiSlice';
import { formatINR } from '../../utils/formatCurrency';
const CartPage = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { items, coupon } = useSelector((state) => state.cart);
const [couponCode, setCouponCode] = useState('');
useEffect(() => {
dispatch(fetchCart());
}, [dispatch]);
const subtotal = items.reduce((acc, item) => {
const itemPrice = item.product?.salePrice || item.product?.price || item.price;
return acc + itemPrice * item.quantity;
}, 0);
const discountAmount = coupon ? (coupon.discountType === 'percentage' ? (subtotal * coupon.discountValue) / 100 : coupon.discountValue) : 0;
const freeShippingThreshold = 5000; // ₹5,000 threshold for India
const shippingFee = subtotal >= freeShippingThreshold || subtotal === 0 ? 0 : 499;
const totalAmount = Math.max(0, subtotal - discountAmount + shippingFee);
const freeShippingProgress = Math.min(100, (subtotal / freeShippingThreshold) * 100);
const handleApplyCoupon = (e) => {
e.preventDefault();
if (couponCode.trim()) {
dispatch(applyCouponAsync(couponCode.trim()))
.unwrap()
.then(() => dispatch(showToast({ message: 'Coupon applied successfully!', type: 'success' })))
.catch((err) => dispatch(showToast({ message: err || 'Invalid coupon', type: 'error' })));
}
};
if (items.length === 0) {
return (
<div className="max-w-7xl mx-auto px-4 py-20 text-center space-y-4 bg-slate-50">
<div className="w-16 h-16 bg-white border border-slate-200 rounded-full flex items-center justify-center mx-auto text-slate-400 shadow-sm">
<ShoppingBag className="w-8 h-8" />
</div>
<h2 className="text-2xl font-bold text-slate-900 font-serif">Your Shopping Cart is Empty</h2>
<p className="text-xs text-slate-500">Discover hand-crafted custom shop electric, acoustic, and bass guitars.</p>
<Link to="/shop" className="inline-block py-3 px-8 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-2xl text-xs shadow-sm">
Explore Guitar Catalog
</Link>
</div>
);
}
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-8 bg-slate-50 text-slate-900">
<h1 className="text-3xl font-extrabold text-slate-900 font-serif">Your Shopping Cart ({items.length} Items)</h1>
{/* Free Shipping Progress Bar */}
<div className="p-4 bg-white border border-slate-200 rounded-2xl space-y-2 shadow-sm">
<div className="flex items-center justify-between text-xs font-semibold">
<span className="text-slate-700 flex items-center gap-2">
<Truck className="w-4 h-4 text-amber-600" />
{subtotal >= freeShippingThreshold
? '🎉 You qualify for FREE Express Delivery in India!'
: `Add ${formatINR(freeShippingThreshold - subtotal)} more to unlock FREE Delivery`}
</span>
<span className="text-amber-600 font-bold">{Math.round(freeShippingProgress)}%</span>
</div>
<div className="h-2 w-full bg-slate-100 rounded-full overflow-hidden">
<div className="h-full bg-amber-500 transition-all duration-500" style={{ width: `${freeShippingProgress}%` }} />
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Cart Items Table */}
<div className="lg:col-span-2 space-y-4">
{items.map((item) => {
const product = item.product || {};
const itemPrice = product.salePrice > 0 ? product.salePrice : (product.price || item.price);
return (
<div key={item._id} className="p-4 bg-white border border-slate-200 rounded-2xl flex flex-col sm:flex-row items-center justify-between gap-4 shadow-sm">
<div className="flex items-center gap-4 w-full sm:w-auto">
<img
src={product.images?.[0] || 'https://images.unsplash.com/photo-1564186763535-ebb21ef5277f?w=300'}
alt={product.name}
className="w-20 h-20 object-cover rounded-xl border border-slate-200"
/>
<div>
<span className="text-[10px] font-bold text-amber-600 uppercase">{product.brand?.name}</span>
<h3 className="text-sm font-bold text-slate-900 line-clamp-1">{product.name}</h3>
{item.variantColor && (
<div className="text-xs text-slate-500">Variant: {item.variantColor} ({item.variantFinish})</div>
)}
<div className="text-xs font-bold text-slate-900 mt-1">{formatINR(itemPrice)} each</div>
</div>
</div>
<div className="flex items-center justify-between sm:justify-end gap-6 w-full sm:w-auto">
{/* Quantity selector */}
<div className="flex items-center bg-slate-100 border border-slate-200 rounded-xl p-1">
<button
onClick={() => dispatch(updateCartItemAsync({ itemId: item._id, quantity: item.quantity - 1 }))}
className="p-1.5 text-slate-500 hover:text-slate-900"
>
<Minus className="w-3.5 h-3.5" />
</button>
<span className="w-8 text-center text-xs font-bold text-slate-900">{item.quantity}</span>
<button
onClick={() => dispatch(updateCartItemAsync({ itemId: item._id, quantity: item.quantity + 1 }))}
className="p-1.5 text-slate-500 hover:text-slate-900"
>
<Plus className="w-3.5 h-3.5" />
</button>
</div>
<div className="text-sm font-extrabold text-amber-700">{formatINR(itemPrice * item.quantity)}</div>
<button
onClick={() => dispatch(removeCartItemAsync(item._id))}
className="p-2 text-slate-400 hover:text-rose-600"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
);
})}
</div>
{/* Order Summary Column */}
<div className="space-y-6">
<div className="p-6 bg-white border border-slate-200 rounded-3xl space-y-6 shadow-sm">
<h3 className="text-lg font-bold text-slate-900 font-serif">Order Summary</h3>
<div className="space-y-3 text-xs border-b border-slate-100 pb-4">
<div className="flex justify-between text-slate-600">
<span>Subtotal</span>
<span className="font-bold text-slate-900">{formatINR(subtotal)}</span>
</div>
{coupon && (
<div className="flex justify-between text-emerald-700">
<span>Discount ({coupon.code})</span>
<span className="font-bold">-{formatINR(discountAmount)}</span>
</div>
)}
<div className="flex justify-between text-slate-600">
<span>Estimated Shipping</span>
<span className="font-bold text-slate-900">{shippingFee === 0 ? 'FREE' : formatINR(shippingFee)}</span>
</div>
</div>
{/* Coupon Code Input */}
<form onSubmit={handleApplyCoupon} className="flex gap-2">
<input
type="text"
placeholder="Coupon (e.g. WELCOME10)"
value={couponCode}
onChange={(e) => setCouponCode(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs text-slate-900 uppercase focus:outline-none focus:border-amber-500"
/>
<button type="submit" className="py-2 px-4 bg-slate-900 hover:bg-slate-800 text-white font-bold rounded-xl text-xs">
Apply
</button>
</form>
<div className="flex justify-between items-baseline pt-2">
<span className="text-sm font-bold text-slate-900">Total Payable</span>
<span className="text-2xl font-extrabold text-amber-600">{formatINR(totalAmount)}</span>
</div>
<button
onClick={() => navigate('/checkout')}
className="w-full py-4 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-2xl text-xs sm:text-sm flex items-center justify-center gap-2 shadow-sm transition-all"
>
Proceed to Secure Checkout <ArrowRight className="w-4 h-4" />
</button>
<div className="flex items-center justify-center gap-2 text-xs text-slate-500 pt-2">
<ShieldCheck className="w-4 h-4 text-emerald-600" /> 256-Bit Encrypted Payment
</div>
</div>
</div>
</div>
</div>
);
};
export default CartPage;