changing the player display button.

This commit is contained in:
Robert Johnson 2025-05-26 07:59:05 -04:00
parent eb114910f8
commit 085303fbab

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import { initializeApp } from 'firebase/app'; import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth'; import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth';
import { getFirestore, doc, setDoc, addDoc, getDoc, getDocs, collection, onSnapshot, updateDoc, deleteDoc, query, writeBatch } from 'firebase/firestore'; import { getFirestore, doc, setDoc, addDoc, getDoc, getDocs, collection, onSnapshot, updateDoc, deleteDoc, query, writeBatch } from 'firebase/firestore';
import { ArrowLeft, PlusCircle, Users, Swords, Shield, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, ChevronDown, ChevronUp, UserCheck, UserX, HeartCrack, HeartPulse, Zap, Share2, Copy as CopyIcon, Image as ImageIcon, EyeOff } from 'lucide-react'; import { ArrowLeft, PlusCircle, Users, Swords, Shield, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, ChevronDown, ChevronUp, UserCheck, UserX, HeartCrack, HeartPulse, Zap, Share2, Copy as CopyIcon, Image as ImageIcon, EyeOff, ExternalLink } from 'lucide-react'; // Added ExternalLink
// --- Firebase Configuration --- // --- Firebase Configuration ---
const firebaseConfig = { const firebaseConfig = {
@ -38,26 +38,26 @@ if (missingKeys.length > 0) {
const APP_ID = process.env.REACT_APP_TRACKER_APP_ID || 'ttrpg-initiative-tracker-default'; const APP_ID = process.env.REACT_APP_TRACKER_APP_ID || 'ttrpg-initiative-tracker-default';
const PUBLIC_DATA_PATH = `artifacts/${APP_ID}/public/data`; const PUBLIC_DATA_PATH = `artifacts/${APP_ID}/public/data`;
const CAMPAIGNS_COLLECTION = `${PUBLIC_DATA_PATH}/campaigns`; const CAMPAIGNS_COLLECTION = `${PUBLIC_DATA_PATH}/campaigns`;
const ACTIVE_DISPLAY_DOC = `${PUBLIC_DATA_PATH}/activeDisplay/status`; // Single source for what player display shows const ACTIVE_DISPLAY_DOC = `${PUBLIC_DATA_PATH}/activeDisplay/status`;
// --- Helper Functions --- // --- Helper Functions ---
const generateId = () => crypto.randomUUID(); const generateId = () => crypto.randomUUID();
// getShareableLinkBase is no longer needed for individual encounter links if we have one player display URL
// function getShareableLinkBase() {
// return window.location.origin + window.location.pathname;
// }
// --- Main App Component --- // --- Main App Component ---
function App() { function App() {
const [userId, setUserId] = useState(null); const [userId, setUserId] = useState(null);
const [isAuthReady, setIsAuthReady] = useState(false); const [isAuthReady, setIsAuthReady] = useState(false);
const [viewMode, setViewMode] = useState('admin');
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
// directDisplayParams and hash routing removed, Player Display solely relies on ACTIVE_DISPLAY_DOC const [isPlayerViewOnlyMode, setIsPlayerViewOnlyMode] = useState(false);
useEffect(() => { useEffect(() => {
// Check for query parameter to determine if this is a player-only display window
const queryParams = new URLSearchParams(window.location.search);
if (queryParams.get('playerView') === 'true') {
setIsPlayerViewOnlyMode(true);
}
if (!auth) { if (!auth) {
setError("Firebase Auth not initialized. Check your Firebase configuration."); setError("Firebase Auth not initialized. Check your Firebase configuration.");
setIsLoading(false); setIsLoading(false);
@ -113,46 +113,54 @@ function App() {
); );
} }
const goToAdminView = () => { const openPlayerWindow = () => {
setViewMode('admin'); // Construct the URL for the player view.
// It's the current path + ?playerView=true
// window.location.origin gives http://localhost:3000
// window.location.pathname gives / (or your base path if deployed in a subfolder)
const playerViewUrl = window.location.origin + window.location.pathname + '?playerView=true';
window.open(playerViewUrl, '_blank', 'noopener,noreferrer,width=1024,height=768'); // Opens in a new tab/window
}; };
if (isPlayerViewOnlyMode) {
// Render only the DisplayView if in player-only mode
return (
<div className="min-h-screen bg-slate-800 text-slate-100 font-sans">
{isAuthReady && <DisplayView />}
{!isAuthReady && !error && <p>Authenticating for Player Display...</p>}
</div>
);
}
// Default Admin View
return ( return (
<div className="min-h-screen bg-slate-800 text-slate-100 font-sans"> <div className="min-h-screen bg-slate-800 text-slate-100 font-sans">
<header className="bg-slate-900 p-4 shadow-lg"> <header className="bg-slate-900 p-4 shadow-lg">
<div className="container mx-auto flex justify-between items-center"> <div className="container mx-auto flex justify-between items-center">
<h1 <h1
className="text-3xl font-bold text-sky-400 cursor-pointer hover:text-sky-300 transition-colors" className="text-3xl font-bold text-sky-400" // No longer needs to be clickable to go to admin, as this IS admin view
onClick={goToAdminView}
title="Go to Admin View"
> >
TTRPG Initiative Tracker TTRPG Initiative Tracker
</h1> </h1>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
{userId && <span className="text-xs text-slate-400">UID: {userId}</span>} {userId && <span className="text-xs text-slate-400">UID: {userId}</span>}
<button <button
onClick={() => { onClick={openPlayerWindow}
if (viewMode === 'display') { className={`px-4 py-2 rounded-md text-sm font-medium transition-colors bg-teal-500 hover:bg-teal-600 text-white flex items-center`}
goToAdminView();
} else {
setViewMode('display');
}
}}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${viewMode === 'display' ? 'bg-teal-500 text-white' : 'bg-slate-700 hover:bg-slate-600'}`}
> >
{viewMode === 'display' ? 'Admin Controls' : 'Player Display'} <ExternalLink size={16} className="mr-2"/> Open Player Window
</button> </button>
</div> </div>
</div> </div>
</header> </header>
<main className={`container mx-auto p-4 md:p-8`}> <main className={`container mx-auto p-4 md:p-8`}>
{viewMode === 'admin' && isAuthReady && userId && <AdminView userId={userId} />} {isAuthReady && userId && <AdminView userId={userId} />}
{viewMode === 'display' && isAuthReady && <DisplayView />} {/* DisplayView no longer needs URL params */}
{!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.16 TTRPG Initiative Tracker v0.1.17
</footer> </footer>
</div> </div>
); );
@ -377,7 +385,6 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
const [selectedEncounterId, setSelectedEncounterId] = useState(null); const [selectedEncounterId, setSelectedEncounterId] = useState(null);
const [showCreateEncounterModal, setShowCreateEncounterModal] = useState(false); const [showCreateEncounterModal, setShowCreateEncounterModal] = useState(false);
const [activeDisplayInfo, setActiveDisplayInfo] = useState(null); const [activeDisplayInfo, setActiveDisplayInfo] = useState(null);
// copiedLinkEncounterId removed as shareable links per encounter are removed
const encountersPath = `${CAMPAIGNS_COLLECTION}/${campaignId}/encounters`; const encountersPath = `${CAMPAIGNS_COLLECTION}/${campaignId}/encounters`;
const selectedEncounterIdRef = useRef(selectedEncounterId); const selectedEncounterIdRef = useRef(selectedEncounterId);
@ -451,14 +458,12 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
const currentActiveEncounter = activeDisplayInfo?.activeEncounterId; const currentActiveEncounter = activeDisplayInfo?.activeEncounterId;
if (currentActiveCampaign === campaignId && currentActiveEncounter === encounterId) { if (currentActiveCampaign === campaignId && currentActiveEncounter === encounterId) {
// Currently active, so toggle off (clear the active display)
await setDoc(doc(db, ACTIVE_DISPLAY_DOC), { await setDoc(doc(db, ACTIVE_DISPLAY_DOC), {
activeCampaignId: null, activeCampaignId: null,
activeEncounterId: null, activeEncounterId: null,
}, { merge: true }); // Use set with merge to ensure document exists or is overwritten }, { merge: true });
console.log("Player Display for this encounter turned OFF."); console.log("Player Display for this encounter turned OFF.");
} else { } else {
// Not active or different encounter, so set this one as active for players
await setDoc(doc(db, ACTIVE_DISPLAY_DOC), { await setDoc(doc(db, ACTIVE_DISPLAY_DOC), {
activeCampaignId: campaignId, activeCampaignId: campaignId,
activeEncounterId: encounterId, activeEncounterId: encounterId,
@ -470,9 +475,6 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
} }
}; };
// Shareable link per encounter removed
// const handleCopyToClipboard = (encounterId) => { ... };
const selectedEncounter = encounters.find(e => e.id === selectedEncounterId); const selectedEncounter = encounters.find(e => e.id === selectedEncounterId);
return ( return (
@ -501,11 +503,9 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
> >
{isLiveOnPlayerDisplay ? <EyeOff size={18} /> : <Eye size={18} />} {isLiveOnPlayerDisplay ? <EyeOff size={18} /> : <Eye size={18} />}
</button> </button>
{/* Share button for individual encounter link removed */}
<button onClick={(e) => { e.stopPropagation(); handleDeleteEncounter(encounter.id); }} className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500" title="Delete Encounter"><Trash2 size={18} /></button> <button onClick={(e) => { e.stopPropagation(); handleDeleteEncounter(encounter.id); }} className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500" title="Delete Encounter"><Trash2 size={18} /></button>
</div> </div>
</div> </div>
{/* Shareable link display removed */}
</div> </div>
); );
})} })}
@ -831,8 +831,12 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
console.warn("Attempting to end encounter without confirmation"); console.warn("Attempting to end encounter without confirmation");
try { try {
await updateDoc(doc(db, encounterPath), { isStarted: false, currentTurnParticipantId: null, round: 0, turnOrderIds: [] }); await updateDoc(doc(db, encounterPath), { isStarted: false, currentTurnParticipantId: null, round: 0, turnOrderIds: [] });
// Optionally, also clear the active display when an encounter ends // When an encounter ends, also deactivate it from the Player Display
// await updateDoc(doc(db, ACTIVE_DISPLAY_DOC), { activeCampaignId: null, activeEncounterId: null }); await setDoc(doc(db, ACTIVE_DISPLAY_DOC), {
activeCampaignId: null,
activeEncounterId: null
}, { merge: true });
console.log("Encounter ended and deactivated from Player Display.");
} catch (err) { console.error("Error ending encounter:", err); } } catch (err) { console.error("Error ending encounter:", err); }
}; };
@ -855,12 +859,12 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
); );
} }
function DisplayView() { // Removed campaignIdFromUrl, encounterIdFromUrl props function DisplayView() {
const [activeEncounterData, setActiveEncounterData] = useState(null); const [activeEncounterData, setActiveEncounterData] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [campaignBackgroundUrl, setCampaignBackgroundUrl] = useState(''); const [campaignBackgroundUrl, setCampaignBackgroundUrl] = useState('');
const [isPlayerDisplayActive, setIsPlayerDisplayActive] = useState(false); // New state const [isPlayerDisplayActive, setIsPlayerDisplayActive] = useState(false);
useEffect(() => { useEffect(() => {
if (!db) { if (!db) {
@ -876,22 +880,20 @@ function DisplayView() { // Removed campaignIdFromUrl, encounterIdFromUrl props
if (docSnap.exists()) { if (docSnap.exists()) {
const { activeCampaignId, activeEncounterId } = docSnap.data(); const { activeCampaignId, activeEncounterId } = docSnap.data();
if (activeCampaignId && activeEncounterId) { if (activeCampaignId && activeEncounterId) {
setIsPlayerDisplayActive(true); // An encounter is active for display setIsPlayerDisplayActive(true);
// Fetch campaign background
const campaignDocRef = doc(db, CAMPAIGNS_COLLECTION, activeCampaignId); const campaignDocRef = doc(db, CAMPAIGNS_COLLECTION, activeCampaignId);
if (unsubscribeCampaign) unsubscribeCampaign(); // Clean up previous listener if (unsubscribeCampaign) unsubscribeCampaign();
unsubscribeCampaign = onSnapshot(campaignDocRef, (campSnap) => { unsubscribeCampaign = onSnapshot(campaignDocRef, (campSnap) => {
if (campSnap.exists()) { if (campSnap.exists()) {
setCampaignBackgroundUrl(campSnap.data().playerDisplayBackgroundUrl || ''); setCampaignBackgroundUrl(campSnap.data().playerDisplayBackgroundUrl || '');
} else { } else {
setCampaignBackgroundUrl(''); // Campaign not found setCampaignBackgroundUrl('');
} }
}, (err) => console.error("Error fetching campaign background for display:", err)); }, (err) => console.error("Error fetching campaign background for display:", err));
// Fetch encounter data
const encounterPath = `${CAMPAIGNS_COLLECTION}/${activeCampaignId}/encounters/${activeEncounterId}`; const encounterPath = `${CAMPAIGNS_COLLECTION}/${activeCampaignId}/encounters/${activeEncounterId}`;
if (unsubscribeEncounter) unsubscribeEncounter(); // Clean up previous listener if (unsubscribeEncounter) unsubscribeEncounter();
unsubscribeEncounter = onSnapshot(doc(db, encounterPath), (encDocSnap) => { unsubscribeEncounter = onSnapshot(doc(db, encounterPath), (encDocSnap) => {
if (encDocSnap.exists()) { if (encDocSnap.exists()) {
setActiveEncounterData({ id: encDocSnap.id, ...encDocSnap.data() }); setActiveEncounterData({ id: encDocSnap.id, ...encDocSnap.data() });
@ -907,14 +909,12 @@ function DisplayView() { // Removed campaignIdFromUrl, encounterIdFromUrl props
setIsLoading(false); setIsLoading(false);
}); });
} else { } else {
// No active encounter set by DM
setActiveEncounterData(null); setActiveEncounterData(null);
setCampaignBackgroundUrl(''); setCampaignBackgroundUrl('');
setIsPlayerDisplayActive(false); // No encounter is active for display setIsPlayerDisplayActive(false);
setIsLoading(false); setIsLoading(false);
} }
} else { } else {
// ACTIVE_DISPLAY_DOC doesn't exist
setActiveEncounterData(null); setActiveEncounterData(null);
setCampaignBackgroundUrl(''); setCampaignBackgroundUrl('');
setIsPlayerDisplayActive(false); setIsPlayerDisplayActive(false);
@ -931,7 +931,7 @@ function DisplayView() { // Removed campaignIdFromUrl, encounterIdFromUrl props
if (unsubscribeEncounter) unsubscribeEncounter(); if (unsubscribeEncounter) unsubscribeEncounter();
if (unsubscribeCampaign) unsubscribeCampaign(); if (unsubscribeCampaign) unsubscribeCampaign();
}; };
}, []); // Removed URL params from dependencies }, []);
if (isLoading) return <div className="text-center py-10 text-2xl text-slate-300">Loading Player Display...</div>; if (isLoading) return <div className="text-center py-10 text-2xl text-slate-300">Loading Player Display...</div>;
if (error) return <div className="text-center py-10 text-2xl text-red-400">{error}</div>; if (error) return <div className="text-center py-10 text-2xl text-red-400">{error}</div>;