- 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>
284 lines
11 KiB
TypeScript
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)}
|
|
/>
|
|
</>
|
|
);
|
|
}; |