Components
Loading preview...
Seat Selection Description: A highly customizable and accessible movie theater seat selection component. It's built with framer-motion for fluid animations and styled using shadcn/ui theme variables for seamless light/dark mode integration. The component accepts a structured layout prop, making it reusable for any seating arrangement.
@lavikatiyar
npx shadcn@latest add https://21st.dev/r/lavikatiyar/seat-selectionimport React, { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
SeatSelection,
SeatCategoryInfo,
} from '@/components/ui/seat-selection';
import { Button } from '@/components/ui/button'; // Assuming shadcn/ui Button
// --- Helper to generate seat layout data programmatically ---
const generateSeats = (start: number, end: number, rowId: string) => {
let seats = [];
for (let i = start; i <= end; i++) {
seats.push({ id: `${rowId}${i}`, number: i });
}
return seats;
};
// --- Demo Data structured based on the provided image ---
const seatLayoutData: SeatCategoryInfo[] = [
{
categoryName: 'CLASSIC',
price: 135.58,
rows: [
{ rowId: 'A', seats: [...generateSeats(1, 9, 'A'), { id: 'A-spacer', isSpacer: true }, ...generateSeats(10, 21, 'A')] },
{ rowId: 'B', seats: [...generateSeats(1, 9, 'B'), { id: 'B-spacer', isSpacer: true }, ...generateSeats(10, 21, 'B')] },
{ rowId: 'C', seats: [...generateSeats(1, 2, 'C'), { id: 'C-spacer-1', isSpacer: true }, ...generateSeats(3, 9, 'C'), { id: 'C-spacer-2', isSpacer: true }, ...generateSeats(10, 12, 'C'), { id: 'C-spacer-3', isSpacer: true }, ...generateSeats(13, 21, 'C')] },
],
},
{
categoryName: 'CLASSIC PLUS',
price: 169.48,
rows: [
{ rowId: 'D', seats: [...generateSeats(1, 9, 'D'), { id: 'D-spacer', isSpacer: true }, ...generateSeats(10, 21, 'D')] },
{ rowId: 'E', seats: [...generateSeats(1, 9, 'E'), { id: 'E-spacer', isSpacer: true }, ...generateSeats(10, 21, 'E')] },
{ rowId: 'F', seats: [...generateSeats(1, 9, 'F'), { id: 'F-spacer', isSpacer: true }, ...generateSeats(10, 21, 'F')] },
{ rowId: 'G', seats: [...generateSeats(1, 9, 'G'), { id: 'G-spacer', isSpacer: true }, ...generateSeats(10, 21, 'G')] },
{ rowId: 'H', seats: [...generateSeats(1, 9, 'H'), { id: 'H-spacer', isSpacer: true }, ...generateSeats(10, 21, 'H')] },
],
},
{
categoryName: 'PRIME',
price: 186.44,
rows: [
{ rowId: 'J', seats: [...generateSeats(1, 13, 'J'), { id: 'J-spacer', isSpacer: true }, ...generateSeats(14, 25, 'J')] },
{ rowId: 'K', seats: [...generateSeats(1, 13, 'K'), { id: 'K-spacer', isSpacer: true }, ...generateSeats(14, 25, 'K')] },
],
},
];
// Seat legend component for the demo
const Legend = () => (
<div className="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 mt-4 p-4 rounded-md border bg-card text-card-foreground">
<div className="flex items-center gap-2"><div className="w-5 h-5 rounded border bg-card"></div><span className="text-sm">Available</span></div>
<div className="flex items-center gap-2"><div className="w-5 h-5 rounded border-primary bg-primary"></div><span className="text-sm">Selected</span></div>
<div className="flex items-center gap-2"><div className="w-5 h-5 rounded border bg-muted opacity-50"></div><span className="text-sm">Occupied</span></div>
</div>
);
const SeatSelectionDemo = () => {
const [selectedSeats, setSelectedSeats] = useState<string[]>(['H12']);
const occupiedSeats = useMemo(() => ['J17', 'J18', 'J19', 'C1', 'C2', 'C10', 'C11', 'G9'], []);
// Handles seat selection logic, allowing toggle
const handleSeatSelect = (seatId: string) => {
setSelectedSeats((prevSelected) =>
prevSelected.includes(seatId)
? prevSelected.filter((id) => id !== seatId)
: [...prevSelected, seatId]
);
};
// Calculates total price based on selected seats
const totalPrice = useMemo(() => {
return selectedSeats.reduce((total, seatId) => {
for (const category of seatLayoutData) {
if (category.rows.some(row => row.seats.some(seat => seat.id === seatId))) {
return total + category.price;
}
}
return total;
}, 0);
}, [selectedSeats]);
return (
<div className="w-full max-w-5xl mx-auto flex flex-col items-center py-8">
<SeatSelection
layout={seatLayoutData}
selectedSeats={selectedSeats}
occupiedSeats={occupiedSeats}
onSeatSelect={handleSeatSelect}
/>
<Legend />
<AnimatePresence>
{selectedSeats.length > 0 && (
<motion.div
className="mt-8 w-full max-w-md p-4 bg-card border rounded-lg shadow-lg"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.3 }}
>
<h3 className="text-lg font-semibold mb-2 text-foreground">Your Selection</h3>
<div className="flex flex-wrap gap-2 mb-4">
{selectedSeats.sort().map(seatId => (
<span key={seatId} className="bg-primary text-primary-foreground text-sm font-medium px-3 py-1 rounded-full">
{seatId}
</span>
))}
</div>
<div className="border-t pt-4 flex justify-between items-center">
<span className="text-md font-medium text-muted-foreground">Total Price:</span>
<span className="text-xl font-bold text-foreground">
{new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR' }).format(totalPrice)}
<span className="text-xs font-normal text-muted-foreground"> + GST</span>
</span>
</div>
<Button className="w-full mt-4">Proceed to Payment</Button>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default SeatSelectionDemo;