@zonetrix/shared 2.4.1 → 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.mjs CHANGED
@@ -59,6 +59,7 @@ function validateSeatMapConfig(config) {
59
59
  if (!config.colors) {
60
60
  warnings.push("Missing color configuration");
61
61
  } else {
62
+ const colors = config.colors;
62
63
  const requiredColors = [
63
64
  "canvasBackground",
64
65
  "stageColor",
@@ -69,7 +70,7 @@ function validateSeatMapConfig(config) {
69
70
  "gridLines"
70
71
  ];
71
72
  requiredColors.forEach((color) => {
72
- if (!config.colors[color] || typeof config.colors[color] !== "string") {
73
+ if (!colors[color] || typeof colors[color] !== "string") {
73
74
  warnings.push(`Missing or invalid color: ${color}`);
74
75
  }
75
76
  });
@@ -129,10 +130,10 @@ function validateSeatMapConfig(config) {
129
130
  };
130
131
  }
131
132
  function isValidSeatState(state) {
132
- return ["available", "reserved", "selected", "unavailable"].includes(state);
133
+ return typeof state === "string" && ["available", "reserved", "selected", "unavailable"].includes(state);
133
134
  }
134
135
  function isValidSeatShape(shape) {
135
- return ["circle", "square", "rounded-square"].includes(shape);
136
+ return typeof shape === "string" && ["circle", "square", "rounded-square"].includes(shape);
136
137
  }
137
138
  function generateId(prefix = "obj") {
138
139
  return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
@@ -437,6 +438,335 @@ function createIndexUpdates(seatMapId, eventId, subEventId) {
437
438
  [`indexes/by_sub_event/${subEventId}/${seatMapId}`]: true
438
439
  };
439
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
+ }
440
770
  export {
441
771
  DEFAULT_COLORS,
442
772
  FirebasePaths,
@@ -444,6 +774,7 @@ export {
444
774
  calculateAvailableSeats,
445
775
  calculateCapacity,
446
776
  calculateSeatPrice,
777
+ clearFirebaseInstance,
447
778
  cloneConfig,
448
779
  createDefaultConfig,
449
780
  createIndexUpdates,
@@ -457,10 +788,16 @@ export {
457
788
  fromFirebaseSeatMap,
458
789
  fromFirebaseState,
459
790
  generateId,
791
+ getFirebaseDatabase,
460
792
  getSelectedSeats,
461
793
  importConfigFromJSON,
794
+ initializeFirebaseForViewer,
795
+ isFirebaseInitialized,
462
796
  toFirebaseSeatMap,
463
797
  toFirebaseState,
464
798
  updateConfigTimestamp,
799
+ useFirebaseConfig,
800
+ useFirebaseSeatStates,
801
+ useRealtimeSeatMap,
465
802
  validateSeatMapConfig
466
803
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zonetrix/shared",
3
- "version": "2.4.1",
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"