diff --git a/src/App.js b/src/App.js index c60d3e2..a51e5f6 100644 --- a/src/App.js +++ b/src/App.js @@ -38,14 +38,15 @@ if (missingKeys.length > 0) { const APP_ID = process.env.REACT_APP_TRACKER_APP_ID || 'ttrpg-initiative-tracker-default'; const PUBLIC_DATA_PATH = `artifacts/${APP_ID}/public/data`; const CAMPAIGNS_COLLECTION = `${PUBLIC_DATA_PATH}/campaigns`; -const ACTIVE_DISPLAY_DOC = `${PUBLIC_DATA_PATH}/activeDisplay/status`; +const ACTIVE_DISPLAY_DOC = `${PUBLIC_DATA_PATH}/activeDisplay/status`; // Single source for what player display shows // --- Helper Functions --- const generateId = () => crypto.randomUUID(); -function getShareableLinkBase() { - return window.location.origin + window.location.pathname; -} +// 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 --- function App() { @@ -54,7 +55,7 @@ function App() { const [viewMode, setViewMode] = useState('admin'); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const [directDisplayParams, setDirectDisplayParams] = useState(null); + // directDisplayParams and hash routing removed, Player Display solely relies on ACTIVE_DISPLAY_DOC useEffect(() => { if (!auth) { @@ -63,22 +64,6 @@ function App() { setIsAuthReady(false); return; } - const handleHashChange = () => { - const hash = window.location.hash; - if (hash.startsWith('#/display/')) { - const parts = hash.substring('#/display/'.length).split('/'); - if (parts.length === 2 && parts[0] && parts[1]) { - setDirectDisplayParams({ campaignId: parts[0], encounterId: parts[1] }); - setViewMode('display'); - } else { - setDirectDisplayParams(null); - } - } else { - setDirectDisplayParams(null); - } - }; - window.addEventListener('hashchange', handleHashChange); - handleHashChange(); const initAuth = async () => { try { @@ -101,7 +86,6 @@ function App() { }); initAuth(); return () => { - window.removeEventListener('hashchange', handleHashChange); unsubscribe(); }; }, []); @@ -131,13 +115,10 @@ function App() { const goToAdminView = () => { setViewMode('admin'); - setDirectDisplayParams(null); - window.location.hash = ''; }; return (
- {!directDisplayParams && (

- )} -
- {directDisplayParams && isAuthReady && ( - - )} - {!directDisplayParams && viewMode === 'admin' && isAuthReady && userId && } - {!directDisplayParams && viewMode === 'display' && isAuthReady && } +
+ {viewMode === 'admin' && isAuthReady && userId && } + {viewMode === 'display' && isAuthReady && } {/* DisplayView no longer needs URL params */} {!isAuthReady && !error &&

Authenticating...

}
- {!directDisplayParams && ( -
- TTRPG Initiative Tracker v0.1.15 -
- )} +
); } @@ -402,7 +377,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac const [selectedEncounterId, setSelectedEncounterId] = useState(null); const [showCreateEncounterModal, setShowCreateEncounterModal] = useState(false); const [activeDisplayInfo, setActiveDisplayInfo] = useState(null); - const [copiedLinkEncounterId, setCopiedLinkEncounterId] = useState(null); + // copiedLinkEncounterId removed as shareable links per encounter are removed const encountersPath = `${CAMPAIGNS_COLLECTION}/${campaignId}/encounters`; const selectedEncounterIdRef = useRef(selectedEncounterId); @@ -464,44 +439,39 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac await deleteDoc(doc(db, encountersPath, encounterId)); if (selectedEncounterId === encounterId) setSelectedEncounterId(null); if (activeDisplayInfo && activeDisplayInfo.activeEncounterId === encounterId) { - await updateDoc(doc(db, ACTIVE_DISPLAY_DOC), { activeEncounterId: null }); + await updateDoc(doc(db, ACTIVE_DISPLAY_DOC), { activeCampaignId: null, activeEncounterId: null }); } } catch (err) { console.error("Error deleting encounter:", err); } }; - const handleSetEncounterAsActiveDisplay = async (encounterId) => { + const handleTogglePlayerDisplayForEncounter = async (encounterId) => { if (!db) return; try { const currentActiveCampaign = activeDisplayInfo?.activeCampaignId; const currentActiveEncounter = activeDisplayInfo?.activeEncounterId; if (currentActiveCampaign === campaignId && currentActiveEncounter === encounterId) { - // Currently active, so toggle off - await updateDoc(doc(db, ACTIVE_DISPLAY_DOC), { + // Currently active, so toggle off (clear the active display) + await setDoc(doc(db, ACTIVE_DISPLAY_DOC), { activeCampaignId: null, activeEncounterId: null, - }); - console.log("Encounter display toggled OFF for DM."); + }, { merge: true }); // Use set with merge to ensure document exists or is overwritten + console.log("Player Display for this encounter turned OFF."); } else { - // Not active or different encounter, so set this one as active + // Not active or different encounter, so set this one as active for players await setDoc(doc(db, ACTIVE_DISPLAY_DOC), { activeCampaignId: campaignId, activeEncounterId: encounterId, }, { merge: true }); - console.log("Encounter set as active for DM's main display!"); + console.log("Encounter set as active for Player Display!"); } } catch (err) { - console.error("Error toggling active display:", err); + console.error("Error toggling Player Display for encounter:", err); } }; - const handleCopyToClipboard = (encounterId) => { - const link = `${getShareableLinkBase()}#/display/${campaignId}/${encounterId}`; - navigator.clipboard.writeText(link).then(() => { - setCopiedLinkEncounterId(encounterId); - setTimeout(() => setCopiedLinkEncounterId(null), 2000); - }).catch(err => console.error('Failed to copy link: ', err)); - }; + // Shareable link per encounter removed + // const handleCopyToClipboard = (encounterId) => { ... }; const selectedEncounter = encounters.find(e => e.id === selectedEncounterId); @@ -514,39 +484,28 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac {encounters.length === 0 &&

