"use client"; 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"; import { populateMap, updateCluePointStatus } from "@/utils/map-populate"; export default function MapPage() { const { data: session, status } = useSession(); const [loading, setLoading] = useState(true); const [mapError, setMapError] = useState(false); 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) => { window.location.href = url; }, []); // Enhanced fetchTeamData function const fetchTeamData = useCallback(async () => { try { const response = await fetch("/api/user/team", { method: "GET", headers: { "Content-Type": "application/json", "Cache-Control": "no-cache, no-store, must-revalidate", Pragma: "no-cache", }, credentials: "include", }); if (response.status === 401) { softRedirect("/login"); return null; } if (response.status === 404) { softRedirect("/team-selection"); return null; } if (!response.ok) { throw new Error("Failed to fetch team data"); } const data = await response.json(); console.log("Team data loaded:", data); setTeamData(data); return data; } catch (err) { console.error("Failed to fetch team data:", err); setError(err.message); return null; } }, [softRedirect]); // Fetch all clues with their status const fetchClues = useCallback(async () => { try { const response = await fetch("/api/clues", { method: "GET", headers: { "Content-Type": "application/json", "Cache-Control": "no-cache, no-store, must-revalidate", Pragma: "no-cache", }, credentials: "include", }); if (!response.ok) { throw new Error("Failed to fetch clues"); } const data = await response.json(); // Process the clues to determine which are available // A clue is available if it's either found already or the next in sequence const processedClues = data.map((clue, index, arr) => { // Find the first unfound clue's index const firstUnfoundIndex = arr.findIndex((c) => !c.found); // A clue is available if: // 1. It's already found, OR // 2. It's the next unfound clue in the sequence const available = clue.found || index === firstUnfoundIndex; return { ...clue, available }; }); setClues(processedClues); return processedClues; } catch (err) { console.error("Failed to fetch clues:", err); setError(err.message); return []; } }, []); // Function to load and enhance the SVG map const loadAndEnhanceMap = useCallback(async () => { try { // Fetch the SVG file const response = await fetch("/images/map.svg"); if (!response.ok) { console.error( "Failed to load SVG:", response.status, response.statusText ); setMapError(true); return; } const svgText = await response.text(); console.log("SVG loaded, length:", svgText.length); // Ensure SVG has correct namespaces and viewBox let fixedSvg = svgText.replace( / { if (!mapContainerRef.current) return; // Find the SVG element inside the map container const svgElement = mapContainerRef.current.querySelector("svg"); if (!svgElement) { console.error("No SVG element found in map container"); return; } console.log("SVG element found, starting enhancement"); // Use our utility to populate the map with clue points populateMap(svgElement) .then((success) => { if (success) { console.log("Map populated successfully"); // After populating the map, update the status of each clue point updateCluePointStatus(svgElement, clues); // Add click handlers to each clue point const cluePoints = svgElement.querySelectorAll(".clue-point"); cluePoints.forEach((circle, index) => { if (index < clues.length) { const clue = clues[index]; const isAvailable = clue.available; // Make circle interactive circle.style.cursor = isAvailable ? "pointer" : "not-allowed"; // Add click handler circle.onclick = (e) => { e.preventDefault(); e.stopPropagation(); if (isAvailable) { console.log(`Navigating to clue ${clue.id}`); router.push(`/clue/${clue.id}`); } else { alert("Complete previous clues first to unlock this one!"); } }; // Add hover effects circle.onmouseenter = () => { if (isAvailable) { circle.style.fill = "#ffb74d"; // Lighter orange for hover } }; circle.onmouseleave = () => { if (clue.found) { circle.style.fill = "#4caf50"; // Back to green for found clues } else if (isAvailable) { circle.style.fill = "#ff9800"; // Back to orange for available clues } else { circle.style.fill = "#9e9e9e"; // Back to grey for unavailable clues } }; // Add tooltip with clue information const label = document.createElementNS( "http://www.w3.org/2000/svg", "title" ); label.textContent = `Clue #${index + 1}: ${ clue.title || "Mystery clue" }${!isAvailable ? " (Locked)" : ""}`; circle.appendChild(label); } }); } else { console.error("Failed to populate map"); } }) .catch((error) => { console.error("Error enhancing SVG map:", error); setMapError(true); }); }, [clues, router]); // 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) { hardRedirect("/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); } } // Add this function before it's used in initializeTimer async function fetchTeamHuntStartTime() { try { // First get the user's team ID const userResponse = await fetch("/api/user", { method: "GET", headers: { "Cache-Control": "no-store, no-cache, must-revalidate", Pragma: "no-cache", }, credentials: "include", }); if (!userResponse.ok) { console.error("Failed to fetch user data:", userResponse.statusText); return null; } const userData = await userResponse.json(); const teamId = userData.teamId; if (!teamId) { console.error("User does not belong to a team"); return null; } // Now fetch the team's hunt session using the team ID const teamHuntResponse = await fetch( `/api/teams/${teamId}/hunt-session`, { method: "GET", headers: { "Cache-Control": "no-store, no-cache, must-revalidate", Pragma: "no-cache", }, credentials: "include", } ); if (!teamHuntResponse.ok) { console.error( "Failed to fetch team hunt session:", teamHuntResponse.statusText ); // Try the global hunt timer as fallback const globalResponse = await fetch("/api/hunt/timer", { method: "GET", headers: { "Cache-Control": "no-store, no-cache, must-revalidate", Pragma: "no-cache", }, credentials: "include", }); if (globalResponse.ok) { const globalData = await globalResponse.json(); return globalData.startTime || null; } return null; } const teamHuntData = await teamHuntResponse.json(); return teamHuntData.startTime || null; } catch (error) { console.error("Error fetching team hunt start time:", error); return null; } } // Use team hunt start time for elapsed time calculation useEffect(() => { // Initialize the timer without waiting for teamData // This ensures timer starts as soon as possible const initializeTimer = async () => { try { // First try to get timer data from the centralized service let timerData = getHuntTimer(); // If no timer data yet, fetch it from API if (!timerData || !timerData.startTime) { console.log("No timer data found, fetching from API..."); const startTimeStr = await fetchTeamHuntStartTime(); console.log("Got start time:", startTimeStr); if (startTimeStr) { setHuntTimer(startTimeStr); timerData = getHuntTimer(); } } // Set up interval for timer updates const timerInterval = setInterval(() => { if (!isMounted.current) { clearInterval(timerInterval); return; } const currentTimerData = getHuntTimer(); if (currentTimerData && currentTimerData.startTime) { const formattedTime = formatElapsedTime(currentTimerData.startTime); console.log("Updated timer:", formattedTime); setElapsedTime(formattedTime); } else { setElapsedTime("00:00:00"); } }, 1000); // Initial timer update if (timerData && timerData.startTime) { setElapsedTime(formatElapsedTime(timerData.startTime)); } return () => { clearInterval(timerInterval); }; } catch (error) { console.error("Error initializing timer:", error); } }; initializeTimer(); }, []); // Empty dependency array since fetchTeamHuntStartTime is defined inside the component // 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}`, { method: "GET", headers: { "Content-Type": "application/json", "Cache-Control": "no-store, no-cache, must-revalidate", Pragma: "no-cache", }, credentials: "include", }); if (!response.ok) { console.error("Failed to fetch hunt status:", response.statusText); return; } const data = await response.json(); console.log("Map page hunt status check:", data); // If hunt is not active, check if it was active before (hunt ended) if (!data.active) { // 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(); if (isMounted.current) { hardRedirect("/leaderboard?huntEnded=true"); } return; } // If hunt was never active, redirect to waiting page console.log("Hunt not active, redirecting to waiting page"); 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"); if (isMounted.current) { clearHuntTimer(); hardRedirect("/leaderboard?huntEnded=true"); } return; } // Otherwise, hunt is active and timer is running if (isMounted.current) { setHuntActive(true); } } catch (err) { console.error("Failed to check hunt status:", err); } }; // Check immediately and every second checkHuntStatus(); const intervalId = setInterval(checkHuntStatus, 1000); return () => clearInterval(intervalId); }, [softRedirect, hardRedirect]); // Add this useEffect to coordinate loading the map and clues useEffect(() => { // Only proceed if authenticated if (status === "authenticated") { const initMap = async () => { try { // Set loading state setLoading(true); // Load both map and clues in parallel await Promise.all([fetchClues(), loadAndEnhanceMap()]); // Data is loaded, we'll let the next effect handle enhancement } catch (error) { console.error("Error initializing map data:", error); setError("Failed to load map data"); setLoading(false); } }; initMap(); } else if (status === "unauthenticated") { router.push("/login"); } }, [status, fetchClues, loadAndEnhanceMap, router]); // Modified useEffect to better handle SVG loading and enhancement useEffect(() => { // Only try to enhance the map if both the SVG and clues are loaded if (!mapSvg || !clues.length) return; console.log("Both SVG and clues loaded, enhancing map"); // Use requestAnimationFrame to ensure the DOM has been updated with the SVG requestAnimationFrame(() => { // Short timeout to ensure the SVG is fully rendered in the DOM setTimeout(() => { if ( mapContainerRef.current && mapContainerRef.current.querySelector("svg") ) { console.log("SVG found in DOM, enhancing map"); enhanceSvgMap(); } else { console.error("SVG not found in DOM after rendering"); setMapError(true); } }, 100); }); }, [mapSvg, clues, enhanceSvgMap]); // Add useEffect to load team data useEffect(() => { if (status === "authenticated") { fetchTeamData(); } }, [status, fetchTeamData]); if (status === "loading" || loading) { return (
Loading map...
); } // If hunt status is known to be inactive, show message if (huntActive === false) { return (

Hunt Not Active

The treasure hunt has not started yet.

); } if (error) { return (

Error

{error}

); } if (mapError) { return (

Error Loading Map

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

); } return (
{/* Map Banner with team info and timer */}
{teamData?.team?.name || "Loading..."}
{elapsedTime}
{teamData?.team?.code || ""}
{/* Main map content area */}
{mapSvg ? (
console.log("SVG container loaded")} /> ) : (
)}
{/* Navigation bar */}
); }