Added small counters in Campaign cards.
This commit is contained in:
parent
9563ce7959
commit
d754f8657c
72
src/App.js
72
src/App.js
@ -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'}`}>
|
||||||
|
<div>
|
||||||
<h3 className="text-xl font-semibold text-white">{campaign.name}</h3>
|
<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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user