@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.d.mts +261 -2
- package/dist/index.d.ts +261 -2
- package/dist/index.js +347 -3
- package/dist/index.mjs +340 -3
- package/package.json +5 -1
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 (!
|
|
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.
|
|
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"
|