@zonetrix/viewer
v2.6.0
Published
Lightweight React component for rendering interactive seat maps
Downloads
999
Maintainers
Readme
@zonetrix/viewer
Lightweight React component for rendering interactive seat maps in your booking applications, event ticketing systems, and venue management platforms.
Overview
@zonetrix/viewer is a production-ready React component that renders beautiful, interactive seat maps with support for selection, zooming, and real-time state updates. Perfect for theaters, stadiums, cinemas, and event venues.
Features
- 🎯 Read-only Display - Optimized for end-user viewing and selection
- 🖱️ Interactive Selection - Click seats to select/deselect with visual feedback
- 🔄 Real-time Updates - Support for dynamic seat state changes
- 🌐 Flexible Config Loading - Load from JSON files or API endpoints
- 🔍 Mouse Wheel Zoom - Smooth zoom with configurable limits
- 🎨 Customizable Colors - Override default colors to match your brand
- 🏢 Multi-floor Support - Filter and display seats by floor
- 👁️ Hidden Seats - Automatically filters out hidden seats
- 📱 Responsive - Works seamlessly across all screen sizes
- ⚡ Lightweight - Minimal dependencies
- 🔒 Type-safe - Full TypeScript support
Installation
npm install @zonetrix/viewer
# or
yarn add @zonetrix/viewer
# or
pnpm add @zonetrix/viewerPeer Dependencies
npm install react react-dom konva react-konvaExports
// Main component
import { SeatMapViewer } from '@zonetrix/viewer';
import type { SeatMapViewerProps } from '@zonetrix/viewer';
// Types
import type {
SeatState,
SeatShape,
SeatData,
SeatMapConfig,
ColorSettings,
FloorConfig
} from '@zonetrix/viewer';
// Hooks
import { useConfigFetcher } from '@zonetrix/viewer';
// Constants
import { DEFAULT_COLORS } from '@zonetrix/viewer';Quick Start
Basic Usage with JSON Config
import { SeatMapViewer } from '@zonetrix/viewer';
import type { SeatData, SeatMapConfig } from '@zonetrix/viewer';
import venueConfig from './venue-config.json';
function BookingApp() {
return (
<SeatMapViewer
config={venueConfig}
onSeatSelect={(seat) => console.log('Selected:', seat)}
onSeatDeselect={(seat) => console.log('Deselected:', seat)}
/>
);
}Load Config from API
import { SeatMapViewer } from '@zonetrix/viewer';
function BookingApp() {
return (
<SeatMapViewer
configUrl="https://api.example.com/venues/123/config"
onSeatSelect={(seat) => addToCart(seat)}
onSeatDeselect={(seat) => removeFromCart(seat)}
/>
);
}Props API
SeatMapViewerProps
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| config | SeatMapConfig | No* | Seat map configuration object |
| configUrl | string | No* | URL to fetch configuration from |
| floorId | string | No | Filter seats/stages by floor ID (controlled mode) |
| onFloorChange | (floorId: string) => void | No | Callback when floor changes |
| reservedSeats | string[] | No | Array of seat IDs/numbers to mark as reserved |
| unavailableSeats | string[] | No | Array of seat IDs/numbers to mark as unavailable |
| selectedSeats | string[] | No | Array of seat IDs for controlled selection mode |
| onSeatSelect | (seat: SeatData) => void | No | Callback when a seat is selected |
| onSeatDeselect | (seat: SeatData) => void | No | Callback when a seat is deselected |
| onSelectionChange | (seats: SeatData[]) => void | No | Callback when selection changes |
| colorOverrides | Partial<ColorSettings> | No | Custom colors for seat states |
| showTooltip | boolean | No | Show seat tooltips on hover with seat info, price, status (default: true) |
| zoomEnabled | boolean | No | Enable/disable zoom functionality (default: true) |
| className | string | No | Custom CSS class for the container |
| onConfigLoad | (config: SeatMapConfig) => void | No | Callback when config is loaded |
| onError | (error: Error) => void | No | Callback when an error occurs |
| showFloorSelector | boolean | No | Show/hide built-in floor selector (default: true when floors > 1) |
| floorSelectorPosition | string | No | Position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' |
| floorSelectorClassName | string | No | Custom CSS class for floor selector |
| showAllFloorsOption | boolean | No | Show "All" button in floor selector (default: true) |
| allFloorsLabel | string | No | Custom label for "All" button (default: 'All') |
| fitToView | boolean | No | Auto-fit content on load (default: true) |
| fitPadding | number | No | Padding around content when fitting (default: 40) |
| showZoomControls | boolean | No | Show zoom +/- buttons (default: true) |
| zoomControlsPosition | string | No | Position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' (default: 'bottom-right') |
| zoomControlsClassName | string | No | Custom CSS class for zoom controls |
| maxZoom | number | No | Maximum zoom level (default: 3) |
| zoomStep | number | No | Zoom increment per click (default: 0.25) |
*Note: Either config or configUrl must be provided.
Usage Examples
1. Booking System with Cart
import { useState } from 'react';
import { SeatMapViewer } from '@zonetrix/viewer';
import type { SeatData } from '@zonetrix/viewer';
function TheaterBooking() {
const [cart, setCart] = useState<SeatData[]>([]);
const handleSeatSelect = (seat: SeatData) => {
setCart((prev) => [...prev, seat]);
};
const handleSeatDeselect = (seat: SeatData) => {
setCart((prev) => prev.filter((s) => s.seatNumber !== seat.seatNumber));
};
const totalPrice = cart.reduce((sum, seat) => sum + (seat.price || 0), 0);
return (
<div>
<SeatMapViewer
configUrl="https://api.theater.com/venues/main-hall/config"
reservedSeats={['A-1', 'A-2', 'B-5']}
onSeatSelect={handleSeatSelect}
onSeatDeselect={handleSeatDeselect}
/>
<div className="cart">
<h3>Your Selection</h3>
<p>Seats: {cart.map(s => s.seatNumber).join(', ')}</p>
<p>Total: ${totalPrice.toFixed(2)}</p>
<button onClick={() => checkout(cart)}>Proceed to Checkout</button>
</div>
</div>
);
}2. Custom Colors (Brand Matching)
import { SeatMapViewer } from '@zonetrix/viewer';
function BrandedVenue() {
const customColors = {
seatAvailable: '#10b981', // Green
seatReserved: '#ef4444', // Red
seatSelected: '#3b82f6', // Blue
canvasBackground: '#ffffff', // White
};
return (
<SeatMapViewer
config={venueConfig}
colorOverrides={customColors}
onSeatSelect={(seat) => console.log('Selected:', seat)}
/>
);
}3. Real-time Updates from API
import { useState, useEffect } from 'react';
import { SeatMapViewer } from '@zonetrix/viewer';
function LiveEventSeating() {
const [reservedSeats, setReservedSeats] = useState<string[]>([]);
// Poll API every 5 seconds for reserved seats
useEffect(() => {
const interval = setInterval(async () => {
const response = await fetch('/api/venue/123/reserved-seats');
const data = await response.json();
setReservedSeats(data.reserved);
}, 5000);
return () => clearInterval(interval);
}, []);
return (
<SeatMapViewer
config={venueConfig}
reservedSeats={reservedSeats}
onSeatSelect={(seat) => bookSeat(seat)}
/>
);
}4. Tracking Selection Changes
import { SeatMapViewer } from '@zonetrix/viewer';
function SelectionTracker() {
const handleSelectionChange = (selectedSeats) => {
console.log('Current selection:', selectedSeats);
// Update analytics
analytics.track('Seats Selected', {
count: selectedSeats.length,
seats: selectedSeats.map(s => s.seatNumber),
});
};
return (
<SeatMapViewer
config={venueConfig}
onSelectionChange={handleSelectionChange}
/>
);
}5. Customize Zoom Controls
import { SeatMapViewer } from '@zonetrix/viewer';
function CustomZoom() {
return (
<SeatMapViewer
config={venueConfig}
// Zoom controls appear in bottom-right by default
zoomControlsPosition="bottom-left"
maxZoom={5} // Allow up to 5x zoom
zoomStep={0.5} // Larger zoom increments
onSeatSelect={(seat) => handleSelection(seat)}
/>
);
}
// Hide zoom controls entirely
function NoZoomControls() {
return (
<SeatMapViewer
config={venueConfig}
showZoomControls={false}
/>
);
}6. Multi-floor Venue (Built-in Floor Selector)
The viewer includes a built-in floor selector that automatically appears when your config has multiple floors.
import { SeatMapViewer } from '@zonetrix/viewer';
function MultiFloorVenue() {
return (
<SeatMapViewer
config={venueConfig}
onSeatSelect={(seat) => handleSelection(seat)}
// Floor selector auto-shows when config.floors.length > 1
// Customize position and labels:
floorSelectorPosition="top-right"
allFloorsLabel="All Floors"
/>
);
}Custom Floor Selector (Controlled Mode)
For full control over the floor selector UI, use controlled mode:
import { useState } from 'react';
import { SeatMapViewer } from '@zonetrix/viewer';
function CustomFloorSelector() {
const [currentFloor, setCurrentFloor] = useState<string | null>(null);
return (
<div>
{/* Your custom floor selector */}
<div className="floor-tabs">
<button onClick={() => setCurrentFloor(null)}>All</button>
{venueConfig.floors?.map((floor) => (
<button
key={floor.id}
onClick={() => setCurrentFloor(floor.id)}
className={currentFloor === floor.id ? 'active' : ''}
>
{floor.name}
</button>
))}
</div>
<SeatMapViewer
config={venueConfig}
showFloorSelector={false} // Hide built-in selector
floorId={currentFloor || undefined}
onFloorChange={setCurrentFloor}
onSeatSelect={(seat) => handleSelection(seat)}
/>
</div>
);
}7. Controlled Selection Mode
For complete control over seat selection state (e.g., syncing with external state, URL params, or database):
import { useState } from 'react';
import { SeatMapViewer } from '@zonetrix/viewer';
import type { SeatData } from '@zonetrix/viewer';
function ControlledSelection() {
const [selectedSeatIds, setSelectedSeatIds] = useState<string[]>([]);
const handleSeatSelect = (seat: SeatData) => {
setSelectedSeatIds(prev => [...prev, seat.id]);
};
const handleSeatDeselect = (seat: SeatData) => {
setSelectedSeatIds(prev => prev.filter(id => id !== seat.id));
};
return (
<SeatMapViewer
config={venueConfig}
selectedSeats={selectedSeatIds} // Controlled mode
onSeatSelect={handleSeatSelect}
onSeatDeselect={handleSeatDeselect}
/>
);
}Use cases for controlled selection mode:
- Syncing selection with URL query parameters
- Persisting selection in Redux/Zustand store
- Loading pre-selected seats from database
- Implementing undo/redo functionality
- Syncing selection across multiple components
- Integrating with shopping cart state management
Example with cart integration:
import { useState } from 'react';
import { SeatMapViewer } from '@zonetrix/viewer';
import type { SeatData } from '@zonetrix/viewer';
function CartIntegration() {
const [cartItems, setCartItems] = useState<SeatData[]>([]);
// Derive selected IDs from cart
const selectedSeatIds = cartItems.map(seat => seat.id);
const handleSeatSelect = (seat: SeatData) => {
setCartItems(prev => [...prev, seat]);
};
const handleSeatDeselect = (seat: SeatData) => {
setCartItems(prev => prev.filter(item => item.id !== seat.id));
};
const totalPrice = cartItems.reduce((sum, seat) => sum + (seat.price || 0), 0);
return (
<div>
<SeatMapViewer
config={venueConfig}
selectedSeats={selectedSeatIds}
onSeatSelect={handleSeatSelect}
onSeatDeselect={handleSeatDeselect}
/>
<div className="cart">
<h3>Shopping Cart</h3>
<p>Selected: {cartItems.length} seat(s)</p>
<p>Total: ${totalPrice.toFixed(2)}</p>
</div>
</div>
);
}8. Error Handling
import { SeatMapViewer } from '@zonetrix/viewer';
function RobustViewer() {
const handleConfigLoad = (config) => {
console.log('Config loaded:', config.metadata.name);
console.log('Total seats:', config.seats.length);
};
const handleError = (error) => {
console.error('Failed to load seat map:', error.message);
// Show error notification to user
};
return (
<SeatMapViewer
configUrl="https://api.example.com/venues/123/config"
onConfigLoad={handleConfigLoad}
onError={handleError}
onSeatSelect={(seat) => console.log('Selected:', seat)}
/>
);
}Configuration Format
The viewer accepts a SeatMapConfig object. You can create these configurations using our creator studio or build them programmatically.
Example Configuration
{
"version": "1.0.0",
"metadata": {
"name": "Main Auditorium",
"venue": "Grand Theater",
"capacity": 500,
"createdAt": "2025-01-01T00:00:00Z",
"updatedAt": "2025-01-01T00:00:00Z"
},
"canvas": {
"width": 1200,
"height": 800,
"backgroundColor": "#1a1a1a"
},
"colors": {
"canvasBackground": "#1a1a1a",
"stageColor": "#808080",
"seatAvailable": "#2C2B30",
"seatReserved": "#FCEA00",
"seatSelected": "#3A7DE5",
"seatUnavailable": "#6b7280",
"gridLines": "#404040",
"currency": "USD"
},
"seats": [
{
"id": "seat_001",
"position": { "x": 100, "y": 100 },
"shape": "rounded-square",
"state": "available",
"sectionName": "Orchestra",
"rowLabel": "A",
"columnLabel": "1",
"seatNumber": "A-1",
"price": 50.00
}
],
"sections": [],
"stages": [],
"floors": [
{ "id": "floor_1", "name": "Ground Floor", "order": 0 },
{ "id": "floor_2", "name": "First Floor", "order": 1 }
]
}TypeScript Types
SeatData
interface SeatData {
id: string;
state: SeatState;
shape?: SeatShape;
sectionName?: string;
rowLabel?: string;
columnLabel?: string;
price?: number;
seatNumber?: string;
floorId?: string;
}SeatState
type SeatState = 'available' | 'reserved' | 'selected' | 'unavailable' | 'hidden';Note: Hidden seats are automatically filtered out from the viewer.
SeatShape
type SeatShape = 'circle' | 'square' | 'rounded-square';ColorSettings
interface ColorSettings {
canvasBackground: string;
stageColor: string;
seatAvailable: string;
seatReserved: string;
seatSelected: string;
seatUnavailable: string;
seatHidden: string;
gridLines: string;
currency: string;
}FloorConfig
interface FloorConfig {
id: string; // Unique identifier (e.g., "floor_1")
name: string; // Display name (e.g., "Ground Floor")
order: number; // Sort order (0 = first)
color?: string; // Optional floor color
}Seat States Explained
| State | Description | User Can Select? | Visual |
|-------|-------------|------------------|--------|
| available | Seat is free and can be selected | ✅ Yes | Default color |
| reserved | Seat is booked by another user | ❌ No | Yellow/Warning color |
| selected | Seat is selected by current user | ✅ Yes (to deselect) | Primary/Blue color |
| unavailable | Seat is blocked (maintenance, etc.) | ❌ No | Gray color |
| hidden | Seat exists but is not displayed | ❌ No | Not rendered |
Events & Callbacks
onSeatSelect
Called when a user selects an available seat.
const handleSelect = (seat: SeatData) => {
console.log('Seat selected:', seat.seatNumber);
console.log('Price:', seat.price);
console.log('Section:', seat.sectionName);
};onSeatDeselect
Called when a user deselects a previously selected seat.
const handleDeselect = (seat: SeatData) => {
console.log('Seat deselected:', seat.seatNumber);
};onSelectionChange
Called whenever the selection changes (includes all selected seats).
const handleSelectionChange = (selectedSeats: SeatData[]) => {
console.log('Total selected:', selectedSeats.length);
const total = selectedSeats.reduce((sum, s) => sum + (s.price || 0), 0);
console.log('Total price:', total);
};Styling
The component uses inline styles generated from the configuration. To customize the container, wrap it in a styled div:
<div style={{ width: '100%', height: '600px', border: '1px solid #ccc' }}>
<SeatMapViewer config={venueConfig} />
</div>Performance Tips
- Large Venues (500+ seats): Consider splitting into sections
- API Loading: Show a loading spinner while
configUrlis being fetched - Mobile: Disable zoom on mobile devices for better UX
- Memoization: Wrap callbacks with
useCallbackto prevent unnecessary re-renders
Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
Common Issues
Configuration not loading from API
Ensure your API returns valid JSON and has proper CORS headers:
// Server-side (Express example)
res.setHeader('Access-Control-Allow-Origin', '*');
res.json(seatMapConfig);Seats not responding to clicks
Make sure seat states are correctly set. Only available and selected seats can be clicked.
Canvas size issues
The canvas size is determined by the canvas.width and canvas.height in your configuration. Adjust these values or wrap the component in a responsive container.
Related Packages
- @zonetrix/shared - Shared types and utilities
Examples Repository
Check out our examples repository for more use cases:
- Booking system integration
- API polling for real-time updates
- Custom theming
- Mobile-optimized layouts
Contributing
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
License
MIT
Author
Fahad Khan (@fahadkhan1740)
Links
Support
For questions and support:
- Open an issue on GitHub
- Email: [email protected]
Made with ❤️ by Fahad Khan
