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>}
</main>
<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>
</div>
);
@ -242,28 +242,48 @@ function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
// --- Admin View Component ---
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 [campaigns, setCampaigns] = useState([]);
const [campaignsWithDetails, setCampaignsWithDetails] = useState([]);
const [selectedCampaignId, setSelectedCampaignId] = useState(null);
const [showCreateCampaignModal, setShowCreateCampaignModal] = useState(false);
const [showDeleteCampaignConfirm, setShowDeleteCampaignConfirm] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null);
useEffect(() => {
if (campaignsData) {
setCampaigns(campaignsData.map(c => ({ ...c, characters: c.players || [] })));
if (campaignsData && db) {
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]);
useEffect(() => {
if (initialActiveInfoData && initialActiveInfoData.activeCampaignId && campaigns.length > 0 && !selectedCampaignId) {
const campaignExists = campaigns.some(c => c.id === initialActiveInfoData.activeCampaignId);
if (initialActiveInfoData && initialActiveInfoData.activeCampaignId && campaignsWithDetails.length > 0 && !selectedCampaignId) {
const campaignExists = campaignsWithDetails.some(c => c.id === initialActiveInfoData.activeCampaignId);
if (campaignExists) {
setSelectedCampaignId(initialActiveInfoData.activeCampaignId);
}
}
}, [initialActiveInfoData, campaigns, selectedCampaignId]);
}, [initialActiveInfoData, campaignsWithDetails, selectedCampaignId]);
const handleCreateCampaign = async (name, backgroundUrl) => {
if (!db || !name.trim()) return;
@ -297,6 +317,7 @@ function AdminView({ userId }) {
await batch.commit();
await deleteDoc(doc(db, getCampaignDocPath(campaignId)));
if (selectedCampaignId === campaignId) setSelectedCampaignId(null);
const activeDisplayRef = doc(db, getActiveDisplayDocPath());
const activeDisplaySnap = await getDoc(activeDisplayRef);
if (activeDisplaySnap.exists() && activeDisplaySnap.data().activeCampaignId === campaignId) {
@ -307,11 +328,14 @@ function AdminView({ userId }) {
setItemToDelete(null);
};
const selectedCampaign = campaigns.find(c => c.id === selectedCampaignId);
const selectedCampaign = campaignsWithDetails.find(c => c.id === selectedCampaignId);
if (isLoadingCampaigns) {
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 (
<>
@ -323,15 +347,25 @@ function AdminView({ userId }) {
<PlusCircle size={20} className="mr-2" /> Create Campaign
</button>
</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">
{campaigns.map(campaign => {
{campaignsWithDetails.map(campaign => {
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 (
<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'}`}>
<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>
</div>
</div>);
@ -342,9 +376,9 @@ function AdminView({ userId }) {
{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 || []} />
<CharacterManager campaignId={selectedCampaignId} campaignCharacters={selectedCampaign.characters || []} />
<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>
@ -353,10 +387,7 @@ function AdminView({ userId }) {
);
}
// --- CreateCampaignForm, CharacterManager, EncounterManager, CreateEncounterForm, ParticipantManager, EditParticipantModal, InitiativeControls, DisplayView, Modal, Icons ---
// 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.
// --- CreateCampaignForm (No Change from v0.1.33) ---
function CreateCampaignForm({ onCreate, onCancel }) {
const [name, setName] = useState('');
const [backgroundUrl, setBackgroundUrl] = useState('');
@ -379,7 +410,7 @@ function CreateCampaignForm({ onCreate, onCancel }) {
);
}
// --- CharacterManager (No Change from v0.1.33) ---
function CharacterManager({ campaignId, campaignCharacters }) {
const [characterName, setCharacterName] = useState('');
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 }) {
const [name, setName] = useState(participant.name);
const [initiative, setInitiative] = useState(participant.initiative);
@ -966,7 +1000,7 @@ function DisplayView() {
const activeParticipants = participants.filter(p => p.isActive);
participantsToRender = [...activeParticipants].sort((a, b) => {
if (a.initiative === b.initiative) {
const indexA = participants.findIndex(p => p.id === a.id);
const indexA = participants.findIndex(p => p.id === a.id);
const indexB = participants.findIndex(p => p.id === b.id);
return indexA - indexB;
}