Added collapsible Characters.
This commit is contained in:
107
src/App.js
107
src/App.js
@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth';
|
||||
import { getFirestore, doc, setDoc, getDoc, getDocs, collection, onSnapshot, updateDoc, deleteDoc, query, writeBatch } from 'firebase/firestore';
|
||||
import { PlusCircle, Users, Swords, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, UserCheck, UserX, HeartCrack, HeartPulse, Zap, EyeOff, ExternalLink, AlertTriangle, Play as PlayIcon, Pause as PauseIcon, SkipForward as SkipForwardIcon, StopCircle as StopCircleIcon, Users2, Dices } from 'lucide-react';
|
||||
import { PlusCircle, Users, Swords, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, UserCheck, UserX, HeartCrack, HeartPulse, Zap, EyeOff, ExternalLink, AlertTriangle, Play as PlayIcon, Pause as PauseIcon, SkipForward as SkipForwardIcon, StopCircle as StopCircleIcon, Users2, Dices, ChevronDown, ChevronUp } from 'lucide-react'; // Added ChevronDown, ChevronUp
|
||||
|
||||
// --- Firebase Configuration ---
|
||||
const firebaseConfig = {
|
||||
@ -214,16 +214,13 @@ 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.2.0
|
||||
TTRPG Initiative Tracker v0.2.1
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Confirmation Modal Component ---
|
||||
// ... (ConfirmationModal, AdminView, CreateCampaignForm, CharacterManager, EncounterManager, CreateEncounterForm, ParticipantManager, EditParticipantModal, InitiativeControls, DisplayView, Modal, Icons remain the same as v0.1.29)
|
||||
// For brevity, only the changed App component's footer is shown. Assume all other components are the same as the last complete code block you received.
|
||||
|
||||
function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
@ -243,6 +240,7 @@ function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
|
||||
);
|
||||
}
|
||||
|
||||
// --- Admin View Component ---
|
||||
function AdminView({ userId }) {
|
||||
const { data: campaignsData, isLoading: isLoadingCampaigns, error: campaignsError } = useFirestoreCollection(getCampaignsCollectionPath());
|
||||
const { data: initialActiveInfoData } = useFirestoreDocument(getActiveDisplayDocPath());
|
||||
@ -416,6 +414,8 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
||||
const [editingCharacter, setEditingCharacter] = useState(null);
|
||||
const [showDeleteCharConfirm, setShowDeleteCharConfirm] = useState(false);
|
||||
const [itemToDelete, setItemToDelete] = useState(null);
|
||||
const [isCharactersSectionOpen, setIsCharactersSectionOpen] = useState(true); // ADDED THIS LINE
|
||||
|
||||
const handleAddCharacter = async () => {
|
||||
if (!db ||!characterName.trim() || !campaignId) return;
|
||||
const hp = parseInt(defaultMaxHp, 10);
|
||||
@ -445,52 +445,69 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
||||
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="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4 items-end">
|
||||
<div className="sm:col-span-1">
|
||||
<label htmlFor="characterName" className="block text-xs font-medium text-slate-400">Name</label>
|
||||
<input type="text" id="characterName" value={characterName} onChange={(e) => setCharacterName(e.target.value)} placeholder="New character name" className="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="w-full sm:w-auto">
|
||||
<label htmlFor="defaultMaxHp" className="block text-xs font-medium text-slate-400">Default HP</label>
|
||||
<input type="number" id="defaultMaxHp" value={defaultMaxHp} onChange={(e) => setDefaultMaxHp(e.target.value)} placeholder="HP" className="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="w-full sm:w-auto">
|
||||
<label htmlFor="defaultInitMod" className="block text-xs font-medium text-slate-400">Init Mod</label>
|
||||
<input type="number" id="defaultInitMod" value={defaultInitMod} onChange={(e) => setDefaultInitMod(e.target.value)} placeholder="+0" className="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>
|
||||
<button type="submit" className="sm:col-span-3 sm:w-auto sm:justify-self-end px-4 py-2 text-sm font-medium text-white bg-sky-500 hover:bg-sky-600 rounded-md transition-colors flex items-center justify-center"><PlusCircle size={18} className="mr-1" /> Add Character</button>
|
||||
</form>
|
||||
{campaignCharacters.length === 0 && <p className="text-sm text-slate-400">No characters added.</p>}
|
||||
<ul className="space-y-2">
|
||||
{campaignCharacters.map(character => (
|
||||
<li key={character.id} className="flex justify-between items-center p-3 bg-slate-700 rounded-md">
|
||||
{editingCharacter && editingCharacter.id === character.id ? (
|
||||
<form onSubmit={(e) => {e.preventDefault(); handleUpdateCharacter(character.id, editingCharacter.name, editingCharacter.defaultMaxHp, editingCharacter.defaultInitMod);}} className="flex-grow flex flex-wrap gap-2 items-center">
|
||||
<input type="text" value={editingCharacter.name} onChange={(e) => setEditingCharacter({...editingCharacter, name: e.target.value})} className="flex-grow min-w-[100px] px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white"/>
|
||||
<input type="number" value={editingCharacter.defaultMaxHp} onChange={(e) => setEditingCharacter({...editingCharacter, defaultMaxHp: e.target.value})} className="w-20 px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white" title="Default Max HP"/>
|
||||
<input type="number" value={editingCharacter.defaultInitMod} onChange={(e) => setEditingCharacter({...editingCharacter, defaultInitMod: e.target.value})} className="w-20 px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white" title="Default Init Mod"/>
|
||||
<button type="submit" className="p-1 text-green-400 hover:text-green-300"><Save size={18}/></button>
|
||||
<button type="button" onClick={() => setEditingCharacter(null)} className="p-1 text-slate-400 hover:text-slate-200"><XCircle size={18}/></button>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-slate-100">{character.name} <span className="text-xs text-slate-400">(HP: {character.defaultMaxHp || 'N/A'}, Init Mod: {character.defaultInitMod !== undefined ? (character.defaultInitMod >= 0 ? `+${character.defaultInitMod}` : character.defaultInitMod) : 'N/A'})</span></span>
|
||||
<div className="flex space-x-2">
|
||||
<button onClick={() => setEditingCharacter({id: character.id, name: character.name, defaultMaxHp: character.defaultMaxHp || 10, defaultInitMod: character.defaultInitMod || 0})} 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={() => 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 className="flex justify-between items-center mb-3"> {/* ADDED THIS DIV FOR HEADER AND TOGGLE */}
|
||||
<h3 className="text-xl font-semibold text-sky-300 flex items-center"><Users size={24} className="mr-2" /> Campaign Characters</h3>
|
||||
<button
|
||||
onClick={() => setIsCharactersSectionOpen(!isCharactersSectionOpen)}
|
||||
className="p-1 text-slate-400 hover:text-slate-200"
|
||||
aria-label={isCharactersSectionOpen ? "Collapse Characters Section" : "Expand Characters Section"}
|
||||
>
|
||||
{isCharactersSectionOpen ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isCharactersSectionOpen && ( // ADDED CONDITIONAL RENDERING
|
||||
<>
|
||||
<form onSubmit={(e) => { e.preventDefault(); handleAddCharacter(); }} className="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4 items-end">
|
||||
<div className="sm:col-span-1">
|
||||
<label htmlFor="characterName" className="block text-xs font-medium text-slate-400">Name</label>
|
||||
<input type="text" id="characterName" value={characterName} onChange={(e) => setCharacterName(e.target.value)} placeholder="New character name" className="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="w-full sm:w-auto">
|
||||
<label htmlFor="defaultMaxHp" className="block text-xs font-medium text-slate-400">Default HP</label>
|
||||
<input type="number" id="defaultMaxHp" value={defaultMaxHp} onChange={(e) => setDefaultMaxHp(e.target.value)} placeholder="HP" className="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="w-full sm:w-auto">
|
||||
<label htmlFor="defaultInitMod" className="block text-xs font-medium text-slate-400">Init Mod</label>
|
||||
<input type="number" id="defaultInitMod" value={defaultInitMod} onChange={(e) => setDefaultInitMod(e.target.value)} placeholder="+0" className="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>
|
||||
<button type="submit" className="sm:col-span-3 sm:w-auto sm:justify-self-end px-4 py-2 text-sm font-medium text-white bg-sky-500 hover:bg-sky-600 rounded-md transition-colors flex items-center justify-center"><PlusCircle size={18} className="mr-1" /> Add Character</button>
|
||||
</form>
|
||||
{campaignCharacters.length === 0 && <p className="text-sm text-slate-400">No characters added.</p>}
|
||||
<ul className="space-y-2">
|
||||
{campaignCharacters.map(character => (
|
||||
<li key={character.id} className="flex justify-between items-center p-3 bg-slate-700 rounded-md">
|
||||
{editingCharacter && editingCharacter.id === character.id ? (
|
||||
<form onSubmit={(e) => {e.preventDefault(); handleUpdateCharacter(character.id, editingCharacter.name, editingCharacter.defaultMaxHp, editingCharacter.defaultInitMod);}} className="flex-grow flex flex-wrap gap-2 items-center">
|
||||
<input type="text" value={editingCharacter.name} onChange={(e) => setEditingCharacter({...editingCharacter, name: e.target.value})} className="flex-grow min-w-[100px] px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white"/>
|
||||
<input type="number" value={editingCharacter.defaultMaxHp} onChange={(e) => setEditingCharacter({...editingCharacter, defaultMaxHp: e.target.value})} className="w-20 px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white" title="Default Max HP"/>
|
||||
<input type="number" value={editingCharacter.defaultInitMod} onChange={(e) => setEditingCharacter({...editingCharacter, defaultInitMod: e.target.value})} className="w-20 px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white" title="Default Init Mod"/>
|
||||
<button type="submit" className="p-1 text-green-400 hover:text-green-300"><Save size={18}/></button>
|
||||
<button type="button" onClick={() => setEditingCharacter(null)} className="p-1 text-slate-400 hover:text-slate-200"><XCircle size={18}/></button>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-slate-100">{character.name} <span className="text-xs text-slate-400">(HP: {character.defaultMaxHp || 'N/A'}, Init Mod: {character.defaultInitMod !== undefined ? (character.defaultInitMod >= 0 ? `+${character.defaultInitMod}` : character.defaultInitMod) : 'N/A'})</span></span>
|
||||
<div className="flex space-x-2">
|
||||
<button onClick={() => setEditingCharacter({id: character.id, name: character.name, defaultMaxHp: character.defaultMaxHp || 10, defaultInitMod: character.defaultInitMod || 0})} 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={() => 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, CreateEncounterForm, ParticipantManager, EditParticipantModal, InitiativeControls, DisplayView, Modal, Icons)
|
||||
// The rest of the components are assumed to be the same as v0.1.30 for this update.
|
||||
|
||||
function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharacters }) {
|
||||
const {data: encountersData, isLoading: isLoadingEncounters } = useFirestoreCollection(campaignId ? getEncountersCollectionPath(campaignId) : null);
|
||||
const {data: activeDisplayInfoFromHook } = useFirestoreDocument(getActiveDisplayDocPath());
|
||||
|
Reference in New Issue
Block a user