diff --git a/src/App.js b/src/App.js index a9107e6..5827f67 100644 --- a/src/App.js +++ b/src/App.js @@ -214,7 +214,7 @@ function App() { {!isAuthReady && !error &&

Authenticating...

} ); @@ -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

Loading campaigns...

; } + if (campaignsError) { + return

Error loading campaigns: {campaignsError.message || String(campaignsError)}

; + } return ( <> @@ -323,15 +347,25 @@ function AdminView({ userId }) { Create Campaign - {campaigns.length === 0 &&

No campaigns yet.

} + {campaignsWithDetails.length === 0 && !isLoadingCampaigns &&

No campaigns yet.

}
- {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 (
setSelectedCampaignId(campaign.id)} className={cardClasses} style={cardStyle}>
-

{campaign.name}

+
+

{campaign.name}

+
+ + {campaign.characters?.length || 0} Characters + + + {campaign.encounterCount === undefined ? '...' : campaign.encounterCount} Encounters + +
+
); @@ -342,9 +376,9 @@ function AdminView({ userId }) { {selectedCampaign && (

Managing: {selectedCampaign.name}

- +
- +
)}
@@ -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; }