Added small counters in Campaign cards.
This commit is contained in:
parent
9563ce7959
commit
d754f8657c
76
src/App.js
76
src/App.js
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user