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 (
@@ -116,6 +323,22 @@ export default function MapPage() { ); } + // 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 (
@@ -126,32 +349,98 @@ export default function MapPage() { ); } + 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 ( -
-
-
-

- {teamData?.team ? teamData.team.name : "Loading..."} -

-
- {teamData?.progress !== undefined - ? `${teamData.progress}% Complete` - : "..."} -
+
+ {/* Map Banner with team info and timer */} +
+
+ Team Name: {teamData?.team?.name || "Loading..."}
+
+ {elapsedTime} +
+
{teamData?.team?.code || ""}
-
-
- Treasure Map + {/* 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 */} +
+
diff --git a/app/(main)/stats/page.js b/app/(main)/stats/page.js index 3225245..6d7c624 100644 --- a/app/(main)/stats/page.js +++ b/app/(main)/stats/page.js @@ -12,7 +12,6 @@ export default function StatsPage() { const [error, setError] = useState(null); const [teamStats, setTeamStats] = useState(null); const [expanded, setExpanded] = useState(false); - const [huntActive, setHuntActive] = useState(null); const router = useRouter(); useEffect(() => { @@ -24,34 +23,9 @@ export default function StatsPage() { if (status === "authenticated") { fetchTeamStats(); - checkHuntStatus(); } }, [status, router]); - // Check hunt status but don't redirect - 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); - // We're just checking status, no redirect needed for stats page - } - } catch (err) { - console.error("Failed to check hunt status:", err); - } - }; - const fetchTeamStats = async () => { try { setLoading(true); @@ -120,7 +94,6 @@ export default function StatsPage() { {foundClues?.length > 0 ? ( foundClues.map((clue) => (
  • -
    βœ“
    {clue.title || `Clue #${clue.id}`} @@ -142,8 +115,7 @@ export default function StatsPage() {
      {skippedClues?.length > 0 ? ( skippedClues.map((clue) => ( -
    • -
      β†·
      +
    • {clue.title || `Clue #${clue.id}`} @@ -208,12 +180,6 @@ export default function StatsPage() {

      {teamStats.team.name}

      - {!huntActive && teamStats && ( -
      -

      The treasure hunt is not currently active. Team progress will be visible here once the hunt begins.

      -
      - )} -
      @@ -266,22 +232,63 @@ export default function StatsPage() {
      diff --git a/app/(main)/team/[teamId]/page.js b/app/(main)/team/[teamId]/page.js index 7922d57..7665a22 100644 --- a/app/(main)/team/[teamId]/page.js +++ b/app/(main)/team/[teamId]/page.js @@ -3,6 +3,7 @@ import { useState, useEffect, useCallback } from "react"; import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import Link from "next/link"; +import NavBar from "@/components/NavBar"; export default function TeamPage({ params }) { const { status, data: session } = useSession(); @@ -135,6 +136,7 @@ export default function TeamPage({ params }) { } }, [effectiveTeamId, status, fetchTeamData, router]); + // Add a function to properly handle the "Continue Hunt" button click const continueTreasureHunt = async (e) => { e.preventDefault(); setLoading(true); @@ -390,35 +392,8 @@ export default function TeamPage({ params }) {
      )} -
      -
      - -
      πŸ—ΊοΈ
      -
      Map
      - -
      -
      - -
      πŸ“Š
      -
      Stats
      - -
      -
      - -
      πŸ‘₯
      -
      Team
      - -
      -
      - -
      πŸ†
      -
      Leaderboard
      - -
      -
      + {/* Replace hardcoded navbar with NavBar component */} +