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 */}