diff --git a/app/(main)/admin/page.js b/app/(main)/admin/page.js index 6a85bdb..b3a9e7b 100644 --- a/app/(main)/admin/page.js +++ b/app/(main)/admin/page.js @@ -7,6 +7,7 @@ import { clearHuntTimer, getHuntTimer, formatElapsedTime, + syncTimerWithServer, } from "@/utils/timerService"; export default function AdminDashboard() { @@ -14,7 +15,6 @@ export default function AdminDashboard() { const [timerActive, setTimerActive] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); - const [elapsedTime, setElapsedTime] = useState(0); // in seconds const [timerDisplay, setTimerDisplay] = useState("--:--:--"); const [presetHours, setPresetHours] = useState(""); const [presetMinutes, setPresetMinutes] = useState(""); @@ -22,6 +22,7 @@ export default function AdminDashboard() { const [timerEndTime, setTimerEndTime] = useState(null); const timerRef = useRef(null); const timerIntervalRef = useRef(null); + const timerStartTimeRef = useRef(null); // New ref to store the start time const router = useRouter(); // Load timer status on page load @@ -33,8 +34,12 @@ export default function AdminDashboard() { // First, check if we already have timer data in localStorage const existingTimer = getHuntTimer(); if (existingTimer && existingTimer.startTime) { + // Store the start time in the ref to prevent it from being lost on re-renders + timerStartTimeRef.current = existingTimer.startTime; + // If we have timer data, immediately set the display - setTimerActive(existingTimer.isActive); + setTimerActive(existingTimer.isActive !== false); + setTimerDisplay(formatElapsedTime(existingTimer.startTime)); if (existingTimer.endTime) { setTimerEndTime(new Date(existingTimer.endTime).getTime()); } @@ -61,10 +66,15 @@ export default function AdminDashboard() { if (isActive && data.startTime) { setTimerActive(true); + // Store the start time in the ref for persistence across renders + timerStartTimeRef.current = data.startTime; + // Store the start time in the centralized service - setHuntTimer(data.startTime, data.endTime); + setHuntTimer(data.startTime, data.endTime, true); // Explicitly set isActive=true + + // Update the display immediately with the correct time + setTimerDisplay(formatElapsedTime(data.startTime)); - // If there's an end time, set it if (data.endTime) { setTimerEndTime(new Date(data.endTime).getTime()); } else { @@ -72,10 +82,12 @@ export default function AdminDashboard() { } } else { // Only clear local timer if server explicitly says timer is not active - // This prevents clearing a valid timer if the server response is ambiguous if (data.active === false && data.timerActive === false) { clearHuntTimer(); + timerStartTimeRef.current = null; // Clear the start time ref setTimerActive(false); + setTimerDisplay("--:--:--"); + setTimerEndTime(null); } } @@ -111,48 +123,53 @@ export default function AdminDashboard() { } }, [session, status, router]); - // Create a separate effect for updating the timer display + // Create a separate effect for updating the timer display - but only have one instance useEffect(() => { - let timerId; + // Clear any existing interval to avoid duplicates + if (timerIntervalRef.current) { + clearInterval(timerIntervalRef.current); + timerIntervalRef.current = null; + } - // Initialize timer - async function initializeTimer() { - // Set up timer updates - const updateTimer = () => { - const timerData = getHuntTimer(); - if (timerData && timerData.startTime) { - setTimerDisplay(formatElapsedTime(timerData.startTime)); + const updateTimer = () => { + // Use the ref to access the persisted start time + const startTime = timerStartTimeRef.current; - // If there's an end time, check if we've reached it - if (timerEndTime && Date.now() >= timerEndTime) { - stopTimer(); - } - } else { - setTimerDisplay("--:--:--"); + if (timerActive && startTime) { + // Format time using the start time from our ref, which persists across renders + setTimerDisplay(formatElapsedTime(startTime)); + + // Check if timer has ended + if (timerEndTime && Date.now() >= timerEndTime) { + stopTimer(); } - }; - - // Initial update - updateTimer(); - - // Update every second - timerId = setInterval(updateTimer, 1000); - } - - // Only initialize timer if the timer is active - if (timerActive) { - initializeTimer(); - } else { - setTimerDisplay("--:--:--"); - } - - // Clean up interval on unmount - return () => { - if (timerId) { - clearInterval(timerId); + } else if (timerActive) { + // If timer is active but no data, try to restore + syncTimerWithServer().then((result) => { + if (result.success && result.data) { + timerStartTimeRef.current = result.data.startTime; + } + }); + } else { + setTimerDisplay("--:--:--"); } }; - }, [timerActive, timerEndTime]); + + // Initial update + updateTimer(); + + // Set up interval for timer updates + if (timerActive) { + timerIntervalRef.current = setInterval(updateTimer, 1000); + } + + return () => { + if (timerIntervalRef.current) { + clearInterval(timerIntervalRef.current); + timerIntervalRef.current = null; + } + }; + }, [timerActive, timerEndTime]); // Include timerEndTime as a dependency // Add a function to fetch timer status directly from the server const fetchTimerStatusFromServer = async () => { @@ -177,8 +194,8 @@ export default function AdminDashboard() { if (isActive && data.startTime) { setTimerActive(true); + timerStartTimeRef.current = data.startTime; // Update the ref setHuntTimer(data.startTime, data.endTime); - if (data.endTime) { setTimerEndTime(new Date(data.endTime).getTime()); } else { @@ -189,6 +206,7 @@ export default function AdminDashboard() { } else if (data.active === false && data.timerActive === false) { // Only clear timer if server explicitly says timer is inactive setTimerActive(false); + timerStartTimeRef.current = null; // Clear the ref clearHuntTimer(); setTimerDisplay("--:--:--"); } @@ -250,6 +268,9 @@ export default function AdminDashboard() { if (data.success) { setTimerActive(true); + // Store the start time in the ref + timerStartTimeRef.current = data.startTime; + if (endTime) { setTimerEndTime(new Date(endTime).getTime()); } else { @@ -293,6 +314,7 @@ export default function AdminDashboard() { if (data.success) { setTimerActive(false); + timerStartTimeRef.current = null; // Clear the ref setTimerEndTime(null); setTimerDisplay("--:--:--"); @@ -465,6 +487,7 @@ export default function AdminDashboard() { padding: 1rem; display: flex; flex-direction: column; + justify-content: center; align-items: center; padding-bottom: 70px; /* Add space for the navbar */ } diff --git a/app/(main)/clue/[clueId]/page.js b/app/(main)/clue/[clueId]/page.js index 0870169..5d06b08 100644 --- a/app/(main)/clue/[clueId]/page.js +++ b/app/(main)/clue/[clueId]/page.js @@ -47,6 +47,7 @@ export default function CluePage({ params }) { const notificationTimeoutRef = useRef(null); const navErrorTimeoutRef = useRef(null); const timerIdRef = useRef(null); + const timerInitializedRef = useRef(false); // Add this ref to track if timer is already initialized // Check if user is admin const isAdmin = session?.user?.role === "admin"; @@ -194,7 +195,7 @@ export default function CluePage({ params }) { }; }, [router, previousHuntState]); - // Function to ensure we have timer data from the server + // Function to ensure we have timer data from the server - only if needed async function fetchTeamHuntStartTime() { try { // First try to get any existing timer data @@ -204,6 +205,7 @@ export default function CluePage({ params }) { } // If no timer data exists, sync with server + console.log("No timer data found, syncing with server..."); const result = await syncTimerWithServer(); return result.success; } catch (err) { @@ -212,44 +214,53 @@ export default function CluePage({ params }) { } } - // Updated timer effect that follows the same pattern as the map page + // Updated timer effect with protection against multiple initializations useEffect(() => { + // Prevent multiple initializations of the timer + if (timerInitializedRef.current) { + return; + } + // Initialize timer async function initializeTimer() { // First ensure we have timer data await fetchTeamHuntStartTime(); - // Set up timer updates - const updateTimer = () => { - const timerData = getHuntTimer(); - if (timerData && timerData.startTime) { - setElapsedTime(formatElapsedTime(timerData.startTime)); + // Set up timer updates if not already set + if (!timerIdRef.current) { + const updateTimer = () => { + const timerData = getHuntTimer(); + if (timerData && timerData.startTime) { + setElapsedTime(formatElapsedTime(timerData.startTime)); - // Check if timer has ended (if there's an endTime) - if (timerData.endTime) { - const now = Date.now(); - const endTime = new Date(timerData.endTime).getTime(); + // Check if timer has ended (if there's an endTime) + if (timerData.endTime) { + const now = Date.now(); + const endTime = new Date(timerData.endTime).getTime(); - // If current time is past the end time, hunt has ended - if (now >= endTime) { - setHuntActive(false); - clearInterval(timerIdRef.current); + // If current time is past the end time, hunt has ended + if (now >= endTime) { + setHuntActive(false); + clearInterval(timerIdRef.current); + timerIdRef.current = null; - // Redirect to leaderboard with huntEnded flag - console.log( - "Hunt end time reached! Redirecting to leaderboard..." - ); - router.push("/leaderboard?huntEnded=true"); + // Redirect to leaderboard with huntEnded flag + console.log( + "Hunt end time reached! Redirecting to leaderboard..." + ); + router.push("/leaderboard?huntEnded=true"); + } } } - } - }; + }; - // Initial update - updateTimer(); + // Initial update + updateTimer(); - // Update every second - timerIdRef.current = setInterval(updateTimer, 1000); + // Update every second + timerIdRef.current = setInterval(updateTimer, 1000); + timerInitializedRef.current = true; + } } initializeTimer(); @@ -259,9 +270,52 @@ export default function CluePage({ params }) { if (timerIdRef.current) { clearInterval(timerIdRef.current); timerIdRef.current = null; + timerInitializedRef.current = false; } }; - }, [router]); + }, [router]); // Only depend on router + + // Make sure hunt status check doesn't interfere with the timer + useEffect(() => { + // This function should NOT reset the timer or recreate it + async function checkHuntStatus() { + try { + const response = await fetch("/api/hunt-status", { + method: "GET", + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache, no-store, must-revalidate", + }, + credentials: "include", + }); + + if (response.ok) { + const data = await response.json(); + + // Only update hunt state, don't touch the timer + const newHuntState = !!data.active; + + // If hunt was active but is now inactive, redirect to leaderboard + if (previousHuntState === true && newHuntState === false) { + console.log("Hunt has ended! Redirecting to leaderboard..."); + router.push("/leaderboard?huntEnded=true"); + return; + } + + // Update the previous hunt state for next comparison + setPreviousHuntState(newHuntState); + } + } catch (err) { + console.error("Failed to check hunt status:", err); + } + } + + const intervalId = setInterval(checkHuntStatus, 5000); + + return () => { + clearInterval(intervalId); + }; + }, [router, previousHuntState]); // Fetch team data on component mount useEffect(() => { diff --git a/app/(main)/map/page.js b/app/(main)/map/page.js index af8e670..6cdaa3c 100644 --- a/app/(main)/map/page.js +++ b/app/(main)/map/page.js @@ -27,6 +27,8 @@ export default function MapPage() { // Add local state for hunt active status const [huntActive, setHuntActive] = useState(null); + // Add previous hunt state to detect when hunt ends + const [previousHuntState, setPreviousHuntState] = useState(null); // Use useCallback for functions called in useEffect to avoid dependency issues const softRedirect = useCallback( @@ -293,6 +295,18 @@ export default function MapPage() { } }, [status, fetchTeamData]); + // Add this useEffect to detect when hunt ends + useEffect(() => { + // If previous state was active but current state is not active, the hunt just ended + if (previousHuntState === true && huntActive === false) { + console.log("Hunt has ended! Redirecting to leaderboard..."); + router.push("/leaderboard?huntEnded=true"); + } + + // Update previous state after checking + setPreviousHuntState(huntActive); + }, [huntActive, previousHuntState, router]); + if (status === "loading" || loading) { return (