@zonetrix/viewer
v2.13.3
Published
Lightweight React component for rendering interactive seat maps
Downloads
227
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 (others' reservations) |
| unavailableSeats | string[] | No | Array of seat IDs/numbers to mark as unavailable |
| selectedSeats | string[] | No | Array of seat IDs for controlled selection mode |
| myReservedSeats | string[] | No | Array of seat IDs reserved by current user (shown as selected/blue) |
| 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 |
| minZoom | number | No | Minimum zoom level. If not set, defaults to 50% of fit-to-view scale for comfortable breathing room (default: undefined) |
| 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. Firebase Real-time Integration
The viewer includes built-in Firebase Realtime Database integration for instant seat state updates across all users.
Setup
npm install firebase @zonetrix/sharedInitialize Firebase in your app:
import { initializeFirebaseForViewer } from '@zonetrix/viewer';
// Initialize once at app startup
initializeFirebaseForViewer({
apiKey: "your-api-key",
authDomain: "your-project.firebaseapp.com",
databaseURL: "https://your-project.firebaseio.com",
projectId: "your-project",
});Basic Real-time Usage
import { useRealtimeSeatMap, SeatMapViewer } from '@zonetrix/viewer';
function BookingPage({ seatMapId }) {
const {
config,
otherReservedSeats, // Reserved by others → yellow
unavailableSeats,
loading,
error
} = useRealtimeSeatMap({ seatMapId });
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
return (
<SeatMapViewer
config={config}
reservedSeats={otherReservedSeats}
unavailableSeats={unavailableSeats}
onSeatSelect={handleSeatSelect}
/>
);
}User-Aware Reservations (Multi-User Booking)
When multiple users are booking simultaneously, pass userId to show each user's own reservations as "selected" while showing others' reservations as "reserved":
import { useRealtimeSeatMap, SeatMapViewer } from '@zonetrix/viewer';
function BookingPage({ seatMapId, userId }) {
const {
config,
myReservedSeats, // Seats I reserved → blue (selected)
otherReservedSeats, // Seats others reserved → yellow (reserved)
unavailableSeats,
loading
} = useRealtimeSeatMap({ seatMapId, userId });
if (loading) return <LoadingSpinner />;
return (
<SeatMapViewer
config={config}
myReservedSeats={myReservedSeats} // Show as selected (blue)
reservedSeats={otherReservedSeats} // Show as reserved (yellow)
unavailableSeats={unavailableSeats}
onSeatSelect={handleSeatSelect}
/>
);
}Firebase Data Structure
Seat states are stored at seat_states/{seatMapId}/{seatId}:
// Reserved seat (with user tracking)
{
state: "reserved",
userId: "user-123",
timestamp: 1704931200000
}
// Unavailable seat
{
state: "unavailable",
timestamp: 1704931200000
}
// Available seats: key doesn't exist (deleted)Booking App Integration
Your booking backend should update Firebase when users reserve seats:
import { getDatabase, ref, set, remove } from 'firebase/database';
const db = getDatabase();
// Reserve a seat for a user
async function reserveSeat(seatMapId, seatId, userId) {
await set(ref(db, `seat_states/${seatMapId}/${seatId}`), {
state: 'reserved',
userId: userId,
timestamp: Date.now()
});
}
// Release a seat (make available)
async function releaseSeat(seatMapId, seatId) {
await remove(ref(db, `seat_states/${seatMapId}/${seatId}`));
}
// Mark seat as unavailable (sold, blocked)
async function markUnavailable(seatMapId, seatId) {
await set(ref(db, `seat_states/${seatMapId}/${seatId}`), {
state: 'unavailable',
timestamp: Date.now()
});
}Hooks Reference
| Hook | Description |
|------|-------------|
| useFirebaseSeatStates | Subscribe to real-time seat states only |
| useFirebaseConfig | Load seat map config from Firebase |
| useRealtimeSeatMap | Combined hook (config + real-time states) |
9. 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)}
/>
);
}9. Custom Minimum Zoom
By default, users can zoom out to 50% of the "fit to screen" level, providing comfortable breathing room around all content. To customize this behavior:
import { SeatMapViewer } from '@zonetrix/viewer';
// Allow zooming out to 25% for better overview
function WideViewMap() {
return (
<SeatMapViewer
config={venueConfig}
minZoom={0.25}
maxZoom={5}
onSeatSelect={(seat) => handleSelection(seat)}
/>
);
}
// Extreme zoom out for large stadiums
function BirdEyeView() {
return (
<SeatMapViewer
config={largeStadiumConfig}
minZoom={0.1}
onSeatSelect={(seat) => handleSelection(seat)}
/>
);
}
// Restrict zoom out to fit-to-screen scale (legacy behavior)
function RestrictedZoom() {
return (
<SeatMapViewer
config={venueConfig}
minZoom={1.0} // Set explicitly to match fit scale
onSeatSelect={(seat) => handleSelection(seat)}
/>
);
}Note: Very low minZoom values (< 0.1) may impact rendering performance with large seat maps.
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
