From ad11bbc648ec105024ec098f0f7df17f6705e26d Mon Sep 17 00:00:00 2001 From: robert Date: Mon, 26 May 2025 21:34:37 -0400 Subject: [PATCH] Fixed linting and pause button. --- src/App.js | 663 ++++++++++++----------------------------------------- 1 file changed, 144 insertions(+), 519 deletions(-) diff --git a/src/App.js b/src/App.js index e1ccd6b..a8b07f1 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 } from 'lucide-react'; // ImageIcon removed +import { PlusCircle, Users, Swords, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, UserCheck, UserX, HeartCrack, HeartPulse, Zap, /* ImageIcon removed */ EyeOff, ExternalLink, AlertTriangle, Play as PlayIcon, Pause as PauseIcon, SkipForward as SkipForwardIcon, StopCircle as StopCircleIcon } from 'lucide-react'; // ImageIcon removed // --- Firebase Configuration --- const firebaseConfig = { @@ -62,10 +62,8 @@ function useFirestoreDocument(docPath) { setError(docPath ? "Firestore not available." : "Document path not provided."); return; } - setIsLoading(true); setError(null); - const docRef = doc(db, docPath); const unsubscribe = onSnapshot(docRef, (docSnap) => { if (docSnap.exists()) { @@ -80,10 +78,8 @@ function useFirestoreDocument(docPath) { setIsLoading(false); setData(null); }); - return () => unsubscribe(); }, [docPath]); - return { data, isLoading, error }; } @@ -91,7 +87,6 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) { const [data, setData] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const queryString = useMemo(() => JSON.stringify(queryConstraints), [queryConstraints]); useEffect(() => { @@ -101,12 +96,9 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) { setError(collectionPath ? "Firestore not available." : "Collection path not provided."); return; } - setIsLoading(true); setError(null); - 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); @@ -117,11 +109,9 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) { setIsLoading(false); setData([]); }); - return () => unsubscribe(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [collectionPath, queryString]); - return { data, isLoading, error }; } @@ -139,14 +129,12 @@ function App() { if (queryParams.get('playerView') === 'true') { setIsPlayerViewOnlyMode(true); } - if (!auth) { setError("Firebase Auth not initialized. Check your Firebase configuration."); setIsLoading(false); setIsAuthReady(false); return; } - const initAuth = async () => { try { const token = window.__initial_auth_token; @@ -160,16 +148,13 @@ function App() { setError("Failed to authenticate. Please try again later."); } }; - const unsubscribe = onAuthStateChanged(auth, (user) => { setUserId(user ? user.uid : null); setIsAuthReady(true); setIsLoading(false); }); initAuth(); - return () => { - unsubscribe(); - }; + return () => unsubscribe(); }, []); if (!db || !auth) { @@ -184,7 +169,6 @@ function App() { ); } - if (isLoading || !isAuthReady) { return (
@@ -200,7 +184,6 @@ function App() { window.open(playerViewUrl, '_blank', 'noopener,noreferrer,width=1024,height=768'); }; - if (isPlayerViewOnlyMode) { return (
@@ -214,13 +197,8 @@ function App() {
-

- TTRPG Initiative Tracker -

+

TTRPG Initiative Tracker

- {/* UID display removed */}
-
{isAuthReady && userId && } {!isAuthReady && !error &&

Authenticating...

}
- TTRPG Initiative Tracker v0.1.25 + TTRPG Initiative Tracker v0.1.25
); } // --- Confirmation Modal Component --- +// ... (ConfirmationModal remains the same) function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) { if (!isOpen) return null; - return (
@@ -255,30 +232,18 @@ function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {

{message || "Are you sure you want to proceed?"}

- - + +
); } - // --- Admin View Component --- function AdminView({ userId }) { const { data: campaignsData, isLoading: isLoadingCampaigns } = useFirestoreCollection(getCampaignsCollectionPath()); const { data: initialActiveInfoData } = useFirestoreDocument(getActiveDisplayDocPath()); - const [campaigns, setCampaigns] = useState([]); const [selectedCampaignId, setSelectedCampaignId] = useState(null); const [showCreateCampaignModal, setShowCreateCampaignModal] = useState(false); @@ -300,7 +265,6 @@ function AdminView({ userId }) { } }, [initialActiveInfoData, campaigns, selectedCampaignId]); - const handleCreateCampaign = async (name, backgroundUrl) => { if (!db || !name.trim()) return; const newCampaignId = generateId(); @@ -333,7 +297,6 @@ function AdminView({ userId }) { await batch.commit(); await deleteDoc(doc(db, getCampaignDocPath(campaignId))); if (selectedCampaignId === campaignId) setSelectedCampaignId(null); - const activeDisplayRef = doc(db, getActiveDisplayDocPath()); const activeDisplaySnap = await getDoc(activeDisplayRef); if (activeDisplaySnap.exists() && activeDisplaySnap.data().activeCampaignId === campaignId) { @@ -363,33 +326,16 @@ function AdminView({ userId }) { {campaigns.length === 0 &&

