diff --git a/src/App.js b/src/App.js index a51e5f6..90185ee 100644 --- a/src/App.js +++ b/src/App.js @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { initializeApp } from 'firebase/app'; 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 { 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 --- 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 PUBLIC_DATA_PATH = `artifacts/${APP_ID}/public/data`; 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 --- 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 --- function App() { const [userId, setUserId] = useState(null); const [isAuthReady, setIsAuthReady] = useState(false); - const [viewMode, setViewMode] = useState('admin'); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - // directDisplayParams and hash routing removed, Player Display solely relies on ACTIVE_DISPLAY_DOC + const [isPlayerViewOnlyMode, setIsPlayerViewOnlyMode] = useState(false); 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) { setError("Firebase Auth not initialized. Check your Firebase configuration."); setIsLoading(false); @@ -113,46 +113,54 @@ function App() { ); } - const goToAdminView = () => { - setViewMode('admin'); + const openPlayerWindow = () => { + // 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 ( +
+ {isAuthReady && } + {!isAuthReady && !error &&

Authenticating for Player Display...

} +
+ ); + } + + // Default Admin View return (

TTRPG Initiative Tracker

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

Authenticating...

}
); @@ -377,7 +385,6 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac const [selectedEncounterId, setSelectedEncounterId] = useState(null); const [showCreateEncounterModal, setShowCreateEncounterModal] = useState(false); const [activeDisplayInfo, setActiveDisplayInfo] = useState(null); - // copiedLinkEncounterId removed as shareable links per encounter are removed const encountersPath = `${CAMPAIGNS_COLLECTION}/${campaignId}/encounters`; const selectedEncounterIdRef = useRef(selectedEncounterId); @@ -451,14 +458,12 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac const currentActiveEncounter = activeDisplayInfo?.activeEncounterId; if (currentActiveCampaign === campaignId && currentActiveEncounter === encounterId) { - // Currently active, so toggle off (clear the active display) await setDoc(doc(db, ACTIVE_DISPLAY_DOC), { activeCampaignId: 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."); } else { - // Not active or different encounter, so set this one as active for players await setDoc(doc(db, ACTIVE_DISPLAY_DOC), { activeCampaignId: campaignId, 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); return ( @@ -501,11 +503,9 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac > {isLiveOnPlayerDisplay ? : } - {/* Share button for individual encounter link removed */} - {/* Shareable link display removed */} ); })} @@ -831,8 +831,12 @@ 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 }); + // When an encounter ends, also deactivate it from the Player Display + 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); } }; @@ -855,12 +859,12 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) { ); } -function DisplayView() { // Removed campaignIdFromUrl, encounterIdFromUrl props +function DisplayView() { 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 + const [isPlayerDisplayActive, setIsPlayerDisplayActive] = useState(false); useEffect(() => { if (!db) { @@ -876,22 +880,20 @@ function DisplayView() { // Removed campaignIdFromUrl, encounterIdFromUrl props if (docSnap.exists()) { const { activeCampaignId, activeEncounterId } = docSnap.data(); if (activeCampaignId && activeEncounterId) { - setIsPlayerDisplayActive(true); // An encounter is active for display + setIsPlayerDisplayActive(true); - // Fetch campaign background const campaignDocRef = doc(db, CAMPAIGNS_COLLECTION, activeCampaignId); - if (unsubscribeCampaign) unsubscribeCampaign(); // Clean up previous listener + if (unsubscribeCampaign) unsubscribeCampaign(); unsubscribeCampaign = onSnapshot(campaignDocRef, (campSnap) => { if (campSnap.exists()) { setCampaignBackgroundUrl(campSnap.data().playerDisplayBackgroundUrl || ''); } else { - setCampaignBackgroundUrl(''); // Campaign not found + setCampaignBackgroundUrl(''); } }, (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 + if (unsubscribeEncounter) unsubscribeEncounter(); unsubscribeEncounter = onSnapshot(doc(db, encounterPath), (encDocSnap) => { if (encDocSnap.exists()) { setActiveEncounterData({ id: encDocSnap.id, ...encDocSnap.data() }); @@ -907,14 +909,12 @@ function DisplayView() { // Removed campaignIdFromUrl, encounterIdFromUrl props setIsLoading(false); }); } else { - // No active encounter set by DM setActiveEncounterData(null); setCampaignBackgroundUrl(''); - setIsPlayerDisplayActive(false); // No encounter is active for display + setIsPlayerDisplayActive(false); setIsLoading(false); } } else { - // ACTIVE_DISPLAY_DOC doesn't exist setActiveEncounterData(null); setCampaignBackgroundUrl(''); setIsPlayerDisplayActive(false); @@ -931,7 +931,7 @@ function DisplayView() { // Removed campaignIdFromUrl, encounterIdFromUrl props if (unsubscribeEncounter) unsubscribeEncounter(); if (unsubscribeCampaign) unsubscribeCampaign(); }; - }, []); // Removed URL params from dependencies + }, []); if (isLoading) return
Loading Player Display...
; if (error) return
{error}
;