diff --git a/src/App.js b/src/App.js
index 3bdbca4..880f427 100644
--- a/src/App.js
+++ b/src/App.js
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useMemo } from 'react';
import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth';
import { getFirestore, doc, setDoc, getDoc, getDocs, collection, onSnapshot, updateDoc, deleteDoc, query, writeBatch } from 'firebase/firestore';
-import { PlusCircle, Users, Swords, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, UserCheck, UserX, HeartCrack, HeartPulse, Zap, EyeOff, ExternalLink, AlertTriangle, Play as PlayIcon, Pause as PauseIcon, SkipForward as SkipForwardIcon, StopCircle as StopCircleIcon, Users2, Dices } from 'lucide-react';
+import { PlusCircle, Users, Swords, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, UserCheck, UserX, HeartCrack, HeartPulse, Zap, EyeOff, ExternalLink, AlertTriangle, Play as PlayIcon, Pause as PauseIcon, SkipForward as SkipForwardIcon, StopCircle as StopCircleIcon, Users2, Dices } from 'lucide-react'; // ImageIcon was already removed, ensuring it stays removed.
// --- Firebase Configuration ---
const firebaseConfig = {
@@ -88,6 +88,8 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
+
+ // Memoize the stringified queryConstraints. This is the key to stabilizing the effect's dependency.
const queryString = useMemo(() => JSON.stringify(queryConstraints), [queryConstraints]);
useEffect(() => {
@@ -99,7 +101,10 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) {
}
setIsLoading(true);
setError(null);
+
+ // queryConstraints is used here to build the query.
const q = query(collection(db, collectionPath), ...queryConstraints);
+
const unsubscribe = onSnapshot(q, (snapshot) => {
const items = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
setData(items);
@@ -110,9 +115,14 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) {
setIsLoading(false);
setData([]);
});
+
return () => unsubscribe();
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // The effect depends on collectionPath and the memoized queryString.
+ // This prevents re-running the effect if queryConstraints is a new array reference
+ // but its content (and thus queryString) is the same.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [collectionPath, queryString]);
+
return { data, isLoading, error };
}
@@ -214,13 +224,14 @@ function App() {
{!isAuthReady && !error &&
Authenticating...
}
);
}
// --- Confirmation Modal Component ---
+// ... (ConfirmationModal remains the same)
function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
if (!isOpen) return null;
return (
@@ -241,6 +252,7 @@ function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
}
// --- Admin View Component ---
+// ... (AdminView remains the same)
function AdminView({ userId }) {
const { data: campaignsData, isLoading: isLoadingCampaigns } = useFirestoreCollection(getCampaignsCollectionPath());
const { data: initialActiveInfoData } = useFirestoreDocument(getActiveDisplayDocPath());
@@ -332,6 +344,7 @@ function AdminView({ userId }) {
setSelectedCampaignId(campaign.id)} className={cardClasses} style={cardStyle}>
{campaign.name}
+ {/* ImageIcon display removed from here */}
);
@@ -353,6 +366,10 @@ function AdminView({ userId }) {
);
}
+// --- CreateCampaignForm, CharacterManager, EncounterManager, CreateEncounterForm, ParticipantManager, EditParticipantModal, InitiativeControls, DisplayView, Modal, Icons ---
+// These components are identical to the previous version (v0.1.24) and are included below for completeness.
+// The changes for ESLint warnings were primarily in the imports at the top of App.js and in the useFirestoreCollection hook.
+
function CreateCampaignForm({ onCreate, onCancel }) {
const [name, setName] = useState('');
const [backgroundUrl, setBackgroundUrl] = useState('');
@@ -375,6 +392,7 @@ function CreateCampaignForm({ onCreate, onCancel }) {
);
}
+
function CharacterManager({ campaignId, campaignCharacters }) {
const [characterName, setCharacterName] = useState('');
const [defaultMaxHp, setDefaultMaxHp] = useState(10);
@@ -382,49 +400,24 @@ function CharacterManager({ campaignId, campaignCharacters }) {
const [editingCharacter, setEditingCharacter] = useState(null);
const [showDeleteCharConfirm, setShowDeleteCharConfirm] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null);
-
const handleAddCharacter = async () => {
if (!db ||!characterName.trim() || !campaignId) return;
const hp = parseInt(defaultMaxHp, 10);
const initMod = parseInt(defaultInitMod, 10);
- if (isNaN(hp) || hp <= 0) {
- alert("Please enter a valid positive number for Default Max HP.");
- return;
- }
- if (isNaN(initMod)) {
- alert("Please enter a valid number for Default Initiative Modifier.");
- return;
- }
+ if (isNaN(hp) || hp <= 0) { alert("Please enter a valid positive number for Default Max HP."); return; }
+ if (isNaN(initMod)) { alert("Please enter a valid number for Default Initiative Modifier."); return; }
const newCharacter = { id: generateId(), name: characterName.trim(), defaultMaxHp: hp, defaultInitMod: initMod };
- try {
- await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: [...campaignCharacters, newCharacter] });
- setCharacterName('');
- setDefaultMaxHp(10);
- setDefaultInitMod(0);
- } catch (err) { console.error("Error adding character:", err); }
+ try { await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: [...campaignCharacters, newCharacter] }); setCharacterName(''); setDefaultMaxHp(10); setDefaultInitMod(0); } catch (err) { console.error("Error adding character:", err); }
};
-
const handleUpdateCharacter = async (characterId, newName, newDefaultMaxHp, newDefaultInitMod) => {
if (!db ||!newName.trim() || !campaignId) return;
const hp = parseInt(newDefaultMaxHp, 10);
const initMod = parseInt(newDefaultInitMod, 10);
- if (isNaN(hp) || hp <= 0) {
- alert("Please enter a valid positive number for Default Max HP.");
- setEditingCharacter(null);
- return;
- }
- if (isNaN(initMod)) {
- alert("Please enter a valid number for Default Initiative Modifier.");
- setEditingCharacter(null);
- return;
- }
+ if (isNaN(hp) || hp <= 0) { alert("Please enter a valid positive number for Default Max HP."); setEditingCharacter(null); return; }
+ if (isNaN(initMod)) { alert("Please enter a valid number for Default Initiative Modifier."); setEditingCharacter(null); return; }
const updatedCharacters = campaignCharacters.map(c => c.id === characterId ? { ...c, name: newName.trim(), defaultMaxHp: hp, defaultInitMod: initMod } : c);
- try {
- await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: updatedCharacters });
- setEditingCharacter(null);
- } catch (err) { console.error("Error updating character:", err); }
+ try { await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: updatedCharacters }); setEditingCharacter(null); } catch (err) { console.error("Error updating character:", err); }
};
-
const requestDeleteCharacter = (characterId, charName) => { setItemToDelete({ id: characterId, name: charName, type: 'character' }); setShowDeleteCharConfirm(true); };
const confirmDeleteCharacter = async () => {
if (!db || !itemToDelete || itemToDelete.type !== 'character') return;
@@ -433,7 +426,6 @@ function CharacterManager({ campaignId, campaignCharacters }) {
try { await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: updatedCharacters }); } catch (err) { console.error("Error deleting character:", err); }
setShowDeleteCharConfirm(false); setItemToDelete(null);
};
-
return (
<>
@@ -588,28 +580,22 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
const [participantName, setParticipantName] = useState('');
const [participantType, setParticipantType] = useState('monster');
const [selectedCharacterId, setSelectedCharacterId] = useState('');
- const [monsterInitMod, setMonsterInitMod] = useState(2); // Default monster init mod
+ const [monsterInitMod, setMonsterInitMod] = useState(2);
const [maxHp, setMaxHp] = useState(10);
const [editingParticipant, setEditingParticipant] = useState(null);
const [hpChangeValues, setHpChangeValues] = useState({});
const [draggedItemId, setDraggedItemId] = useState(null);
const [showDeleteParticipantConfirm, setShowDeleteParticipantConfirm] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null);
- const [lastRollDetails, setLastRollDetails] = useState(null); // { name, roll, mod, total }
+ const [lastRollDetails, setLastRollDetails] = useState(null);
const participants = encounter.participants || [];
+ const MONSTER_DEFAULT_INIT_MOD = 2;
useEffect(() => {
if (participantType === 'character' && selectedCharacterId) {
const selectedChar = campaignCharacters.find(c => c.id === selectedCharacterId);
- if (selectedChar && selectedChar.defaultMaxHp) {
- setMaxHp(selectedChar.defaultMaxHp);
- } else {
- setMaxHp(10);
- }
- } else if (participantType === 'monster') {
- setMaxHp(10);
- setMonsterInitMod(2); // Reset monster init mod when switching to monster
- }
+ if (selectedChar && selectedChar.defaultMaxHp) { setMaxHp(selectedChar.defaultMaxHp); } else { setMaxHp(10); }
+ } else if (participantType === 'monster') { setMaxHp(10); setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD); }
}, [selectedCharacterId, participantType, campaignCharacters]);
const handleAddParticipant = async () => {
@@ -628,17 +614,16 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
currentMaxHp = character.defaultMaxHp || currentMaxHp;
modifier = character.defaultInitMod || 0;
finalInitiative = initiativeRoll + modifier;
- } else { // Monster
- modifier = parseInt(monsterInitMod, 10) || 0; // Use state for monster mod
+ } else {
+ modifier = parseInt(monsterInitMod, 10) || 0;
finalInitiative = initiativeRoll + modifier;
}
-
const newParticipant = { id: generateId(), name: nameToAdd, type: participantType, originalCharacterId: participantType === 'character' ? selectedCharacterId : null, initiative: finalInitiative, maxHp: currentMaxHp, currentHp: currentMaxHp, conditions: [], isActive: true, };
try {
await updateDoc(doc(db, encounterPath), { participants: [...participants, newParticipant] });
setLastRollDetails({ name: nameToAdd, roll: initiativeRoll, mod: modifier, total: finalInitiative });
- setTimeout(() => setLastRollDetails(null), 5000); // Clear after 5 seconds
- setParticipantName(''); setMaxHp(10); setSelectedCharacterId(''); setMonsterInitMod(2);
+ setTimeout(() => setLastRollDetails(null), 5000);
+ setParticipantName(''); setMaxHp(10); setSelectedCharacterId(''); setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD);
} catch (err) { console.error("Error adding participant:", err); }
};
@@ -728,7 +713,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
Add All (Roll Init)
-