diff --git a/app/(main)/map/page.js b/app/(main)/map/page.js index 6f9786c..8bbb0cb 100644 --- a/app/(main)/map/page.js +++ b/app/(main)/map/page.js @@ -1,68 +1,53 @@ +/* eslint-disable @next/next/no-img-element */ "use client"; -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import Link from "next/link"; -import Image from "next/image"; 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 [teamData, setTeamData] = useState(null); const [loading, setLoading] = useState(true); + const [mapError, setMapError] = useState(false); const [error, setError] = useState(null); - const [mapLoaded, setMapLoaded] = useState(false); - const [huntActive, setHuntActive] = 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 mapRef = useRef(null); + const isMounted = useRef(true); - useEffect(() => { - // Redirect to login if not authenticated - if (status === "unauthenticated") { - router.push("/login"); - return; - } + // 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); - if (status === "authenticated") { - checkHuntStatus(); - fetchTeamData(); - } - }, [status, router]); - - const checkHuntStatus = async () => { - try { - const timestamp = Date.now(); - 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) { - const data = await response.json(); - setHuntActive(data.active); - - // Redirect to waiting page if hunt is not active - if (!data.active) { - router.push("/waiting"); - return; - } - } else { - throw new Error("Failed to check hunt status"); + // Use useCallback for functions called in useEffect to avoid dependency issues + const softRedirect = useCallback( + (url) => { + if (isMounted.current) { + router.push(url); } - } catch (err) { - console.error("Failed to check hunt status:", err); - setError("Failed to check if hunt is active. Please try again."); - setLoading(false); - } - }; + }, + [router] + ); - const fetchTeamData = async () => { + // 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", @@ -75,12 +60,12 @@ export default function MapPage() { }); if (response.status === 401) { - router.push("/login"); + softRedirect("/login"); return null; } if (response.status === 404) { - router.push("/team-selection"); + softRedirect("/team-selection"); return null; } @@ -97,17 +82,239 @@ export default function MapPage() { setError(err.message); return null; } - }; + }, [softRedirect]); - const handleMapLoad = () => { - setMapLoaded(true); - }; + // 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", + }); - const handleMapError = () => { - setError("Failed to load the map. Please try again."); - }; + 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, + }); - if (loading) { + 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 (
The treasure hunt has not started yet.
+ +We couldn't load the treasure map. Please try again.
+ +The treasure hunt is not currently active. Team progress will be visible here once the hunt begins.
-