From 788e3cd1a2ddd046b12395910e4ba427dff833f7 Mon Sep 17 00:00:00 2001 From: robert Date: Tue, 27 May 2025 11:02:03 -0400 Subject: [PATCH] Tightened up the UI a bit. --- src/App.js | 86 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/src/App.js b/src/App.js index 836d432..59cd4c0 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, 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 removed // --- Firebase Configuration --- const firebaseConfig = { @@ -111,7 +111,7 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) { setData([]); }); return () => unsubscribe(); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [collectionPath, queryString]); return { data, isLoading, error }; } @@ -214,7 +214,7 @@ function App() { {!isAuthReady && !error &&

Authenticating...

} ); @@ -327,12 +327,12 @@ function AdminView({ userId }) {
{campaigns.map(campaign => { 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'}`; + const cardClasses = `h-36 flex flex-col justify-between 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}> -
+

{campaign.name}

- +
); })} @@ -353,6 +353,9 @@ function AdminView({ userId }) { ); } +// --- CreateCampaignForm, CharacterManager, EncounterManager, CreateEncounterForm, ParticipantManager, EditParticipantModal, InitiativeControls, DisplayView, Modal, Icons --- +// These components are identical to the previous version (v0.1.28) and are included below for completeness. + function CreateCampaignForm({ onCreate, onCancel }) { const [name, setName] = useState(''); const [backgroundUrl, setBackgroundUrl] = useState(''); @@ -375,6 +378,7 @@ function CreateCampaignForm({ onCreate, onCancel }) { ); } + function CharacterManager({ campaignId, campaignCharacters }) { const [characterName, setCharacterName] = useState(''); const [defaultMaxHp, setDefaultMaxHp] = useState(10); @@ -382,7 +386,6 @@ function CharacterManager({ campaignId, campaignCharacters }) { 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 hp = parseInt(defaultMaxHp, 10); @@ -565,7 +568,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { const [selectedCharacterId, setSelectedCharacterId] = useState(''); const [monsterInitMod, setMonsterInitMod] = useState(2); const [maxHp, setMaxHp] = useState(10); - const [isNpc, setIsNpc] = useState(false); // New state for NPC checkbox + const [isNpc, setIsNpc] = useState(false); const [editingParticipant, setEditingParticipant] = useState(null); const [hpChangeValues, setHpChangeValues] = useState({}); const [draggedItemId, setDraggedItemId] = useState(null); @@ -579,11 +582,10 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { if (participantType === 'character' && selectedCharacterId) { const selectedChar = campaignCharacters.find(c => c.id === selectedCharacterId); if (selectedChar && selectedChar.defaultMaxHp) { setMaxHp(selectedChar.defaultMaxHp); } else { setMaxHp(10); } - setIsNpc(false); // Characters cannot be NPCs in this model + setIsNpc(false); } else if (participantType === 'monster') { setMaxHp(10); setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD); - // setIsNpc(false); // Reset NPC status when switching to monster, or keep last state? Let's reset. } }, [selectedCharacterId, participantType, campaignCharacters]); @@ -604,16 +606,16 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { currentMaxHp = character.defaultMaxHp || currentMaxHp; modifier = character.defaultInitMod || 0; finalInitiative = initiativeRoll + modifier; - } else { // Monster + } else { modifier = parseInt(monsterInitMod, 10) || 0; finalInitiative = initiativeRoll + modifier; - participantIsNpc = isNpc; // Use the state of the NPC checkbox + participantIsNpc = isNpc; } const newParticipant = { id: generateId(), name: nameToAdd, type: participantType, originalCharacterId: participantType === 'character' ? selectedCharacterId : null, initiative: finalInitiative, maxHp: currentMaxHp, currentHp: currentMaxHp, - isNpc: participantType === 'monster' ? participantIsNpc : false, // Store isNpc for monsters + isNpc: participantType === 'monster' ? participantIsNpc : false, conditions: [], isActive: true, }; try { @@ -635,7 +637,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { const modifier = char.defaultInitMod || 0; const finalInitiative = initiativeRoll + modifier; consoleRollLog += `${char.name}: Rolled D20 (${initiativeRoll}) + ${modifier} (mod) = ${finalInitiative} init\n`; - return { id: generateId(), name: char.name, type: 'character', originalCharacterId: char.id, initiative: finalInitiative, maxHp: char.defaultMaxHp || 10, currentHp: char.defaultMaxHp || 10, conditions: [], isActive: true, isNpc: false }; // Characters are not NPCs + return { id: generateId(), name: char.name, type: 'character', originalCharacterId: char.id, initiative: finalInitiative, maxHp: char.defaultMaxHp || 10, currentHp: char.defaultMaxHp || 10, conditions: [], isActive: true, isNpc: false }; }); if (newParticipants.length === 0) { alert("All campaign characters are already in this encounter."); return; } console.log(consoleRollLog); @@ -645,7 +647,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { const handleUpdateParticipant = async (updatedData) => { if (!db || !editingParticipant) return; - const updatedParticipants = participants.map(p => p.id === editingParticipant.id ? { ...p, ...updatedData } : p ); // updatedData now includes isNpc + const updatedParticipants = participants.map(p => p.id === editingParticipant.id ? { ...p, ...updatedData } : p ); 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); }; @@ -711,37 +713,60 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { Add All (Roll Init)
-
{ e.preventDefault(); handleAddParticipant(); }} className="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3 mb-4 p-3 bg-slate-700 rounded"> -
+ { e.preventDefault(); handleAddParticipant(); }} className="grid grid-cols-1 md:grid-cols-6 gap-x-4 gap-y-2 mb-4 p-3 bg-slate-700 rounded items-end"> +
+ {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" />
-
+
setMonsterInitMod(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" /> +
+
setIsNpc(e.target.checked)} className="h-4 w-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500" />
)} - {participantType === 'character' && (
)} -
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" />
-
+ {participantType === 'character' && ( + <> +
+ + +
+
+ + 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" /> +
+ + )} + +
+ +
{lastRollDetails && (

- {lastRollDetails.name} ({lastRollDetails.type}): Rolled D20 ({lastRollDetails.roll}) {lastRollDetails.mod >= 0 ? '+' : ''} {lastRollDetails.mod} (mod) = {lastRollDetails.total} Initiative + {lastRollDetails.name} ({lastRollDetails.type === 'character' ? 'Character' : lastRollDetails.type}): Rolled D20 ({lastRollDetails.roll}) {lastRollDetails.mod >= 0 ? '+' : ''} {lastRollDetails.mod} (mod) = {lastRollDetails.total} Initiative

)} {participants.length === 0 &&

No participants.

} @@ -750,7 +775,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { const isCurrentTurn = encounter.isStarted && p.id === encounter.currentTurnParticipantId; const isDraggable = (!encounter.isStarted || encounter.isPaused) && tiedInitiatives.includes(Number(p.initiative)); const participantDisplayType = p.type === 'monster' ? (p.isNpc ? 'NPC' : 'Monster') : 'Character'; - let bgColor = p.type === 'character' ? 'bg-sky-800' : (p.isNpc ? 'bg-slate-600' : 'bg-red-800'); + let bgColor = p.type === 'character' ? 'bg-sky-800' : (p.isNpc ? 'bg-slate-600' : 'bg-red-800'); if (isCurrentTurn && !encounter.isPaused) bgColor = 'bg-green-600'; return ( @@ -793,7 +818,7 @@ function EditParticipantModal({ participant, onClose, onSave }) { initiative: parseInt(initiative, 10), currentHp: parseInt(currentHp, 10), maxHp: parseInt(maxHp, 10), - isNpc: participant.type === 'monster' ? isNpc : false, // Only save isNpc for monsters + isNpc: participant.type === 'monster' ? isNpc : false, }); }; return ( @@ -820,10 +845,6 @@ function EditParticipantModal({ participant, onClose, onSave }) { ); } -// ... (InitiativeControls, DisplayView, Modal, Icons) -// The rest of the components are assumed to be the same as v0.1.25 for this update. -// DisplayView will need to be updated to reflect NPC styling. - function InitiativeControls({ campaignId, encounter, encounterPath }) { const [showEndEncounterConfirm, setShowEndEncounterConfirm] = useState(false); const handleStartEncounter = async () => { @@ -964,7 +985,7 @@ function DisplayView() { let participantBgColor = 'bg-red-700'; // Default for monster if (p.type === 'character') { participantBgColor = 'bg-sky-700'; - } else if (p.isNpc) { + } else if (p.isNpc) { // Monster that is an NPC participantBgColor = 'bg-slate-700'; // Muted gray for NPC } if (p.id === currentTurnParticipantId && isStarted && !isPaused) { @@ -987,8 +1008,7 @@ function DisplayView() {
{p.conditions?.length > 0 &&

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

} {!p.isActive &&

(Inactive)

} -
- ); +
); })}