No encounters yet.

}
{encounters.map(encounter => { - const isDmActiveDisplay = activeDisplayInfo && activeDisplayInfo.activeCampaignId === campaignId && activeDisplayInfo.activeEncounterId === encounter.id; + const isLiveOnPlayerDisplay = activeDisplayInfo && activeDisplayInfo.activeCampaignId === campaignId && activeDisplayInfo.activeEncounterId === encounter.id; return ( -
+
setSelectedEncounterId(encounter.id)} className="cursor-pointer flex-grow">

{encounter.name}

Participants: {encounter.participants?.length || 0}

- {isDmActiveDisplay && LIVE ON DM DISPLAY} + {isLiveOnPlayerDisplay && LIVE ON PLAYER DISPLAY}
- - {copiedLinkEncounterId === encounter.id && Copied!} + {/* Share button for individual encounter link removed */}
- {selectedEncounterId === encounter.id && ( -
-

Shareable Link for Players:

-
- - -
-
- )} + {/* Shareable link display removed */}
); })} @@ -872,6 +831,8 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) { console.warn("Attempting to end encounter without confirmation"); try { await updateDoc(doc(db, encounterPath), { isStarted: false, currentTurnParticipantId: null, round: 0, turnOrderIds: [] }); + // Optionally, also clear the active display when an encounter ends + // await updateDoc(doc(db, ACTIVE_DISPLAY_DOC), { activeCampaignId: null, activeEncounterId: null }); } catch (err) { console.error("Error ending encounter:", err); } }; @@ -894,72 +855,97 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) { ); } -function DisplayView({ campaignIdFromUrl, encounterIdFromUrl }) { +function DisplayView() { // Removed campaignIdFromUrl, encounterIdFromUrl props const [activeEncounterData, setActiveEncounterData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [campaignBackgroundUrl, setCampaignBackgroundUrl] = useState(''); + const [isPlayerDisplayActive, setIsPlayerDisplayActive] = useState(false); // New state useEffect(() => { if (!db) { setError("Firestore not available."); setIsLoading(false); return; } - setIsLoading(true); setError(null); setActiveEncounterData(null); setCampaignBackgroundUrl(''); + setIsLoading(true); setError(null); setActiveEncounterData(null); setCampaignBackgroundUrl(''); setIsPlayerDisplayActive(false); + let unsubscribeEncounter; let unsubscribeCampaign; - const fetchCampaignBackground = async (cId) => { - if (!cId) return; - const campaignDocRef = doc(db, CAMPAIGNS_COLLECTION, cId); - unsubscribeCampaign = onSnapshot(campaignDocRef, (docSnap) => { - if (docSnap.exists()) { - setCampaignBackgroundUrl(docSnap.data().playerDisplayBackgroundUrl || ''); - } - }, (err) => { - console.error("Error fetching campaign background:", err); - }); - }; + const activeDisplayRef = doc(db, ACTIVE_DISPLAY_DOC); + const unsubDisplayConfig = onSnapshot(activeDisplayRef, async (docSnap) => { + if (docSnap.exists()) { + const { activeCampaignId, activeEncounterId } = docSnap.data(); + if (activeCampaignId && activeEncounterId) { + setIsPlayerDisplayActive(true); // An encounter is active for display + + // Fetch campaign background + const campaignDocRef = doc(db, CAMPAIGNS_COLLECTION, activeCampaignId); + if (unsubscribeCampaign) unsubscribeCampaign(); // Clean up previous listener + unsubscribeCampaign = onSnapshot(campaignDocRef, (campSnap) => { + if (campSnap.exists()) { + setCampaignBackgroundUrl(campSnap.data().playerDisplayBackgroundUrl || ''); + } else { + setCampaignBackgroundUrl(''); // Campaign not found + } + }, (err) => console.error("Error fetching campaign background for display:", err)); + + // Fetch encounter data + const encounterPath = `${CAMPAIGNS_COLLECTION}/${activeCampaignId}/encounters/${activeEncounterId}`; + if (unsubscribeEncounter) unsubscribeEncounter(); // Clean up previous listener + unsubscribeEncounter = onSnapshot(doc(db, encounterPath), (encDocSnap) => { + if (encDocSnap.exists()) { + setActiveEncounterData({ id: encDocSnap.id, ...encDocSnap.data() }); + setError(null); + } else { + setActiveEncounterData(null); + setError("Active encounter data not found. The DM might have deleted it or it's misconfigured."); + } + setIsLoading(false); + }, (err) => { + console.error("Error fetching active encounter details for display:", err); + setError("Error loading active encounter data."); + setIsLoading(false); + }); + } else { + // No active encounter set by DM + setActiveEncounterData(null); + setCampaignBackgroundUrl(''); + setIsPlayerDisplayActive(false); // No encounter is active for display + setIsLoading(false); + } + } else { + // ACTIVE_DISPLAY_DOC doesn't exist + setActiveEncounterData(null); + setCampaignBackgroundUrl(''); + setIsPlayerDisplayActive(false); + setIsLoading(false); + } + }, (err) => { + console.error("Error fetching active display config:", err); + setError("Could not load display configuration."); + setIsLoading(false); + }); - if (campaignIdFromUrl && encounterIdFromUrl) { - fetchCampaignBackground(campaignIdFromUrl); - const encounterPath = `${CAMPAIGNS_COLLECTION}/${campaignIdFromUrl}/encounters/${encounterIdFromUrl}`; - unsubscribeEncounter = onSnapshot(doc(db, encounterPath), (encDocSnap) => { - if (encDocSnap.exists()) setActiveEncounterData({ id: encDocSnap.id, ...encDocSnap.data() }); - else setError("The requested encounter was not found or is not accessible."); - setIsLoading(false); - }, (err) => { console.error("Error fetching specific encounter for display:", err); setError("Error loading encounter data from link."); setIsLoading(false); }); - } else { - const activeDisplayRef = doc(db, ACTIVE_DISPLAY_DOC); - const unsubDisplayConfig = onSnapshot(activeDisplayRef, async (docSnap) => { - if (docSnap.exists()) { - const { activeCampaignId, activeEncounterId } = docSnap.data(); - if (activeCampaignId && activeEncounterId) { - fetchCampaignBackground(activeCampaignId); - const encounterPath = `${CAMPAIGNS_COLLECTION}/${activeCampaignId}/encounters/${activeEncounterId}`; - if(unsubscribeEncounter) unsubscribeEncounter(); - unsubscribeEncounter = onSnapshot(doc(db, encounterPath), (encDocSnap) => { - if (encDocSnap.exists()) setActiveEncounterData({ id: encDocSnap.id, ...encDocSnap.data() }); - else { setActiveEncounterData(null); setError("Active encounter not found. The DM might have deleted it.");} - setIsLoading(false); - }, (err) => { console.error("Error fetching active encounter details:", err); setError("Error loading active encounter data."); setIsLoading(false);}); - } else { setActiveEncounterData(null); setIsLoading(false); } - } else { setActiveEncounterData(null); setIsLoading(false); } - }, (err) => { console.error("Error fetching active display config:", err); setError("Could not load display configuration."); setIsLoading(false); }); - return () => { - unsubDisplayConfig(); - if (unsubscribeEncounter) unsubscribeEncounter(); - if (unsubscribeCampaign) unsubscribeCampaign(); - }; - } return () => { - if (unsubscribeEncounter) unsubscribeEncounter(); - if (unsubscribeCampaign) unsubscribeCampaign(); + unsubDisplayConfig(); + if (unsubscribeEncounter) unsubscribeEncounter(); + if (unsubscribeCampaign) unsubscribeCampaign(); }; - }, [campaignIdFromUrl, encounterIdFromUrl]); + }, []); // Removed URL params from dependencies if (isLoading) return
Loading Player Display...
; if (error) return
{error}
; - if (!activeEncounterData) return
No active encounter to display.
; + + if (!isPlayerDisplayActive || !activeEncounterData) { + return ( +
+ +

Game Session Paused

+

The Dungeon Master has not activated an encounter for display.

+
+ ); + } + const { name, participants, round, currentTurnParticipantId, isStarted } = activeEncounterData;