"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, attachClueClickHandlers, } 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 []; } }, []); // Enhanced function to populate the map with clue points const enhanceSvgMap = useCallback(() => { 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"); setMapError(true); return; } console.log("SVG element found, starting enhancement process"); // Clear out any existing map-background elements to ensure we only show one map const existingMapBgs = document.querySelectorAll( `.${styles["map-background"]}` ); existingMapBgs.forEach((bg) => (bg.style.display = "none")); // First populate the map with basic clue points populateMap(svgElement) .then((success) => { if (success) { console.log("Map populated with clue points successfully"); // Process the clues to determine which are available const processedClues = clues.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 }; }); // Debug info about clue statuses processedClues.forEach((clue, index) => { console.log( `Clue ${clue.id} (index ${index}): Available: ${clue.available}, Found: ${clue.found}` ); }); // Update the clue point appearances based on their statuses updateCluePointStatus(svgElement, processedClues); // Define a direct navigation function that will be used for click handling const navigateToClue = (clueId) => { console.log(`Navigating to clue ${clueId}`); router.push(`/clue/${clueId}`); }; // Attach click handlers to all clue points and click areas attachClueClickHandlers(svgElement, processedClues, navigateToClue); } else { console.error("Failed to populate map with clue points"); setMapError(true); } }) .catch((error) => { console.error("Error enhancing SVG map:", error); setMapError(true); }); }, [clues, router, styles]); // 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} characters`); // Ensure SVG has correct namespaces and viewBox let fixedSvg = svgText.replace( /
🎯 Hunt Complete!
The treasure hunt has ended.
Redirecting to results...
`; document.body.appendChild(huntEndedMessage); // Use the appropriate redirect method after a short delay setTimeout(() => { if (isMounted.current) { hardRedirect("/leaderboard?huntEnded=true"); } }, 2000); 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 // Show a brief message before redirecting const timerEndedMessage = document.createElement("div"); timerEndedMessage.innerHTML = `
⏰ Time's Up!
The treasure hunt has ended.
Redirecting to results...
`; document.body.appendChild(timerEndedMessage); if (isMounted.current) { setTimeout(() => { hardRedirect("/leaderboard?huntEnded=true"); }, 2000); } 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 are loaded, preparing to enhance map"); // Ensure the SVG is fully loaded in the DOM before enhancing requestAnimationFrame(() => { // Longer 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 now"); enhanceSvgMap(); } else { console.error("SVG not found in DOM after rendering timeout"); setMapError(true); } }, 500); // Increased timeout for better reliability on slower devices }); }, [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 ? (
) : (
Loading map...
)}
{/* Navigation bar */}
); }