From 16319f13333769c1fc23fdcedc88755b2564fc4c Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Sat, 15 Mar 2025 16:30:04 +0000 Subject: [PATCH] fixed the map, made it so the map is scrollable and synced the timing between the admin page and map page. They now share the same elapsed time. need to fix map css for altering the svg. Grey = cant click Red = current Green = complete blue = skipped? Need to add Clue page Need to fix Leaderboard. --- .idea/sqldialects.xml | 7 - app/(main)/admin/page.js | 17 +- app/(main)/clue/[clueId]/layout.js | 3 + app/(main)/clue/[clueId]/page.js | 283 +++++------ app/(main)/map/map.module.css | 55 ++- app/(main)/map/page.js | 467 +++++++++++++------ app/api/clues/[clueId]/route.js | 131 +++--- app/api/clues/route.js | 150 ++---- app/api/hunt-status/route.js | 17 +- app/api/hunt/timer/route.js | 27 +- app/api/teams/[teamId]/hunt-session/route.js | 16 +- app/waiting/page.js | 16 +- clue_hunt.db | Bin 69632 -> 77824 bytes utils/timerService.js | 74 +++ 14 files changed, 791 insertions(+), 472 deletions(-) delete mode 100644 .idea/sqldialects.xml create mode 100644 app/(main)/clue/[clueId]/layout.js create mode 100644 utils/timerService.js diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml deleted file mode 100644 index 083c77d..0000000 --- a/.idea/sqldialects.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/(main)/admin/page.js b/app/(main)/admin/page.js index fd05f53..9056d33 100644 --- a/app/(main)/admin/page.js +++ b/app/(main)/admin/page.js @@ -2,6 +2,11 @@ import { useState, useEffect, useRef } from "react"; import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; +import { + setHuntTimer, + clearHuntTimer, + formatElapsedTime, +} from "@/utils/timerService"; export default function AdminDashboard() { const { data: session, status } = useSession(); @@ -38,6 +43,9 @@ export default function AdminDashboard() { const currentTime = Date.now(); const elapsed = Math.floor((currentTime - startTime) / 1000); setElapsedTime(elapsed); + + // Update global timer data for all clients + setHuntTimer(data.startTime, data.endTime); } // If there's an end time, set it @@ -182,6 +190,9 @@ export default function AdminDashboard() { setTimerEndTime(null); } + // Update global timer data for all clients + setHuntTimer(data.startTime, data.endTime); + setError(""); } else { setError(data.error || "Failed to start timer"); @@ -210,7 +221,11 @@ export default function AdminDashboard() { if (data.success) { setTimerActive(false); setTimerEndTime(null); - setError(""); + setElapsedTime(0); + formatTimeDisplay(0); + + // Clear timer data for all clients + clearHuntTimer(); } else { setError(data.error || "Failed to stop timer"); } diff --git a/app/(main)/clue/[clueId]/layout.js b/app/(main)/clue/[clueId]/layout.js new file mode 100644 index 0000000..48512fe --- /dev/null +++ b/app/(main)/clue/[clueId]/layout.js @@ -0,0 +1,3 @@ +export default function ClueLayout({ children }) { + return
{children}
; +} diff --git a/app/(main)/clue/[clueId]/page.js b/app/(main)/clue/[clueId]/page.js index 41b3529..45f8407 100644 --- a/app/(main)/clue/[clueId]/page.js +++ b/app/(main)/clue/[clueId]/page.js @@ -1,96 +1,90 @@ "use client"; - import { useState, useEffect } from "react"; -import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; -import { useHuntStatusCheck } from "@/hooks/useHuntStatusCheck"; -import Link from "next/link"; -import { use } from "react"; // Add this import for React.use() +import { useSession } from "next-auth/react"; +import { formatElapsedTime, getHuntTimer } from "@/utils/timerService"; export default function CluePage({ params }) { - const { data: session, status } = useSession(); - // Unwrap the params Promise using React.use() - const unwrappedParams = use(params); - const { clueId } = unwrappedParams; - const [clue, setClue] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [foundNow, setFoundNow] = useState(false); + const [elapsedTime, setElapsedTime] = useState("00:00:00"); const router = useRouter(); - - // Add this line to use the hunt status hook - const huntActive = useHuntStatusCheck(); + const { status } = useSession(); + const { clueId } = params; useEffect(() => { - async function fetchClueDetails() { + if (status === "unauthenticated") { + router.push("/login"); + return; + } + + if (status === "loading") { + return; + } + + // Fetch clue data + async function fetchClue() { try { - // Try to check hunt status first with robust error handling - try { - const statusResponse = await fetch("/api/hunt-status", { - credentials: "include", - headers: { Accept: "application/json" }, - }); - - // Read as text first - const statusText = await statusResponse.text(); - - try { - const statusData = JSON.parse(statusText); - if (!statusData.active && statusData.authenticated) { - router.push("/"); - return; - } - } catch (parseError) { - console.error("Failed to parse hunt status response:", parseError); - // Continue anyway to try fetching clue details - } - } catch (statusErr) { - console.error("Error checking hunt status:", statusErr); - // Continue anyway to try fetching clue details - } - - // Now fetch clue details with robust error handling + setLoading(true); const response = await fetch(`/api/clues/${clueId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache, no-store, must-revalidate", + }, credentials: "include", - headers: { Accept: "application/json" }, }); - // Read as text first - const responseText = await response.text(); - - // Try to parse as JSON - let data; - try { - data = JSON.parse(responseText); - } catch (parseError) { - console.error("Failed to parse clue details response:", parseError); - console.log("Response received:", responseText.slice(0, 200)); - throw new Error("Invalid response from server. Please try again."); + if (!response.ok) { + if (response.status === 404) { + throw new Error("Clue not found"); + } else { + const data = await response.json(); + throw new Error(data.error || "Failed to fetch clue"); + } } - // Check for error in the response - if (data.error) { - throw new Error(data.error); - } - - setClue(data.clue); + const data = await response.json(); + setClue(data); + setFoundNow(data.foundNow); + setLoading(false); } catch (err) { - console.error("Error fetching clue details:", err); + console.error("Error fetching clue:", err); setError(err.message); - } finally { setLoading(false); } } - if (clueId) { - fetchClueDetails(); + fetchClue(); + }, [clueId, router, status]); + + // Update timer display + useEffect(() => { + // Set up timer display updates + const timerData = getHuntTimer(); + if (!timerData || !timerData.isActive) { + return; } - }, [clueId, router]); + + const timer = setInterval(() => { + setElapsedTime(formatElapsedTime(timerData.startTime)); + }, 1000); + + // Initial set + setElapsedTime(formatElapsedTime(timerData.startTime)); + + return () => clearInterval(timer); + }, []); if (loading) { return (
-
Loading clue details...
+
+
+

Loading clue...

+
); } @@ -130,6 +124,15 @@ export default function CluePage({ params }) { return (
+ {foundNow && ( +
+
🎉
+
New Clue Discovered!
+
+ )} + +
{elapsedTime}
+

{clue.title}

@@ -165,59 +168,13 @@ export default function CluePage({ params }) { } .clue-scroll { - background-color: #f5e6ca; - border: 2px solid #9c6b30; + background-color: #f8f3e3; border-radius: 10px; padding: 30px; - max-width: 800px; width: 100%; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); + max-width: 600px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); position: relative; - } - - .clue-scroll::before, - .clue-scroll::after { - content: ""; - position: absolute; - height: 30px; - width: 100%; - left: 0; - } - - .clue-scroll::before { - top: 0; - background: linear-gradient( - 180deg, - rgba(156, 107, 48, 0.2) 0%, - rgba(156, 107, 48, 0) 100% - ); - border-radius: 8px 8px 0 0; - } - - .clue-scroll::after { - bottom: 0; - background: linear-gradient( - 0deg, - rgba(156, 107, 48, 0.2) 0%, - rgba(156, 107, 48, 0) 100% - ); - border-radius: 0 0 8px 8px; - } - - .clue-title { - color: #5d4037; - font-size: 2rem; - text-align: center; - margin-bottom: 20px; - font-family: "Pirates", cursive, serif; - border-bottom: 2px dashed #9c6b30; - padding-bottom: 15px; - } - - .clue-content { - padding: 20px; - background-color: #f9f3e3; - border-radius: 8px; margin-bottom: 20px; border: 1px dashed #9c6b30; } @@ -253,51 +210,109 @@ export default function CluePage({ params }) { padding: 10px 20px; border: none; border-radius: 5px; - font-size: 1rem; cursor: pointer; - transition: background-color 0.3s, transform 0.2s; + font-weight: bold; + transition: all 0.2s; } .map-button { - background-color: #9c6b30; + background-color: #68543c; color: white; } .home-button { - background-color: #5d4037; + background-color: #a98467; color: white; } .map-button:hover, .home-button:hover { - transform: translateY(-3px); - } - - .map-button:hover { - background-color: #7d562a; - } - - .home-button:hover { - background-color: #4a332e; + transform: scale(1.05); } .loading, .error, .not-found { - padding: 50px; text-align: center; - border: 2px dashed #9c6b30; - border-radius: 8px; - margin: 30px 0; - color: #5d4037; - background: #f5e6ca; - font-size: 1.2rem; + padding: 30px; + } + + .loading-spinner { + border: 4px solid #f3f3f3; + border-top: 4px solid #9c6b30; + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; + margin: 0 auto 20px; + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } .error { - border-color: #d32f2f; color: #d32f2f; } + + .clue-title { + color: #68543c; + font-size: 2rem; + text-align: center; + margin-bottom: 20px; + border-bottom: 2px solid #9c6b30; + padding-bottom: 10px; + } + + .timer-display { + position: absolute; + top: 15px; + right: 15px; + background-color: rgba(0, 0, 0, 0.1); + padding: 5px 10px; + border-radius: 5px; + font-family: monospace; + font-weight: bold; + font-size: 1rem; + } + + .new-discovery { + background-color: #feefb3; + border: 1px solid #9f6000; + color: #9f6000; + padding: 15px; + border-radius: 5px; + margin-bottom: 20px; + display: flex; + align-items: center; + animation: pulse 2s infinite; + } + + @keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.7; + } + 100% { + opacity: 1; + } + } + + .discovery-icon { + font-size: 1.5rem; + margin-right: 10px; + } + + .discovery-text { + font-weight: bold; + } `}
); diff --git a/app/(main)/map/map.module.css b/app/(main)/map/map.module.css index 07ab6af..568c217 100644 --- a/app/(main)/map/map.module.css +++ b/app/(main)/map/map.module.css @@ -5,7 +5,7 @@ height: 100vh; /* Full viewport height */ overflow: hidden; /* Hide overflow at page level */ position: relative; - background-color: #69B5A8; + background-color: #69b5a8; } .map-banner { @@ -59,40 +59,69 @@ /* Map container takes all available space */ .map-container { flex: 1; /* Take all available space */ - overflow: auto; /* Enable scrolling */ position: relative; width: 100%; height: 100%; - min-height: calc(100vh - 120px); /* Adjust for header and navbar height */ - background-color: #69B5A8; /* Updated background color */ + background-color: #69b5a8; /* Background color */ + overflow: auto; /* Enable scrolling */ } .treasure-map { + position: relative; /* Changed from absolute to relative */ width: 100%; height: 100%; + min-width: 100%; + min-height: 100%; +} + +/* SVG container for better scrolling and zooming */ +.svg-container { position: relative; + width: 100%; + height: 100%; display: flex; justify-content: center; align-items: center; + overflow: auto; } -/* New class for the background map image */ -.map-background { - width: 100%; - height: 100%; +.svg-container svg { + min-width: 100%; min-height: 100%; + width: auto; + height: auto; + display: block; + transform-origin: center center; +} + +/* Map background - display as full-page map */ +.map-background { position: absolute; top: 0; left: 0; - right: 0; - bottom: 0; - background-image: url("/images/map.svg"); /* Updated path to the correct SVG file */ - background-size: contain; + width: 100%; + height: 100%; + min-width: 100%; + min-height: 100%; + background-image: url("/images/map.svg"); + background-size: contain; /* Change back to contain for proper aspect ratio */ background-position: center; background-repeat: no-repeat; - z-index: 5; /* Ensure the map is above the background */ + z-index: 5; } +/* Circle elements in the SVG map - attach to a container class */ +.svg-container circle { + cursor: pointer; + transition: fill 0.3s ease, transform 0.3s ease; +} + +.svg-container circle:hover { + fill: #ffb74d !important; + transform: scale(1.1); +} + +/* Remaining styles unchanged */ .map-svg { width: 100% !important; /* Override Next.js Image default width */ height: auto !important; /* Override Next.js Image default height */ diff --git a/app/(main)/map/page.js b/app/(main)/map/page.js index 89ba2e9..923c62a 100644 --- a/app/(main)/map/page.js +++ b/app/(main)/map/page.js @@ -1,9 +1,15 @@ "use client"; -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import styles from "./map.module.css"; +import { + getHuntTimer, + setHuntTimer, + clearHuntTimer, + formatElapsedTime, +} from "@/utils/timerService"; export default function MapPage() { const { data: session, status } = useSession(); @@ -12,14 +18,34 @@ export default function MapPage() { const [error, setError] = useState(null); const [teamData, setTeamData] = useState(null); const [elapsedTime, setElapsedTime] = useState("00:00:00"); + const [mapSvg, setMapSvg] = useState(null); + const [clues, setClues] = useState([]); const mapContainerRef = useRef(null); const router = useRouter(); + const isMounted = useRef(true); // Add local state for hunt active status const [huntActive, setHuntActive] = useState(null); + // Use useCallback for functions called in useEffect to avoid dependency issues + const softRedirect = useCallback( + (url) => { + if (isMounted.current) { + router.push(url); + } + }, + [router] + ); + + // Hard redirect function (for when a full page reload is needed) + const hardRedirect = useCallback((url) => { + if (typeof window !== "undefined" && isMounted.current) { + window.location.href = url; + } + }, []); + // Add function to fetch team hunt start time - const fetchTeamHuntStartTime = async () => { + const fetchTeamHuntStartTime = useCallback(async () => { try { // First check localStorage for cached start time const cachedStartTime = localStorage.getItem("teamHuntStartTime"); @@ -59,124 +85,12 @@ export default function MapPage() { console.error("Failed to fetch team hunt start time:", error); return new Date().toISOString(); } - }; + }, [session?.user?.teamId]); - // Initial hunt status check - useEffect(() => { - // Check if hunt is active - checkHuntAndTimer(); + // IMPORTANT: Move fetchTeamData up here, before it's referenced in useEffect dependencies + const fetchTeamData = useCallback(async () => { + if (!isMounted.current) return; - // Set up periodic checks - const interval = setInterval(checkHuntAndTimer, 3000); - return () => clearInterval(interval); - }, []); - - // Handle authentication and hunt status - useEffect(() => { - // Only redirect if explicitly not authenticated - if (status === "unauthenticated") { - router.push("/login"); - return; - } - - // If hunt is not active, redirect to waiting page - if (huntActive === false) { - console.log("Hunt is not active, redirecting to waiting page"); - router.push("/waiting"); - return; - } - - // Only fetch team data if authenticated and hunt is active or status unknown - if (status === "authenticated" && huntActive !== false) { - fetchTeamData(); - } - }, [status, huntActive, router]); - - // Function to check hunt status and timer - async function checkHuntAndTimer() { - 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(); - console.log("Hunt status check from map:", data); - - // If hunt is not active, check if it has ended - if (!data.active) { - console.log("Hunt not active, checking if it ended..."); - - // Check if we had an active hunt before (using localStorage) - const wasActive = localStorage.getItem("teamHuntStartTime"); - - if (wasActive) { - // Hunt was active before but not now - it ended - console.log("Hunt has ended, redirecting to leaderboard"); - localStorage.removeItem("teamHuntStartTime"); // Clean up - window.location.href = "/leaderboard?huntEnded=true"; - return; - } - - // Otherwise just show the waiting page - setHuntActive(false); - router.push("/waiting"); - return; - } - - // If timer endTime exists and has passed, redirect to leaderboard - if (data.endTime && new Date(data.endTime) <= new Date()) { - console.log("Timer expired, redirecting to leaderboard"); - // Clean up the storage - localStorage.removeItem("teamHuntStartTime"); - router.push("/leaderboard?huntEnded=true"); - return; - } - - // Otherwise, hunt is active and timer is running - setHuntActive(true); - } - } catch (err) { - console.error("Failed to check hunt status:", err); - } - } - - // Use team hunt start time for elapsed time calculation - useEffect(() => { - if (!teamData?.team) return; - - let startTimePromise = fetchTeamHuntStartTime(); - startTimePromise.then((startTimeStr) => { - // Parse the ISO string to milliseconds - const startTime = new Date(startTimeStr).getTime(); - - const timer = setInterval(() => { - const now = Date.now(); - const elapsed = Math.floor((now - startTime) / 1000); - - const hours = Math.floor(elapsed / 3600) - .toString() - .padStart(2, "0"); - const minutes = Math.floor((elapsed % 3600) / 60) - .toString() - .padStart(2, "0"); - const seconds = Math.floor(elapsed % 60) - .toString() - .padStart(2, "0"); - - setElapsedTime(`${hours}:${minutes}:${seconds}`); - }, 1000); - - return () => clearInterval(timer); - }); - }, [teamData, session]); - - const fetchTeamData = async () => { try { setLoading(true); setError(null); @@ -207,7 +121,11 @@ export default function MapPage() { setError("You need to join a team before viewing the map"); setLoading(false); // Give user time to read the error message before redirect - setTimeout(() => router.push("/team-selection"), 2000); + setTimeout(() => { + if (isMounted.current) { + softRedirect("/team-selection"); + } + }, 2000); return; } else { throw new Error(errorData.error || "Failed to fetch team data"); @@ -215,18 +133,279 @@ export default function MapPage() { } const data = await response.json(); - setTeamData(data); - setLoading(false); + if (isMounted.current) { + setTeamData(data); + setLoading(false); + } } catch (err) { console.error("Error fetching team data:", err); - setError(err.message); - setLoading(false); + if (isMounted.current) { + setError(err.message); + setLoading(false); + } } - }; + }, [softRedirect]); + + // Function to fetch clues + const fetchClues = useCallback(async () => { + try { + const response = await fetch("/api/clues", { + headers: { "Cache-Control": "no-cache, no-store, must-revalidate" }, + credentials: "include", + }); + + if (response.ok) { + const cluesData = await response.json(); + setClues(cluesData); + } + } catch (error) { + console.error("Failed to fetch clues:", error); + } + }, []); + + // Function to load and enhance the SVG map + const loadAndEnhanceMap = useCallback(async () => { + try { + const response = await fetch("/images/map.svg"); + + if (!response.ok) { + setMapError(true); + return; + } + + const svgText = await response.text(); + setMapSvg(svgText); + } catch (error) { + console.error("Failed to load map SVG:", error); + setMapError(true); + } + }, []); + + // Function to enhance SVG with clickable areas + const enhanceSvgMap = useCallback(() => { + if (!mapContainerRef.current || !clues.length) return; + + // Find all circles in the SVG + const circleSvgElements = + mapContainerRef.current.querySelectorAll("circle"); + + // Convert NodeList to Array for easier manipulation + Array.from(circleSvgElements).forEach((circle, index) => { + // Only process if we have a clue for this circle + if (clues[index]) { + // Make circle interactive + circle.style.cursor = "pointer"; + circle.style.fill = "#ff9800"; + circle.style.stroke = "#d87f00"; + circle.style.strokeWidth = "2"; + + // Add click handler to navigate to the clue page + circle.onclick = () => { + router.push(`/clue/${clues[index].id}`); + }; + + // Add hover effects + circle.onmouseenter = () => { + circle.style.fill = "#ffb74d"; + circle.style.transform = "scale(1.1)"; + circle.style.transition = "all 0.3s ease"; + }; + + circle.onmouseleave = () => { + circle.style.fill = "#ff9800"; + circle.style.transform = "scale(1)"; + }; + + // Optional: Add label or tooltip + const label = document.createElementNS( + "http://www.w3.org/2000/svg", + "title" + ); + label.textContent = `Clue #${index + 1}: ${ + clues[index].title || "Mystery clue" + }`; + circle.appendChild(label); + } + }); + }, [clues, router]); + + // Clean up function for unmounting + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + + // Initial hunt status check + useEffect(() => { + // Check if hunt is active + checkHuntAndTimer(); + + // Set up periodic checks + const interval = setInterval(checkHuntAndTimer, 3000); + return () => clearInterval(interval); + }, []); + + // Handle authentication and hunt status + useEffect(() => { + // Only redirect if explicitly not authenticated + if (status === "unauthenticated") { + softRedirect("/login"); + return; + } + + // If hunt is not active, redirect to waiting page + if (huntActive === false) { + console.log("Hunt is not active, redirecting to waiting page"); + softRedirect("/waiting"); + return; + } + + // Only fetch team data if authenticated and hunt is active or status unknown + if (status === "authenticated" && huntActive !== false) { + fetchTeamData(); + loadAndEnhanceMap(); + fetchClues(); + } + }, [ + status, + huntActive, + softRedirect, + fetchTeamData, + loadAndEnhanceMap, + fetchClues, + ]); + + // Add effect to enhance SVG once it's loaded and clues are fetched + useEffect(() => { + if (mapSvg && clues.length > 0) { + // Let the DOM update first with the SVG content + setTimeout(() => { + enhanceSvgMap(); + }, 100); + } + }, [mapSvg, clues, enhanceSvgMap]); + + // Function to check hunt status and timer + async function checkHuntAndTimer() { + if (!isMounted.current) return; + + 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(); + console.log("Hunt status check from map:", data); + + // If hunt is not active, check if it has ended + if (!data.active) { + console.log("Hunt not active, checking if it ended..."); + + // Check if we had an active hunt before + const timerData = getHuntTimer(); + + if (timerData && timerData.isActive) { + // Hunt was active before but not now - it has ended + console.log("Hunt has ended, redirecting to leaderboard"); + clearHuntTimer(); // Clean up all timer data + + // Use the appropriate redirect method + if (isMounted.current) { + hardRedirect("/leaderboard?huntEnded=true"); + } + return; + } + + // Otherwise just show the waiting page + if (isMounted.current) { + setHuntActive(false); + softRedirect("/waiting"); + } + return; + } + + // If timer endTime exists and has passed, redirect to leaderboard + if (data.endTime && new Date(data.endTime) <= new Date()) { + console.log("Timer expired, redirecting to leaderboard"); + clearHuntTimer(); // Clean up all timer data + + if (isMounted.current) { + softRedirect("/leaderboard?huntEnded=true"); + } + return; + } + + // Otherwise, hunt is active and timer is running + if (isMounted.current) { + setHuntActive(true); + + // Update hunt timer data with the server's values + if (data.startTime) { + setHuntTimer(data.startTime, data.endTime); + } + } + } + } catch (err) { + console.error("Failed to check hunt status:", err); + } + } + + // Use team hunt start time for elapsed time calculation + useEffect(() => { + if (!teamData?.team) return; + + // Get hunt timer data from the centralized service + let timerData = getHuntTimer(); + + // If no timer data yet, fetch it + if (!timerData) { + fetchTeamHuntStartTime().then((startTimeStr) => { + setHuntTimer(startTimeStr); + timerData = getHuntTimer(); + startTimer(timerData); + }); + } else { + startTimer(timerData); + } + + function startTimer(timerData) { + const timer = setInterval(() => { + if (!isMounted.current) { + clearInterval(timer); + return; + } + + // Refresh timer data on each interval to catch any updates + const currentTimerData = getHuntTimer(); + if (!currentTimerData || !currentTimerData.isActive) { + setElapsedTime("00:00:00"); + return; + } + + setElapsedTime(formatElapsedTime(currentTimerData.startTime)); + }, 1000); + + return () => clearInterval(timer); + } + + // Cleanup function + return () => { + isMounted.current = false; + }; + }, [teamData, fetchTeamHuntStartTime]); // Check hunt status every second to ensure immediate updates useEffect(() => { const checkHuntStatus = async () => { + if (!isMounted.current) return; + try { const timestamp = Date.now(); // Add cache-busting timestamp const response = await fetch(`/api/hunt-status?t=${timestamp}`, { @@ -250,19 +429,25 @@ export default function MapPage() { // If hunt is not active, force redirect to waiting page if (!data.active) { console.log("Hunt not active, redirecting to waiting page"); - window.location.href = "/waiting"; + if (isMounted.current) { + softRedirect("/waiting"); + } return; } // If timer endTime exists and has passed, redirect to leaderboard if (data.endTime && new Date(data.endTime) <= new Date()) { console.log("Timer expired, redirecting to leaderboard"); - window.location.href = "/leaderboard?huntEnded=true"; + if (isMounted.current) { + softRedirect("/leaderboard?huntEnded=true"); + } return; } // Otherwise, hunt is active and timer is running - setHuntActive(true); + if (isMounted.current) { + setHuntActive(true); + } } catch (err) { console.error("Failed to check hunt status:", err); } @@ -272,7 +457,7 @@ export default function MapPage() { checkHuntStatus(); const intervalId = setInterval(checkHuntStatus, 1000); return () => clearInterval(intervalId); - }, []); + }, [softRedirect]); if (status === "loading" || loading) { return ( @@ -289,7 +474,7 @@ export default function MapPage() {

Hunt Not Active

The treasure hunt has not started yet.

-
@@ -311,7 +496,7 @@ export default function MapPage() {

Error Loading Map

We couldn't load the treasure map. Please try again.

- +
); } @@ -329,19 +514,25 @@ export default function MapPage() {
{teamData?.team?.code || ""}
- {/* Main map content area - showing map as background */} + {/* Main map content area - map fills the entire container */}
- {/* Using background-image for the map SVG */} -
+ {mapSvg ? ( +
+ ) : ( +
+ )}
- {/* Navigation bar - Map as first button */} + {/* Navigation bar at the bottom */}