"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, syncTimerWithServer, } 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 [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); setLoading(false); // Set loading to false after clues are fetched return processedClues; } catch (err) { console.error("Failed to fetch clues:", err); setError(err.message); return []; } }, []); // Handle clue button click const handleClueClick = useCallback( (clueId, isAvailable, isFound) => { if (isFound || isAvailable) { router.push(`/clue/${clueId}`); } else { alert("Complete previous clues to unlock this one!"); } }, [router] ); // Size and position the overlay to match the map image const setupOverlay = useCallback(() => { const overlay = document.getElementById("map-overlay-container"); const mapImg = document.getElementById("treasure-map-img"); if (overlay && mapImg) { // Wait for the image to load to get its dimensions mapImg.onload = () => { const mapRect = mapImg.getBoundingClientRect(); overlay.style.width = `${mapRect.width}px`; overlay.style.height = `${mapRect.height}px`; }; // Handle resize events const resizeOverlay = () => { const mapRect = mapImg.getBoundingClientRect(); overlay.style.width = `${mapRect.width}px`; overlay.style.height = `${mapRect.height}px`; }; // Initial resize and listen for window resize setTimeout(resizeOverlay, 300); window.addEventListener("resize", resizeOverlay); return () => { window.removeEventListener("resize", resizeOverlay); }; } }, []); // Add this useEffect to coordinate loading the map and clues useEffect(() => { if (status === "authenticated") { fetchClues(); fetchTeamData(); } else if (status === "unauthenticated") { softRedirect("/login"); } // Setup cleanup function for component unmount return () => { isMounted.current = false; }; }, [status, fetchClues, fetchTeamData, softRedirect]); // Setup the overlay when the component is mounted useEffect(() => { if (!loading && clues.length > 0) { return setupOverlay(); } }, [loading, clues.length, setupOverlay]); // 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", Pragma: "no-cache", }, credentials: "include", }); if (response.ok) { const data = await response.json(); setHuntActive(data.active); // If hunt is active, ensure we have timer data if (data.active && data.startTime) { setHuntTimer(data.startTime); } } } catch (err) { console.error("Failed to check hunt status:", err); } } // Add this function before it's used in initializeTimer async function fetchTeamHuntStartTime() { try { // First try to get any existing timer data const timerData = getHuntTimer(); if (timerData && timerData.startTime) { return true; } // If no timer data exists, sync with server const result = await syncTimerWithServer(); return result.success; } catch (err) { console.error("Error fetching team hunt start time:", err); return false; } } // Use team hunt start time for elapsed time calculation useEffect(() => { let timerId; // Initialize timer async function initializeTimer() { // First ensure we have timer data await fetchTeamHuntStartTime(); // Set up timer updates const updateTimer = () => { const timerData = getHuntTimer(); if (timerData && timerData.startTime) { setElapsedTime(formatElapsedTime(timerData.startTime)); } }; // Initial update updateTimer(); // Update every second timerId = setInterval(updateTimer, 1000); } initializeTimer(); // Clean up interval on unmount return () => { if (timerId) clearInterval(timerId); }; }, []); // Check hunt status every 10 seconds to ensure immediate updates useEffect(() => { // Initial check checkHuntAndTimer(); // Set up interval to check hunt status const huntStatusInterval = setInterval(checkHuntAndTimer, 10000); return () => { clearInterval(huntStatusInterval); }; }, [softRedirect, hardRedirect]); // 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.

); } // Function to determine clue button class based on status const getClueButtonClass = (clue) => { if (clue.found) return `${styles["clue-button"]} ${styles["clue-found"]}`; if (clue.available) return `${styles["clue-button"]} ${styles["clue-available"]}`; return `${styles["clue-button"]} ${styles["clue-locked"]}`; }; // Get the first clue if it exists const firstClue = clues.length > 0 ? clues[0] : null; return (
{/* Map Banner with team info and timer */}
{teamData?.team?.name || "Loading..."}
{elapsedTime}
{teamData?.team?.code || ""}
{/* Main map content area */}
{/* Simpler map implementation using an img element */}
Treasure Map {/* Overlay div with the clue button - positioned exactly over the map */}
{/* Hardcoded Clue 1 button */} {firstClue && ( )}
{/* Navigation bar */}
); }