@@ -64,16 +64,25 @@ let participantSubmissions = []; // Track this participant's submissions
6464let lastSubmitTime = 0 ;
6565
6666/* =============================================================
67- STORAGE-BASED REAL-TIME SYNC
68- Uses localStorage + storage events for cross-tab communication.
69- Replace with WebSocket/Firebase for production multi-device use.
67+ REAL-TIME SYNC LAYER
68+ Uses Firebase Realtime Database for cross-device sync.
69+ Falls back to localStorage + BroadcastChannel if Firebase
70+ is not configured.
7071 ============================================================= */
7172const STORAGE_KEY_PREFIX = 'wc_' ;
73+ let firebaseListener = null ; // Track active Firebase listener
7274
7375function storageKey ( roomId , key ) {
7476 return `${ STORAGE_KEY_PREFIX } ${ roomId } _${ key } ` ;
7577}
7678
79+ // --- Firebase sync ---
80+
81+ function getRoomRef ( roomId ) {
82+ if ( ! firebaseDB ) return null ;
83+ return firebaseDB . ref ( 'rooms/' + roomId ) ;
84+ }
85+
7786function saveRoomState ( ) {
7887 if ( ! state . roomId ) return ;
7988 const payload = {
@@ -84,19 +93,78 @@ function saveRoomState() {
8493 participants : [ ...state . participants ] ,
8594 updatedAt : Date . now ( )
8695 } ;
96+
97+ // Always save to localStorage (local fallback)
8798 localStorage . setItem ( storageKey ( state . roomId , 'state' ) , JSON . stringify ( payload ) ) ;
99+
100+ // Save to Firebase if available
101+ const ref = getRoomRef ( state . roomId ) ;
102+ if ( ref ) {
103+ ref . set ( payload ) . catch ( e => console . warn ( '[WordCloud] Firebase write error:' , e . message ) ) ;
104+ }
88105}
89106
90107function loadRoomState ( roomId ) {
108+ // Try localStorage first (synchronous, for init)
91109 const raw = localStorage . getItem ( storageKey ( roomId , 'state' ) ) ;
92110 if ( ! raw ) return null ;
93111 try { return JSON . parse ( raw ) ; }
94112 catch { return null ; }
95113}
96114
97- // Listen for cross-tab updates
115+ async function loadRoomStateFromFirebase ( roomId ) {
116+ const ref = getRoomRef ( roomId ) ;
117+ if ( ! ref ) return null ;
118+ try {
119+ const snapshot = await ref . once ( 'value' ) ;
120+ return snapshot . val ( ) ;
121+ } catch ( e ) {
122+ console . warn ( '[WordCloud] Firebase read error:' , e . message ) ;
123+ return null ;
124+ }
125+ }
126+
127+ function subscribeToRoom ( roomId ) {
128+ // Unsubscribe from previous room
129+ if ( firebaseListener ) {
130+ firebaseListener . off ( ) ;
131+ firebaseListener = null ;
132+ }
133+
134+ const ref = getRoomRef ( roomId ) ;
135+ if ( ! ref ) return ;
136+
137+ firebaseListener = ref ;
138+ ref . on ( 'value' , ( snapshot ) => {
139+ const data = snapshot . val ( ) ;
140+ if ( ! data ) return ;
141+
142+ // Update local state from Firebase
143+ state . question = data . question || state . question ;
144+ state . submissions = data . submissions || [ ] ;
145+ state . locked = data . locked || false ;
146+ state . allowDuplicates = data . allowDuplicates || false ;
147+ state . participants = new Set ( data . participants || [ ] ) ;
148+
149+ // Also update localStorage cache
150+ localStorage . setItem ( storageKey ( roomId , 'state' ) , JSON . stringify ( data ) ) ;
151+
152+ // Re-render
153+ const facilView = document . getElementById ( 'facilitator-view' ) ;
154+ const partView = document . getElementById ( 'participant-view' ) ;
155+ if ( facilView && ! facilView . classList . contains ( 'hidden' ) ) {
156+ renderCurrentView ( ) ;
157+ }
158+ if ( partView && ! partView . classList . contains ( 'hidden' ) ) {
159+ renderParticipant ( ) ;
160+ }
161+ } ) ;
162+ }
163+
164+ // --- localStorage / BroadcastChannel fallback ---
165+
98166window . addEventListener ( 'storage' , ( e ) => {
99- if ( ! state . roomId ) return ;
167+ if ( ! state . roomId || firebaseDB ) return ; // Skip if Firebase is active
100168 const key = storageKey ( state . roomId , 'state' ) ;
101169 if ( e . key === key && e . newValue ) {
102170 try {
@@ -111,11 +179,11 @@ window.addEventListener('storage', (e) => {
111179 }
112180} ) ;
113181
114- // BroadcastChannel for same-origin real-time
115182let bc ;
116183try {
117184 bc = new BroadcastChannel ( 'wordcloud_sync' ) ;
118185 bc . onmessage = ( e ) => {
186+ if ( firebaseDB ) return ; // Skip if Firebase is active
119187 if ( e . data . roomId !== state . roomId ) return ;
120188 if ( e . data . type === 'state_update' ) {
121189 state . question = e . data . state . question ;
@@ -130,7 +198,8 @@ try {
130198
131199function broadcastState ( ) {
132200 saveRoomState ( ) ;
133- if ( bc ) {
201+ // BroadcastChannel for same-browser fallback
202+ if ( bc && ! firebaseDB ) {
134203 bc . postMessage ( {
135204 type : 'state_update' ,
136205 roomId : state . roomId ,
@@ -185,11 +254,12 @@ function init() {
185254 }
186255}
187256
188- function initFacilitator ( ) {
257+ async function initFacilitator ( ) {
189258 // Check for existing room or create new
190259 const savedRoom = sessionStorage . getItem ( 'wc_host_room' ) ;
191260 if ( savedRoom ) {
192- const existing = loadRoomState ( savedRoom ) ;
261+ // Try Firebase first, then localStorage
262+ const existing = ( firebaseDB ? await loadRoomStateFromFirebase ( savedRoom ) : null ) || loadRoomState ( savedRoom ) ;
193263 if ( existing ) {
194264 state . roomId = savedRoom ;
195265 state . question = existing . question ;
@@ -204,10 +274,15 @@ function initFacilitator() {
204274 createNewRoom ( ) ;
205275 }
206276
277+ // Subscribe to Firebase real-time updates
278+ subscribeToRoom ( state . roomId ) ;
279+
207280 document . getElementById ( 'facilitator-view' ) . classList . remove ( 'hidden' ) ;
208281 renderFacilitator ( ) ;
209- // Poll for updates (fallback for storage event edge cases)
210- setInterval ( pollUpdates , 800 ) ;
282+ // Poll for updates (fallback when Firebase is not available)
283+ if ( ! firebaseDB ) {
284+ setInterval ( pollUpdates , 800 ) ;
285+ }
211286}
212287
213288function createNewRoom ( ) {
@@ -217,23 +292,31 @@ function createNewRoom() {
217292 state . participants = new Set ( ) ;
218293 sessionStorage . setItem ( 'wc_host_room' , state . roomId ) ;
219294 saveRoomState ( ) ;
295+ // Subscribe to the new room in Firebase
296+ subscribeToRoom ( state . roomId ) ;
220297}
221298
222- function initParticipant ( roomId ) {
299+ async function initParticipant ( roomId ) {
223300 state . roomId = roomId ;
224- const existing = loadRoomState ( roomId ) ;
301+
302+ // Try Firebase first, then localStorage
303+ const existing = ( firebaseDB ? await loadRoomStateFromFirebase ( roomId ) : null ) || loadRoomState ( roomId ) ;
225304 if ( existing ) {
226305 state . question = existing . question ;
227306 state . locked = existing . locked ;
228307 state . allowDuplicates = existing . allowDuplicates ;
229308 state . submissions = existing . submissions || [ ] ;
309+ state . participants = new Set ( existing . participants || [ ] ) ;
230310 }
231311
232312 // Register participant
233313 const pid = getParticipantId ( ) ;
234314 state . participants . add ( pid ) ;
235315 broadcastState ( ) ;
236316
317+ // Subscribe to Firebase real-time updates
318+ subscribeToRoom ( state . roomId ) ;
319+
237320 document . getElementById ( 'participant-view' ) . classList . remove ( 'hidden' ) ;
238321 renderParticipant ( ) ;
239322
@@ -244,16 +327,18 @@ function initParticipant(roomId) {
244327 } ) ;
245328 pInput . focus ( ) ;
246329
247- // Poll for question/lock changes
248- setInterval ( ( ) => {
249- const data = loadRoomState ( roomId ) ;
250- if ( data ) {
251- state . question = data . question ;
252- state . locked = data . locked ;
253- state . allowDuplicates = data . allowDuplicates ;
254- renderParticipant ( ) ;
255- }
256- } , 1500 ) ;
330+ // Poll for question/lock changes (fallback when Firebase is not available)
331+ if ( ! firebaseDB ) {
332+ setInterval ( ( ) => {
333+ const data = loadRoomState ( roomId ) ;
334+ if ( data ) {
335+ state . question = data . question ;
336+ state . locked = data . locked ;
337+ state . allowDuplicates = data . allowDuplicates ;
338+ renderParticipant ( ) ;
339+ }
340+ } , 1500 ) ;
341+ }
257342}
258343
259344/* =============================================================
0 commit comments