diff --git a/src/App.js b/src/App.js index 2a5c949..9e78de7 100644 --- a/src/App.js +++ b/src/App.js @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef, useMemo } from 'react'; import { initializeApp } from './storage'; import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from './storage'; -import { getFirestore, doc, setDoc, getDoc, getDocs, addDoc, collection, onSnapshot, updateDoc, deleteDoc, query, orderBy, limit, writeBatch, getStorage } from './storage'; +import { getFirestore, doc, setDoc, addDoc, collection, onSnapshot, updateDoc, deleteDoc, query, orderBy, limit, writeBatch, getStorage } from './storage'; import { PlusCircle, Users, Swords, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, UserCheck, UserX, HeartCrack, HeartPulse, Zap, EyeOff, ExternalLink, AlertTriangle, @@ -95,6 +95,7 @@ const PUBLIC_DATA_PATH = `artifacts/${APP_ID}/public/data`; let app; let db; let auth; +let storage; // Initialize Firebase const initializeFirebase = () => { @@ -110,6 +111,7 @@ const initializeFirebase = () => { app = initializeApp(firebaseConfig); db = getFirestore(app); auth = getAuth(app); + storage = getStorage(); return true; } catch (error) { console.error("Error initializing Firebase:", error); @@ -162,7 +164,7 @@ const logAction = async (message, context = {}, undoData = null) => { try { const entry = { timestamp: Date.now(), message, ...context }; if (undoData) entry.undo = undoData; - await addDoc(collection(db, getPath.logs()), entry); + await storage.addDoc(getPath.logs(), entry); } catch (err) { console.error('Error writing log:', err); } @@ -565,7 +567,7 @@ function CharacterManager({ campaignId, campaignCharacters }) { }; try { - await updateDoc(doc(db, getPath.campaign(campaignId)), { + await storage.updateDoc(getPath.campaign(campaignId), { players: [...campaignCharacters, newCharacter] }); setCharacterName(''); @@ -601,7 +603,7 @@ function CharacterManager({ campaignId, campaignCharacters }) { ); try { - await updateDoc(doc(db, getPath.campaign(campaignId)), { players: updatedCharacters }); + await storage.updateDoc(getPath.campaign(campaignId), { players: updatedCharacters }); setEditingCharacter(null); } catch (err) { console.error("Error updating character:", err); @@ -620,7 +622,7 @@ function CharacterManager({ campaignId, campaignCharacters }) { const updatedCharacters = campaignCharacters.filter(c => c.id !== itemToDelete.id); try { - await updateDoc(doc(db, getPath.campaign(campaignId)), { players: updatedCharacters }); + await storage.updateDoc(getPath.campaign(campaignId), { players: updatedCharacters }); } catch (err) { console.error("Error deleting character:", err); alert("Failed to delete character. Please try again."); @@ -876,7 +878,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { }; try { - await updateDoc(doc(db, encounterPath), { + await storage.updateDoc(encounterPath, { participants: [...participants, newParticipant] }); logAction(`${nameToAdd} added to encounter (Initiative: ${finalInitiative})`, { encounterName: encounter.name }, { @@ -941,7 +943,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { } try { - await updateDoc(doc(db, encounterPath), { + await storage.updateDoc(encounterPath, { participants: [...participants, ...newParticipants] }); console.log(`Added ${newParticipants.length} characters to the encounter.`); @@ -959,7 +961,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { ); try { - await updateDoc(doc(db, encounterPath), { participants: updatedParticipants }); + await storage.updateDoc(encounterPath, { participants: updatedParticipants }); setEditingParticipant(null); } catch (err) { console.error("Error updating participant:", err); @@ -988,7 +990,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { }; try { - await updateDoc(doc(db, encounterPath), { + await storage.updateDoc(encounterPath, { participants: updatedParticipants, ...computeTurnOrderAfterRemoval(encounter, itemToDelete.id, updatedParticipants) }); @@ -1018,7 +1020,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { : computeTurnOrderAfterRemoval(encounter, participantId, updatedParticipants); try { - await updateDoc(doc(db, encounterPath), { participants: updatedParticipants, ...turnUpdates }); + await storage.updateDoc(encounterPath, { participants: updatedParticipants, ...turnUpdates }); logAction(`${participant.name} ${newIsActive ? 'reactivated' : 'deactivated'}`, { encounterName: encounter.name }, { encounterPath, updates: { @@ -1102,7 +1104,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { }; try { - await updateDoc(doc(db, encounterPath), { participants: updatedParticipants, ...turnUpdates }); + await storage.updateDoc(encounterPath, { participants: updatedParticipants, ...turnUpdates }); setHpChangeValues(prev => ({ ...prev, [participantId]: '' })); const hpLine = `${participant.currentHp} → ${newHp} HP`; const deathSuffix = (isDead && !wasDead) ? (participant.type === 'character' ? ' — Unconscious' : ' — Defeated') : ''; @@ -1133,13 +1135,13 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { ); try { - await updateDoc(doc(db, encounterPath), { participants: updatedParticipants }); + await storage.updateDoc(encounterPath, { participants: updatedParticipants }); // Wait for animation to complete on player display (2 seconds) then remove participant setTimeout(async () => { const finalParticipants = participants.filter(p => p.id !== participantId); try { - await updateDoc(doc(db, encounterPath), { participants: finalParticipants }); + await storage.updateDoc(encounterPath, { participants: finalParticipants }); } catch (err) { console.error("Error removing dead participant:", err); } @@ -1154,7 +1156,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { ); try { - await updateDoc(doc(db, encounterPath), { participants: updatedParticipants }); + await storage.updateDoc(encounterPath, { participants: updatedParticipants }); } catch (err) { console.error("Error updating death saves:", err); } @@ -1175,7 +1177,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { return { ...p, conditions: next }; }); try { - await updateDoc(doc(db, encounterPath), { participants: updatedParticipants }); + await storage.updateDoc(encounterPath, { participants: updatedParticipants }); const cond = CONDITIONS.find(c => c.id === conditionId); const condLabel = cond ? `${cond.label} ${cond.emoji}` : conditionId; logAction(`${participant.name} ${wasActive ? 'lost' : 'gained'} ${condLabel}`, { encounterName: encounter.name }, { @@ -1228,7 +1230,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { currentParticipants.splice(targetIndex, 0, removedItem); try { - await updateDoc(doc(db, encounterPath), { participants: currentParticipants }); + await storage.updateDoc(encounterPath, { participants: currentParticipants }); } catch (err) { console.error("Error reordering participants:", err); } @@ -1600,7 +1602,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) { const handleToggleHidePlayerHp = async () => { if (!db) return; try { - await setDoc(doc(db, getPath.activeDisplay()), { hidePlayerHp: !hidePlayerHp }, { merge: true }); + await storage.setDoc(getPath.activeDisplay(), { hidePlayerHp: !hidePlayerHp }, { merge: true }); } catch (err) { console.error("Error toggling hidePlayerHp:", err); } @@ -1621,7 +1623,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) { const sortedParticipants = sortParticipantsByInitiative(activeParticipants, encounter.participants); try { - await updateDoc(doc(db, encounterPath), { + await storage.updateDoc(encounterPath, { isStarted: true, isPaused: false, round: 1, @@ -1629,7 +1631,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) { turnOrderIds: sortedParticipants.map(p => p.id) }); - await setDoc(doc(db, getPath.activeDisplay()), { + await storage.setDoc(getPath.activeDisplay(), { activeCampaignId: campaignId, activeEncounterId: encounter.id }, { merge: true }); @@ -1664,7 +1666,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) { } try { - await updateDoc(doc(db, encounterPath), { + await storage.updateDoc(encounterPath, { isPaused: newPausedState, turnOrderIds: newTurnOrderIds }); @@ -1690,7 +1692,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) { if (activePsInOrder.length === 0) { alert("No active participants left."); - await updateDoc(doc(db, encounterPath), { + await storage.updateDoc(encounterPath, { isStarted: false, isPaused: false, currentTurnParticipantId: null, @@ -1730,7 +1732,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) { if (!nextParticipant) return; try { - await updateDoc(doc(db, encounterPath), { + await storage.updateDoc(encounterPath, { currentTurnParticipantId: nextParticipant.id, round: nextRound, turnOrderIds: newTurnOrderIds, @@ -1752,7 +1754,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) { if (!db) return; try { - await updateDoc(doc(db, encounterPath), { + await storage.updateDoc(encounterPath, { isStarted: false, isPaused: false, currentTurnParticipantId: null, @@ -1760,7 +1762,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) { turnOrderIds: [] }); - await setDoc(doc(db, getPath.activeDisplay()), { + await storage.setDoc(getPath.activeDisplay(), { activeCampaignId: null, activeEncounterId: null }, { merge: true }); @@ -1920,7 +1922,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac const newEncounterId = generateId(); try { - await setDoc(doc(db, getPath.encounters(campaignId), newEncounterId), { + await storage.setDoc(`${getPath.encounters(campaignId)}/${newEncounterId}`, { name: name.trim(), createdAt: new Date().toISOString(), participants: [], @@ -1949,14 +1951,14 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac const encounterId = itemToDelete.id; try { - await deleteDoc(doc(db, getPath.encounter(campaignId, encounterId))); + await storage.deleteDoc(getPath.encounter(campaignId, encounterId)); if (selectedEncounterId === encounterId) { setSelectedEncounterId(null); } if (activeDisplayInfo && activeDisplayInfo.activeEncounterId === encounterId) { - await updateDoc(doc(db, getPath.activeDisplay()), { + await storage.updateDoc(getPath.activeDisplay(), { activeCampaignId: null, activeEncounterId: null }); @@ -1978,12 +1980,12 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac const currentActiveEncounter = activeDisplayInfo?.activeEncounterId; if (currentActiveCampaign === campaignId && currentActiveEncounter === encounterId) { - await setDoc(doc(db, getPath.activeDisplay()), { + await storage.setDoc(getPath.activeDisplay(), { activeCampaignId: null, activeEncounterId: null, }, { merge: true }); } else { - await setDoc(doc(db, getPath.activeDisplay()), { + await storage.setDoc(getPath.activeDisplay(), { activeCampaignId: campaignId, activeEncounterId: encounterId, }, { merge: true }); @@ -2139,8 +2141,8 @@ function AdminView({ userId }) { let encounterCount = 0; try { - const encountersSnapshot = await getDocs(collection(db, getPath.encounters(campaign.id))); - encounterCount = encountersSnapshot.size; + const encounters = await storage.getCollection(getPath.encounters(campaign.id)); + encounterCount = encounters.length; } catch (err) { console.error(`Failed to fetch encounters for campaign ${campaign.id}:`, err); } @@ -2180,7 +2182,7 @@ function AdminView({ userId }) { const newCampaignId = generateId(); try { - await setDoc(doc(db, getPath.campaign(newCampaignId)), { + await storage.setDoc(getPath.campaign(newCampaignId), { name: name.trim(), playerDisplayBackgroundUrl: backgroundUrl.trim() || '', ownerId: userId, @@ -2208,23 +2210,23 @@ function AdminView({ userId }) { try { const encountersPath = getPath.encounters(campaignId); - const encountersSnapshot = await getDocs(collection(db, encountersPath)); - const batch = writeBatch(db); + const encounters = await storage.getCollection(encountersPath); + const deleteOps = encounters.map(e => { + const id = e.id || e.path?.split('/').pop(); + return { type: 'delete', path: `${encountersPath}/${id}` }; + }); + if (deleteOps.length > 0) await storage.batchWrite(deleteOps); - encountersSnapshot.docs.forEach(encounterDoc => batch.delete(encounterDoc.ref)); - await batch.commit(); - - await deleteDoc(doc(db, getPath.campaign(campaignId))); + await storage.deleteDoc(getPath.campaign(campaignId)); if (selectedCampaignId === campaignId) { setSelectedCampaignId(null); } - const activeDisplayRef = doc(db, getPath.activeDisplay()); - const activeDisplaySnap = await getDoc(activeDisplayRef); + const activeDisplay = await storage.getDoc(getPath.activeDisplay()); - if (activeDisplaySnap.exists() && activeDisplaySnap.data().activeCampaignId === campaignId) { - await updateDoc(activeDisplayRef, { + if (activeDisplay && activeDisplay.activeCampaignId === campaignId) { + await storage.updateDoc(getPath.activeDisplay(), { activeCampaignId: null, activeEncounterId: null }); @@ -2672,13 +2674,14 @@ function LogsView() { const [undoingId, setUndoingId] = useState(null); const handleClearLogs = async () => { - if (!db) return; try { - const snapshot = await getDocs(collection(db, getPath.logs())); - if (!snapshot.empty) { - const batch = writeBatch(db); - snapshot.docs.forEach(d => batch.delete(d.ref)); - await batch.commit(); + const logs = await storage.getCollection(getPath.logs()); + if (logs.length > 0) { + const ops = logs.map(l => { + const id = l.id || l.path?.split('/').pop(); + return { type: 'delete', path: `${getPath.logs()}/${id}` }; + }); + await storage.batchWrite(ops); } } catch (err) { console.error('Error clearing logs:', err); @@ -2690,8 +2693,8 @@ function LogsView() { if (!db || !entry.undo) return; setUndoingId(entry.id); try { - await updateDoc(doc(db, entry.undo.encounterPath), entry.undo.updates); - await updateDoc(doc(db, getPath.logs(), entry.id), { undone: true }); + await storage.updateDoc(entry.undo.encounterPath, entry.undo.updates); + await storage.updateDoc(`${getPath.logs()}/${entry.id}`, { undone: true }); } catch (err) { console.error('Error undoing action:', err); alert('Failed to roll back. The encounter may have changed or no longer exists.'); diff --git a/src/storage/firebase.js b/src/storage/firebase.js index 70fb4dc..b019a5f 100644 --- a/src/storage/firebase.js +++ b/src/storage/firebase.js @@ -11,8 +11,8 @@ import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth'; import { - getFirestore, doc, setDoc, updateDoc, deleteDoc, addDoc, collection, - onSnapshot, query, orderBy, limit, writeBatch, serverTimestamp, + getFirestore, doc, setDoc, getDoc as getDocReal, getDocs as getDocsReal, addDoc, collection, + onSnapshot, updateDoc, deleteDoc, query, orderBy, limit, writeBatch, serverTimestamp, } from 'firebase/firestore'; // Path helpers mirror App.js getPath object. @@ -74,7 +74,7 @@ export function createFirebaseStorage() { return { async getDoc(path) { - const snap = await import('firebase/firestore').then(({ getDoc: gd, doc: d }) => gd(d(db, path))); + const snap = await getDocReal(doc(db, path)); return snap.exists() ? { id: snap.id, ...snap.data() } : null; }, @@ -96,7 +96,7 @@ export function createFirebaseStorage() { }, async getCollection(collectionPath) { - const snapshot = await import('firebase/firestore').then(({ getDocs: gd, collection: c }) => gd(c(db, collectionPath))); + const snapshot = await getDocsReal(collection(db, collectionPath)); return snapshot.docs.map(d => ({ id: d.id, ...d.data() })); },