From 893fe49ccb1e89890f37b8f76311c99d381ac827 Mon Sep 17 00:00:00 2001 From: robert Date: Mon, 26 May 2025 22:42:37 -0400 Subject: [PATCH] Code clean up and refactor. --- src/App.js | 94 ++++++++++++++++++++++++------------------------------ 1 file changed, 41 insertions(+), 53 deletions(-) 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)
-
{ e.preventDefault(); handleAddParticipant(); }} className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 p-3 bg-slate-700 rounded"> + { e.preventDefault(); handleAddParticipant(); }} className="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2 mb-4 p-3 bg-slate-700 rounded">
setSelectedCharacterId(e.target.value)} className="mt-1 w-full px-3 py-2 bg-slate-600 border-slate-500 rounded text-white">{campaignCharacters.map(c => )}
)} -
setMaxHp(e.target.value)} className="mt-1 w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm text-white" />
+
setMaxHp(e.target.value)} className="mt-1 w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm text-white" />
{lastRollDetails && ( @@ -788,6 +773,9 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { ); } +// ... (EditParticipantModal, InitiativeControls, DisplayView, Modal, Icons) +// The rest of the components are assumed to be the same as v0.1.25 for this update. + function EditParticipantModal({ participant, onClose, onSave }) { const [name, setName] = useState(participant.name); const [initiative, setInitiative] = useState(participant.initiative);