Code clean up and refactor.
This commit is contained in:
parent
6adcd0f8e0
commit
893fe49ccb
92
src/App.js
92
src/App.js
@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useMemo } from 'react';
|
|||||||
import { initializeApp } from 'firebase/app';
|
import { initializeApp } from 'firebase/app';
|
||||||
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth';
|
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth';
|
||||||
import { getFirestore, doc, setDoc, getDoc, getDocs, collection, onSnapshot, updateDoc, deleteDoc, query, writeBatch } from 'firebase/firestore';
|
import { getFirestore, doc, setDoc, getDoc, getDocs, collection, onSnapshot, updateDoc, deleteDoc, query, writeBatch } from 'firebase/firestore';
|
||||||
import { PlusCircle, Users, Swords, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, UserCheck, UserX, HeartCrack, HeartPulse, Zap, EyeOff, ExternalLink, AlertTriangle, Play as PlayIcon, Pause as PauseIcon, SkipForward as SkipForwardIcon, StopCircle as StopCircleIcon, Users2, Dices } from 'lucide-react';
|
import { PlusCircle, Users, Swords, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, UserCheck, UserX, HeartCrack, HeartPulse, Zap, EyeOff, ExternalLink, AlertTriangle, Play as PlayIcon, Pause as PauseIcon, SkipForward as SkipForwardIcon, StopCircle as StopCircleIcon, Users2, Dices } from 'lucide-react'; // ImageIcon was already removed, ensuring it stays removed.
|
||||||
|
|
||||||
// --- Firebase Configuration ---
|
// --- Firebase Configuration ---
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
@ -88,6 +88,8 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) {
|
|||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
// Memoize the stringified queryConstraints. This is the key to stabilizing the effect's dependency.
|
||||||
const queryString = useMemo(() => JSON.stringify(queryConstraints), [queryConstraints]);
|
const queryString = useMemo(() => JSON.stringify(queryConstraints), [queryConstraints]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -99,7 +101,10 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) {
|
|||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
// queryConstraints is used here to build the query.
|
||||||
const q = query(collection(db, collectionPath), ...queryConstraints);
|
const q = query(collection(db, collectionPath), ...queryConstraints);
|
||||||
|
|
||||||
const unsubscribe = onSnapshot(q, (snapshot) => {
|
const unsubscribe = onSnapshot(q, (snapshot) => {
|
||||||
const items = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
|
const items = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
|
||||||
setData(items);
|
setData(items);
|
||||||
@ -110,9 +115,14 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setData([]);
|
setData([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => unsubscribe();
|
return () => unsubscribe();
|
||||||
|
// The effect depends on collectionPath and the memoized queryString.
|
||||||
|
// This prevents re-running the effect if queryConstraints is a new array reference
|
||||||
|
// but its content (and thus queryString) is the same.
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [collectionPath, queryString]);
|
}, [collectionPath, queryString]);
|
||||||
|
|
||||||
return { data, isLoading, error };
|
return { data, isLoading, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,13 +224,14 @@ function App() {
|
|||||||
{!isAuthReady && !error && <p>Authenticating...</p>}
|
{!isAuthReady && !error && <p>Authenticating...</p>}
|
||||||
</main>
|
</main>
|
||||||
<footer className="bg-slate-900 p-4 text-center text-sm text-slate-400 mt-8">
|
<footer className="bg-slate-900 p-4 text-center text-sm text-slate-400 mt-8">
|
||||||
TTRPG Initiative Tracker v0.1.28
|
TTRPG Initiative Tracker v0.1.30
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Confirmation Modal Component ---
|
// --- Confirmation Modal Component ---
|
||||||
|
// ... (ConfirmationModal remains the same)
|
||||||
function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
|
function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
return (
|
return (
|
||||||
@ -241,6 +252,7 @@ function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Admin View Component ---
|
// --- Admin View Component ---
|
||||||
|
// ... (AdminView remains the same)
|
||||||
function AdminView({ userId }) {
|
function AdminView({ userId }) {
|
||||||
const { data: campaignsData, isLoading: isLoadingCampaigns } = useFirestoreCollection(getCampaignsCollectionPath());
|
const { data: campaignsData, isLoading: isLoadingCampaigns } = useFirestoreCollection(getCampaignsCollectionPath());
|
||||||
const { data: initialActiveInfoData } = useFirestoreDocument(getActiveDisplayDocPath());
|
const { data: initialActiveInfoData } = useFirestoreDocument(getActiveDisplayDocPath());
|
||||||
@ -332,6 +344,7 @@ function AdminView({ userId }) {
|
|||||||
<div key={campaign.id} onClick={() => setSelectedCampaignId(campaign.id)} className={cardClasses} style={cardStyle}>
|
<div key={campaign.id} onClick={() => setSelectedCampaignId(campaign.id)} className={cardClasses} style={cardStyle}>
|
||||||
<div className={`relative z-10 ${campaign.playerDisplayBackgroundUrl ? 'bg-black bg-opacity-60 p-3 rounded-md' : ''}`}>
|
<div className={`relative z-10 ${campaign.playerDisplayBackgroundUrl ? 'bg-black bg-opacity-60 p-3 rounded-md' : ''}`}>
|
||||||
<h3 className="text-xl font-semibold text-white">{campaign.name}</h3>
|
<h3 className="text-xl font-semibold text-white">{campaign.name}</h3>
|
||||||
|
{/* ImageIcon display removed from here */}
|
||||||
<button onClick={(e) => { 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"><Trash2 size={14} className="mr-1" /> Delete</button>
|
<button onClick={(e) => { 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"><Trash2 size={14} className="mr-1" /> Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
</div>);
|
||||||
@ -353,6 +366,10 @@ function AdminView({ userId }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- CreateCampaignForm, CharacterManager, EncounterManager, CreateEncounterForm, ParticipantManager, EditParticipantModal, InitiativeControls, DisplayView, Modal, Icons ---
|
||||||
|
// These components are identical to the previous version (v0.1.24) and are included below for completeness.
|
||||||
|
// The changes for ESLint warnings were primarily in the imports at the top of App.js and in the useFirestoreCollection hook.
|
||||||
|
|
||||||
function CreateCampaignForm({ onCreate, onCancel }) {
|
function CreateCampaignForm({ onCreate, onCancel }) {
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [backgroundUrl, setBackgroundUrl] = useState('');
|
const [backgroundUrl, setBackgroundUrl] = useState('');
|
||||||
@ -375,6 +392,7 @@ function CreateCampaignForm({ onCreate, onCancel }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function CharacterManager({ campaignId, campaignCharacters }) {
|
function CharacterManager({ campaignId, campaignCharacters }) {
|
||||||
const [characterName, setCharacterName] = useState('');
|
const [characterName, setCharacterName] = useState('');
|
||||||
const [defaultMaxHp, setDefaultMaxHp] = useState(10);
|
const [defaultMaxHp, setDefaultMaxHp] = useState(10);
|
||||||
@ -382,49 +400,24 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
|||||||
const [editingCharacter, setEditingCharacter] = useState(null);
|
const [editingCharacter, setEditingCharacter] = useState(null);
|
||||||
const [showDeleteCharConfirm, setShowDeleteCharConfirm] = useState(false);
|
const [showDeleteCharConfirm, setShowDeleteCharConfirm] = useState(false);
|
||||||
const [itemToDelete, setItemToDelete] = useState(null);
|
const [itemToDelete, setItemToDelete] = useState(null);
|
||||||
|
|
||||||
const handleAddCharacter = async () => {
|
const handleAddCharacter = async () => {
|
||||||
if (!db ||!characterName.trim() || !campaignId) return;
|
if (!db ||!characterName.trim() || !campaignId) return;
|
||||||
const hp = parseInt(defaultMaxHp, 10);
|
const hp = parseInt(defaultMaxHp, 10);
|
||||||
const initMod = parseInt(defaultInitMod, 10);
|
const initMod = parseInt(defaultInitMod, 10);
|
||||||
if (isNaN(hp) || hp <= 0) {
|
if (isNaN(hp) || hp <= 0) { alert("Please enter a valid positive number for Default Max HP."); return; }
|
||||||
alert("Please enter a valid positive number for Default Max HP.");
|
if (isNaN(initMod)) { alert("Please enter a valid number for Default Initiative Modifier."); return; }
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isNaN(initMod)) {
|
|
||||||
alert("Please enter a valid number for Default Initiative Modifier.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newCharacter = { id: generateId(), name: characterName.trim(), defaultMaxHp: hp, defaultInitMod: initMod };
|
const newCharacter = { id: generateId(), name: characterName.trim(), defaultMaxHp: hp, defaultInitMod: initMod };
|
||||||
try {
|
try { await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: [...campaignCharacters, newCharacter] }); setCharacterName(''); setDefaultMaxHp(10); setDefaultInitMod(0); } catch (err) { console.error("Error adding character:", err); }
|
||||||
await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: [...campaignCharacters, newCharacter] });
|
|
||||||
setCharacterName('');
|
|
||||||
setDefaultMaxHp(10);
|
|
||||||
setDefaultInitMod(0);
|
|
||||||
} catch (err) { console.error("Error adding character:", err); }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateCharacter = async (characterId, newName, newDefaultMaxHp, newDefaultInitMod) => {
|
const handleUpdateCharacter = async (characterId, newName, newDefaultMaxHp, newDefaultInitMod) => {
|
||||||
if (!db ||!newName.trim() || !campaignId) return;
|
if (!db ||!newName.trim() || !campaignId) return;
|
||||||
const hp = parseInt(newDefaultMaxHp, 10);
|
const hp = parseInt(newDefaultMaxHp, 10);
|
||||||
const initMod = parseInt(newDefaultInitMod, 10);
|
const initMod = parseInt(newDefaultInitMod, 10);
|
||||||
if (isNaN(hp) || hp <= 0) {
|
if (isNaN(hp) || hp <= 0) { alert("Please enter a valid positive number for Default Max HP."); setEditingCharacter(null); return; }
|
||||||
alert("Please enter a valid positive number for Default Max HP.");
|
if (isNaN(initMod)) { alert("Please enter a valid number for Default Initiative Modifier."); setEditingCharacter(null); return; }
|
||||||
setEditingCharacter(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isNaN(initMod)) {
|
|
||||||
alert("Please enter a valid number for Default Initiative Modifier.");
|
|
||||||
setEditingCharacter(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const updatedCharacters = campaignCharacters.map(c => c.id === characterId ? { ...c, name: newName.trim(), defaultMaxHp: hp, defaultInitMod: initMod } : c);
|
const updatedCharacters = campaignCharacters.map(c => c.id === characterId ? { ...c, name: newName.trim(), defaultMaxHp: hp, defaultInitMod: initMod } : c);
|
||||||
try {
|
try { await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: updatedCharacters }); setEditingCharacter(null); } catch (err) { console.error("Error updating character:", err); }
|
||||||
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 () => {
|
const confirmDeleteCharacter = async () => {
|
||||||
if (!db || !itemToDelete || itemToDelete.type !== 'character') return;
|
if (!db || !itemToDelete || itemToDelete.type !== 'character') return;
|
||||||
@ -433,7 +426,6 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
|||||||
try { await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: updatedCharacters }); } catch (err) { console.error("Error deleting character:", err); }
|
try { await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: updatedCharacters }); } catch (err) { console.error("Error deleting character:", err); }
|
||||||
setShowDeleteCharConfirm(false); setItemToDelete(null);
|
setShowDeleteCharConfirm(false); setItemToDelete(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="p-4 bg-slate-800 rounded-lg shadow">
|
<div className="p-4 bg-slate-800 rounded-lg shadow">
|
||||||
@ -588,28 +580,22 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
|||||||
const [participantName, setParticipantName] = useState('');
|
const [participantName, setParticipantName] = useState('');
|
||||||
const [participantType, setParticipantType] = useState('monster');
|
const [participantType, setParticipantType] = useState('monster');
|
||||||
const [selectedCharacterId, setSelectedCharacterId] = useState('');
|
const [selectedCharacterId, setSelectedCharacterId] = useState('');
|
||||||
const [monsterInitMod, setMonsterInitMod] = useState(2); // Default monster init mod
|
const [monsterInitMod, setMonsterInitMod] = useState(2);
|
||||||
const [maxHp, setMaxHp] = useState(10);
|
const [maxHp, setMaxHp] = useState(10);
|
||||||
const [editingParticipant, setEditingParticipant] = useState(null);
|
const [editingParticipant, setEditingParticipant] = useState(null);
|
||||||
const [hpChangeValues, setHpChangeValues] = useState({});
|
const [hpChangeValues, setHpChangeValues] = useState({});
|
||||||
const [draggedItemId, setDraggedItemId] = useState(null);
|
const [draggedItemId, setDraggedItemId] = useState(null);
|
||||||
const [showDeleteParticipantConfirm, setShowDeleteParticipantConfirm] = useState(false);
|
const [showDeleteParticipantConfirm, setShowDeleteParticipantConfirm] = useState(false);
|
||||||
const [itemToDelete, setItemToDelete] = useState(null);
|
const [itemToDelete, setItemToDelete] = useState(null);
|
||||||
const [lastRollDetails, setLastRollDetails] = useState(null); // { name, roll, mod, total }
|
const [lastRollDetails, setLastRollDetails] = useState(null);
|
||||||
const participants = encounter.participants || [];
|
const participants = encounter.participants || [];
|
||||||
|
const MONSTER_DEFAULT_INIT_MOD = 2;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (participantType === 'character' && selectedCharacterId) {
|
if (participantType === 'character' && selectedCharacterId) {
|
||||||
const selectedChar = campaignCharacters.find(c => c.id === selectedCharacterId);
|
const selectedChar = campaignCharacters.find(c => c.id === selectedCharacterId);
|
||||||
if (selectedChar && selectedChar.defaultMaxHp) {
|
if (selectedChar && selectedChar.defaultMaxHp) { setMaxHp(selectedChar.defaultMaxHp); } else { setMaxHp(10); }
|
||||||
setMaxHp(selectedChar.defaultMaxHp);
|
} else if (participantType === 'monster') { setMaxHp(10); setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD); }
|
||||||
} else {
|
|
||||||
setMaxHp(10);
|
|
||||||
}
|
|
||||||
} else if (participantType === 'monster') {
|
|
||||||
setMaxHp(10);
|
|
||||||
setMonsterInitMod(2); // Reset monster init mod when switching to monster
|
|
||||||
}
|
|
||||||
}, [selectedCharacterId, participantType, campaignCharacters]);
|
}, [selectedCharacterId, participantType, campaignCharacters]);
|
||||||
|
|
||||||
const handleAddParticipant = async () => {
|
const handleAddParticipant = async () => {
|
||||||
@ -628,17 +614,16 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
|||||||
currentMaxHp = character.defaultMaxHp || currentMaxHp;
|
currentMaxHp = character.defaultMaxHp || currentMaxHp;
|
||||||
modifier = character.defaultInitMod || 0;
|
modifier = character.defaultInitMod || 0;
|
||||||
finalInitiative = initiativeRoll + modifier;
|
finalInitiative = initiativeRoll + modifier;
|
||||||
} else { // Monster
|
} else {
|
||||||
modifier = parseInt(monsterInitMod, 10) || 0; // Use state for monster mod
|
modifier = parseInt(monsterInitMod, 10) || 0;
|
||||||
finalInitiative = initiativeRoll + modifier;
|
finalInitiative = initiativeRoll + modifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newParticipant = { id: generateId(), name: nameToAdd, type: participantType, originalCharacterId: participantType === 'character' ? selectedCharacterId : null, initiative: finalInitiative, maxHp: currentMaxHp, currentHp: currentMaxHp, conditions: [], isActive: true, };
|
const newParticipant = { id: generateId(), name: nameToAdd, type: participantType, originalCharacterId: participantType === 'character' ? selectedCharacterId : null, initiative: finalInitiative, maxHp: currentMaxHp, currentHp: currentMaxHp, conditions: [], isActive: true, };
|
||||||
try {
|
try {
|
||||||
await updateDoc(doc(db, encounterPath), { participants: [...participants, newParticipant] });
|
await updateDoc(doc(db, encounterPath), { participants: [...participants, newParticipant] });
|
||||||
setLastRollDetails({ name: nameToAdd, roll: initiativeRoll, mod: modifier, total: finalInitiative });
|
setLastRollDetails({ name: nameToAdd, roll: initiativeRoll, mod: modifier, total: finalInitiative });
|
||||||
setTimeout(() => setLastRollDetails(null), 5000); // Clear after 5 seconds
|
setTimeout(() => setLastRollDetails(null), 5000);
|
||||||
setParticipantName(''); setMaxHp(10); setSelectedCharacterId(''); setMonsterInitMod(2);
|
setParticipantName(''); setMaxHp(10); setSelectedCharacterId(''); setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD);
|
||||||
} catch (err) { console.error("Error adding participant:", err); }
|
} catch (err) { console.error("Error adding participant:", err); }
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -728,7 +713,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
|||||||
<Users2 size={16} className="mr-1.5"/><Dices size={16} className="mr-1.5"/> Add All (Roll Init)
|
<Users2 size={16} className="mr-1.5"/><Dices size={16} className="mr-1.5"/> Add All (Roll Init)
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<form onSubmit={(e) => { e.preventDefault(); handleAddParticipant(); }} className="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2 mb-4 p-3 bg-slate-700 rounded">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-300">Type</label>
|
<label className="block text-sm font-medium text-slate-300">Type</label>
|
||||||
<select value={participantType} onChange={(e) => {setParticipantType(e.target.value); setSelectedCharacterId('');}} className="mt-1 w-full px-3 py-2 bg-slate-600 border-slate-500 rounded text-white">
|
<select value={participantType} onChange={(e) => {setParticipantType(e.target.value); setSelectedCharacterId('');}} className="mt-1 w-full px-3 py-2 bg-slate-600 border-slate-500 rounded text-white">
|
||||||
@ -749,7 +734,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{participantType === 'character' && (<div><label className="block text-sm font-medium text-slate-300">Select Character</label><select value={selectedCharacterId} onChange={(e) => setSelectedCharacterId(e.target.value)} className="mt-1 w-full px-3 py-2 bg-slate-600 border-slate-500 rounded text-white"><option value="">-- Select from Campaign --</option>{campaignCharacters.map(c => <option key={c.id} value={c.id}>{c.name} (HP: {c.defaultMaxHp || 'N/A'}, Mod: {c.defaultInitMod >=0 ? `+${c.defaultInitMod}` : c.defaultInitMod})</option>)}</select></div>)}
|
{participantType === 'character' && (<div><label className="block text-sm font-medium text-slate-300">Select Character</label><select value={selectedCharacterId} onChange={(e) => setSelectedCharacterId(e.target.value)} className="mt-1 w-full px-3 py-2 bg-slate-600 border-slate-500 rounded text-white"><option value="">-- Select from Campaign --</option>{campaignCharacters.map(c => <option key={c.id} value={c.id}>{c.name} (HP: {c.defaultMaxHp || 'N/A'}, Mod: {c.defaultInitMod >=0 ? `+${c.defaultInitMod}` : c.defaultInitMod})</option>)}</select></div>)}
|
||||||
<div><label className="block text-sm font-medium text-slate-300">Max HP</label><input type="number" value={maxHp} onChange={(e) => 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" /></div>
|
<div className={participantType === 'monster' ? 'md:col-span-2' : ''}><label className="block text-sm font-medium text-slate-300">Max HP</label><input type="number" value={maxHp} onChange={(e) => 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" /></div>
|
||||||
<div className="md:col-span-2 flex justify-end"><button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-green-500 hover:bg-green-600 rounded-md transition-colors flex items-center"><Dices size={18} className="mr-1.5" /> Add to Encounter (Roll Init)</button></div>
|
<div className="md:col-span-2 flex justify-end"><button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-green-500 hover:bg-green-600 rounded-md transition-colors flex items-center"><Dices size={18} className="mr-1.5" /> Add to Encounter (Roll Init)</button></div>
|
||||||
</form>
|
</form>
|
||||||
{lastRollDetails && (
|
{lastRollDetails && (
|
||||||
@ -788,6 +773,9 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ... (EditParticipantModal, InitiativeControls, DisplayView, Modal, Icons)
|
||||||
|
// The rest of the components are assumed to be the same as v0.1.25 for this update.
|
||||||
|
|
||||||
function EditParticipantModal({ participant, onClose, onSave }) {
|
function EditParticipantModal({ participant, onClose, onSave }) {
|
||||||
const [name, setName] = useState(participant.name);
|
const [name, setName] = useState(participant.name);
|
||||||
const [initiative, setInitiative] = useState(participant.initiative);
|
const [initiative, setInitiative] = useState(participant.initiative);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user