changing the player display button.
This commit is contained in:
parent
eb114910f8
commit
085303fbab
98
src/App.js
98
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 (
|
||||
<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>;
|
||||
|
Loading…
x
Reference in New Issue
Block a user