No campaigns yet.

}
{campaigns.map(campaign => { - const cardStyle = campaign.playerDisplayBackgroundUrl ? { - backgroundImage: `url(${campaign.playerDisplayBackgroundUrl})`, - } : {}; + const cardStyle = campaign.playerDisplayBackgroundUrl ? { backgroundImage: `url(${campaign.playerDisplayBackgroundUrl})`} : {}; const cardClasses = `p-4 rounded-lg shadow-md cursor-pointer transition-all relative overflow-hidden bg-cover bg-center ${selectedCampaignId === campaign.id ? 'ring-4 ring-sky-400' : ''} ${!campaign.playerDisplayBackgroundUrl ? 'bg-slate-700 hover:bg-slate-600' : 'hover:shadow-xl'}`; - return ( -
setSelectedCampaignId(campaign.id)} - className={cardClasses} - style={cardStyle} - > +
setSelectedCampaignId(campaign.id)} className={cardClasses} style={cardStyle}>

{campaign.name}

- {/* ImageIcon removed from here */} - + {/* ImageIcon display removed from here */} +
-
- ); +
); })}
@@ -399,38 +345,22 @@ function AdminView({ userId }) {

Managing: {selectedCampaign.name}


- + )} - setShowDeleteCampaignConfirm(false)} - onConfirm={confirmDeleteCampaign} - title="Delete Campaign?" - message={`Are you sure you want to delete the campaign "${itemToDelete?.name}" and all its encounters? This action cannot be undone.`} - /> + setShowDeleteCampaignConfirm(false)} onConfirm={confirmDeleteCampaign} title="Delete Campaign?" message={`Are you sure you want to delete the campaign "${itemToDelete?.name}" and all its encounters? This action cannot be undone.`}/> ); } -// --- CreateCampaignForm, CharacterManager, EncounterManager, CreateEncounterForm, ParticipantManager, EditParticipantModal, InitiativeControls, DisplayView, Modal, Icons --- -// The rest of the components are identical to the previous version (v0.1.23) 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. +// ... (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. function CreateCampaignForm({ onCreate, onCancel }) { const [name, setName] = useState(''); const [backgroundUrl, setBackgroundUrl] = useState(''); - - const handleSubmit = (e) => { - e.preventDefault(); - onCreate(name, backgroundUrl); - }; - + const handleSubmit = (e) => { e.preventDefault(); onCreate(name, backgroundUrl); }; return (
@@ -449,48 +379,29 @@ function CreateCampaignForm({ onCreate, onCancel }) { ); } - function CharacterManager({ campaignId, campaignCharacters }) { const [characterName, setCharacterName] = useState(''); 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 newCharacter = { id: generateId(), name: characterName.trim() }; - try { - await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: [...campaignCharacters, newCharacter] }); - setCharacterName(''); - } catch (err) { console.error("Error adding character:", err); } + try { await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: [...campaignCharacters, newCharacter] }); setCharacterName(''); } catch (err) { console.error("Error adding character:", err); } }; - const handleUpdateCharacter = async (characterId, newName) => { if (!db ||!newName.trim() || !campaignId) return; const updatedCharacters = campaignCharacters.map(c => c.id === characterId ? { ...c, name: newName.trim() } : 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 requestDeleteCharacter = (characterId, charName) => { setItemToDelete({ id: characterId, name: charName, type: 'character' }); setShowDeleteCharConfirm(true); }; const confirmDeleteCharacter = async () => { if (!db || !itemToDelete || itemToDelete.type !== 'character') return; const characterId = itemToDelete.id; const updatedCharacters = campaignCharacters.filter(c => c.id !== characterId); - try { - await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: updatedCharacters }); - } catch (err) { console.error("Error deleting character:", err); } - setShowDeleteCharConfirm(false); - setItemToDelete(null); + try { await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: updatedCharacters }); } catch (err) { console.error("Error deleting character:", err); } + setShowDeleteCharConfirm(false); setItemToDelete(null); }; - return ( <>
@@ -514,119 +425,55 @@ function CharacterManager({ campaignId, campaignCharacters }) { ))}
- setShowDeleteCharConfirm(false)} - onConfirm={confirmDeleteCharacter} - title="Delete Character?" - message={`Are you sure you want to remove the character "${itemToDelete?.name}" from this campaign?`} - /> + setShowDeleteCharConfirm(false)} onConfirm={confirmDeleteCharacter} title="Delete Character?" message={`Are you sure you want to remove the character "${itemToDelete?.name}" from this campaign?`}/> ); } function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharacters }) { const {data: encountersData, isLoading: isLoadingEncounters } = useFirestoreCollection(campaignId ? getEncountersCollectionPath(campaignId) : null); - const {data: activeDisplayInfo } = useFirestoreDocument(getActiveDisplayDocPath()); - + const {data: activeDisplayInfoFromHook } = useFirestoreDocument(getActiveDisplayDocPath()); const [encounters, setEncounters] = useState([]); const [selectedEncounterId, setSelectedEncounterId] = useState(null); const [showCreateEncounterModal, setShowCreateEncounterModal] = useState(false); const [showDeleteEncounterConfirm, setShowDeleteEncounterConfirm] = useState(false); const [itemToDelete, setItemToDelete] = useState(null); - const selectedEncounterIdRef = useRef(selectedEncounterId); - + useEffect(() => { if(encountersData) setEncounters(encountersData); }, [encountersData]); + useEffect(() => { selectedEncounterIdRef.current = selectedEncounterId; }, [selectedEncounterId]); useEffect(() => { - if(encountersData) setEncounters(encountersData); - }, [encountersData]); - - useEffect(() => { - selectedEncounterIdRef.current = selectedEncounterId; - }, [selectedEncounterId]); - - useEffect(() => { - if (!campaignId) { - setSelectedEncounterId(null); - return; - } + if (!campaignId) { setSelectedEncounterId(null); return; } if (encounters && encounters.length > 0) { const currentSelection = selectedEncounterIdRef.current; if (currentSelection === null || !encounters.some(e => e.id === currentSelection)) { - if (initialActiveEncounterId && encounters.some(e => e.id === initialActiveEncounterId)) { - setSelectedEncounterId(initialActiveEncounterId); - } else if (activeDisplayInfo && activeDisplayInfo.activeCampaignId === campaignId && - encounters.some(e => e.id === activeDisplayInfo.activeEncounterId)) { - setSelectedEncounterId(activeDisplayInfo.activeEncounterId); - } + if (initialActiveEncounterId && encounters.some(e => e.id === initialActiveEncounterId)) { setSelectedEncounterId(initialActiveEncounterId); } + else if (activeDisplayInfoFromHook && activeDisplayInfoFromHook.activeCampaignId === campaignId && encounters.some(e => e.id === activeDisplayInfoFromHook.activeEncounterId)) { setSelectedEncounterId(activeDisplayInfoFromHook.activeEncounterId); } } - } else if (encounters && encounters.length === 0) { - setSelectedEncounterId(null); - } - }, [campaignId, initialActiveEncounterId, activeDisplayInfo, encounters]); - - + } else if (encounters && encounters.length === 0) { setSelectedEncounterId(null); } + }, [campaignId, initialActiveEncounterId, activeDisplayInfoFromHook, encounters]); const handleCreateEncounter = async (name) => { if (!db ||!name.trim() || !campaignId) return; const newEncounterId = generateId(); - try { - await setDoc(doc(db, getEncountersCollectionPath(campaignId), newEncounterId), { - name: name.trim(), createdAt: new Date().toISOString(), participants: [], round: 0, currentTurnParticipantId: null, isStarted: false, - }); - setShowCreateEncounterModal(false); - setSelectedEncounterId(newEncounterId); - } catch (err) { console.error("Error creating encounter:", err); } + try { await setDoc(doc(db, getEncountersCollectionPath(campaignId), newEncounterId), { name: name.trim(), createdAt: new Date().toISOString(), participants: [], round: 0, currentTurnParticipantId: null, isStarted: false, isPaused: false }); setShowCreateEncounterModal(false); setSelectedEncounterId(newEncounterId); } catch (err) { console.error("Error creating encounter:", err); } }; - - const requestDeleteEncounter = (encounterId, encounterName) => { - setItemToDelete({ id: encounterId, name: encounterName, type: 'encounter' }); - setShowDeleteEncounterConfirm(true); - }; - + const requestDeleteEncounter = (encounterId, encounterName) => { setItemToDelete({ id: encounterId, name: encounterName, type: 'encounter' }); setShowDeleteEncounterConfirm(true); }; const confirmDeleteEncounter = async () => { if (!db || !itemToDelete || itemToDelete.type !== 'encounter') return; const encounterId = itemToDelete.id; - try { - await deleteDoc(doc(db, getEncounterDocPath(campaignId, encounterId))); - if (selectedEncounterId === encounterId) setSelectedEncounterId(null); - if (activeDisplayInfo && activeDisplayInfo.activeEncounterId === encounterId) { - await updateDoc(doc(db, getActiveDisplayDocPath()), { activeCampaignId: null, activeEncounterId: null }); - } - } catch (err) { console.error("Error deleting encounter:", err); } - setShowDeleteEncounterConfirm(false); - setItemToDelete(null); + try { await deleteDoc(doc(db, getEncounterDocPath(campaignId, encounterId))); if (selectedEncounterId === encounterId) setSelectedEncounterId(null); if (activeDisplayInfoFromHook && activeDisplayInfoFromHook.activeEncounterId === encounterId) { await updateDoc(doc(db, getActiveDisplayDocPath()), { activeCampaignId: null, activeEncounterId: null }); } } catch (err) { console.error("Error deleting encounter:", err); } + setShowDeleteEncounterConfirm(false); setItemToDelete(null); }; - const handleTogglePlayerDisplayForEncounter = async (encounterId) => { if (!db) return; try { - const currentActiveCampaign = activeDisplayInfo?.activeCampaignId; - const currentActiveEncounter = activeDisplayInfo?.activeEncounterId; - - if (currentActiveCampaign === campaignId && currentActiveEncounter === encounterId) { - await setDoc(doc(db, getActiveDisplayDocPath()), { - activeCampaignId: null, - activeEncounterId: null, - }, { merge: true }); - console.log("Player Display for this encounter turned OFF."); - } else { - await setDoc(doc(db, getActiveDisplayDocPath()), { - activeCampaignId: campaignId, - activeEncounterId: encounterId, - }, { merge: true }); - console.log("Encounter set as active for Player Display!"); - } - } catch (err) { - console.error("Error toggling Player Display for encounter:", err); - } + const currentActiveCampaign = activeDisplayInfoFromHook?.activeCampaignId; + const currentActiveEncounter = activeDisplayInfoFromHook?.activeEncounterId; + if (currentActiveCampaign === campaignId && currentActiveEncounter === encounterId) { await setDoc(doc(db, getActiveDisplayDocPath()), { activeCampaignId: null, activeEncounterId: null, }, { merge: true }); console.log("Player Display for this encounter turned OFF."); } + else { await setDoc(doc(db, getActiveDisplayDocPath()), { activeCampaignId: campaignId, activeEncounterId: encounterId, }, { merge: true }); console.log("Encounter set as active for Player Display!"); } + } catch (err) { console.error("Error toggling Player Display for encounter:", err); } }; - const selectedEncounter = encounters?.find(e => e.id === selectedEncounterId); - - if (isLoadingEncounters && campaignId) { - return

Loading encounters...

; - } - + if (isLoadingEncounters && campaignId) { return

Loading encounters...

; } return ( <>
@@ -637,7 +484,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac {(!encounters || encounters.length === 0) &&

No encounters yet.

}
{encounters?.map(encounter => { - const isLiveOnPlayerDisplay = activeDisplayInfo && activeDisplayInfo.activeCampaignId === campaignId && activeDisplayInfo.activeEncounterId === encounter.id; + const isLiveOnPlayerDisplay = activeDisplayInfoFromHook && activeDisplayInfoFromHook.activeCampaignId === campaignId && activeDisplayInfoFromHook.activeEncounterId === encounter.id; return (
@@ -647,18 +494,11 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac {isLiveOnPlayerDisplay && LIVE ON PLAYER DISPLAY}
- +
-
- ); +
); })}
{showCreateEncounterModal && setShowCreateEncounterModal(false)} title="Create New Encounter"> setShowCreateEncounterModal(false)} />} @@ -670,13 +510,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac )} - setShowDeleteEncounterConfirm(false)} - onConfirm={confirmDeleteEncounter} - title="Delete Encounter?" - message={`Are you sure you want to delete the encounter "${itemToDelete?.name}"? This action cannot be undone.`} - /> + setShowDeleteEncounterConfirm(false)} onConfirm={confirmDeleteEncounter} title="Delete Encounter?" message={`Are you sure you want to delete the encounter "${itemToDelete?.name}"? This action cannot be undone.`}/> ); } @@ -708,70 +542,41 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { const [draggedItemId, setDraggedItemId] = useState(null); const [showDeleteParticipantConfirm, setShowDeleteParticipantConfirm] = useState(false); const [itemToDelete, setItemToDelete] = useState(null); - - const participants = encounter.participants || []; - const handleAddParticipant = async () => { if (!db || (participantType === 'monster' && !participantName.trim()) || (participantType === 'character' && !selectedCharacterId)) return; let nameToAdd = participantName.trim(); if (participantType === 'character') { const character = campaignCharacters.find(c => c.id === selectedCharacterId); if (!character) { console.error("Selected character not found"); return; } - if (participants.some(p => p.type === 'character' && p.originalCharacterId === selectedCharacterId)) { - alert(`${character.name} is already in this encounter.`); return; - } + if (participants.some(p => p.type === 'character' && p.originalCharacterId === selectedCharacterId)) { alert(`${character.name} is already in this encounter.`); return; } nameToAdd = character.name; } - const newParticipant = { - id: generateId(), name: nameToAdd, type: participantType, - originalCharacterId: participantType === 'character' ? selectedCharacterId : null, - initiative: parseInt(initiative, 10) || 0, maxHp: parseInt(maxHp, 10) || 1, currentHp: parseInt(maxHp, 10) || 1, - conditions: [], isActive: true, - }; - try { - await updateDoc(doc(db, encounterPath), { participants: [...participants, newParticipant] }); - setParticipantName(''); setInitiative(10); setMaxHp(10); setSelectedCharacterId(''); - } catch (err) { console.error("Error adding participant:", err); } + const newParticipant = { id: generateId(), name: nameToAdd, type: participantType, originalCharacterId: participantType === 'character' ? selectedCharacterId : null, initiative: parseInt(initiative, 10) || 0, maxHp: parseInt(maxHp, 10) || 1, currentHp: parseInt(maxHp, 10) || 1, conditions: [], isActive: true, }; + try { await updateDoc(doc(db, encounterPath), { participants: [...participants, newParticipant] }); setParticipantName(''); setInitiative(10); setMaxHp(10); setSelectedCharacterId(''); } catch (err) { console.error("Error adding participant:", err); } }; - const handleUpdateParticipant = async (updatedData) => { if (!db || !editingParticipant) return; const { flavorText, ...restOfData } = updatedData; const updatedParticipants = participants.map(p => p.id === editingParticipant.id ? { ...p, ...restOfData } : p ); - try { - await updateDoc(doc(db, encounterPath), { participants: updatedParticipants }); - setEditingParticipant(null); - } catch (err) { console.error("Error updating participant:", err); } + try { await updateDoc(doc(db, encounterPath), { participants: updatedParticipants }); setEditingParticipant(null); } catch (err) { console.error("Error updating participant:", err); } }; - - const requestDeleteParticipant = (participantId, participantName) => { - setItemToDelete({ id: participantId, name: participantName, type: 'participant' }); - setShowDeleteParticipantConfirm(true); - }; - + const requestDeleteParticipant = (participantId, participantName) => { setItemToDelete({ id: participantId, name: participantName, type: 'participant' }); setShowDeleteParticipantConfirm(true); }; const confirmDeleteParticipant = async () => { if (!db || !itemToDelete || itemToDelete.type !== 'participant') return; const participantId = itemToDelete.id; const updatedParticipants = participants.filter(p => p.id !== participantId); - try { - await updateDoc(doc(db, encounterPath), { participants: updatedParticipants }); - } catch (err) { console.error("Error deleting participant:", err); } - setShowDeleteParticipantConfirm(false); - setItemToDelete(null); + try { await updateDoc(doc(db, encounterPath), { participants: updatedParticipants }); } catch (err) { console.error("Error deleting participant:", err); } + setShowDeleteParticipantConfirm(false); setItemToDelete(null); }; - const toggleParticipantActive = async (participantId) => { if (!db) return; const pToToggle = participants.find(p => p.id === participantId); if (!pToToggle) return; const updatedPs = participants.map(p => p.id === participantId ? { ...p, isActive: !p.isActive } : p); - try { await updateDoc(doc(db, encounterPath), { participants: updatedPs }); } - catch (err) { console.error("Error toggling active state:", err); } + try { await updateDoc(doc(db, encounterPath), { participants: updatedPs }); } catch (err) { console.error("Error toggling active state:", err); } }; - const handleHpInputChange = (participantId, value) => setHpChangeValues(prev => ({ ...prev, [participantId]: value })); - const applyHpChange = async (participantId, changeType) => { if (!db) return; const amountStr = hpChangeValues[participantId]; @@ -784,80 +589,32 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { if (changeType === 'damage') newHp = Math.max(0, pToChange.currentHp - amount); else if (changeType === 'heal') newHp = Math.min(pToChange.maxHp, pToChange.currentHp + amount); const updatedPs = participants.map(p => p.id === participantId ? { ...p, currentHp: newHp } : p); - try { - await updateDoc(doc(db, encounterPath), { participants: updatedPs }); - setHpChangeValues(prev => ({ ...prev, [participantId]: '' })); - } catch (err) { console.error("Error applying HP change:", err); } + try { await updateDoc(doc(db, encounterPath), { participants: updatedPs }); setHpChangeValues(prev => ({ ...prev, [participantId]: '' })); } catch (err) { console.error("Error applying HP change:", err); } }; - - const handleDragStart = (e, id) => { - setDraggedItemId(id); - e.dataTransfer.effectAllowed = 'move'; - }; - - const handleDragOver = (e) => { - e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; - }; - + const handleDragStart = (e, id) => { setDraggedItemId(id); e.dataTransfer.effectAllowed = 'move'; }; + const handleDragOver = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }; const handleDrop = async (e, targetId) => { e.preventDefault(); - if (!db || draggedItemId === null || draggedItemId === targetId) { - setDraggedItemId(null); - return; - } - + if (!db || draggedItemId === null || draggedItemId === targetId) { setDraggedItemId(null); return; } const currentParticipants = [...participants]; const draggedItemIndex = currentParticipants.findIndex(p => p.id === draggedItemId); const targetItemIndex = currentParticipants.findIndex(p => p.id === targetId); - - if (draggedItemIndex === -1 || targetItemIndex === -1) { - console.error("Dragged or target item not found in participants list."); - setDraggedItemId(null); - return; - } - + if (draggedItemIndex === -1 || targetItemIndex === -1) { console.error("Dragged or target item not found."); setDraggedItemId(null); return; } const draggedItem = currentParticipants[draggedItemIndex]; const targetItem = currentParticipants[targetItemIndex]; - - if (draggedItem.initiative !== targetItem.initiative) { - console.log("Drag-and-drop for tie-breaking only allowed between participants with the same initiative score."); - setDraggedItemId(null); - return; - } - + if (draggedItem.initiative !== targetItem.initiative) { console.log("Drag-drop only for same initiative."); setDraggedItemId(null); return; } const [removedItem] = currentParticipants.splice(draggedItemIndex, 1); currentParticipants.splice(targetItemIndex, 0, removedItem); - - try { - await updateDoc(doc(db, encounterPath), { participants: currentParticipants }); - console.log("Participants reordered in Firestore for tie-breaking."); - } catch (err) { - console.error("Error updating participants after drag-drop:", err); - } + try { await updateDoc(doc(db, encounterPath), { participants: currentParticipants }); console.log("Participants reordered."); } catch (err) { console.error("Error D&D update:", err); } setDraggedItemId(null); }; - - const handleDragEnd = () => { - setDraggedItemId(null); - }; - + const handleDragEnd = () => { setDraggedItemId(null); }; const sortedAdminParticipants = [...participants].sort((a, b) => { - if (a.initiative === b.initiative) { - const indexA = participants.findIndex(p => p.id === a.id); - const indexB = participants.findIndex(p => p.id === b.id); - return indexA - indexB; - } + if (a.initiative === b.initiative) { const indexA = participants.findIndex(p => p.id === a.id); const indexB = participants.findIndex(p => p.id === b.id); return indexA - indexB; } return b.initiative - a.initiative; }); - - const initiativeGroups = participants.reduce((acc, p) => { - acc[p.initiative] = (acc[p.initiative] || 0) + 1; - return acc; - }, {}); + const initiativeGroups = participants.reduce((acc, p) => { acc[p.initiative] = (acc[p.initiative] || 0) + 1; return acc; }, {}); const tiedInitiatives = Object.keys(initiativeGroups).filter(init => initiativeGroups[init] > 1).map(Number); - - return ( <>
@@ -870,49 +627,25 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
- - {participantType === 'monster' && ( -
- - setParticipantName(e.target.value)} placeholder="e.g., Dire Wolf" 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" /> -
- )} + {participantType === 'monster' && (
setParticipantName(e.target.value)} placeholder="e.g., Dire Wolf" 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" />
)} {participantType === 'character' && (
)} -
setInitiative(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" />
- -
- -
+
{participants.length === 0 &&

No participants.

}
    - {sortedAdminParticipants.map((p, index) => { + {sortedAdminParticipants.map((p) => { const isCurrentTurn = encounter.isStarted && p.id === encounter.currentTurnParticipantId; - const isDraggable = !encounter.isStarted && tiedInitiatives.includes(Number(p.initiative)); + const isDraggable = (!encounter.isStarted || encounter.isPaused) && tiedInitiatives.includes(Number(p.initiative)); return ( -
  • handleDragStart(e, p.id) : undefined} - onDragOver={isDraggable ? handleDragOver : undefined} - onDrop={isDraggable ? (e) => handleDrop(e, p.id) : undefined} - onDragEnd={isDraggable ? handleDragEnd : undefined} - className={`p-3 rounded-md flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 transition-all - ${isCurrentTurn ? 'bg-green-600 ring-2 ring-green-300 shadow-lg' : (p.type === 'character' ? 'bg-sky-800' : 'bg-red-800')} - ${!p.isActive ? 'opacity-50' : ''} - ${isDraggable ? 'cursor-grab' : ''} - ${draggedItemId === p.id ? 'opacity-50 ring-2 ring-yellow-400' : ''} - `} - > +
  • handleDragStart(e, p.id) : undefined} onDragOver={isDraggable ? handleDragOver : undefined} onDrop={isDraggable ? (e) => handleDrop(e, p.id) : undefined} onDragEnd={isDraggable ? handleDragEnd : undefined} + className={`p-3 rounded-md flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 transition-all ${isCurrentTurn && !encounter.isPaused ? 'bg-green-600 ring-2 ring-green-300 shadow-lg' : (p.type === 'character' ? 'bg-sky-800' : 'bg-red-800')} ${!p.isActive ? 'opacity-50' : ''} ${isDraggable ? 'cursor-grab' : ''} ${draggedItemId === p.id ? 'opacity-50 ring-2 ring-yellow-400' : ''}`}>
    {isDraggable && }
    -

    {p.name} ({p.type}){isCurrentTurn && CURRENT}

    -

    Init: {p.initiative} | HP: {p.currentHp}/{p.maxHp}

    +

    {p.name} ({p.type}){isCurrentTurn && !encounter.isPaused && CURRENT}

    +

    Init: {p.initiative} | HP: {p.currentHp}/{p.maxHp}

    @@ -921,19 +654,12 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
    -
  • - ); + ); })}
{editingParticipant && setEditingParticipant(null)} onSave={handleUpdateParticipant} />} - setShowDeleteParticipantConfirm(false)} - onConfirm={confirmDeleteParticipant} - title="Delete Participant?" - message={`Are you sure you want to remove "${itemToDelete?.name}" from this encounter?`} - /> + setShowDeleteParticipantConfirm(false)} onConfirm={confirmDeleteParticipant} title="Delete Participant?" message={`Are you sure you want to remove "${itemToDelete?.name}" from this encounter?`}/> ); } @@ -943,14 +669,7 @@ function EditParticipantModal({ participant, onClose, onSave }) { const [initiative, setInitiative] = useState(participant.initiative); const [currentHp, setCurrentHp] = useState(participant.currentHp); const [maxHp, setMaxHp] = useState(participant.maxHp); - - const handleSubmit = (e) => { - e.preventDefault(); - onSave({ - name: name.trim(), initiative: parseInt(initiative, 10), - currentHp: parseInt(currentHp, 10), maxHp: parseInt(maxHp, 10), - }); - }; + const handleSubmit = (e) => { e.preventDefault(); onSave({ name: name.trim(), initiative: parseInt(initiative, 10), currentHp: parseInt(currentHp, 10), maxHp: parseInt(maxHp, 10), }); }; return (
@@ -976,57 +695,60 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) { if (!db ||!encounter.participants || encounter.participants.length === 0) { alert("Add participants first."); return; } const activePs = encounter.participants.filter(p => p.isActive); if (activePs.length === 0) { alert("No active participants."); return; } - const sortedPs = [...activePs].sort((a, b) => { - if (a.initiative === b.initiative) { - const indexA = encounter.participants.findIndex(p => p.id === a.id); - const indexB = encounter.participants.findIndex(p => p.id === b.id); - return indexA - indexB; - } + if (a.initiative === b.initiative) { const indexA = encounter.participants.findIndex(p => p.id === a.id); const indexB = encounter.participants.findIndex(p => p.id === b.id); return indexA - indexB; } return b.initiative - a.initiative; }); - try { - await updateDoc(doc(db, encounterPath), { - isStarted: true, round: 1, currentTurnParticipantId: sortedPs[0].id, turnOrderIds: sortedPs.map(p => p.id) - }); - await setDoc(doc(db, getActiveDisplayDocPath()), { - activeCampaignId: campaignId, - activeEncounterId: encounter.id - }, { merge: true }); + await updateDoc(doc(db, encounterPath), { isStarted: true, isPaused: false, round: 1, currentTurnParticipantId: sortedPs[0].id, turnOrderIds: sortedPs.map(p => p.id) }); + await setDoc(doc(db, getActiveDisplayDocPath()), { activeCampaignId: campaignId, activeEncounterId: encounter.id }, { merge: true }); console.log("Encounter started and set as active display."); - } catch (err) { console.error("Error starting encounter:", err); } }; - const handleNextTurn = async () => { - if (!db ||!encounter.isStarted || !encounter.currentTurnParticipantId || !encounter.turnOrderIds || encounter.turnOrderIds.length === 0) return; - const activePsInOrder = encounter.turnOrderIds.map(id => encounter.participants.find(p => p.id === id && p.isActive)).filter(Boolean); - if (activePsInOrder.length === 0) { - alert("No active participants left."); - await updateDoc(doc(db, encounterPath), { isStarted: false, currentTurnParticipantId: null, round: encounter.round }); return; + const handleTogglePause = async () => { + if (!db || !encounter || !encounter.isStarted) return; + const newPausedState = !encounter.isPaused; + let newTurnOrderIds = encounter.turnOrderIds; + + if (!newPausedState && encounter.isPaused) { + const activeParticipants = encounter.participants.filter(p => p.isActive); + const sortedActiveParticipants = [...activeParticipants].sort((a, b) => { + if (a.initiative === b.initiative) { + const indexA = encounter.participants.findIndex(p => p.id === a.id); + const indexB = encounter.participants.findIndex(p => p.id === b.id); + return indexA - indexB; + } + return b.initiative - a.initiative; + }); + newTurnOrderIds = sortedActiveParticipants.map(p => p.id); } + try { + await updateDoc(doc(db, encounterPath), { + isPaused: newPausedState, + turnOrderIds: newTurnOrderIds + }); + console.log(`Encounter ${newPausedState ? 'paused' : 'resumed'}.`); + } catch (err) { console.error("Error toggling pause state:", err); } + }; + + const handleNextTurn = async () => { + if (!db ||!encounter.isStarted || encounter.isPaused || !encounter.currentTurnParticipantId || !encounter.turnOrderIds || encounter.turnOrderIds.length === 0) return; + const activePsInOrder = encounter.turnOrderIds.map(id => encounter.participants.find(p => p.id === id && p.isActive)).filter(Boolean); + if (activePsInOrder.length === 0) { alert("No active participants left."); await updateDoc(doc(db, encounterPath), { isStarted: false, isPaused: false, currentTurnParticipantId: null, round: encounter.round }); return; } const currentIndex = activePsInOrder.findIndex(p => p.id === encounter.currentTurnParticipantId); let nextIndex = (currentIndex + 1) % activePsInOrder.length; let nextRound = encounter.round; if (nextIndex === 0 && currentIndex !== -1) nextRound += 1; - try { - await updateDoc(doc(db, encounterPath), { currentTurnParticipantId: activePsInOrder[nextIndex].id, round: nextRound }); - } catch (err) { console.error("Error advancing turn:", err); } + try { await updateDoc(doc(db, encounterPath), { currentTurnParticipantId: activePsInOrder[nextIndex].id, round: nextRound }); } catch (err) { console.error("Error advancing turn:", err); } }; - const requestEndEncounter = () => { - setShowEndEncounterConfirm(true); - }; - + const requestEndEncounter = () => { setShowEndEncounterConfirm(true); }; const confirmEndEncounter = async () => { if (!db) return; try { - await updateDoc(doc(db, encounterPath), { isStarted: false, currentTurnParticipantId: null, round: 0, turnOrderIds: [] }); - await setDoc(doc(db, getActiveDisplayDocPath()), { - activeCampaignId: null, - activeEncounterId: null - }, { merge: true }); + await updateDoc(doc(db, encounterPath), { isStarted: false, isPaused: false, currentTurnParticipantId: null, round: 0, turnOrderIds: [] }); + await setDoc(doc(db, getActiveDisplayDocPath()), { activeCampaignId: null, activeEncounterId: null }, { merge: true }); console.log("Encounter ended and deactivated from Player Display."); } catch (err) { console.error("Error ending encounter:", err); } setShowEndEncounterConfirm(false); @@ -1042,27 +764,25 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) { ) : ( <> - + +

Round: {encounter.round}

+ {encounter.isPaused &&

(Paused)

} )} - setShowEndEncounterConfirm(false)} - onConfirm={confirmEndEncounter} - title="End Encounter?" - message={`Are you sure you want to end this encounter? Initiative order will be reset and it will be removed from the Player Display.`} - /> + setShowEndEncounterConfirm(false)} onConfirm={confirmEndEncounter} title="End Encounter?" message={`Are you sure you want to end this encounter? Initiative order will be reset and it will be removed from the Player Display.`}/> ); } function DisplayView() { const { data: activeDisplayData, isLoading: isLoadingActiveDisplay, error: activeDisplayError } = useFirestoreDocument(getActiveDisplayDocPath()); - const [activeEncounterData, setActiveEncounterData] = useState(null); const [isLoadingEncounter, setIsLoadingEncounter] = useState(true); const [encounterError, setEncounterError] = useState(null); @@ -1070,72 +790,28 @@ function DisplayView() { const [isPlayerDisplayActive, setIsPlayerDisplayActive] = useState(false); useEffect(() => { - if (!db) { - setEncounterError("Firestore not available."); - setIsLoadingEncounter(false); - setIsPlayerDisplayActive(false); - return; - } - + if (!db) { setEncounterError("Firestore not available."); setIsLoadingEncounter(false); setIsPlayerDisplayActive(false); return; } let unsubscribeEncounter; let unsubscribeCampaign; - if (activeDisplayData) { const { activeCampaignId, activeEncounterId } = activeDisplayData; if (activeCampaignId && activeEncounterId) { - setIsPlayerDisplayActive(true); - setIsLoadingEncounter(true); - setEncounterError(null); - + setIsPlayerDisplayActive(true); setIsLoadingEncounter(true); setEncounterError(null); const campaignDocRef = doc(db, getCampaignDocPath(activeCampaignId)); - unsubscribeCampaign = onSnapshot(campaignDocRef, (campSnap) => { - if (campSnap.exists()) { - setCampaignBackgroundUrl(campSnap.data().playerDisplayBackgroundUrl || ''); - } else { - setCampaignBackgroundUrl(''); - } - }, (err) => console.error("Error fetching campaign background for display:", err)); - + unsubscribeCampaign = onSnapshot(campaignDocRef, (campSnap) => { if (campSnap.exists()) { setCampaignBackgroundUrl(campSnap.data().playerDisplayBackgroundUrl || ''); } else { setCampaignBackgroundUrl(''); } }, (err) => console.error("Error fetching campaign background:", err)); const encounterPath = getEncounterDocPath(activeCampaignId, activeEncounterId); unsubscribeEncounter = onSnapshot(doc(db, encounterPath), (encDocSnap) => { - if (encDocSnap.exists()) { - setActiveEncounterData({ id: encDocSnap.id, ...encDocSnap.data() }); - } else { - setActiveEncounterData(null); - setEncounterError("Active encounter data not found."); - } + if (encDocSnap.exists()) { setActiveEncounterData({ id: encDocSnap.id, ...encDocSnap.data() }); } + else { setActiveEncounterData(null); setEncounterError("Active encounter data not found."); } setIsLoadingEncounter(false); - }, (err) => { - console.error("Error fetching active encounter details for display:", err); - setEncounterError("Error loading active encounter data."); - setIsLoadingEncounter(false); - }); - } else { - setActiveEncounterData(null); - setCampaignBackgroundUrl(''); - setIsPlayerDisplayActive(false); - setIsLoadingEncounter(false); - } - } else if (!isLoadingActiveDisplay) { - setActiveEncounterData(null); - setCampaignBackgroundUrl(''); - setIsPlayerDisplayActive(false); - setIsLoadingEncounter(false); - } - - return () => { - if (unsubscribeEncounter) unsubscribeEncounter(); - if (unsubscribeCampaign) unsubscribeCampaign(); - }; + }, (err) => { console.error("Error fetching active encounter details:", err); setEncounterError("Error loading active encounter data."); setIsLoadingEncounter(false);}); + } else { setActiveEncounterData(null); setCampaignBackgroundUrl(''); setIsPlayerDisplayActive(false); setIsLoadingEncounter(false); } + } else if (!isLoadingActiveDisplay) { setActiveEncounterData(null); setCampaignBackgroundUrl(''); setIsPlayerDisplayActive(false); setIsLoadingEncounter(false); } + return () => { if (unsubscribeEncounter) unsubscribeEncounter(); if (unsubscribeCampaign) unsubscribeCampaign(); }; }, [activeDisplayData, isLoadingActiveDisplay]); - - if (isLoadingActiveDisplay || (isPlayerDisplayActive && isLoadingEncounter)) { - return
Loading Player Display...
; - } - if (activeDisplayError || (isPlayerDisplayActive && encounterError)) { - return
{activeDisplayError || encounterError}
; - } + if (isLoadingActiveDisplay || (isPlayerDisplayActive && isLoadingEncounter)) { return
Loading Player Display...
; } + if (activeDisplayError || (isPlayerDisplayActive && encounterError)) { return
{activeDisplayError || encounterError}
; } if (!isPlayerDisplayActive || !activeEncounterData) { return ( @@ -1143,100 +819,51 @@ function DisplayView() {

Game Session Paused

The Dungeon Master has not activated an encounter for display.

- - ); + ); } - - const { name, participants, round, currentTurnParticipantId, isStarted } = activeEncounterData; - + const { name, participants, round, currentTurnParticipantId, isStarted, isPaused } = activeEncounterData; let participantsToRender = []; if (participants) { if (isStarted && activeEncounterData.turnOrderIds?.length > 0 ) { - const inTurnOrderAndActive = activeEncounterData.turnOrderIds - .map(id => participants.find(p => p.id === id)) - .filter(p => p && p.isActive); - - const notInTurnOrderButActive = participants.filter(p => - p.isActive && - !activeEncounterData.turnOrderIds.includes(p.id) - ).sort((a,b) => { - if(a.initiative === b.initiative) { - const indexA = participants.findIndex(originalP => originalP.id === a.id); - const indexB = participants.findIndex(originalP => originalP.id === b.id); - return indexA - indexB; - } - return b.initiative - a.initiative; - }); - + const inTurnOrderAndActive = activeEncounterData.turnOrderIds.map(id => participants.find(p => p.id === id)).filter(p => p && p.isActive); + const notInTurnOrderButActive = participants.filter(p => p.isActive && !activeEncounterData.turnOrderIds.includes(p.id)).sort((a,b) => { if(a.initiative === b.initiative) { const indexA = participants.findIndex(op => op.id === a.id); const indexB = participants.findIndex(op => op.id === b.id); return indexA - indexB; } return b.initiative - a.initiative; }); participantsToRender = [...inTurnOrderAndActive, ...notInTurnOrderButActive]; - } else { - participantsToRender = [...participants].filter(p => p.isActive) - .sort((a, b) => { - if (a.initiative === b.initiative) { - const indexA = participants.findIndex(p => p.id === a.id); - const indexB = participants.findIndex(p => p.id === b.id); - return indexA - indexB; - } - return b.initiative - a.initiative; - }); + participantsToRender = [...participants].filter(p => p.isActive).sort((a, b) => { if (a.initiative === b.initiative) { const indexA = participants.findIndex(p => p.id === a.id); const indexB = participants.findIndex(p => p.id === b.id); return indexA - indexB; } return b.initiative - a.initiative; }); } } - - - const displayStyles = campaignBackgroundUrl ? { - backgroundImage: `url(${campaignBackgroundUrl})`, - backgroundSize: 'cover', - backgroundPosition: 'center center', - backgroundRepeat: 'no-repeat', - minHeight: '100vh' - } : { minHeight: '100vh' }; - - + const displayStyles = campaignBackgroundUrl ? { backgroundImage: `url(${campaignBackgroundUrl})`, backgroundSize: 'cover', backgroundPosition: 'center center', backgroundRepeat: 'no-repeat', minHeight: '100vh' } : { minHeight: '100vh' }; return ( -
+

{name}

- {isStarted &&

Round: {round}

} + {isStarted &&

Round: {round}

} + {isStarted && isPaused &&

(Combat Paused)

} {!isStarted && participants?.length > 0 &&

Awaiting Start

} {!isStarted && (!participants || participants.length === 0) &&

No participants.

} {participantsToRender.length === 0 && isStarted &&

No active participants.

} -
{participantsToRender.map(p => ( -
+
-

{p.name}{p.id === currentTurnParticipantId && isStarted && (Current)}

- Init: {p.initiative} +

{p.name}{p.id === currentTurnParticipantId && isStarted && !isPaused && (Current)}

+ Init: {p.initiative}
- {p.type !== 'monster' && ( - - HP: {p.currentHp} / {p.maxHp} - - )} + {p.type !== 'monster' && ( HP: {p.currentHp} / {p.maxHp})}
{p.conditions?.length > 0 &&

Conditions: {p.conditions.join(', ')}

} {!p.isActive &&

(Inactive)

} -
- ))} +
))}
-
- ); +
); } -// --- Modal Component --- (Standard modal for forms, etc.) function Modal({ onClose, title, children }) { useEffect(() => { const handleEsc = (event) => { if (event.key === 'Escape') onClose(); }; @@ -1257,8 +884,6 @@ function Modal({ onClose, title, children }) { } // --- Icons --- -const PlayIcon = ({ size = 24, className = '' }) => ; -const SkipForwardIcon = ({ size = 24, className = '' }) => ; -const StopCircleIcon = ({size=24, className=''}) => ; +// PlayIcon, SkipForwardIcon, StopCircleIcon are now imported from lucide-react at the top export default App;