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 { 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 (
<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 (
<div className="min-h-screen bg-slate-800 text-slate-100 font-sans">
<header className="bg-slate-900 p-4 shadow-lg">
<div className="container mx-auto flex justify-between items-center">
<h1
className="text-3xl font-bold text-sky-400 cursor-pointer hover:text-sky-300 transition-colors"
onClick={goToAdminView}
title="Go to Admin View"
className="text-3xl font-bold text-sky-400" // No longer needs to be clickable to go to admin, as this IS admin view
>
TTRPG Initiative Tracker
</h1>
<div className="flex items-center space-x-4">
{userId && <span className="text-xs text-slate-400">UID: {userId}</span>}
<button
onClick={() => {
if (viewMode === 'display') {
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'}`}
onClick={openPlayerWindow}
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`}
>
{viewMode === 'display' ? 'Admin Controls' : 'Player Display'}
<ExternalLink size={16} className="mr-2"/> Open Player Window
</button>
</div>
</div>
</header>
<main className={`container mx-auto p-4 md:p-8`}>
{viewMode === 'admin' && isAuthReady && userId && <AdminView userId={userId} />}
{viewMode === 'display' && isAuthReady && <DisplayView />} {/* DisplayView no longer needs URL params */}
{isAuthReady && userId && <AdminView userId={userId} />}
{!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.16
TTRPG Initiative Tracker v0.1.17
</footer>
</div>
);
@ -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 ? <EyeOff size={18} /> : <Eye size={18} />}
</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>
</div>
</div>
{/* Shareable link display removed */}
</div>
);
})}
@ -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 <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>;