/* eslint-disable @next/next/no-img-element */ "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 NavBar from "@/components/NavBar"; 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); // Add previous hunt state to detect when hunt ends const [previousHuntState, setPreviousHuntState] = 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) { // Extract the error message from the response if possible const errorData = await response.json().catch(() => ({})); console.error("API response details:", { status: response.status, statusText: response.statusText, errorData, }); throw new Error( errorData.error || `Failed to fetch clues: ${response.status} ${response.statusText}` ); } 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) => { // The API now returns clues with availability already determined return { ...clue, available: clue.available || clue.found || clue.skipped, }; }); 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); // Make sure we set the error state setLoading(false); 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 inactive, redirect to waiting page if (data.active === false) { router.push("/waiting"); return; } // 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]); // Add this useEffect to detect when hunt ends useEffect(() => { // If previous state was active but current state is not active, the hunt just ended if (previousHuntState === true && huntActive === false) { console.log("Hunt has ended! Redirecting to leaderboard..."); router.push("/leaderboard?huntEnded=true"); } // Update previous state after checking setPreviousHuntState(huntActive); }, [huntActive, previousHuntState, router]); if (status === "loading" || loading) { return (
Loading map...
); } // Remove or modify this conditional block since we're now handling hunt ending differently // Instead of showing an error message, we'll redirect in the useEffect if (huntActive === false && previousHuntState === null) { // Only show this if we just landed on the page and hunt is inactive // (not when the hunt just ended) 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.skipped) return `${styles["clue-button"]} ${styles["clue-skipped"]}`; 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; // Render a clue button only if the clue exists const renderClueButton = (index, position) => { if (index >= clues.length) return null; // Don't render if clue doesn't exist const clue = clues[index]; return ( ); }; return (
{/* Map Banner with team info and timer */}
Team Name: {teamData?.team?.name || "Loading..."}
{elapsedTime}
{teamData?.team?.code || ""}
{/* Main map content area */}
{/* Simpler map implementation using an img element */}
Treasure Map
{/* Render clue buttons conditionally */} {renderClueButton(0, { left: "230px", top: "212px" })} {renderClueButton(1, { left: "320px", top: "285px" })} {renderClueButton(2, { left: "265px", top: "400px" })} {renderClueButton(3, { left: "343px", top: "520px" })} {renderClueButton(4, { left: "320px", top: "646px" })} {renderClueButton(5, { left: "298px", top: "774px" })} {renderClueButton(6, { left: "240px", top: "880px" })} {renderClueButton(7, { left: "355px", top: "960px" })} {renderClueButton(8, { left: "266px", top: "1037px" })} {renderClueButton(9, { left: "280px", top: "1175px" })} {renderClueButton(10, { left: "336px", top: "1303px" })} {renderClueButton(11, { left: "250px", top: "1390px" })} {renderClueButton(12, { left: "165px", top: "1485px" })} {renderClueButton(13, { left: "305px", top: "1530px" })} {renderClueButton(14, { left: "340px", top: "1650px" })} {renderClueButton(15, { left: "225px", top: "1725px" })} {renderClueButton(16, { left: "220px", top: "1820px" })} {renderClueButton(17, { left: "275px", top: "1885px" })} {renderClueButton(18, { left: "362px", top: "1917px" })} {renderClueButton(19, { left: "300px", top: "2040px" })} {/* Additional clue buttons can be added here when needed */}
{/* Replace hardcoded navbar with NavBar component */}
); }