@zonetrix/shared 2.4.2 → 2.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +225 -1
- package/dist/index.d.ts +225 -1
- package/dist/index.js +343 -0
- package/dist/index.mjs +336 -0
- package/package.json +5 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Database } from 'firebase/database';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Centralized type definitions for the Seat Map Studio packages
|
|
3
5
|
*/
|
|
@@ -18,6 +20,7 @@ interface SeatData {
|
|
|
18
20
|
price?: number;
|
|
19
21
|
seatNumber?: string;
|
|
20
22
|
floorId?: string;
|
|
23
|
+
size?: number;
|
|
21
24
|
}
|
|
22
25
|
/**
|
|
23
26
|
* Section configuration for creating seat groups
|
|
@@ -129,6 +132,7 @@ interface SerializedSeat {
|
|
|
129
132
|
seatNumber?: string;
|
|
130
133
|
price?: number;
|
|
131
134
|
floorId?: string;
|
|
135
|
+
size?: number;
|
|
132
136
|
}
|
|
133
137
|
/**
|
|
134
138
|
* Serialized section for JSON export/import
|
|
@@ -510,4 +514,224 @@ declare function deriveSeatArraysFromStates(states: FirebaseSeatStates, currentU
|
|
|
510
514
|
*/
|
|
511
515
|
declare function createIndexUpdates(seatMapId: string, eventId: number, subEventId: number): Record<string, boolean>;
|
|
512
516
|
|
|
513
|
-
|
|
517
|
+
/**
|
|
518
|
+
* Firebase client singleton for the viewer package
|
|
519
|
+
* This allows the host application to initialize Firebase once
|
|
520
|
+
* and the viewer to use the same instance
|
|
521
|
+
*/
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Initialize the Firebase database instance for the viewer
|
|
525
|
+
* This should be called by the host application after Firebase is initialized
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```tsx
|
|
529
|
+
* import { initializeApp } from 'firebase/app';
|
|
530
|
+
* import { getDatabase } from 'firebase/database';
|
|
531
|
+
* import { initializeFirebaseForViewer } from '@zonetrix/viewer';
|
|
532
|
+
*
|
|
533
|
+
* const app = initializeApp(firebaseConfig);
|
|
534
|
+
* const db = getDatabase(app);
|
|
535
|
+
* initializeFirebaseForViewer(db);
|
|
536
|
+
* ```
|
|
537
|
+
*/
|
|
538
|
+
declare function initializeFirebaseForViewer(database: Database): void;
|
|
539
|
+
/**
|
|
540
|
+
* Get the Firebase database instance
|
|
541
|
+
* @throws Error if Firebase hasn't been initialized
|
|
542
|
+
*/
|
|
543
|
+
declare function getFirebaseDatabase(): Database;
|
|
544
|
+
/**
|
|
545
|
+
* Check if Firebase has been initialized for the viewer
|
|
546
|
+
*/
|
|
547
|
+
declare function isFirebaseInitialized(): boolean;
|
|
548
|
+
/**
|
|
549
|
+
* Clear the Firebase database instance (useful for testing)
|
|
550
|
+
*/
|
|
551
|
+
declare function clearFirebaseInstance(): void;
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Hook for real-time seat state updates from Firebase
|
|
555
|
+
* Uses useSyncExternalStore for stable snapshot handling
|
|
556
|
+
* Supports user-aware seat state derivation
|
|
557
|
+
*/
|
|
558
|
+
|
|
559
|
+
interface UseFirebaseSeatStatesOptions {
|
|
560
|
+
/** The seat map ID to subscribe to */
|
|
561
|
+
seatMapId: string | null;
|
|
562
|
+
/** Current user ID for user-aware state derivation */
|
|
563
|
+
currentUserId?: string;
|
|
564
|
+
/** Whether the subscription is enabled (default: true) */
|
|
565
|
+
enabled?: boolean;
|
|
566
|
+
/** Callback when states change */
|
|
567
|
+
onStateChange?: (states: FirebaseSeatStates) => void;
|
|
568
|
+
/** Callback on error */
|
|
569
|
+
onError?: (error: Error) => void;
|
|
570
|
+
}
|
|
571
|
+
interface UseFirebaseSeatStatesResult {
|
|
572
|
+
/** Current seat states map */
|
|
573
|
+
states: FirebaseSeatStates | null;
|
|
574
|
+
/** Whether initial load is in progress */
|
|
575
|
+
loading: boolean;
|
|
576
|
+
/** Any error that occurred */
|
|
577
|
+
error: Error | null;
|
|
578
|
+
/** Timestamp of last update */
|
|
579
|
+
lastUpdated: number | null;
|
|
580
|
+
/** Seats reserved by current user (show as selected) */
|
|
581
|
+
myReservedSeats: string[];
|
|
582
|
+
/** Seats reserved by other users (show as reserved) */
|
|
583
|
+
otherReservedSeats: string[];
|
|
584
|
+
/** Seats unavailable for everyone */
|
|
585
|
+
unavailableSeats: string[];
|
|
586
|
+
/** @deprecated Use otherReservedSeats instead */
|
|
587
|
+
reservedSeats: string[];
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Subscribe to real-time seat state updates from Firebase
|
|
591
|
+
* Uses useSyncExternalStore for stable snapshot handling
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* ```tsx
|
|
595
|
+
* const { myReservedSeats, otherReservedSeats, unavailableSeats, loading } = useFirebaseSeatStates({
|
|
596
|
+
* seatMapId: '123',
|
|
597
|
+
* currentUserId: 'user-abc',
|
|
598
|
+
* });
|
|
599
|
+
*
|
|
600
|
+
* return (
|
|
601
|
+
* <SeatMapViewer
|
|
602
|
+
* config={config}
|
|
603
|
+
* myReservedSeats={myReservedSeats}
|
|
604
|
+
* reservedSeats={otherReservedSeats}
|
|
605
|
+
* unavailableSeats={unavailableSeats}
|
|
606
|
+
* />
|
|
607
|
+
* );
|
|
608
|
+
* ```
|
|
609
|
+
*/
|
|
610
|
+
declare function useFirebaseSeatStates(options: UseFirebaseSeatStatesOptions): UseFirebaseSeatStatesResult;
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Hook for loading seat map configuration from Firebase
|
|
614
|
+
* Optionally subscribes to design changes
|
|
615
|
+
*/
|
|
616
|
+
|
|
617
|
+
interface UseFirebaseConfigOptions {
|
|
618
|
+
/** The seat map ID to load */
|
|
619
|
+
seatMapId: string | null;
|
|
620
|
+
/** Whether loading is enabled (default: true) */
|
|
621
|
+
enabled?: boolean;
|
|
622
|
+
/** Subscribe to design changes in real-time (default: false) */
|
|
623
|
+
subscribeToChanges?: boolean;
|
|
624
|
+
/** Callback when config loads or changes */
|
|
625
|
+
onConfigLoad?: (config: SeatMapConfig) => void;
|
|
626
|
+
/** Callback on error */
|
|
627
|
+
onError?: (error: Error) => void;
|
|
628
|
+
}
|
|
629
|
+
interface UseFirebaseConfigResult {
|
|
630
|
+
/** The loaded configuration */
|
|
631
|
+
config: SeatMapConfig | null;
|
|
632
|
+
/** Whether loading is in progress */
|
|
633
|
+
loading: boolean;
|
|
634
|
+
/** Any error that occurred */
|
|
635
|
+
error: Error | null;
|
|
636
|
+
/** Manually refetch the config */
|
|
637
|
+
refetch: () => Promise<void>;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Load seat map configuration from Firebase
|
|
641
|
+
*
|
|
642
|
+
* @example
|
|
643
|
+
* ```tsx
|
|
644
|
+
* // One-time load
|
|
645
|
+
* const { config, loading, error } = useFirebaseConfig({
|
|
646
|
+
* seatMapId: '123',
|
|
647
|
+
* });
|
|
648
|
+
*
|
|
649
|
+
* // With real-time design updates (for admin/editor preview)
|
|
650
|
+
* const { config } = useFirebaseConfig({
|
|
651
|
+
* seatMapId: '123',
|
|
652
|
+
* subscribeToChanges: true,
|
|
653
|
+
* });
|
|
654
|
+
* ```
|
|
655
|
+
*/
|
|
656
|
+
declare function useFirebaseConfig(options: UseFirebaseConfigOptions): UseFirebaseConfigResult;
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Combined hook for loading seat map config and subscribing to real-time state updates
|
|
660
|
+
* This is the recommended hook for most use cases
|
|
661
|
+
* Supports user-aware seat state derivation
|
|
662
|
+
*/
|
|
663
|
+
|
|
664
|
+
interface UseRealtimeSeatMapOptions {
|
|
665
|
+
/** The seat map ID to load and subscribe to */
|
|
666
|
+
seatMapId: string | null;
|
|
667
|
+
/** Current user ID for user-aware state derivation */
|
|
668
|
+
userId?: string;
|
|
669
|
+
/** Whether the hook is enabled (default: true) */
|
|
670
|
+
enabled?: boolean;
|
|
671
|
+
/** Subscribe to design changes in real-time (default: false) */
|
|
672
|
+
subscribeToDesignChanges?: boolean;
|
|
673
|
+
/** Callback when config loads */
|
|
674
|
+
onConfigLoad?: (config: SeatMapConfig) => void;
|
|
675
|
+
/** Callback when seat states change */
|
|
676
|
+
onStateChange?: (states: FirebaseSeatStates) => void;
|
|
677
|
+
/** Callback on any error */
|
|
678
|
+
onError?: (error: Error) => void;
|
|
679
|
+
}
|
|
680
|
+
interface UseRealtimeSeatMapResult {
|
|
681
|
+
/** The seat map configuration */
|
|
682
|
+
config: SeatMapConfig | null;
|
|
683
|
+
/** Whether initial loading is in progress */
|
|
684
|
+
loading: boolean;
|
|
685
|
+
/** Any error that occurred */
|
|
686
|
+
error: Error | null;
|
|
687
|
+
/** Seats reserved by current user (show as selected) */
|
|
688
|
+
myReservedSeats: string[];
|
|
689
|
+
/** Seats reserved by other users (show as reserved) */
|
|
690
|
+
otherReservedSeats: string[];
|
|
691
|
+
/** Seats unavailable for everyone */
|
|
692
|
+
unavailableSeats: string[];
|
|
693
|
+
/** @deprecated Use otherReservedSeats instead */
|
|
694
|
+
reservedSeats: string[];
|
|
695
|
+
/** Raw seat states map */
|
|
696
|
+
seatStates: FirebaseSeatStates | null;
|
|
697
|
+
/** Timestamp of last state update */
|
|
698
|
+
lastUpdated: number | null;
|
|
699
|
+
/** Manually refetch the config */
|
|
700
|
+
refetch: () => Promise<void>;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Combined hook for loading config and subscribing to real-time seat states
|
|
704
|
+
*
|
|
705
|
+
* @example
|
|
706
|
+
* ```tsx
|
|
707
|
+
* import { useRealtimeSeatMap, SeatMapViewer } from '@zonetrix/viewer';
|
|
708
|
+
*
|
|
709
|
+
* function BookingPage({ seatMapId, userId }) {
|
|
710
|
+
* const {
|
|
711
|
+
* config,
|
|
712
|
+
* myReservedSeats,
|
|
713
|
+
* otherReservedSeats,
|
|
714
|
+
* unavailableSeats,
|
|
715
|
+
* loading,
|
|
716
|
+
* error
|
|
717
|
+
* } = useRealtimeSeatMap({ seatMapId, userId });
|
|
718
|
+
*
|
|
719
|
+
* if (loading) return <LoadingSpinner />;
|
|
720
|
+
* if (error) return <ErrorMessage error={error} />;
|
|
721
|
+
* if (!config) return null;
|
|
722
|
+
*
|
|
723
|
+
* return (
|
|
724
|
+
* <SeatMapViewer
|
|
725
|
+
* config={config}
|
|
726
|
+
* myReservedSeats={myReservedSeats}
|
|
727
|
+
* reservedSeats={otherReservedSeats}
|
|
728
|
+
* unavailableSeats={unavailableSeats}
|
|
729
|
+
* onSeatSelect={handleSeatSelect}
|
|
730
|
+
* />
|
|
731
|
+
* );
|
|
732
|
+
* }
|
|
733
|
+
* ```
|
|
734
|
+
*/
|
|
735
|
+
declare function useRealtimeSeatMap(options: UseRealtimeSeatMapOptions): UseRealtimeSeatMapResult;
|
|
736
|
+
|
|
737
|
+
export { type AlignmentGuide, type BookingSelection, type CanvasConfig, type CanvasState, type ColorSettings, DEFAULT_COLORS, type EditorMode, type FirebaseCanvasConfig, type FirebaseConfigResult, type FirebaseHookOptions, type FirebaseIndexEntry, FirebasePaths, type FirebasePosition, type FirebaseSeat, type FirebaseSeatMap, type FirebaseSeatMapConfig, type FirebaseSeatMapMeta, type FirebaseSeatState, type FirebaseSeatStateEntry, type FirebaseSeatStateValue, type FirebaseSeatStates, type FirebaseSeatStatesResult, type FirebaseStage, type ObjectConfig, type ObjectType, type RowPricing, type SeatData, type SeatMapConfig, type SeatMapMetadata, type SeatShape, type SeatState, type SectionConfig, type SerializedSeat, type SerializedSection, type SerializedStage, type StageConfig, type Tool, type UseFirebaseConfigOptions, type UseFirebaseConfigResult, type UseFirebaseSeatStatesOptions, type UseFirebaseSeatStatesResult, type UseRealtimeSeatMapOptions, type UseRealtimeSeatMapResult, type ValidationResult, applySeatStateOverrides, calculateAvailableSeats, calculateCapacity, calculateSeatPrice, clearFirebaseInstance, cloneConfig, createDefaultConfig, createIndexUpdates, decodeSeatId, deriveSeatArraysFromStates, downloadConfigAsFile, encodeSeatId, exportConfigAsJSON, extractSeatStates, formatDate, fromFirebaseSeatMap, fromFirebaseState, generateId, getFirebaseDatabase, getSelectedSeats, importConfigFromJSON, initializeFirebaseForViewer, isFirebaseInitialized, toFirebaseSeatMap, toFirebaseState, updateConfigTimestamp, useFirebaseConfig, useFirebaseSeatStates, useRealtimeSeatMap, validateSeatMapConfig };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Database } from 'firebase/database';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Centralized type definitions for the Seat Map Studio packages
|
|
3
5
|
*/
|
|
@@ -18,6 +20,7 @@ interface SeatData {
|
|
|
18
20
|
price?: number;
|
|
19
21
|
seatNumber?: string;
|
|
20
22
|
floorId?: string;
|
|
23
|
+
size?: number;
|
|
21
24
|
}
|
|
22
25
|
/**
|
|
23
26
|
* Section configuration for creating seat groups
|
|
@@ -129,6 +132,7 @@ interface SerializedSeat {
|
|
|
129
132
|
seatNumber?: string;
|
|
130
133
|
price?: number;
|
|
131
134
|
floorId?: string;
|
|
135
|
+
size?: number;
|
|
132
136
|
}
|
|
133
137
|
/**
|
|
134
138
|
* Serialized section for JSON export/import
|
|
@@ -510,4 +514,224 @@ declare function deriveSeatArraysFromStates(states: FirebaseSeatStates, currentU
|
|
|
510
514
|
*/
|
|
511
515
|
declare function createIndexUpdates(seatMapId: string, eventId: number, subEventId: number): Record<string, boolean>;
|
|
512
516
|
|
|
513
|
-
|
|
517
|
+
/**
|
|
518
|
+
* Firebase client singleton for the viewer package
|
|
519
|
+
* This allows the host application to initialize Firebase once
|
|
520
|
+
* and the viewer to use the same instance
|
|
521
|
+
*/
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Initialize the Firebase database instance for the viewer
|
|
525
|
+
* This should be called by the host application after Firebase is initialized
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```tsx
|
|
529
|
+
* import { initializeApp } from 'firebase/app';
|
|
530
|
+
* import { getDatabase } from 'firebase/database';
|
|
531
|
+
* import { initializeFirebaseForViewer } from '@zonetrix/viewer';
|
|
532
|
+
*
|
|
533
|
+
* const app = initializeApp(firebaseConfig);
|
|
534
|
+
* const db = getDatabase(app);
|
|
535
|
+
* initializeFirebaseForViewer(db);
|
|
536
|
+
* ```
|
|
537
|
+
*/
|
|
538
|
+
declare function initializeFirebaseForViewer(database: Database): void;
|
|
539
|
+
/**
|
|
540
|
+
* Get the Firebase database instance
|
|
541
|
+
* @throws Error if Firebase hasn't been initialized
|
|
542
|
+
*/
|
|
543
|
+
declare function getFirebaseDatabase(): Database;
|
|
544
|
+
/**
|
|
545
|
+
* Check if Firebase has been initialized for the viewer
|
|
546
|
+
*/
|
|
547
|
+
declare function isFirebaseInitialized(): boolean;
|
|
548
|
+
/**
|
|
549
|
+
* Clear the Firebase database instance (useful for testing)
|
|
550
|
+
*/
|
|
551
|
+
declare function clearFirebaseInstance(): void;
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Hook for real-time seat state updates from Firebase
|
|
555
|
+
* Uses useSyncExternalStore for stable snapshot handling
|
|
556
|
+
* Supports user-aware seat state derivation
|
|
557
|
+
*/
|
|
558
|
+
|
|
559
|
+
interface UseFirebaseSeatStatesOptions {
|
|
560
|
+
/** The seat map ID to subscribe to */
|
|
561
|
+
seatMapId: string | null;
|
|
562
|
+
/** Current user ID for user-aware state derivation */
|
|
563
|
+
currentUserId?: string;
|
|
564
|
+
/** Whether the subscription is enabled (default: true) */
|
|
565
|
+
enabled?: boolean;
|
|
566
|
+
/** Callback when states change */
|
|
567
|
+
onStateChange?: (states: FirebaseSeatStates) => void;
|
|
568
|
+
/** Callback on error */
|
|
569
|
+
onError?: (error: Error) => void;
|
|
570
|
+
}
|
|
571
|
+
interface UseFirebaseSeatStatesResult {
|
|
572
|
+
/** Current seat states map */
|
|
573
|
+
states: FirebaseSeatStates | null;
|
|
574
|
+
/** Whether initial load is in progress */
|
|
575
|
+
loading: boolean;
|
|
576
|
+
/** Any error that occurred */
|
|
577
|
+
error: Error | null;
|
|
578
|
+
/** Timestamp of last update */
|
|
579
|
+
lastUpdated: number | null;
|
|
580
|
+
/** Seats reserved by current user (show as selected) */
|
|
581
|
+
myReservedSeats: string[];
|
|
582
|
+
/** Seats reserved by other users (show as reserved) */
|
|
583
|
+
otherReservedSeats: string[];
|
|
584
|
+
/** Seats unavailable for everyone */
|
|
585
|
+
unavailableSeats: string[];
|
|
586
|
+
/** @deprecated Use otherReservedSeats instead */
|
|
587
|
+
reservedSeats: string[];
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Subscribe to real-time seat state updates from Firebase
|
|
591
|
+
* Uses useSyncExternalStore for stable snapshot handling
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* ```tsx
|
|
595
|
+
* const { myReservedSeats, otherReservedSeats, unavailableSeats, loading } = useFirebaseSeatStates({
|
|
596
|
+
* seatMapId: '123',
|
|
597
|
+
* currentUserId: 'user-abc',
|
|
598
|
+
* });
|
|
599
|
+
*
|
|
600
|
+
* return (
|
|
601
|
+
* <SeatMapViewer
|
|
602
|
+
* config={config}
|
|
603
|
+
* myReservedSeats={myReservedSeats}
|
|
604
|
+
* reservedSeats={otherReservedSeats}
|
|
605
|
+
* unavailableSeats={unavailableSeats}
|
|
606
|
+
* />
|
|
607
|
+
* );
|
|
608
|
+
* ```
|
|
609
|
+
*/
|
|
610
|
+
declare function useFirebaseSeatStates(options: UseFirebaseSeatStatesOptions): UseFirebaseSeatStatesResult;
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Hook for loading seat map configuration from Firebase
|
|
614
|
+
* Optionally subscribes to design changes
|
|
615
|
+
*/
|
|
616
|
+
|
|
617
|
+
interface UseFirebaseConfigOptions {
|
|
618
|
+
/** The seat map ID to load */
|
|
619
|
+
seatMapId: string | null;
|
|
620
|
+
/** Whether loading is enabled (default: true) */
|
|
621
|
+
enabled?: boolean;
|
|
622
|
+
/** Subscribe to design changes in real-time (default: false) */
|
|
623
|
+
subscribeToChanges?: boolean;
|
|
624
|
+
/** Callback when config loads or changes */
|
|
625
|
+
onConfigLoad?: (config: SeatMapConfig) => void;
|
|
626
|
+
/** Callback on error */
|
|
627
|
+
onError?: (error: Error) => void;
|
|
628
|
+
}
|
|
629
|
+
interface UseFirebaseConfigResult {
|
|
630
|
+
/** The loaded configuration */
|
|
631
|
+
config: SeatMapConfig | null;
|
|
632
|
+
/** Whether loading is in progress */
|
|
633
|
+
loading: boolean;
|
|
634
|
+
/** Any error that occurred */
|
|
635
|
+
error: Error | null;
|
|
636
|
+
/** Manually refetch the config */
|
|
637
|
+
refetch: () => Promise<void>;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Load seat map configuration from Firebase
|
|
641
|
+
*
|
|
642
|
+
* @example
|
|
643
|
+
* ```tsx
|
|
644
|
+
* // One-time load
|
|
645
|
+
* const { config, loading, error } = useFirebaseConfig({
|
|
646
|
+
* seatMapId: '123',
|
|
647
|
+
* });
|
|
648
|
+
*
|
|
649
|
+
* // With real-time design updates (for admin/editor preview)
|
|
650
|
+
* const { config } = useFirebaseConfig({
|
|
651
|
+
* seatMapId: '123',
|
|
652
|
+
* subscribeToChanges: true,
|
|
653
|
+
* });
|
|
654
|
+
* ```
|
|
655
|
+
*/
|
|
656
|
+
declare function useFirebaseConfig(options: UseFirebaseConfigOptions): UseFirebaseConfigResult;
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Combined hook for loading seat map config and subscribing to real-time state updates
|
|
660
|
+
* This is the recommended hook for most use cases
|
|
661
|
+
* Supports user-aware seat state derivation
|
|
662
|
+
*/
|
|
663
|
+
|
|
664
|
+
interface UseRealtimeSeatMapOptions {
|
|
665
|
+
/** The seat map ID to load and subscribe to */
|
|
666
|
+
seatMapId: string | null;
|
|
667
|
+
/** Current user ID for user-aware state derivation */
|
|
668
|
+
userId?: string;
|
|
669
|
+
/** Whether the hook is enabled (default: true) */
|
|
670
|
+
enabled?: boolean;
|
|
671
|
+
/** Subscribe to design changes in real-time (default: false) */
|
|
672
|
+
subscribeToDesignChanges?: boolean;
|
|
673
|
+
/** Callback when config loads */
|
|
674
|
+
onConfigLoad?: (config: SeatMapConfig) => void;
|
|
675
|
+
/** Callback when seat states change */
|
|
676
|
+
onStateChange?: (states: FirebaseSeatStates) => void;
|
|
677
|
+
/** Callback on any error */
|
|
678
|
+
onError?: (error: Error) => void;
|
|
679
|
+
}
|
|
680
|
+
interface UseRealtimeSeatMapResult {
|
|
681
|
+
/** The seat map configuration */
|
|
682
|
+
config: SeatMapConfig | null;
|
|
683
|
+
/** Whether initial loading is in progress */
|
|
684
|
+
loading: boolean;
|
|
685
|
+
/** Any error that occurred */
|
|
686
|
+
error: Error | null;
|
|
687
|
+
/** Seats reserved by current user (show as selected) */
|
|
688
|
+
myReservedSeats: string[];
|
|
689
|
+
/** Seats reserved by other users (show as reserved) */
|
|
690
|
+
otherReservedSeats: string[];
|
|
691
|
+
/** Seats unavailable for everyone */
|
|
692
|
+
unavailableSeats: string[];
|
|
693
|
+
/** @deprecated Use otherReservedSeats instead */
|
|
694
|
+
reservedSeats: string[];
|
|
695
|
+
/** Raw seat states map */
|
|
696
|
+
seatStates: FirebaseSeatStates | null;
|
|
697
|
+
/** Timestamp of last state update */
|
|
698
|
+
lastUpdated: number | null;
|
|
699
|
+
/** Manually refetch the config */
|
|
700
|
+
refetch: () => Promise<void>;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Combined hook for loading config and subscribing to real-time seat states
|
|
704
|
+
*
|
|
705
|
+
* @example
|
|
706
|
+
* ```tsx
|
|
707
|
+
* import { useRealtimeSeatMap, SeatMapViewer } from '@zonetrix/viewer';
|
|
708
|
+
*
|
|
709
|
+
* function BookingPage({ seatMapId, userId }) {
|
|
710
|
+
* const {
|
|
711
|
+
* config,
|
|
712
|
+
* myReservedSeats,
|
|
713
|
+
* otherReservedSeats,
|
|
714
|
+
* unavailableSeats,
|
|
715
|
+
* loading,
|
|
716
|
+
* error
|
|
717
|
+
* } = useRealtimeSeatMap({ seatMapId, userId });
|
|
718
|
+
*
|
|
719
|
+
* if (loading) return <LoadingSpinner />;
|
|
720
|
+
* if (error) return <ErrorMessage error={error} />;
|
|
721
|
+
* if (!config) return null;
|
|
722
|
+
*
|
|
723
|
+
* return (
|
|
724
|
+
* <SeatMapViewer
|
|
725
|
+
* config={config}
|
|
726
|
+
* myReservedSeats={myReservedSeats}
|
|
727
|
+
* reservedSeats={otherReservedSeats}
|
|
728
|
+
* unavailableSeats={unavailableSeats}
|
|
729
|
+
* onSeatSelect={handleSeatSelect}
|
|
730
|
+
* />
|
|
731
|
+
* );
|
|
732
|
+
* }
|
|
733
|
+
* ```
|
|
734
|
+
*/
|
|
735
|
+
declare function useRealtimeSeatMap(options: UseRealtimeSeatMapOptions): UseRealtimeSeatMapResult;
|
|
736
|
+
|
|
737
|
+
export { type AlignmentGuide, type BookingSelection, type CanvasConfig, type CanvasState, type ColorSettings, DEFAULT_COLORS, type EditorMode, type FirebaseCanvasConfig, type FirebaseConfigResult, type FirebaseHookOptions, type FirebaseIndexEntry, FirebasePaths, type FirebasePosition, type FirebaseSeat, type FirebaseSeatMap, type FirebaseSeatMapConfig, type FirebaseSeatMapMeta, type FirebaseSeatState, type FirebaseSeatStateEntry, type FirebaseSeatStateValue, type FirebaseSeatStates, type FirebaseSeatStatesResult, type FirebaseStage, type ObjectConfig, type ObjectType, type RowPricing, type SeatData, type SeatMapConfig, type SeatMapMetadata, type SeatShape, type SeatState, type SectionConfig, type SerializedSeat, type SerializedSection, type SerializedStage, type StageConfig, type Tool, type UseFirebaseConfigOptions, type UseFirebaseConfigResult, type UseFirebaseSeatStatesOptions, type UseFirebaseSeatStatesResult, type UseRealtimeSeatMapOptions, type UseRealtimeSeatMapResult, type ValidationResult, applySeatStateOverrides, calculateAvailableSeats, calculateCapacity, calculateSeatPrice, clearFirebaseInstance, cloneConfig, createDefaultConfig, createIndexUpdates, decodeSeatId, deriveSeatArraysFromStates, downloadConfigAsFile, encodeSeatId, exportConfigAsJSON, extractSeatStates, formatDate, fromFirebaseSeatMap, fromFirebaseState, generateId, getFirebaseDatabase, getSelectedSeats, importConfigFromJSON, initializeFirebaseForViewer, isFirebaseInitialized, toFirebaseSeatMap, toFirebaseState, updateConfigTimestamp, useFirebaseConfig, useFirebaseSeatStates, useRealtimeSeatMap, validateSeatMapConfig };
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ __export(index_exports, {
|
|
|
26
26
|
calculateAvailableSeats: () => calculateAvailableSeats,
|
|
27
27
|
calculateCapacity: () => calculateCapacity,
|
|
28
28
|
calculateSeatPrice: () => calculateSeatPrice,
|
|
29
|
+
clearFirebaseInstance: () => clearFirebaseInstance,
|
|
29
30
|
cloneConfig: () => cloneConfig,
|
|
30
31
|
createDefaultConfig: () => createDefaultConfig,
|
|
31
32
|
createIndexUpdates: () => createIndexUpdates,
|
|
@@ -39,11 +40,17 @@ __export(index_exports, {
|
|
|
39
40
|
fromFirebaseSeatMap: () => fromFirebaseSeatMap,
|
|
40
41
|
fromFirebaseState: () => fromFirebaseState,
|
|
41
42
|
generateId: () => generateId,
|
|
43
|
+
getFirebaseDatabase: () => getFirebaseDatabase,
|
|
42
44
|
getSelectedSeats: () => getSelectedSeats,
|
|
43
45
|
importConfigFromJSON: () => importConfigFromJSON,
|
|
46
|
+
initializeFirebaseForViewer: () => initializeFirebaseForViewer,
|
|
47
|
+
isFirebaseInitialized: () => isFirebaseInitialized,
|
|
44
48
|
toFirebaseSeatMap: () => toFirebaseSeatMap,
|
|
45
49
|
toFirebaseState: () => toFirebaseState,
|
|
46
50
|
updateConfigTimestamp: () => updateConfigTimestamp,
|
|
51
|
+
useFirebaseConfig: () => useFirebaseConfig,
|
|
52
|
+
useFirebaseSeatStates: () => useFirebaseSeatStates,
|
|
53
|
+
useRealtimeSeatMap: () => useRealtimeSeatMap,
|
|
47
54
|
validateSeatMapConfig: () => validateSeatMapConfig
|
|
48
55
|
});
|
|
49
56
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -488,6 +495,335 @@ function createIndexUpdates(seatMapId, eventId, subEventId) {
|
|
|
488
495
|
[`indexes/by_sub_event/${subEventId}/${seatMapId}`]: true
|
|
489
496
|
};
|
|
490
497
|
}
|
|
498
|
+
|
|
499
|
+
// src/firebase/client.ts
|
|
500
|
+
var firebaseDatabase = null;
|
|
501
|
+
function initializeFirebaseForViewer(database) {
|
|
502
|
+
firebaseDatabase = database;
|
|
503
|
+
}
|
|
504
|
+
function getFirebaseDatabase() {
|
|
505
|
+
if (!firebaseDatabase) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
"Firebase database not initialized. Call initializeFirebaseForViewer(db) first."
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
return firebaseDatabase;
|
|
511
|
+
}
|
|
512
|
+
function isFirebaseInitialized() {
|
|
513
|
+
return firebaseDatabase !== null;
|
|
514
|
+
}
|
|
515
|
+
function clearFirebaseInstance() {
|
|
516
|
+
firebaseDatabase = null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/firebase/hooks/useFirebaseSeatStates.ts
|
|
520
|
+
var import_react = require("react");
|
|
521
|
+
var import_database = require("firebase/database");
|
|
522
|
+
function arraysEqualUnordered(a, b) {
|
|
523
|
+
if (a.length !== b.length) return false;
|
|
524
|
+
const sortedA = [...a].sort();
|
|
525
|
+
const sortedB = [...b].sort();
|
|
526
|
+
return sortedA.every((val, idx) => val === sortedB[idx]);
|
|
527
|
+
}
|
|
528
|
+
function deriveUserAwareSeatArrays(statesData, currentUserId) {
|
|
529
|
+
const myReserved = [];
|
|
530
|
+
const otherReserved = [];
|
|
531
|
+
const unavailable = [];
|
|
532
|
+
Object.entries(statesData).forEach(([seatId, entry]) => {
|
|
533
|
+
if (!entry) return;
|
|
534
|
+
if (typeof entry === "object" && entry.state) {
|
|
535
|
+
if (entry.state === "unavailable") {
|
|
536
|
+
unavailable.push(seatId);
|
|
537
|
+
} else if (entry.state === "reserved") {
|
|
538
|
+
if (currentUserId && entry.userId === currentUserId) {
|
|
539
|
+
myReserved.push(seatId);
|
|
540
|
+
} else {
|
|
541
|
+
otherReserved.push(seatId);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
return {
|
|
547
|
+
myReserved: myReserved.sort(),
|
|
548
|
+
otherReserved: otherReserved.sort(),
|
|
549
|
+
unavailable: unavailable.sort()
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
var INITIAL_SNAPSHOT = {
|
|
553
|
+
states: null,
|
|
554
|
+
loading: true,
|
|
555
|
+
error: null,
|
|
556
|
+
lastUpdated: null,
|
|
557
|
+
myReservedSeats: [],
|
|
558
|
+
otherReservedSeats: [],
|
|
559
|
+
unavailableSeats: []
|
|
560
|
+
};
|
|
561
|
+
function useFirebaseSeatStates(options) {
|
|
562
|
+
const {
|
|
563
|
+
seatMapId,
|
|
564
|
+
currentUserId,
|
|
565
|
+
enabled = true,
|
|
566
|
+
onStateChange,
|
|
567
|
+
onError
|
|
568
|
+
} = options;
|
|
569
|
+
const snapshotRef = (0, import_react.useRef)({ ...INITIAL_SNAPSHOT });
|
|
570
|
+
const onStateChangeRef = (0, import_react.useRef)(onStateChange);
|
|
571
|
+
const onErrorRef = (0, import_react.useRef)(onError);
|
|
572
|
+
const currentUserIdRef = (0, import_react.useRef)(currentUserId);
|
|
573
|
+
onStateChangeRef.current = onStateChange;
|
|
574
|
+
onErrorRef.current = onError;
|
|
575
|
+
currentUserIdRef.current = currentUserId;
|
|
576
|
+
const subscribe = (0, import_react.useCallback)(
|
|
577
|
+
(callback) => {
|
|
578
|
+
if (!enabled || !seatMapId) {
|
|
579
|
+
if (snapshotRef.current.loading !== false || snapshotRef.current.states !== null) {
|
|
580
|
+
snapshotRef.current = { ...INITIAL_SNAPSHOT, loading: false };
|
|
581
|
+
callback();
|
|
582
|
+
}
|
|
583
|
+
return () => {
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
if (!isFirebaseInitialized()) {
|
|
587
|
+
const errorMsg = "Firebase not initialized. Call initializeFirebaseForViewer first.";
|
|
588
|
+
if (snapshotRef.current.error?.message !== errorMsg) {
|
|
589
|
+
snapshotRef.current = {
|
|
590
|
+
...INITIAL_SNAPSHOT,
|
|
591
|
+
loading: false,
|
|
592
|
+
error: new Error(errorMsg)
|
|
593
|
+
};
|
|
594
|
+
callback();
|
|
595
|
+
}
|
|
596
|
+
return () => {
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
const db = getFirebaseDatabase();
|
|
600
|
+
const statesRef = (0, import_database.ref)(db, `seatmaps/${seatMapId}/seat_states`);
|
|
601
|
+
console.log("\u{1F525} Firebase Listener - Subscribing to:", `seatmaps/${seatMapId}/seat_states`);
|
|
602
|
+
const handleValue = (dataSnapshot) => {
|
|
603
|
+
const data = dataSnapshot.val();
|
|
604
|
+
const statesData = data || {};
|
|
605
|
+
const derived = deriveUserAwareSeatArrays(
|
|
606
|
+
statesData,
|
|
607
|
+
currentUserIdRef.current
|
|
608
|
+
);
|
|
609
|
+
console.log("\u{1F4E1} Firebase Event Received:", {
|
|
610
|
+
path: `seatmaps/${seatMapId}/seat_states`,
|
|
611
|
+
dataSnapshot: dataSnapshot.exists(),
|
|
612
|
+
rawData: data,
|
|
613
|
+
statesCount: Object.keys(statesData).length,
|
|
614
|
+
derived: {
|
|
615
|
+
myReserved: derived.myReserved,
|
|
616
|
+
otherReserved: derived.otherReserved,
|
|
617
|
+
unavailable: derived.unavailable
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
const current = snapshotRef.current;
|
|
621
|
+
const hasChanged = current.loading || !arraysEqualUnordered(current.myReservedSeats, derived.myReserved) || !arraysEqualUnordered(
|
|
622
|
+
current.otherReservedSeats,
|
|
623
|
+
derived.otherReserved
|
|
624
|
+
) || !arraysEqualUnordered(current.unavailableSeats, derived.unavailable);
|
|
625
|
+
console.log("\u{1F504} Firebase Data Changed?", {
|
|
626
|
+
hasChanged,
|
|
627
|
+
currentOtherReserved: current.otherReservedSeats,
|
|
628
|
+
newOtherReserved: derived.otherReserved
|
|
629
|
+
});
|
|
630
|
+
if (hasChanged) {
|
|
631
|
+
console.log("\u2705 Updating React state with new Firebase data");
|
|
632
|
+
snapshotRef.current = {
|
|
633
|
+
states: statesData,
|
|
634
|
+
loading: false,
|
|
635
|
+
error: null,
|
|
636
|
+
lastUpdated: Date.now(),
|
|
637
|
+
myReservedSeats: derived.myReserved,
|
|
638
|
+
otherReservedSeats: derived.otherReserved,
|
|
639
|
+
unavailableSeats: derived.unavailable
|
|
640
|
+
};
|
|
641
|
+
onStateChangeRef.current?.(statesData);
|
|
642
|
+
callback();
|
|
643
|
+
} else {
|
|
644
|
+
console.log("\u23ED\uFE0F No change detected, skipping update");
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
const handleError = (err) => {
|
|
648
|
+
snapshotRef.current = {
|
|
649
|
+
...snapshotRef.current,
|
|
650
|
+
loading: false,
|
|
651
|
+
error: err
|
|
652
|
+
};
|
|
653
|
+
onErrorRef.current?.(err);
|
|
654
|
+
callback();
|
|
655
|
+
};
|
|
656
|
+
(0, import_database.onValue)(statesRef, handleValue, handleError);
|
|
657
|
+
return () => {
|
|
658
|
+
console.log("\u{1F525} Firebase Listener - Unsubscribing from:", `seatmaps/${seatMapId}/seat_states`);
|
|
659
|
+
(0, import_database.off)(statesRef);
|
|
660
|
+
};
|
|
661
|
+
},
|
|
662
|
+
[seatMapId, enabled]
|
|
663
|
+
// Don't include currentUserId - we use currentUserIdRef.current which is always up-to-date
|
|
664
|
+
);
|
|
665
|
+
const getSnapshot = (0, import_react.useCallback)(() => snapshotRef.current, []);
|
|
666
|
+
const getServerSnapshot = (0, import_react.useCallback)(() => INITIAL_SNAPSHOT, []);
|
|
667
|
+
const snapshot = (0, import_react.useSyncExternalStore)(
|
|
668
|
+
subscribe,
|
|
669
|
+
getSnapshot,
|
|
670
|
+
getServerSnapshot
|
|
671
|
+
);
|
|
672
|
+
return {
|
|
673
|
+
states: snapshot.states,
|
|
674
|
+
loading: snapshot.loading,
|
|
675
|
+
error: snapshot.error,
|
|
676
|
+
lastUpdated: snapshot.lastUpdated,
|
|
677
|
+
myReservedSeats: snapshot.myReservedSeats,
|
|
678
|
+
otherReservedSeats: snapshot.otherReservedSeats,
|
|
679
|
+
unavailableSeats: snapshot.unavailableSeats,
|
|
680
|
+
// Legacy alias
|
|
681
|
+
reservedSeats: snapshot.otherReservedSeats
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/firebase/hooks/useFirebaseConfig.ts
|
|
686
|
+
var import_react2 = require("react");
|
|
687
|
+
var import_database2 = require("firebase/database");
|
|
688
|
+
function useFirebaseConfig(options) {
|
|
689
|
+
const {
|
|
690
|
+
seatMapId,
|
|
691
|
+
enabled = true,
|
|
692
|
+
subscribeToChanges = false,
|
|
693
|
+
onConfigLoad,
|
|
694
|
+
onError
|
|
695
|
+
} = options;
|
|
696
|
+
const [config, setConfig] = (0, import_react2.useState)(null);
|
|
697
|
+
const [loading, setLoading] = (0, import_react2.useState)(true);
|
|
698
|
+
const [error, setError] = (0, import_react2.useState)(null);
|
|
699
|
+
const onConfigLoadRef = (0, import_react2.useRef)(onConfigLoad);
|
|
700
|
+
const onErrorRef = (0, import_react2.useRef)(onError);
|
|
701
|
+
onConfigLoadRef.current = onConfigLoad;
|
|
702
|
+
onErrorRef.current = onError;
|
|
703
|
+
const fetchConfig = (0, import_react2.useCallback)(async () => {
|
|
704
|
+
if (!seatMapId) return;
|
|
705
|
+
if (!isFirebaseInitialized()) {
|
|
706
|
+
setError(
|
|
707
|
+
new Error(
|
|
708
|
+
"Firebase not initialized. Call initializeFirebaseForViewer first."
|
|
709
|
+
)
|
|
710
|
+
);
|
|
711
|
+
setLoading(false);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const db = getFirebaseDatabase();
|
|
715
|
+
const seatmapRef = (0, import_database2.ref)(db, `seatmaps/${seatMapId}`);
|
|
716
|
+
try {
|
|
717
|
+
setLoading(true);
|
|
718
|
+
setError(null);
|
|
719
|
+
const snapshot = await (0, import_database2.get)(seatmapRef);
|
|
720
|
+
const data = snapshot.val();
|
|
721
|
+
if (data) {
|
|
722
|
+
const converted = fromFirebaseSeatMap(data);
|
|
723
|
+
setConfig(converted);
|
|
724
|
+
onConfigLoadRef.current?.(converted);
|
|
725
|
+
} else {
|
|
726
|
+
setError(new Error(`Seat map ${seatMapId} not found in Firebase`));
|
|
727
|
+
}
|
|
728
|
+
} catch (err) {
|
|
729
|
+
const error2 = err instanceof Error ? err : new Error("Unknown error");
|
|
730
|
+
setError(error2);
|
|
731
|
+
onErrorRef.current?.(error2);
|
|
732
|
+
} finally {
|
|
733
|
+
setLoading(false);
|
|
734
|
+
}
|
|
735
|
+
}, [seatMapId]);
|
|
736
|
+
(0, import_react2.useEffect)(() => {
|
|
737
|
+
if (!enabled || !seatMapId) {
|
|
738
|
+
setLoading(false);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
fetchConfig();
|
|
742
|
+
if (subscribeToChanges && isFirebaseInitialized()) {
|
|
743
|
+
const db = getFirebaseDatabase();
|
|
744
|
+
const metaRef = (0, import_database2.ref)(db, `seatmaps/${seatMapId}/meta/updated_at`);
|
|
745
|
+
let isFirstSnapshot = true;
|
|
746
|
+
let lastSeenTimestamp = null;
|
|
747
|
+
const handleUpdate = (snapshot) => {
|
|
748
|
+
if (isFirstSnapshot) {
|
|
749
|
+
isFirstSnapshot = false;
|
|
750
|
+
lastSeenTimestamp = snapshot.val();
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const currentTimestamp = snapshot.val();
|
|
754
|
+
if (snapshot.exists() && currentTimestamp !== lastSeenTimestamp) {
|
|
755
|
+
lastSeenTimestamp = currentTimestamp;
|
|
756
|
+
fetchConfig();
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
(0, import_database2.onValue)(metaRef, handleUpdate);
|
|
760
|
+
return () => {
|
|
761
|
+
(0, import_database2.off)(metaRef);
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}, [seatMapId, enabled, subscribeToChanges]);
|
|
765
|
+
return {
|
|
766
|
+
config,
|
|
767
|
+
loading,
|
|
768
|
+
error,
|
|
769
|
+
refetch: fetchConfig
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/firebase/hooks/useRealtimeSeatMap.ts
|
|
774
|
+
function useRealtimeSeatMap(options) {
|
|
775
|
+
const {
|
|
776
|
+
seatMapId,
|
|
777
|
+
userId,
|
|
778
|
+
enabled = true,
|
|
779
|
+
subscribeToDesignChanges = false,
|
|
780
|
+
onConfigLoad,
|
|
781
|
+
onStateChange,
|
|
782
|
+
onError
|
|
783
|
+
} = options;
|
|
784
|
+
const {
|
|
785
|
+
config,
|
|
786
|
+
loading: configLoading,
|
|
787
|
+
error: configError,
|
|
788
|
+
refetch
|
|
789
|
+
} = useFirebaseConfig({
|
|
790
|
+
seatMapId,
|
|
791
|
+
enabled,
|
|
792
|
+
subscribeToChanges: subscribeToDesignChanges,
|
|
793
|
+
onConfigLoad,
|
|
794
|
+
onError
|
|
795
|
+
});
|
|
796
|
+
const {
|
|
797
|
+
states: seatStates,
|
|
798
|
+
loading: statesLoading,
|
|
799
|
+
error: statesError,
|
|
800
|
+
lastUpdated,
|
|
801
|
+
myReservedSeats,
|
|
802
|
+
otherReservedSeats,
|
|
803
|
+
unavailableSeats,
|
|
804
|
+
reservedSeats
|
|
805
|
+
} = useFirebaseSeatStates({
|
|
806
|
+
seatMapId,
|
|
807
|
+
currentUserId: userId,
|
|
808
|
+
enabled,
|
|
809
|
+
onStateChange,
|
|
810
|
+
onError
|
|
811
|
+
});
|
|
812
|
+
const loading = configLoading || statesLoading;
|
|
813
|
+
const error = configError || statesError;
|
|
814
|
+
return {
|
|
815
|
+
config,
|
|
816
|
+
loading,
|
|
817
|
+
error,
|
|
818
|
+
myReservedSeats,
|
|
819
|
+
otherReservedSeats,
|
|
820
|
+
unavailableSeats,
|
|
821
|
+
reservedSeats,
|
|
822
|
+
seatStates,
|
|
823
|
+
lastUpdated,
|
|
824
|
+
refetch
|
|
825
|
+
};
|
|
826
|
+
}
|
|
491
827
|
// Annotate the CommonJS export names for ESM import in node:
|
|
492
828
|
0 && (module.exports = {
|
|
493
829
|
DEFAULT_COLORS,
|
|
@@ -496,6 +832,7 @@ function createIndexUpdates(seatMapId, eventId, subEventId) {
|
|
|
496
832
|
calculateAvailableSeats,
|
|
497
833
|
calculateCapacity,
|
|
498
834
|
calculateSeatPrice,
|
|
835
|
+
clearFirebaseInstance,
|
|
499
836
|
cloneConfig,
|
|
500
837
|
createDefaultConfig,
|
|
501
838
|
createIndexUpdates,
|
|
@@ -509,10 +846,16 @@ function createIndexUpdates(seatMapId, eventId, subEventId) {
|
|
|
509
846
|
fromFirebaseSeatMap,
|
|
510
847
|
fromFirebaseState,
|
|
511
848
|
generateId,
|
|
849
|
+
getFirebaseDatabase,
|
|
512
850
|
getSelectedSeats,
|
|
513
851
|
importConfigFromJSON,
|
|
852
|
+
initializeFirebaseForViewer,
|
|
853
|
+
isFirebaseInitialized,
|
|
514
854
|
toFirebaseSeatMap,
|
|
515
855
|
toFirebaseState,
|
|
516
856
|
updateConfigTimestamp,
|
|
857
|
+
useFirebaseConfig,
|
|
858
|
+
useFirebaseSeatStates,
|
|
859
|
+
useRealtimeSeatMap,
|
|
517
860
|
validateSeatMapConfig
|
|
518
861
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -438,6 +438,335 @@ function createIndexUpdates(seatMapId, eventId, subEventId) {
|
|
|
438
438
|
[`indexes/by_sub_event/${subEventId}/${seatMapId}`]: true
|
|
439
439
|
};
|
|
440
440
|
}
|
|
441
|
+
|
|
442
|
+
// src/firebase/client.ts
|
|
443
|
+
var firebaseDatabase = null;
|
|
444
|
+
function initializeFirebaseForViewer(database) {
|
|
445
|
+
firebaseDatabase = database;
|
|
446
|
+
}
|
|
447
|
+
function getFirebaseDatabase() {
|
|
448
|
+
if (!firebaseDatabase) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
"Firebase database not initialized. Call initializeFirebaseForViewer(db) first."
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return firebaseDatabase;
|
|
454
|
+
}
|
|
455
|
+
function isFirebaseInitialized() {
|
|
456
|
+
return firebaseDatabase !== null;
|
|
457
|
+
}
|
|
458
|
+
function clearFirebaseInstance() {
|
|
459
|
+
firebaseDatabase = null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/firebase/hooks/useFirebaseSeatStates.ts
|
|
463
|
+
import { useSyncExternalStore, useCallback, useRef } from "react";
|
|
464
|
+
import { ref, onValue, off } from "firebase/database";
|
|
465
|
+
function arraysEqualUnordered(a, b) {
|
|
466
|
+
if (a.length !== b.length) return false;
|
|
467
|
+
const sortedA = [...a].sort();
|
|
468
|
+
const sortedB = [...b].sort();
|
|
469
|
+
return sortedA.every((val, idx) => val === sortedB[idx]);
|
|
470
|
+
}
|
|
471
|
+
function deriveUserAwareSeatArrays(statesData, currentUserId) {
|
|
472
|
+
const myReserved = [];
|
|
473
|
+
const otherReserved = [];
|
|
474
|
+
const unavailable = [];
|
|
475
|
+
Object.entries(statesData).forEach(([seatId, entry]) => {
|
|
476
|
+
if (!entry) return;
|
|
477
|
+
if (typeof entry === "object" && entry.state) {
|
|
478
|
+
if (entry.state === "unavailable") {
|
|
479
|
+
unavailable.push(seatId);
|
|
480
|
+
} else if (entry.state === "reserved") {
|
|
481
|
+
if (currentUserId && entry.userId === currentUserId) {
|
|
482
|
+
myReserved.push(seatId);
|
|
483
|
+
} else {
|
|
484
|
+
otherReserved.push(seatId);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
return {
|
|
490
|
+
myReserved: myReserved.sort(),
|
|
491
|
+
otherReserved: otherReserved.sort(),
|
|
492
|
+
unavailable: unavailable.sort()
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
var INITIAL_SNAPSHOT = {
|
|
496
|
+
states: null,
|
|
497
|
+
loading: true,
|
|
498
|
+
error: null,
|
|
499
|
+
lastUpdated: null,
|
|
500
|
+
myReservedSeats: [],
|
|
501
|
+
otherReservedSeats: [],
|
|
502
|
+
unavailableSeats: []
|
|
503
|
+
};
|
|
504
|
+
function useFirebaseSeatStates(options) {
|
|
505
|
+
const {
|
|
506
|
+
seatMapId,
|
|
507
|
+
currentUserId,
|
|
508
|
+
enabled = true,
|
|
509
|
+
onStateChange,
|
|
510
|
+
onError
|
|
511
|
+
} = options;
|
|
512
|
+
const snapshotRef = useRef({ ...INITIAL_SNAPSHOT });
|
|
513
|
+
const onStateChangeRef = useRef(onStateChange);
|
|
514
|
+
const onErrorRef = useRef(onError);
|
|
515
|
+
const currentUserIdRef = useRef(currentUserId);
|
|
516
|
+
onStateChangeRef.current = onStateChange;
|
|
517
|
+
onErrorRef.current = onError;
|
|
518
|
+
currentUserIdRef.current = currentUserId;
|
|
519
|
+
const subscribe = useCallback(
|
|
520
|
+
(callback) => {
|
|
521
|
+
if (!enabled || !seatMapId) {
|
|
522
|
+
if (snapshotRef.current.loading !== false || snapshotRef.current.states !== null) {
|
|
523
|
+
snapshotRef.current = { ...INITIAL_SNAPSHOT, loading: false };
|
|
524
|
+
callback();
|
|
525
|
+
}
|
|
526
|
+
return () => {
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
if (!isFirebaseInitialized()) {
|
|
530
|
+
const errorMsg = "Firebase not initialized. Call initializeFirebaseForViewer first.";
|
|
531
|
+
if (snapshotRef.current.error?.message !== errorMsg) {
|
|
532
|
+
snapshotRef.current = {
|
|
533
|
+
...INITIAL_SNAPSHOT,
|
|
534
|
+
loading: false,
|
|
535
|
+
error: new Error(errorMsg)
|
|
536
|
+
};
|
|
537
|
+
callback();
|
|
538
|
+
}
|
|
539
|
+
return () => {
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
const db = getFirebaseDatabase();
|
|
543
|
+
const statesRef = ref(db, `seatmaps/${seatMapId}/seat_states`);
|
|
544
|
+
console.log("\u{1F525} Firebase Listener - Subscribing to:", `seatmaps/${seatMapId}/seat_states`);
|
|
545
|
+
const handleValue = (dataSnapshot) => {
|
|
546
|
+
const data = dataSnapshot.val();
|
|
547
|
+
const statesData = data || {};
|
|
548
|
+
const derived = deriveUserAwareSeatArrays(
|
|
549
|
+
statesData,
|
|
550
|
+
currentUserIdRef.current
|
|
551
|
+
);
|
|
552
|
+
console.log("\u{1F4E1} Firebase Event Received:", {
|
|
553
|
+
path: `seatmaps/${seatMapId}/seat_states`,
|
|
554
|
+
dataSnapshot: dataSnapshot.exists(),
|
|
555
|
+
rawData: data,
|
|
556
|
+
statesCount: Object.keys(statesData).length,
|
|
557
|
+
derived: {
|
|
558
|
+
myReserved: derived.myReserved,
|
|
559
|
+
otherReserved: derived.otherReserved,
|
|
560
|
+
unavailable: derived.unavailable
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
const current = snapshotRef.current;
|
|
564
|
+
const hasChanged = current.loading || !arraysEqualUnordered(current.myReservedSeats, derived.myReserved) || !arraysEqualUnordered(
|
|
565
|
+
current.otherReservedSeats,
|
|
566
|
+
derived.otherReserved
|
|
567
|
+
) || !arraysEqualUnordered(current.unavailableSeats, derived.unavailable);
|
|
568
|
+
console.log("\u{1F504} Firebase Data Changed?", {
|
|
569
|
+
hasChanged,
|
|
570
|
+
currentOtherReserved: current.otherReservedSeats,
|
|
571
|
+
newOtherReserved: derived.otherReserved
|
|
572
|
+
});
|
|
573
|
+
if (hasChanged) {
|
|
574
|
+
console.log("\u2705 Updating React state with new Firebase data");
|
|
575
|
+
snapshotRef.current = {
|
|
576
|
+
states: statesData,
|
|
577
|
+
loading: false,
|
|
578
|
+
error: null,
|
|
579
|
+
lastUpdated: Date.now(),
|
|
580
|
+
myReservedSeats: derived.myReserved,
|
|
581
|
+
otherReservedSeats: derived.otherReserved,
|
|
582
|
+
unavailableSeats: derived.unavailable
|
|
583
|
+
};
|
|
584
|
+
onStateChangeRef.current?.(statesData);
|
|
585
|
+
callback();
|
|
586
|
+
} else {
|
|
587
|
+
console.log("\u23ED\uFE0F No change detected, skipping update");
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
const handleError = (err) => {
|
|
591
|
+
snapshotRef.current = {
|
|
592
|
+
...snapshotRef.current,
|
|
593
|
+
loading: false,
|
|
594
|
+
error: err
|
|
595
|
+
};
|
|
596
|
+
onErrorRef.current?.(err);
|
|
597
|
+
callback();
|
|
598
|
+
};
|
|
599
|
+
onValue(statesRef, handleValue, handleError);
|
|
600
|
+
return () => {
|
|
601
|
+
console.log("\u{1F525} Firebase Listener - Unsubscribing from:", `seatmaps/${seatMapId}/seat_states`);
|
|
602
|
+
off(statesRef);
|
|
603
|
+
};
|
|
604
|
+
},
|
|
605
|
+
[seatMapId, enabled]
|
|
606
|
+
// Don't include currentUserId - we use currentUserIdRef.current which is always up-to-date
|
|
607
|
+
);
|
|
608
|
+
const getSnapshot = useCallback(() => snapshotRef.current, []);
|
|
609
|
+
const getServerSnapshot = useCallback(() => INITIAL_SNAPSHOT, []);
|
|
610
|
+
const snapshot = useSyncExternalStore(
|
|
611
|
+
subscribe,
|
|
612
|
+
getSnapshot,
|
|
613
|
+
getServerSnapshot
|
|
614
|
+
);
|
|
615
|
+
return {
|
|
616
|
+
states: snapshot.states,
|
|
617
|
+
loading: snapshot.loading,
|
|
618
|
+
error: snapshot.error,
|
|
619
|
+
lastUpdated: snapshot.lastUpdated,
|
|
620
|
+
myReservedSeats: snapshot.myReservedSeats,
|
|
621
|
+
otherReservedSeats: snapshot.otherReservedSeats,
|
|
622
|
+
unavailableSeats: snapshot.unavailableSeats,
|
|
623
|
+
// Legacy alias
|
|
624
|
+
reservedSeats: snapshot.otherReservedSeats
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/firebase/hooks/useFirebaseConfig.ts
|
|
629
|
+
import { useState, useEffect, useCallback as useCallback2, useRef as useRef2 } from "react";
|
|
630
|
+
import { ref as ref2, get, onValue as onValue2, off as off2 } from "firebase/database";
|
|
631
|
+
function useFirebaseConfig(options) {
|
|
632
|
+
const {
|
|
633
|
+
seatMapId,
|
|
634
|
+
enabled = true,
|
|
635
|
+
subscribeToChanges = false,
|
|
636
|
+
onConfigLoad,
|
|
637
|
+
onError
|
|
638
|
+
} = options;
|
|
639
|
+
const [config, setConfig] = useState(null);
|
|
640
|
+
const [loading, setLoading] = useState(true);
|
|
641
|
+
const [error, setError] = useState(null);
|
|
642
|
+
const onConfigLoadRef = useRef2(onConfigLoad);
|
|
643
|
+
const onErrorRef = useRef2(onError);
|
|
644
|
+
onConfigLoadRef.current = onConfigLoad;
|
|
645
|
+
onErrorRef.current = onError;
|
|
646
|
+
const fetchConfig = useCallback2(async () => {
|
|
647
|
+
if (!seatMapId) return;
|
|
648
|
+
if (!isFirebaseInitialized()) {
|
|
649
|
+
setError(
|
|
650
|
+
new Error(
|
|
651
|
+
"Firebase not initialized. Call initializeFirebaseForViewer first."
|
|
652
|
+
)
|
|
653
|
+
);
|
|
654
|
+
setLoading(false);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const db = getFirebaseDatabase();
|
|
658
|
+
const seatmapRef = ref2(db, `seatmaps/${seatMapId}`);
|
|
659
|
+
try {
|
|
660
|
+
setLoading(true);
|
|
661
|
+
setError(null);
|
|
662
|
+
const snapshot = await get(seatmapRef);
|
|
663
|
+
const data = snapshot.val();
|
|
664
|
+
if (data) {
|
|
665
|
+
const converted = fromFirebaseSeatMap(data);
|
|
666
|
+
setConfig(converted);
|
|
667
|
+
onConfigLoadRef.current?.(converted);
|
|
668
|
+
} else {
|
|
669
|
+
setError(new Error(`Seat map ${seatMapId} not found in Firebase`));
|
|
670
|
+
}
|
|
671
|
+
} catch (err) {
|
|
672
|
+
const error2 = err instanceof Error ? err : new Error("Unknown error");
|
|
673
|
+
setError(error2);
|
|
674
|
+
onErrorRef.current?.(error2);
|
|
675
|
+
} finally {
|
|
676
|
+
setLoading(false);
|
|
677
|
+
}
|
|
678
|
+
}, [seatMapId]);
|
|
679
|
+
useEffect(() => {
|
|
680
|
+
if (!enabled || !seatMapId) {
|
|
681
|
+
setLoading(false);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
fetchConfig();
|
|
685
|
+
if (subscribeToChanges && isFirebaseInitialized()) {
|
|
686
|
+
const db = getFirebaseDatabase();
|
|
687
|
+
const metaRef = ref2(db, `seatmaps/${seatMapId}/meta/updated_at`);
|
|
688
|
+
let isFirstSnapshot = true;
|
|
689
|
+
let lastSeenTimestamp = null;
|
|
690
|
+
const handleUpdate = (snapshot) => {
|
|
691
|
+
if (isFirstSnapshot) {
|
|
692
|
+
isFirstSnapshot = false;
|
|
693
|
+
lastSeenTimestamp = snapshot.val();
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const currentTimestamp = snapshot.val();
|
|
697
|
+
if (snapshot.exists() && currentTimestamp !== lastSeenTimestamp) {
|
|
698
|
+
lastSeenTimestamp = currentTimestamp;
|
|
699
|
+
fetchConfig();
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
onValue2(metaRef, handleUpdate);
|
|
703
|
+
return () => {
|
|
704
|
+
off2(metaRef);
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
}, [seatMapId, enabled, subscribeToChanges]);
|
|
708
|
+
return {
|
|
709
|
+
config,
|
|
710
|
+
loading,
|
|
711
|
+
error,
|
|
712
|
+
refetch: fetchConfig
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/firebase/hooks/useRealtimeSeatMap.ts
|
|
717
|
+
function useRealtimeSeatMap(options) {
|
|
718
|
+
const {
|
|
719
|
+
seatMapId,
|
|
720
|
+
userId,
|
|
721
|
+
enabled = true,
|
|
722
|
+
subscribeToDesignChanges = false,
|
|
723
|
+
onConfigLoad,
|
|
724
|
+
onStateChange,
|
|
725
|
+
onError
|
|
726
|
+
} = options;
|
|
727
|
+
const {
|
|
728
|
+
config,
|
|
729
|
+
loading: configLoading,
|
|
730
|
+
error: configError,
|
|
731
|
+
refetch
|
|
732
|
+
} = useFirebaseConfig({
|
|
733
|
+
seatMapId,
|
|
734
|
+
enabled,
|
|
735
|
+
subscribeToChanges: subscribeToDesignChanges,
|
|
736
|
+
onConfigLoad,
|
|
737
|
+
onError
|
|
738
|
+
});
|
|
739
|
+
const {
|
|
740
|
+
states: seatStates,
|
|
741
|
+
loading: statesLoading,
|
|
742
|
+
error: statesError,
|
|
743
|
+
lastUpdated,
|
|
744
|
+
myReservedSeats,
|
|
745
|
+
otherReservedSeats,
|
|
746
|
+
unavailableSeats,
|
|
747
|
+
reservedSeats
|
|
748
|
+
} = useFirebaseSeatStates({
|
|
749
|
+
seatMapId,
|
|
750
|
+
currentUserId: userId,
|
|
751
|
+
enabled,
|
|
752
|
+
onStateChange,
|
|
753
|
+
onError
|
|
754
|
+
});
|
|
755
|
+
const loading = configLoading || statesLoading;
|
|
756
|
+
const error = configError || statesError;
|
|
757
|
+
return {
|
|
758
|
+
config,
|
|
759
|
+
loading,
|
|
760
|
+
error,
|
|
761
|
+
myReservedSeats,
|
|
762
|
+
otherReservedSeats,
|
|
763
|
+
unavailableSeats,
|
|
764
|
+
reservedSeats,
|
|
765
|
+
seatStates,
|
|
766
|
+
lastUpdated,
|
|
767
|
+
refetch
|
|
768
|
+
};
|
|
769
|
+
}
|
|
441
770
|
export {
|
|
442
771
|
DEFAULT_COLORS,
|
|
443
772
|
FirebasePaths,
|
|
@@ -445,6 +774,7 @@ export {
|
|
|
445
774
|
calculateAvailableSeats,
|
|
446
775
|
calculateCapacity,
|
|
447
776
|
calculateSeatPrice,
|
|
777
|
+
clearFirebaseInstance,
|
|
448
778
|
cloneConfig,
|
|
449
779
|
createDefaultConfig,
|
|
450
780
|
createIndexUpdates,
|
|
@@ -458,10 +788,16 @@ export {
|
|
|
458
788
|
fromFirebaseSeatMap,
|
|
459
789
|
fromFirebaseState,
|
|
460
790
|
generateId,
|
|
791
|
+
getFirebaseDatabase,
|
|
461
792
|
getSelectedSeats,
|
|
462
793
|
importConfigFromJSON,
|
|
794
|
+
initializeFirebaseForViewer,
|
|
795
|
+
isFirebaseInitialized,
|
|
463
796
|
toFirebaseSeatMap,
|
|
464
797
|
toFirebaseState,
|
|
465
798
|
updateConfigTimestamp,
|
|
799
|
+
useFirebaseConfig,
|
|
800
|
+
useFirebaseSeatStates,
|
|
801
|
+
useRealtimeSeatMap,
|
|
466
802
|
validateSeatMapConfig
|
|
467
803
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zonetrix/shared",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.3",
|
|
4
4
|
"description": "Shared types and utilities for seat-map-studio packages",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
],
|
|
28
28
|
"author": "",
|
|
29
29
|
"license": "MIT",
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"firebase": "^11.0.0 || ^12.0.0",
|
|
32
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
33
|
+
},
|
|
30
34
|
"devDependencies": {
|
|
31
35
|
"tsup": "^8.0.1",
|
|
32
36
|
"typescript": "^5.8.3"
|