"use client"; import * as React from "react"; import { cn } from "./lib/utils"; export type SeatTierType = "vip" | "orchestra" | "balcony" | "sold" | "accessible"; export interface SeatTier { id: SeatTierType | string; name: string; price: number; badgeClass?: string; description?: string; } export interface SeatData { id: string; // e.g. "A-1" row: string; // e.g. "A" number: number; // e.g. 1 tier: SeatTierType | string; price?: number; accessible?: boolean; } export interface SeatMapProps extends React.HTMLAttributes { venueName?: string; eventName?: string; eventDate?: string; stageLabel?: string; rows?: string[]; seatsPerRow?: number; aisleAfterSeat?: number; tiers?: Record; initialSoldSeats?: string[]; initialAccessibleSeats?: string[]; selectedSeatIds?: string[]; defaultSelectedSeatIds?: string[]; maxSeats?: number; currency?: string; serviceFeePerTicket?: number; onSelectionChange?: (selectedSeats: SeatData[]) => void; onSeatClick?: (seat: SeatData) => void; onCheckout?: (selectedSeats: SeatData[], totalPrice: number) => void; className?: string; } const DEFAULT_TIERS: Record = { vip: { id: "vip", name: "VIP Prime", price: 135, badgeClass: "border-accent bg-accent text-accent-foreground", description: "Front rows with priority acoustic center", }, orchestra: { id: "orchestra", name: "Orchestra", price: 85, badgeClass: "border-foreground text-foreground bg-secondary", description: "Main floor prime viewing sightlines", }, balcony: { id: "balcony", name: "Balcony Loge", price: 45, badgeClass: "border-border text-muted-foreground bg-card", description: "Elevated tier overview seating", }, accessible: { id: "accessible", name: "Accessible", price: 60, badgeClass: "border-success text-success bg-success/10", description: "Step-free wheelchair companion access", }, sold: { id: "sold", name: "Sold Out", price: 0, badgeClass: "border-border/50 text-muted-foreground bg-muted/40 line-through", description: "Seat unavailable for purchase", }, }; const DEFAULT_ROWS = ["A", "B", "C", "D", "E", "F", "G", "H"]; const DEFAULT_SEATS_PER_ROW = 12; const DEFAULT_AISLE_AFTER = 6; const DEFAULT_SOLD_SEATS = [ "A-3", "A-4", "B-7", "B-8", "C-1", "C-12", "D-5", "D-6", "E-2", "E-9", "F-6", "F-7", "G-3", "G-10", "H-4", ]; const DEFAULT_ACCESSIBLE_SEATS = ["H-1", "H-12"]; function getTierForRow(row: string): "vip" | "orchestra" | "balcony" { if (row === "A" || row === "B") return "vip"; if (row === "C" || row === "D" || row === "E") return "orchestra"; return "balcony"; } /** * SeatMap: Interactive arena seating chart matrix grid with row letters, * seat numbers, tier types, selection toggle, price calculator readout, and ticket punch border. */ export function SeatMap({ venueName = "GRAND AUDITORIUM", eventName = "BIGBULL SYMPHONY · OPENING NIGHT", eventDate = "SAT, OCT 24 · 20:00", stageLabel = "★ MAIN STAGE / ACOUSTIC PODIUM ★", rows = DEFAULT_ROWS, seatsPerRow = DEFAULT_SEATS_PER_ROW, aisleAfterSeat = DEFAULT_AISLE_AFTER, tiers = DEFAULT_TIERS, initialSoldSeats = DEFAULT_SOLD_SEATS, initialAccessibleSeats = DEFAULT_ACCESSIBLE_SEATS, selectedSeatIds, defaultSelectedSeatIds = ["A-6"], maxSeats = 8, currency = "$", serviceFeePerTicket = 4.5, onSelectionChange, onSeatClick, onCheckout, className, ...props }: SeatMapProps) { const isControlled = selectedSeatIds !== undefined; const [internalSelectedIds, setInternalSelectedIds] = React.useState( defaultSelectedSeatIds ); const activeSelectedIds = isControlled ? selectedSeatIds : internalSelectedIds; const [hoveredSeat, setHoveredSeat] = React.useState(null); const [activeTierHighlight, setActiveTierHighlight] = React.useState(null); const [limitNotice, setLimitNotice] = React.useState(false); // Generate seat data map const allSeatsMap = React.useMemo(() => { const map = new Map(); rows.forEach((row) => { for (let num = 1; num <= seatsPerRow; num++) { const id = `${row}-${num}`; const isSold = initialSoldSeats.includes(id); const isAccessible = initialAccessibleSeats.includes(id); let tier: string = getTierForRow(row); if (isAccessible) tier = "accessible"; if (isSold) tier = "sold"; const price = tiers[tier]?.price ?? 50; map.set(id, { id, row, number: num, tier, price, accessible: isAccessible, }); } }); return map; }, [rows, seatsPerRow, tiers, initialSoldSeats, initialAccessibleSeats]); const selectedSeatsList = React.useMemo(() => { return activeSelectedIds .map((id) => allSeatsMap.get(id)) .filter((s): s is SeatData => s !== undefined); }, [activeSelectedIds, allSeatsMap]); // Price calculations const subtotal = React.useMemo(() => { return selectedSeatsList.reduce((acc, s) => acc + (s.price || 0), 0); }, [selectedSeatsList]); const totalFee = selectedSeatsList.length * serviceFeePerTicket; const grandTotal = subtotal > 0 ? subtotal + totalFee : 0; const handleToggleSeat = (seat: SeatData) => { if (seat.tier === "sold") return; if (onSeatClick) { onSeatClick(seat); } const isAlreadySelected = activeSelectedIds.includes(seat.id); let nextIds: string[]; if (isAlreadySelected) { nextIds = activeSelectedIds.filter((id) => id !== seat.id); setLimitNotice(false); } else { if (activeSelectedIds.length >= maxSeats) { setLimitNotice(true); setTimeout(() => setLimitNotice(false), 2400); return; } nextIds = [...activeSelectedIds, seat.id]; } if (!isControlled) { setInternalSelectedIds(nextIds); } if (onSelectionChange) { const nextSeats = nextIds .map((id) => allSeatsMap.get(id)) .filter((s): s is SeatData => s !== undefined); onSelectionChange(nextSeats); } }; const handleClear = () => { if (!isControlled) { setInternalSelectedIds([]); } if (onSelectionChange) { onSelectionChange([]); } }; const handleCheckoutClick = () => { if (onCheckout && selectedSeatsList.length > 0) { onCheckout(selectedSeatsList, grandTotal); } }; return (
{/* Side Punch Notches along Ticket Frame */}