"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"; 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) => { if (typeof window !== "undefined" && isMounted.current) { window.location.href = url; } }, []); // Add function to fetch team hunt start time const fetchTeamHuntStartTime = useCallback(async () => { try { // First check localStorage for cached start time const cachedStartTime = localStorage.getItem("teamHuntStartTime"); // If we have a cached time, use it if (cachedStartTime) { return cachedStartTime; } // Otherwise fetch from the server if (session?.user?.teamId) { const response = await fetch( `/api/teams/${session.user.teamId}/hunt-session`, { 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(); if (data.startTime) { // Cache the start time for future use localStorage.setItem("teamHuntStartTime", data.startTime); return data.startTime; } } } // Fallback to current time if we couldn't get a start time return new Date().toISOString(); } catch (error) { console.error("Failed to fetch team hunt start time:", error); return new Date().toISOString(); } }, [session?.user?.teamId]); // IMPORTANT: Move fetchTeamData up here, before it's referenced in useEffect dependencies const fetchTeamData = useCallback(async () => { if (!isMounted.current) return; try { setLoading(true); setError(null); const response = await fetch("/api/user/team", { method: "GET", headers: { "Content-Type": "application/json", "Cache-Control": "no-cache, no-store, must-revalidate", }, credentials: "include", }); if (!response.ok) { // Handle specific error cases without immediate redirect const errorData = await response.json().catch(() => ({})); if (response.status === 401) { // Set error but don't redirect immediately - let the session handling take care of it setError( "Authentication error: " + (errorData.error || "Not logged in") ); setLoading(false); return; } else if ( response.status === 404 || errorData.error?.includes("not part of a team") ) { setError("You need to join a team before viewing the map"); setLoading(false); // Give user time to read the error message before redirect setTimeout(() => { if (isMounted.current) { softRedirect("/team-selection"); } }, 2000); return; } else { throw new Error(errorData.error || "Failed to fetch team data"); } } const data = await response.json(); if (isMounted.current) { setTeamData(data); setLoading(false); } } catch (err) { console.error("Error fetching team data:", err); 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}`, { 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, force redirect to waiting page if (!data.active) { 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) { softRedirect("/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]); if (status === "loading" || loading) { return (
The treasure hunt has not started yet.
{error}
We couldn't load the treasure map. Please try again.