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?"}
-
- Cancel
-
-
- Confirm
-
+ Cancel
+ Confirm
);
}
-
// --- 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 */}
- {
- e.stopPropagation();
- requestDeleteCampaign(campaign.id, campaign.name);
- }}
- className="mt-2 text-red-300 hover:text-red-100 text-xs flex items-center bg-black bg-opacity-50 hover:bg-opacity-70 px-2 py-1 rounded"
- >
- Delete
-
+ {/* ImageIcon display removed from here */}
+ { e.stopPropagation(); requestDeleteCampaign(campaign.id, campaign.name); }} className="mt-2 text-red-300 hover:text-red-100 text-xs flex items-center bg-black bg-opacity-50 hover:bg-opacity-70 px-2 py-1 rounded"> Delete
-
- );
+
);
})}
@@ -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 (
{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 }) {
setEditingParticipant(p)} className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-slate-600 hover:bg-slate-500" title="Edit">
requestDeleteParticipant(p.id, p.name)} className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500" title="Remove">
-
- );
+ );
})}
{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 (