LaviKatiyar

Seat Selection

Component 62

About

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.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Seat Selection to your project and fits it to your theme.

Code

How Seat Selection is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

import 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;

Dependencies

More by LaviKatiyar

View all
  • Hero Section

    Hero Section

    801
  • Cards

    Cards

    564
  • Service Card

    Service Card

    546
  • Card

    Card

    505
  • Alert Toast

    Alert Toast

    481
  • Bento Grid

    Bento Grid

    381
  • Freelancer Profile Card

    Freelancer Profile Card

    358
  • Referral Card

    Referral Card

    289