Added small counters in Campaign cards.

This commit is contained in:
Robert Johnson 2025-05-28 12:15:01 -04:00
parent 9563ce7959
commit d754f8657c

View File

@ -214,7 +214,7 @@ 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.33 TTRPG Initiative Tracker v0.1.34
</footer> </footer>
</div> </div>
); );
@ -242,28 +242,48 @@ function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
// --- Admin View Component --- // --- Admin View Component ---
function AdminView({ userId }) { function AdminView({ userId }) {
const { data: campaignsData, isLoading: isLoadingCampaigns } = useFirestoreCollection(getCampaignsCollectionPath()); const { data: campaignsData, isLoading: isLoadingCampaigns, error: campaignsError } = useFirestoreCollection(getCampaignsCollectionPath());
const { data: initialActiveInfoData } = useFirestoreDocument(getActiveDisplayDocPath()); const { data: initialActiveInfoData } = useFirestoreDocument(getActiveDisplayDocPath());
const [campaigns, setCampaigns] = useState([]);
const [campaignsWithDetails, setCampaignsWithDetails] = useState([]);
const [selectedCampaignId, setSelectedCampaignId] = useState(null); const [selectedCampaignId, setSelectedCampaignId] = useState(null);
const [showCreateCampaignModal, setShowCreateCampaignModal] = useState(false); const [showCreateCampaignModal, setShowCreateCampaignModal] = useState(false);
const [showDeleteCampaignConfirm, setShowDeleteCampaignConfirm] = useState(false); const [showDeleteCampaignConfirm, setShowDeleteCampaignConfirm] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null); const [itemToDelete, setItemToDelete] = useState(null);
useEffect(() => { useEffect(() => {
if (campaignsData) { if (campaignsData && db) {
setCampaigns(campaignsData.map(c => ({ ...c, characters: c.players || [] }))); const fetchDetails = async () => {
const detailedCampaigns = await Promise.all(
campaignsData.map(async (campaign) => {
const characters = campaign.players || [];
let encounterCount = 0;
try {
const encountersSnapshot = await getDocs(collection(db, getEncountersCollectionPath(campaign.id)));
encounterCount = encountersSnapshot.size;
} catch (err) {
console.error(`Failed to fetch encounters for campaign ${campaign.id} (${campaign.name}):`, err);
}
return { ...campaign, characters, encounterCount };
})
);
setCampaignsWithDetails(detailedCampaigns);
};
fetchDetails();
} else if (campaignsData) {
setCampaignsWithDetails(campaignsData.map(c => ({ ...c, characters: c.players || [], encounterCount: 0 })));
} }
}, [campaignsData]); }, [campaignsData]);
useEffect(() => { useEffect(() => {
if (initialActiveInfoData && initialActiveInfoData.activeCampaignId && campaigns.length > 0 && !selectedCampaignId) { if (initialActiveInfoData && initialActiveInfoData.activeCampaignId && campaignsWithDetails.length > 0 && !selectedCampaignId) {
const campaignExists = campaigns.some(c => c.id === initialActiveInfoData.activeCampaignId); const campaignExists = campaignsWithDetails.some(c => c.id === initialActiveInfoData.activeCampaignId);
if (campaignExists) { if (campaignExists) {
setSelectedCampaignId(initialActiveInfoData.activeCampaignId); setSelectedCampaignId(initialActiveInfoData.activeCampaignId);
} }
} }
}, [initialActiveInfoData, campaigns, selectedCampaignId]); }, [initialActiveInfoData, campaignsWithDetails, selectedCampaignId]);
const handleCreateCampaign = async (name, backgroundUrl) => { const handleCreateCampaign = async (name, backgroundUrl) => {
if (!db || !name.trim()) return; if (!db || !name.trim()) return;
@ -297,6 +317,7 @@ function AdminView({ userId }) {
await batch.commit(); await batch.commit();
await deleteDoc(doc(db, getCampaignDocPath(campaignId))); await deleteDoc(doc(db, getCampaignDocPath(campaignId)));
if (selectedCampaignId === campaignId) setSelectedCampaignId(null); if (selectedCampaignId === campaignId) setSelectedCampaignId(null);
const activeDisplayRef = doc(db, getActiveDisplayDocPath()); const activeDisplayRef = doc(db, getActiveDisplayDocPath());
const activeDisplaySnap = await getDoc(activeDisplayRef); const activeDisplaySnap = await getDoc(activeDisplayRef);
if (activeDisplaySnap.exists() && activeDisplaySnap.data().activeCampaignId === campaignId) { if (activeDisplaySnap.exists() && activeDisplaySnap.data().activeCampaignId === campaignId) {
@ -307,11 +328,14 @@ function AdminView({ userId }) {
setItemToDelete(null); setItemToDelete(null);
}; };
const selectedCampaign = campaigns.find(c => c.id === selectedCampaignId); const selectedCampaign = campaignsWithDetails.find(c => c.id === selectedCampaignId);
if (isLoadingCampaigns) { if (isLoadingCampaigns) {
return <p className="text-center text-slate-300">Loading campaigns...</p>; return <p className="text-center text-slate-300">Loading campaigns...</p>;
} }
if (campaignsError) {
return <p className="text-center text-red-400">Error loading campaigns: {campaignsError.message || String(campaignsError)}</p>;
}
return ( return (
<> <>
@ -323,15 +347,25 @@ function AdminView({ userId }) {
<PlusCircle size={20} className="mr-2" /> Create Campaign <PlusCircle size={20} className="mr-2" /> Create Campaign
</button> </button>
</div> </div>
{campaigns.length === 0 && <p className="text-slate-400">No campaigns yet.</p>} {campaignsWithDetails.length === 0 && !isLoadingCampaigns && <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"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{campaigns.map(campaign => { {campaignsWithDetails.map(campaign => {
const cardStyle = campaign.playerDisplayBackgroundUrl ? { backgroundImage: `url(${campaign.playerDisplayBackgroundUrl})`} : {}; const cardStyle = campaign.playerDisplayBackgroundUrl ? { backgroundImage: `url(${campaign.playerDisplayBackgroundUrl})`} : {};
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'}`; const cardClasses = `h-40 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 ( return (
<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 flex flex-col justify-between h-full ${campaign.playerDisplayBackgroundUrl ? 'bg-black bg-opacity-60 p-3' : 'p-4'}`}> <div className={`relative z-10 flex flex-col justify-between h-full ${campaign.playerDisplayBackgroundUrl ? 'bg-black bg-opacity-60 p-3' : 'p-4'}`}>
<h3 className="text-xl font-semibold text-white">{campaign.name}</h3> <div>
<h3 className="text-xl font-semibold text-white">{campaign.name}</h3>
<div className="text-xs text-slate-100 mt-1 space-x-3">
<span className="inline-flex items-center">
<Users size={12} className="mr-1"/> {campaign.characters?.length || 0} Characters
</span>
<span className="inline-flex items-center">
<Swords size={12} className="mr-1"/> {campaign.encounterCount === undefined ? '...' : campaign.encounterCount} Encounters
</span>
</div>
</div>
<button onClick={(e) => { e.stopPropagation(); requestDeleteCampaign(campaign.id, campaign.name); }} className="mt-auto text-red-300 hover:text-red-100 text-xs flex items-center self-start 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-auto text-red-300 hover:text-red-100 text-xs flex items-center self-start 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>);
@ -342,9 +376,9 @@ function AdminView({ userId }) {
{selectedCampaign && ( {selectedCampaign && (
<div className="mt-6 p-6 bg-slate-750 rounded-lg shadow-xl"> <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> <h2 className="text-2xl font-semibold text-amber-300 mb-4">Managing: {selectedCampaign.name}</h2>
<CharacterManager campaignId={selectedCampaignId} campaignCharacters={selectedCampaign.players || []} /> <CharacterManager campaignId={selectedCampaignId} campaignCharacters={selectedCampaign.characters || []} />
<hr className="my-6 border-slate-600" /> <hr className="my-6 border-slate-600" />
<EncounterManager campaignId={selectedCampaignId} initialActiveEncounterId={initialActiveInfoData && initialActiveInfoData.activeCampaignId === selectedCampaignId ? initialActiveInfoData.activeEncounterId : null} campaignCharacters={selectedCampaign.players || []} /> <EncounterManager campaignId={selectedCampaignId} initialActiveEncounterId={initialActiveInfoData && initialActiveInfoData.activeCampaignId === selectedCampaignId ? initialActiveInfoData.activeEncounterId : null} campaignCharacters={selectedCampaign.characters || []} />
</div> </div>
)} )}
</div> </div>
@ -353,10 +387,7 @@ function AdminView({ userId }) {
); );
} }
// --- CreateCampaignForm, CharacterManager, EncounterManager, CreateEncounterForm, ParticipantManager, EditParticipantModal, InitiativeControls, DisplayView, Modal, Icons --- // --- CreateCampaignForm (No Change from v0.1.33) ---
// The rest of the components are identical to the previous version (v0.1.29) and are included below for completeness.
// Only DisplayView has changes for participant rendering logic.
function CreateCampaignForm({ onCreate, onCancel }) { function CreateCampaignForm({ onCreate, onCancel }) {
const [name, setName] = useState(''); const [name, setName] = useState('');
const [backgroundUrl, setBackgroundUrl] = useState(''); const [backgroundUrl, setBackgroundUrl] = useState('');
@ -379,7 +410,7 @@ function CreateCampaignForm({ onCreate, onCancel }) {
); );
} }
// --- CharacterManager (No Change from v0.1.33) ---
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);
@ -805,6 +836,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.28 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);