- Add AddressController with full CRUD API for saved addresses - Implement address management UI in My Account > Addresses - Add modal-based address selector in checkout (Tokopedia-style) - Hide checkout forms when saved address is selected - Add search functionality in address modal - Auto-select default addresses on page load - Fix variable products to show 'Select Options' instead of 'Add to Cart' - Add admin toggle for multiple addresses feature - Clean up debug logs and fix TypeScript errors
262 lines
9.8 KiB
TypeScript
262 lines
9.8 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|
import { useCartStore, type CartItem } from '@/lib/cart/store';
|
|
import { useCartSettings } from '@/hooks/useAppearanceSettings';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import Container from '@/components/Layout/Container';
|
|
import { formatPrice } from '@/lib/currency';
|
|
import { Trash2, Plus, Minus, ShoppingBag, ArrowLeft } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
|
|
export default function Cart() {
|
|
const navigate = useNavigate();
|
|
const { cart, removeItem, updateQuantity, clearCart } = useCartStore();
|
|
const { layout, elements } = useCartSettings();
|
|
const [showClearDialog, setShowClearDialog] = useState(false);
|
|
|
|
// Calculate total from items
|
|
const total = cart.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
|
|
|
const handleUpdateQuantity = (key: string, newQuantity: number) => {
|
|
if (newQuantity < 1) {
|
|
handleRemoveItem(key);
|
|
return;
|
|
}
|
|
updateQuantity(key, newQuantity);
|
|
};
|
|
|
|
const handleRemoveItem = (key: string) => {
|
|
removeItem(key);
|
|
toast.success('Item removed from cart');
|
|
};
|
|
|
|
const handleClearCart = () => {
|
|
clearCart();
|
|
setShowClearDialog(false);
|
|
toast.success('Cart cleared');
|
|
};
|
|
|
|
if (cart.items.length === 0) {
|
|
return (
|
|
<Container>
|
|
<div className="text-center py-16">
|
|
<ShoppingBag className="mx-auto h-16 w-16 text-gray-400 mb-4" />
|
|
<h2 className="text-2xl font-bold mb-2">Your cart is empty</h2>
|
|
<p className="text-gray-600 mb-6">Add some products to get started!</p>
|
|
<Button onClick={() => navigate('/shop')}>
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
Continue Shopping
|
|
</Button>
|
|
</div>
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Container>
|
|
<div className={`py-8 ${layout.style === 'boxed' ? 'max-w-5xl mx-auto' : ''}`}>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-8">
|
|
<h1 className="text-3xl font-bold">Shopping Cart</h1>
|
|
<Button variant="outline" onClick={() => setShowClearDialog(true)}>
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
Clear Cart
|
|
</Button>
|
|
</div>
|
|
|
|
<div className={`grid gap-8 ${layout.summary_position === 'bottom' ? 'grid-cols-1' : 'lg:grid-cols-3'}`}>
|
|
{/* Cart Items */}
|
|
<div className={`space-y-4 ${layout.summary_position === 'bottom' ? '' : 'lg:col-span-2'}`}>
|
|
{cart.items.map((item: CartItem) => (
|
|
<div
|
|
key={item.key}
|
|
className="flex gap-4 p-4 border rounded-lg bg-white"
|
|
>
|
|
{/* Product Image */}
|
|
{elements.product_images && (
|
|
<div className="relative w-24 h-24 flex-shrink-0 rounded-lg overflow-hidden bg-gray-100">
|
|
{item.image ? (
|
|
<img
|
|
src={item.image}
|
|
alt={item.name}
|
|
className="block w-full !h-full object-cover object-center"
|
|
/>
|
|
) : (
|
|
<div className="w-full !h-full flex items-center justify-center text-gray-400 text-xs">
|
|
No Image
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Product Info */}
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-semibold text-lg mb-1 truncate">
|
|
{item.name}
|
|
</h3>
|
|
|
|
{/* Variation Attributes */}
|
|
{item.attributes && Object.keys(item.attributes).length > 0 && (
|
|
<div className="text-sm text-gray-500 mb-1">
|
|
{Object.entries(item.attributes).map(([key, value]) => {
|
|
// Format attribute name: capitalize first letter
|
|
const formattedKey = key.charAt(0).toUpperCase() + key.slice(1);
|
|
return (
|
|
<span key={key} className="mr-3">
|
|
{formattedKey}: <span className="font-medium">{value}</span>
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
<p className="text-gray-600 mb-2">
|
|
{formatPrice(item.price)}
|
|
</p>
|
|
|
|
{/* Quantity Controls */}
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => handleUpdateQuantity(item.key, item.quantity - 1)}
|
|
className="font-[inherit] p-1 hover:bg-gray-100 rounded"
|
|
>
|
|
<Minus className="h-4 w-4" />
|
|
</button>
|
|
<input
|
|
type="number"
|
|
value={item.quantity}
|
|
onChange={(e) =>
|
|
handleUpdateQuantity(item.key, parseInt(e.target.value) || 1)
|
|
}
|
|
className="w-16 text-center border rounded py-1"
|
|
min="1"
|
|
/>
|
|
<button
|
|
onClick={() => handleUpdateQuantity(item.key, item.quantity + 1)}
|
|
className="font-[inherit] p-1 hover:bg-gray-100 rounded"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Item Total & Remove */}
|
|
<div className="flex flex-col items-end justify-between">
|
|
<button
|
|
onClick={() => handleRemoveItem(item.key)}
|
|
className="font-[inherit] text-red-600 hover:text-red-700 p-2"
|
|
>
|
|
<Trash2 className="h-5 w-5" />
|
|
</button>
|
|
<p className="font-bold text-lg">
|
|
{formatPrice(item.price * item.quantity)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Cart Summary */}
|
|
<div className="lg:col-span-1">
|
|
<div className="border rounded-lg p-6 bg-white sticky top-4">
|
|
<h2 className="text-xl font-bold mb-4">Cart Summary</h2>
|
|
|
|
{/* Coupon Field */}
|
|
{elements.coupon_field && (
|
|
<div className="mb-6">
|
|
<label className="block text-sm font-medium mb-2">Coupon Code</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
placeholder="Enter coupon code"
|
|
className="flex-1 px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
|
/>
|
|
<Button variant="outline" size="sm">Apply</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Shipping Calculator */}
|
|
{elements.shipping_calculator && (
|
|
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
|
|
<h3 className="font-medium mb-3">Calculate Shipping</h3>
|
|
<div className="space-y-2">
|
|
<input
|
|
type="text"
|
|
placeholder="Postal Code"
|
|
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
|
/>
|
|
<Button variant="outline" size="sm" className="w-full">Calculate</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-3 mb-6">
|
|
<div className="flex justify-between text-gray-600">
|
|
<span>Subtotal</span>
|
|
<span>{formatPrice(total)}</span>
|
|
</div>
|
|
<div className="flex justify-between text-gray-600">
|
|
<span>Shipping</span>
|
|
<span>Calculated at checkout</span>
|
|
</div>
|
|
<div className="border-t pt-3 flex justify-between text-lg font-bold">
|
|
<span>Total</span>
|
|
<span>{formatPrice(total)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={() => navigate('/checkout')}
|
|
size="lg"
|
|
className="w-full mb-3"
|
|
>
|
|
Proceed to Checkout
|
|
</Button>
|
|
|
|
{elements.continue_shopping_button && (
|
|
<Button
|
|
onClick={() => navigate('/shop')}
|
|
variant="outline"
|
|
className="w-full"
|
|
>
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
Continue Shopping
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Clear Cart Confirmation Dialog */}
|
|
<Dialog open={showClearDialog} onOpenChange={setShowClearDialog}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Clear Cart?</DialogTitle>
|
|
<DialogDescription>
|
|
Are you sure you want to remove all items from your cart? This action cannot be undone.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setShowClearDialog(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="destructive" onClick={handleClearCart}>
|
|
Clear Cart
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</Container>
|
|
);
|
|
}
|