M2: refactor all firebase write sites to storage adapter

- 37 call sites: setDoc/updateDoc/deleteDoc/addDoc/getDocs/writeBatch -> storage.*
- adapter wraps SDK, path-string interface
- storage instance app-wide (getStorage)
- firebase.js: static imports (getDoc/getDocs alias), no dynamic import

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