Files
blackcanyontickets/reactrebuild0825/src/components/checkout/CartDrawer.tsx
dzinesco 8ed7ae95d1 feat: comprehensive project completion and documentation
- Enhanced event creation wizard with multi-step validation
- Added advanced QR scanning system with offline support
- Implemented comprehensive territory management features
- Expanded analytics with export functionality and KPIs
- Created complete design token system with theme switching
- Added 25+ Playwright test files for comprehensive coverage
- Implemented enterprise-grade permission system
- Enhanced component library with 80+ React components
- Added Firebase integration for deployment
- Completed Phase 3 development goals substantially

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-26 15:04:37 -06:00

284 lines
11 KiB
TypeScript

import React, { useState } from 'react';
import { X, Minus, Plus, Trash2, ShoppingBag, CreditCard } from 'lucide-react';
import { Button } from '../ui/Button';
import { Card } from '../ui/Card';
import { Badge } from '../ui/Badge';
import { useCartStore } from '../../stores/cartStore';
import { CheckoutWizard } from './CheckoutWizard';
export interface CartDrawerProps {
onCheckout?: () => void;
}
export const CartDrawer: React.FC<CartDrawerProps> = ({
onCheckout
}) => {
const {
isOpen,
setIsOpen,
updateQuantity,
removeItem,
clearCart,
getTotals,
getItemsByEvent,
hasItems
} = useCartStore();
const [showCheckoutWizard, setShowCheckoutWizard] = useState(false);
const totals = getTotals();
const itemsByEvent = getItemsByEvent();
const handleClose = () => {
setIsOpen(false);
};
const handleQuantityChange = (itemId: string, newQuantity: number) => {
updateQuantity(itemId, newQuantity);
};
const handleRemoveItem = (itemId: string) => {
removeItem(itemId);
};
const handleClearCart = () => {
if (window.confirm('Are you sure you want to clear your cart?')) {
clearCart();
}
};
const handleCheckout = () => {
setShowCheckoutWizard(true);
onCheckout?.();
};
if (!isOpen) return null;
return (
<>
{/* Backdrop */}
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40"
onClick={handleClose}
/>
{/* Drawer */}
<div
className="fixed right-0 top-0 h-full w-full max-w-md bg-bg-primary border-l border-border-primary z-50 flex flex-col"
role="dialog"
aria-modal="true"
aria-labelledby="cart-title"
aria-describedby="cart-description"
>
{/* Header */}
<div className="flex items-center justify-between p-spacing-lg border-b border-border-primary">
<div className="flex items-center space-x-spacing-sm">
<ShoppingBag className="h-5 w-5 text-text-primary" />
<h2 id="cart-title" className="text-lg font-semibold text-text-primary">
Shopping Cart
</h2>
{totals.totalQuantity > 0 && (
<Badge variant="primary">
{totals.totalQuantity} item{totals.totalQuantity !== 1 ? 's' : ''}
</Badge>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={handleClose}
className="p-2"
aria-label="Close cart"
>
<X className="h-5 w-5" />
</Button>
<p id="cart-description" className="sr-only">
Review and manage items in your shopping cart before checkout
</p>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto">
{!hasItems() ? (
<div className="flex flex-col items-center justify-center h-full p-spacing-xl text-center">
<ShoppingBag className="h-16 w-16 text-text-muted mb-spacing-md" />
<h3 className="text-lg font-medium text-text-primary mb-spacing-sm">
Your cart is empty
</h3>
<p className="text-text-secondary mb-spacing-lg">
Add some tickets to get started!
</p>
<Button variant="primary" onClick={handleClose}>
Continue Shopping
</Button>
</div>
) : (
<div className="p-spacing-lg space-y-spacing-lg">
{/* Cart Items by Event */}
{Object.entries(itemsByEvent).map(([eventId, eventItems]) => (
<div key={eventId}>
<h3 className="text-sm font-medium text-text-primary mb-spacing-md">
{eventItems[0]?.eventTitle}
</h3>
<div className="space-y-spacing-md">
{eventItems.map((item) => (
<Card key={item.id} className="p-spacing-md">
<div className="space-y-spacing-sm">
{/* Item Details */}
<div className="flex justify-between items-start">
<div className="flex-1">
<h4 className="font-medium text-text-primary">
{item.ticketTypeName}
</h4>
{item.ticketTypeDescription && (
<p className="text-sm text-text-secondary mt-1">
{item.ticketTypeDescription}
</p>
)}
<p className="text-lg font-semibold text-text-primary mt-spacing-xs">
${(item.priceInCents / 100).toFixed(2)} each
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveItem(item.id)}
className="text-error-500 hover:text-error-600 p-1"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
{/* Quantity Controls */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-spacing-sm">
<Button
variant="outline"
size="sm"
onClick={() => handleQuantityChange(item.id, item.quantity - 1)}
disabled={item.quantity <= 1}
className="p-2 h-10 w-10 touch-target"
aria-label={`Decrease quantity for ${item.ticketTypeName}`}
>
<Minus className="h-4 w-4" />
</Button>
<div className="bg-surface-secondary border border-border-primary rounded px-spacing-sm py-1 min-w-[2.5rem] text-center">
<span className="text-sm font-medium text-text-primary">
{item.quantity}
</span>
</div>
<Button
variant="outline"
size="sm"
onClick={() => handleQuantityChange(item.id, item.quantity + 1)}
disabled={
item.quantity >= item.maxQuantity ||
(item.inventory !== undefined && item.quantity >= item.inventory)
}
className="p-2 h-10 w-10 touch-target"
aria-label={`Increase quantity for ${item.ticketTypeName}`}
>
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="text-right">
<p className="font-medium text-text-primary">
${((item.priceInCents * item.quantity) / 100).toFixed(2)}
</p>
{item.inventory !== undefined && (
<p className="text-xs text-text-muted">
{item.inventory - item.quantity} left
</p>
)}
</div>
</div>
{/* Seat Numbers (if applicable) */}
{item.seatNumbers && item.seatNumbers.length > 0 && (
<div className="text-sm text-text-secondary">
<span className="font-medium">Seats:</span> {item.seatNumbers.join(', ')}
</div>
)}
</div>
</Card>
))}
</div>
</div>
))}
{/* Clear Cart Button */}
<div className="text-center">
<Button
variant="ghost"
size="sm"
onClick={handleClearCart}
className="text-text-muted hover:text-error-500"
>
Clear Cart
</Button>
</div>
</div>
)}
</div>
{/* Footer with Totals and Checkout */}
{hasItems() && (
<div className="border-t border-border-primary p-spacing-lg space-y-spacing-md">
{/* Totals */}
<div className="space-y-spacing-xs">
<div className="flex justify-between text-sm">
<span className="text-text-secondary">
Subtotal ({totals.totalQuantity} items)
</span>
<span className="text-text-primary font-medium">
${totals.subtotal.toFixed(2)}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-text-secondary">Platform fee</span>
<span className="text-text-primary">
${totals.platformFee.toFixed(2)}
</span>
</div>
<div className="border-t border-border-primary pt-spacing-xs">
<div className="flex justify-between font-semibold text-base">
<span className="text-text-primary">Total</span>
<span className="text-text-primary">
${totals.total.toFixed(2)}
</span>
</div>
</div>
</div>
{/* Checkout Button */}
<Button
variant="primary"
size="lg"
onClick={handleCheckout}
className="w-full"
>
<CreditCard className="h-4 w-4 mr-2" />
Proceed to Checkout
</Button>
<p className="text-xs text-text-muted text-center">
Secure checkout powered by Stripe
</p>
</div>
)}
</div>
{/* Checkout Wizard */}
<CheckoutWizard
isOpen={showCheckoutWizard}
onClose={() => setShowCheckoutWizard(false)}
/>
</>
);
};