Removed UUIDs and added delete confirmation boxes.
This commit is contained in:
parent
a317038345
commit
e09739fc01
287
src/App.js
287
src/App.js
@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth';
|
||||
import { getFirestore, doc, setDoc, addDoc, getDoc, getDocs, collection, onSnapshot, updateDoc, deleteDoc, query, writeBatch } from 'firebase/firestore';
|
||||
import { PlusCircle, Users, Swords, Shield, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, UserCheck, UserX, HeartCrack, HeartPulse, Zap, Image as ImageIcon, EyeOff, ExternalLink } from 'lucide-react';
|
||||
import { PlusCircle, Users, Swords, Shield, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, UserCheck, UserX, HeartCrack, HeartPulse, Zap, Image as ImageIcon, EyeOff, ExternalLink, AlertTriangle } from 'lucide-react'; // Added AlertTriangle
|
||||
|
||||
// --- Firebase Configuration ---
|
||||
const firebaseConfig = {
|
||||
@ -126,7 +126,7 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) {
|
||||
|
||||
// --- Main App Component ---
|
||||
function App() {
|
||||
const [userId, setUserId] = useState(null);
|
||||
const [userId, setUserId] = useState(null); // Kept for potential future use, but not displayed
|
||||
const [isAuthReady, setIsAuthReady] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
@ -218,7 +218,7 @@ function App() {
|
||||
TTRPG Initiative Tracker
|
||||
</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
{userId && <span className="text-xs text-slate-400">UID: {userId}</span>}
|
||||
{/* UID display removed from header */}
|
||||
<button
|
||||
onClick={openPlayerWindow}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors bg-teal-500 hover:bg-teal-600 text-white flex items-center`}
|
||||
@ -234,12 +234,44 @@ function App() {
|
||||
{!isAuthReady && !error && <p>Authenticating...</p>}
|
||||
</main>
|
||||
<footer className="bg-slate-900 p-4 text-center text-sm text-slate-400 mt-8">
|
||||
TTRPG Initiative Tracker v0.1.19
|
||||
TTRPG Initiative Tracker v0.1.23
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Confirmation Modal Component ---
|
||||
function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-50 transition-opacity duration-300 ease-in-out">
|
||||
<div className="bg-slate-800 p-6 rounded-lg shadow-xl w-full max-w-md transform transition-all duration-300 ease-in-out scale-100 opacity-100">
|
||||
<div className="flex items-center mb-4">
|
||||
<AlertTriangle size={24} className="text-yellow-400 mr-3 flex-shrink-0" />
|
||||
<h2 className="text-xl font-semibold text-yellow-300">{title || "Confirm Action"}</h2>
|
||||
</div>
|
||||
<p className="text-slate-300 mb-6">{message || "Are you sure you want to proceed?"}</p>
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-slate-300 bg-slate-600 hover:bg-slate-500 rounded-md transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-md transition-colors"
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// --- Admin View Component ---
|
||||
function AdminView({ userId }) {
|
||||
const { data: campaignsData, isLoading: isLoadingCampaigns } = useFirestoreCollection(getCampaignsCollectionPath());
|
||||
@ -248,6 +280,8 @@ function AdminView({ userId }) {
|
||||
const [campaigns, setCampaigns] = useState([]);
|
||||
const [selectedCampaignId, setSelectedCampaignId] = useState(null);
|
||||
const [showCreateCampaignModal, setShowCreateCampaignModal] = useState(false);
|
||||
const [showDeleteCampaignConfirm, setShowDeleteCampaignConfirm] = useState(false);
|
||||
const [itemToDelete, setItemToDelete] = useState(null); // { id, name, type: 'campaign' }
|
||||
|
||||
useEffect(() => {
|
||||
if (campaignsData) {
|
||||
@ -281,10 +315,14 @@ function AdminView({ userId }) {
|
||||
} catch (err) { console.error("Error creating campaign:", err); }
|
||||
};
|
||||
|
||||
const handleDeleteCampaign = async (campaignId) => {
|
||||
if (!db) return;
|
||||
// TODO: Implement custom confirmation modal for deleting campaigns
|
||||
console.warn("Attempting to delete campaign without confirmation:", campaignId);
|
||||
const requestDeleteCampaign = (campaignId, campaignName) => {
|
||||
setItemToDelete({ id: campaignId, name: campaignName, type: 'campaign' });
|
||||
setShowDeleteCampaignConfirm(true);
|
||||
};
|
||||
|
||||
const confirmDeleteCampaign = async () => {
|
||||
if (!db || !itemToDelete || itemToDelete.type !== 'campaign') return;
|
||||
const campaignId = itemToDelete.id;
|
||||
try {
|
||||
const encountersPath = getEncountersCollectionPath(campaignId);
|
||||
const encountersSnapshot = await getDocs(collection(db, encountersPath));
|
||||
@ -300,6 +338,8 @@ function AdminView({ userId }) {
|
||||
await updateDoc(activeDisplayRef, { activeCampaignId: null, activeEncounterId: null });
|
||||
}
|
||||
} catch (err) { console.error("Error deleting campaign:", err); }
|
||||
setShowDeleteCampaignConfirm(false);
|
||||
setItemToDelete(null);
|
||||
};
|
||||
|
||||
const selectedCampaign = campaigns.find(c => c.id === selectedCampaignId);
|
||||
@ -309,55 +349,65 @@ function AdminView({ userId }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-semibold text-sky-300">Campaigns</h2>
|
||||
<button onClick={() => setShowCreateCampaignModal(true)} className="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-lg flex items-center transition-colors">
|
||||
<PlusCircle size={20} className="mr-2" /> Create Campaign
|
||||
</button>
|
||||
</div>
|
||||
{campaigns.length === 0 && <p className="text-slate-400">No campaigns yet.</p>}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{campaigns.map(campaign => (
|
||||
<div
|
||||
key={campaign.id}
|
||||
onClick={() => setSelectedCampaignId(campaign.id)}
|
||||
className={`p-4 rounded-lg shadow-md cursor-pointer transition-all ${selectedCampaignId === campaign.id ? 'bg-sky-700 ring-2 ring-sky-400' : 'bg-slate-700 hover:bg-slate-600'}`}
|
||||
>
|
||||
<h3 className="text-xl font-semibold text-white">{campaign.name}</h3>
|
||||
<p className="text-xs text-slate-400">ID: {campaign.id}</p>
|
||||
{campaign.playerDisplayBackgroundUrl && <ImageIcon size={14} className="inline-block mr-1 text-slate-400" title="Has custom background"/>}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteCampaign(campaign.id);
|
||||
}}
|
||||
className="mt-2 text-red-400 hover:text-red-300 text-xs flex items-center"
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-semibold text-sky-300">Campaigns</h2>
|
||||
<button onClick={() => setShowCreateCampaignModal(true)} className="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-lg flex items-center transition-colors">
|
||||
<PlusCircle size={20} className="mr-2" /> Create Campaign
|
||||
</button>
|
||||
</div>
|
||||
{campaigns.length === 0 && <p className="text-slate-400">No campaigns yet.</p>}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{campaigns.map(campaign => (
|
||||
<div
|
||||
key={campaign.id}
|
||||
onClick={() => setSelectedCampaignId(campaign.id)}
|
||||
className={`p-4 rounded-lg shadow-md cursor-pointer transition-all ${selectedCampaignId === campaign.id ? 'bg-sky-700 ring-2 ring-sky-400' : 'bg-slate-700 hover:bg-slate-600'}`}
|
||||
>
|
||||
<Trash2 size={14} className="mr-1" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<h3 className="text-xl font-semibold text-white">{campaign.name}</h3>
|
||||
{/* Campaign ID display removed */}
|
||||
{campaign.playerDisplayBackgroundUrl && <ImageIcon size={14} className="inline-block mr-1 text-slate-400" title="Has custom background"/>}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
requestDeleteCampaign(campaign.id, campaign.name);
|
||||
}}
|
||||
className="mt-2 text-red-400 hover:text-red-300 text-xs flex items-center"
|
||||
>
|
||||
<Trash2 size={14} className="mr-1" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{showCreateCampaignModal && <Modal onClose={() => setShowCreateCampaignModal(false)} title="Create New Campaign"><CreateCampaignForm onCreate={handleCreateCampaign} onCancel={() => setShowCreateCampaignModal(false)} /></Modal>}
|
||||
{selectedCampaign && (
|
||||
<div className="mt-6 p-6 bg-slate-750 rounded-lg shadow-xl">
|
||||
<h2 className="text-2xl font-semibold text-amber-300 mb-4">Managing: {selectedCampaign.name}</h2>
|
||||
<CharacterManager campaignId={selectedCampaignId} campaignCharacters={selectedCampaign.players || []} />
|
||||
<hr className="my-6 border-slate-600" />
|
||||
<EncounterManager
|
||||
campaignId={selectedCampaignId}
|
||||
initialActiveEncounterId={initialActiveInfoData && initialActiveInfoData.activeCampaignId === selectedCampaignId ? initialActiveInfoData.activeEncounterId : null}
|
||||
campaignCharacters={selectedCampaign.players || []}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showCreateCampaignModal && <Modal onClose={() => setShowCreateCampaignModal(false)} title="Create New Campaign"><CreateCampaignForm onCreate={handleCreateCampaign} onCancel={() => setShowCreateCampaignModal(false)} /></Modal>}
|
||||
{selectedCampaign && (
|
||||
<div className="mt-6 p-6 bg-slate-750 rounded-lg shadow-xl">
|
||||
<h2 className="text-2xl font-semibold text-amber-300 mb-4">Managing: {selectedCampaign.name}</h2>
|
||||
<CharacterManager campaignId={selectedCampaignId} campaignCharacters={selectedCampaign.players || []} />
|
||||
<hr className="my-6 border-slate-600" />
|
||||
<EncounterManager
|
||||
campaignId={selectedCampaignId}
|
||||
initialActiveEncounterId={initialActiveInfoData && initialActiveInfoData.activeCampaignId === selectedCampaignId ? initialActiveInfoData.activeEncounterId : null}
|
||||
campaignCharacters={selectedCampaign.players || []}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
isOpen={showDeleteCampaignConfirm}
|
||||
onClose={() => 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 remains the same)
|
||||
function CreateCampaignForm({ onCreate, onCancel }) {
|
||||
const [name, setName] = useState('');
|
||||
const [backgroundUrl, setBackgroundUrl] = useState('');
|
||||
@ -385,9 +435,14 @@ function CreateCampaignForm({ onCreate, onCancel }) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// --- CharacterManager ---
|
||||
function CharacterManager({ campaignId, campaignCharacters }) {
|
||||
const [characterName, setCharacterName] = useState('');
|
||||
const [editingCharacter, setEditingCharacter] = useState(null);
|
||||
const [showDeleteCharConfirm, setShowDeleteCharConfirm] = useState(false);
|
||||
const [itemToDelete, setItemToDelete] = useState(null); // { id, name, type: 'character' }
|
||||
|
||||
|
||||
const handleAddCharacter = async () => {
|
||||
if (!db ||!characterName.trim() || !campaignId) return;
|
||||
@ -407,17 +462,24 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
||||
} catch (err) { console.error("Error updating character:", err); }
|
||||
};
|
||||
|
||||
const handleDeleteCharacter = async (characterId) => {
|
||||
if (!db) return;
|
||||
// TODO: Implement custom confirmation modal for deleting characters
|
||||
console.warn("Attempting to delete character without confirmation:", characterId);
|
||||
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);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-4 bg-slate-800 rounded-lg shadow">
|
||||
<h3 className="text-xl font-semibold text-sky-300 mb-3 flex items-center"><Users size={24} className="mr-2" /> Campaign Characters</h3>
|
||||
<form onSubmit={(e) => { e.preventDefault(); handleAddCharacter(); }} className="flex gap-2 mb-4">
|
||||
@ -433,24 +495,40 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
||||
) : ( <span className="text-slate-100">{character.name}</span> )}
|
||||
<div className="flex space-x-2">
|
||||
<button onClick={() => setEditingCharacter(character)} className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-slate-600 hover:bg-slate-500" aria-label="Edit character"><Edit3 size={18} /></button>
|
||||
<button onClick={() => handleDeleteCharacter(character.id)} className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500" aria-label="Delete character"><Trash2 size={18} /></button>
|
||||
<button onClick={() => requestDeleteCharacter(character.id, character.name)} className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500" aria-label="Delete character"><Trash2 size={18} /></button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
isOpen={showDeleteCharConfirm}
|
||||
onClose={() => setShowDeleteCharConfirm(false)}
|
||||
onConfirm={confirmDeleteCharacter}
|
||||
title="Delete Character?"
|
||||
message={`Are you sure you want to remove the character "${itemToDelete?.name}" from this campaign?`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// --- EncounterManager ---
|
||||
function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharacters }) {
|
||||
const {data: encounters, isLoading: isLoadingEncounters } = useFirestoreCollection(campaignId ? getEncountersCollectionPath(campaignId) : null);
|
||||
const {data: encountersData, isLoading: isLoadingEncounters } = useFirestoreCollection(campaignId ? getEncountersCollectionPath(campaignId) : null);
|
||||
const {data: activeDisplayInfo } = 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); // { id, name, type: 'encounter' }
|
||||
|
||||
const selectedEncounterIdRef = useRef(selectedEncounterId);
|
||||
|
||||
useEffect(() => {
|
||||
if(encountersData) setEncounters(encountersData);
|
||||
}, [encountersData]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedEncounterIdRef.current = selectedEncounterId;
|
||||
}, [selectedEncounterId]);
|
||||
@ -488,10 +566,14 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
||||
} catch (err) { console.error("Error creating encounter:", err); }
|
||||
};
|
||||
|
||||
const handleDeleteEncounter = async (encounterId) => {
|
||||
if (!db) return;
|
||||
// TODO: Implement custom confirmation modal for deleting encounters
|
||||
console.warn("Attempting to delete encounter without confirmation:", encounterId);
|
||||
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);
|
||||
@ -499,6 +581,8 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
||||
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) => {
|
||||
@ -532,6 +616,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-6 p-4 bg-slate-800 rounded-lg shadow">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="text-xl font-semibold text-sky-300 flex items-center"><Swords size={24} className="mr-2" /> Encounters</h3>
|
||||
@ -557,7 +642,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
||||
>
|
||||
{isLiveOnPlayerDisplay ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
<button onClick={(e) => { e.stopPropagation(); handleDeleteEncounter(encounter.id); }} className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500" title="Delete Encounter"><Trash2 size={18} /></button>
|
||||
<button onClick={(e) => { e.stopPropagation(); requestDeleteEncounter(encounter.id, encounter.name); }} className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500" title="Delete Encounter"><Trash2 size={18} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -573,9 +658,21 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
isOpen={showDeleteEncounterConfirm}
|
||||
onClose={() => setShowDeleteEncounterConfirm(false)}
|
||||
onConfirm={confirmDeleteEncounter}
|
||||
title="Delete Encounter?"
|
||||
message={`Are you sure you want to delete the encounter "${itemToDelete?.name}"? This action cannot be undone.`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ... (CreateEncounterForm, ParticipantManager, EditParticipantModal, InitiativeControls, DisplayView, Modal, Icons)
|
||||
// The rest of the components remain the same as in v0.1.22, with the following changes in ParticipantManager and InitiativeControls
|
||||
// to use the ConfirmationModal for delete/end actions.
|
||||
|
||||
function CreateEncounterForm({ onCreate, onCancel }) {
|
||||
const [name, setName] = useState('');
|
||||
return (
|
||||
@ -601,6 +698,9 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
const [editingParticipant, setEditingParticipant] = useState(null);
|
||||
const [hpChangeValues, setHpChangeValues] = useState({});
|
||||
const [draggedItemId, setDraggedItemId] = useState(null);
|
||||
const [showDeleteParticipantConfirm, setShowDeleteParticipantConfirm] = useState(false);
|
||||
const [itemToDelete, setItemToDelete] = useState(null); // { id, name, type: 'participant' }
|
||||
|
||||
|
||||
const participants = encounter.participants || [];
|
||||
|
||||
@ -637,14 +737,20 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
} catch (err) { console.error("Error updating participant:", err); }
|
||||
};
|
||||
|
||||
const handleDeleteParticipant = async (participantId) => {
|
||||
if (!db) return;
|
||||
// TODO: Implement custom confirmation modal for deleting participants
|
||||
console.warn("Attempting to delete participant without confirmation:", participantId);
|
||||
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);
|
||||
};
|
||||
|
||||
const toggleParticipantActive = async (participantId) => {
|
||||
@ -676,7 +782,6 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
} catch (err) { console.error("Error applying HP change:", err); }
|
||||
};
|
||||
|
||||
// --- Drag and Drop Handlers ---
|
||||
const handleDragStart = (e, id) => {
|
||||
setDraggedItemId(id);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
@ -746,6 +851,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-3 bg-slate-800 rounded-md mt-4">
|
||||
<h4 className="text-lg font-medium text-sky-200 mb-3">Participants</h4>
|
||||
<form onSubmit={(e) => { e.preventDefault(); handleAddParticipant(); }} className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 p-3 bg-slate-700 rounded">
|
||||
@ -805,7 +911,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
{encounter.isStarted && p.isActive && (<div className="flex items-center space-x-1 bg-slate-700 p-1 rounded-md"><input type="number" placeholder="HP" value={hpChangeValues[p.id] || ''} onChange={(e) => handleHpInputChange(p.id, e.target.value)} className="w-16 p-1 text-sm bg-slate-600 border border-slate-500 rounded-md text-white focus:ring-sky-500 focus:border-sky-500" aria-label={`HP change for ${p.name}`}/><button onClick={() => applyHpChange(p.id, 'damage')} className="p-1 bg-red-500 hover:bg-red-600 text-white rounded-md text-xs" title="Damage"><HeartCrack size={16}/></button><button onClick={() => applyHpChange(p.id, 'heal')} className="p-1 bg-green-500 hover:bg-green-600 text-white rounded-md text-xs" title="Heal"><HeartPulse size={16}/></button></div>)}
|
||||
<button onClick={() => toggleParticipantActive(p.id)} className={`p-1 rounded transition-colors ${p.isActive ? 'text-yellow-400 hover:text-yellow-300 bg-slate-600 hover:bg-slate-500' : 'text-slate-400 hover:text-slate-300 bg-slate-600 hover:bg-slate-500'}`} title={p.isActive ? "Mark Inactive" : "Mark Active"}>{p.isActive ? <UserCheck size={18} /> : <UserX size={18} />}</button>
|
||||
<button onClick={() => setEditingParticipant(p)} className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-slate-600 hover:bg-slate-500" title="Edit"><Edit3 size={18} /></button>
|
||||
<button onClick={() => handleDeleteParticipant(p.id)} className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500" title="Remove"><Trash2 size={18} /></button>
|
||||
<button onClick={() => 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"><Trash2 size={18} /></button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
@ -813,6 +919,14 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
</ul>
|
||||
{editingParticipant && <EditParticipantModal participant={editingParticipant} onClose={() => setEditingParticipant(null)} onSave={handleUpdateParticipant} />}
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
isOpen={showDeleteParticipantConfirm}
|
||||
onClose={() => setShowDeleteParticipantConfirm(false)}
|
||||
onConfirm={confirmDeleteParticipant}
|
||||
title="Delete Participant?"
|
||||
message={`Are you sure you want to remove "${itemToDelete?.name}" from this encounter?`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -848,6 +962,8 @@ function EditParticipantModal({ participant, onClose, onSave }) {
|
||||
}
|
||||
|
||||
function InitiativeControls({ campaignId, encounter, encounterPath }) {
|
||||
const [showEndEncounterConfirm, setShowEndEncounterConfirm] = useState(false);
|
||||
|
||||
const handleStartEncounter = async () => {
|
||||
if (!db ||!encounter.participants || encounter.participants.length === 0) { alert("Add participants first."); return; }
|
||||
const activePs = encounter.participants.filter(p => p.isActive);
|
||||
@ -891,10 +1007,12 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
|
||||
} catch (err) { console.error("Error advancing turn:", err); }
|
||||
};
|
||||
|
||||
const handleEndEncounter = async () => {
|
||||
const requestEndEncounter = () => {
|
||||
setShowEndEncounterConfirm(true);
|
||||
};
|
||||
|
||||
const confirmEndEncounter = async () => {
|
||||
if (!db) return;
|
||||
// TODO: Implement custom confirmation modal for ending encounter
|
||||
console.warn("Attempting to end encounter without confirmation");
|
||||
try {
|
||||
await updateDoc(doc(db, encounterPath), { isStarted: false, currentTurnParticipantId: null, round: 0, turnOrderIds: [] });
|
||||
await setDoc(doc(db, getActiveDisplayDocPath()), {
|
||||
@ -903,10 +1021,12 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
|
||||
}, { merge: true });
|
||||
console.log("Encounter ended and deactivated from Player Display.");
|
||||
} catch (err) { console.error("Error ending encounter:", err); }
|
||||
setShowEndEncounterConfirm(false);
|
||||
};
|
||||
|
||||
if (!encounter || !encounter.participants) return null;
|
||||
return (
|
||||
<>
|
||||
<div className="mt-6 p-3 bg-slate-800 rounded-md">
|
||||
<h4 className="text-lg font-medium text-sky-200 mb-3">Combat Controls</h4>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
@ -915,15 +1035,24 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
|
||||
) : (
|
||||
<>
|
||||
<button onClick={handleNextTurn} className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors flex items-center" disabled={!encounter.currentTurnParticipantId}><SkipForwardIcon size={18} className="mr-2" /> Next Turn</button>
|
||||
<button onClick={handleEndEncounter} className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-md transition-colors flex items-center"><StopCircleIcon size={18} className="mr-2" /> End</button>
|
||||
<button onClick={requestEndEncounter} className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-md transition-colors flex items-center"><StopCircleIcon size={18} className="mr-2" /> End</button>
|
||||
<p className="text-slate-300 self-center">Round: {encounter.round}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
isOpen={showEndEncounterConfirm}
|
||||
onClose={() => 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.`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// DisplayView remains the same as v0.1.19 (before focused view)
|
||||
function DisplayView() {
|
||||
const { data: activeDisplayData, isLoading: isLoadingActiveDisplay, error: activeDisplayError } = useFirestoreDocument(getActiveDisplayDocPath());
|
||||
|
||||
@ -932,7 +1061,6 @@ function DisplayView() {
|
||||
const [encounterError, setEncounterError] = useState(null);
|
||||
const [campaignBackgroundUrl, setCampaignBackgroundUrl] = useState('');
|
||||
const [isPlayerDisplayActive, setIsPlayerDisplayActive] = useState(false);
|
||||
// currentTurnRef removed as we are reverting the scroll-to-view logic for now
|
||||
|
||||
useEffect(() => {
|
||||
if (!db) {
|
||||
@ -994,7 +1122,6 @@ function DisplayView() {
|
||||
};
|
||||
}, [activeDisplayData, isLoadingActiveDisplay]);
|
||||
|
||||
// useEffect for scrollIntoView removed
|
||||
|
||||
if (isLoadingActiveDisplay || (isPlayerDisplayActive && isLoadingEncounter)) {
|
||||
return <div className="text-center py-10 text-2xl text-slate-300">Loading Player Display...</div>;
|
||||
@ -1016,9 +1143,8 @@ function DisplayView() {
|
||||
|
||||
const { name, participants, round, currentTurnParticipantId, isStarted } = activeEncounterData;
|
||||
|
||||
// Reverted to showing all active participants, no focused view logic
|
||||
let participantsToRender = [];
|
||||
if (participants) {
|
||||
let participantsToRender = []; // Renamed from displayParticipants for clarity
|
||||
if (participants) {
|
||||
if (isStarted && activeEncounterData.turnOrderIds?.length > 0 ) {
|
||||
participantsToRender = activeEncounterData.turnOrderIds
|
||||
.map(id => participants.find(p => p.id === id)).filter(p => p && p.isActive);
|
||||
@ -1052,18 +1178,15 @@ function DisplayView() {
|
||||
>
|
||||
<div className={campaignBackgroundUrl ? 'bg-slate-900 bg-opacity-75 p-4 md:p-6 rounded-lg' : ''}>
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-center text-amber-400 mb-2">{name}</h2>
|
||||
{isStarted && <p className="text-2xl text-center text-sky-300 mb-6">Round: {round}</p>} {/* Adjusted margin */}
|
||||
{/* Focused view indicator removed */}
|
||||
{isStarted && <p className="text-2xl text-center text-sky-300 mb-6">Round: {round}</p>}
|
||||
{!isStarted && participants?.length > 0 && <p className="text-2xl text-center text-slate-400 mb-6">Awaiting Start</p>}
|
||||
{!isStarted && (!participants || participants.length === 0) && <p className="text-2xl text-slate-500 mb-6">No participants.</p>}
|
||||
{participantsToRender.length === 0 && isStarted && <p className="text-xl text-slate-400">No active participants.</p>}
|
||||
|
||||
{/* Reverted to simpler list container */}
|
||||
<div className="space-y-4 max-w-3xl mx-auto">
|
||||
{participantsToRender.map(p => (
|
||||
<div
|
||||
key={p.id}
|
||||
// ref removed
|
||||
className={`p-4 md:p-6 rounded-lg shadow-lg transition-all ${p.id === currentTurnParticipantId && isStarted ? 'bg-green-700 ring-4 ring-green-400 scale-105' : (p.type === 'character' ? 'bg-sky-700' : 'bg-red-700')} ${!p.isActive ? 'opacity-40 grayscale' : ''}`}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
@ -1090,6 +1213,7 @@ function DisplayView() {
|
||||
);
|
||||
}
|
||||
|
||||
// --- Modal Component --- (Standard modal for forms, etc.)
|
||||
function Modal({ onClose, title, children }) {
|
||||
useEffect(() => {
|
||||
const handleEsc = (event) => { if (event.key === 'Escape') onClose(); };
|
||||
@ -1109,6 +1233,7 @@ function Modal({ onClose, title, children }) {
|
||||
);
|
||||
}
|
||||
|
||||
// --- Icons ---
|
||||
const PlayIcon = ({ size = 24, className = '' }) => <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>;
|
||||
const SkipForwardIcon = ({ size = 24, className = '' }) => <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><polygon points="5 4 15 12 5 20 5 4"></polygon><line x1="19" y1="5" x2="19" y2="19"></line></svg>;
|
||||
const StopCircleIcon = ({size=24, className=''}) => <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><circle cx="12" cy="12" r="10"></circle><rect x="9" y="9" width="6" height="6"></rect></svg>;
|
||||
|
Loading…
x
Reference in New Issue
Block a user