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 { 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>;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user