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>
This commit is contained in:
284
reactrebuild0825/src/components/checkout/CartDrawer.tsx
Normal file
284
reactrebuild0825/src/components/checkout/CartDrawer.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user