diff --git a/app/(main)/leaderboard/page.js b/app/(main)/leaderboard/page.js index cdf45af..3979504 100644 --- a/app/(main)/leaderboard/page.js +++ b/app/(main)/leaderboard/page.js @@ -4,6 +4,7 @@ import { useSearchParams } from "next/navigation"; import { useRouter } from "next/navigation"; import { useSession } from "next-auth/react"; import Link from "next/link"; +import NavBar from "@/components/NavBar"; export default function LeaderboardPage() { const [teams, setTeams] = useState([]); @@ -160,31 +161,7 @@ export default function LeaderboardPage() { {/* Only show navigation bar when the hunt hasn't ended */} {!huntJustEnded && ( -
- -
πŸ—ΊοΈ
-
Map
- - -
πŸ“Š
-
Stats
- - -
πŸ‘₯
-
Team
- - -
πŸ†
-
Leaderboard
- -
+ )} {/* Add a "back to home" button when the hunt has ended */} diff --git a/app/(main)/map/page.js b/app/(main)/map/page.js index 6cdaa3c..6f9786c 100644 --- a/app/(main)/map/page.js +++ b/app/(main)/map/page.js @@ -1,52 +1,68 @@ -/* eslint-disable @next/next/no-img-element */ "use client"; -import { useState, useEffect, useRef, useCallback } from "react"; +import { useState, useEffect, useRef } 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 { - getHuntTimer, - setHuntTimer, - clearHuntTimer, - formatElapsedTime, - syncTimerWithServer, -} from "@/utils/timerService"; +import NavBar from "@/components/NavBar"; 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 [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [mapLoaded, setMapLoaded] = useState(false); const [huntActive, setHuntActive] = useState(null); - // Add previous hunt state to detect when hunt ends - const [previousHuntState, setPreviousHuntState] = useState(null); + const router = useRouter(); + const mapRef = useRef(null); - // Use useCallback for functions called in useEffect to avoid dependency issues - const softRedirect = useCallback( - (url) => { - if (isMounted.current) { - router.push(url); + useEffect(() => { + // Redirect to login if not authenticated + if (status === "unauthenticated") { + router.push("/login"); + return; + } + + 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"); } - }, - [router] - ); + } catch (err) { + console.error("Failed to check hunt status:", err); + setError("Failed to check if hunt is active. Please try again."); + setLoading(false); + } + }; - // 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 () => { + const fetchTeamData = async () => { try { const response = await fetch("/api/user/team", { method: "GET", @@ -59,12 +75,12 @@ export default function MapPage() { }); if (response.status === 401) { - softRedirect("/login"); + router.push("/login"); return null; } if (response.status === 404) { - softRedirect("/team-selection"); + router.push("/team-selection"); return null; } @@ -81,233 +97,17 @@ export default function MapPage() { 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", - }); + const handleMapLoad = () => { + setMapLoaded(true); + }; - 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, - }); + const handleMapError = () => { + setError("Failed to load the map. Please try again."); + }; - 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 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) { + if (loading) { return (
@@ -316,22 +116,6 @@ 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 (
@@ -342,126 +126,37 @@ 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 ( -
- {/* 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 */} -
+
+
+
+

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

+
+ {teamData?.progress !== undefined + ? `${teamData.progress}% Complete` + : "..."}
- {/* Navigation bar */} - +
+
+ Treasure Map +
+
+ + {/* Replace hardcoded navbar with NavBar component */} +
); } diff --git a/app/(main)/stats/page.js b/app/(main)/stats/page.js index 01f2363..3225245 100644 --- a/app/(main)/stats/page.js +++ b/app/(main)/stats/page.js @@ -4,6 +4,7 @@ import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import styles from "./stats.module.css"; +import NavBar from "@/components/NavBar"; export default function StatsPage() { const { data: session, status } = useSession(); @@ -11,6 +12,7 @@ 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(() => { @@ -22,9 +24,34 @@ 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); @@ -181,6 +208,12 @@ 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.

+
+ )} +
@@ -229,27 +262,8 @@ export default function StatsPage() {
-
- -
πŸ—ΊοΈ
-
Map
- - -
πŸ“Š
-
Stats
- - -
πŸ‘₯
-
Team
- - -
πŸ†
-
Leaderboard
- -
+ {/* Replace hardcoded navbar with NavBar component */} +
); diff --git a/app/(main)/team/[teamId]/page.js b/app/(main)/team/[teamId]/page.js index b7ed246..7922d57 100644 --- a/app/(main)/team/[teamId]/page.js +++ b/app/(main)/team/[teamId]/page.js @@ -135,6 +135,43 @@ export default function TeamPage({ params }) { } }, [effectiveTeamId, status, fetchTeamData, router]); + const continueTreasureHunt = async (e) => { + e.preventDefault(); + setLoading(true); + + try { + // Check if hunt is active first + 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(); + + if (data.active) { + // Hunt is active, redirect to map + router.push("/map"); + } else { + // Hunt is not active, redirect to waiting page + router.push("/waiting"); + } + } else { + throw new Error("Failed to check hunt status"); + } + } catch (err) { + console.error("Error checking hunt status:", err); + setError("Failed to check hunt status. Please try again."); + setLoading(false); + } + }; + if (status === "loading" || loading) { return (
@@ -341,27 +378,46 @@ export default function TeamPage({ params }) { )}
- {/* Replace the outdated navbar with this consistent design */}av-bar"]}> -
v-item"]}> - >πŸ—ΊοΈ
-
πŸ—ΊοΈ
>Map
-
Map
- v-item"]}> - }>πŸ“Š
-
πŸ“Š
tats
-
Stats
- - }>πŸ‘₯
-
πŸ‘₯
Team
-
Team
- v-item"]}> - }>πŸ† -
πŸ†
oard -
Leaderboard
- + {team && team.hasStarted && ( +
+ +
+ )} + +
+
+ +
πŸ—ΊοΈ
+
Map
+ +
+
+ +
πŸ“Š
+
Stats
+ +
+
+ +
πŸ‘₯
+
Team
+ +
+
+ +
πŸ†
+
Leaderboard
+ +
); } diff --git a/clue_hunt.db b/clue_hunt.db index 107dfc4..404aa6d 100644 Binary files a/clue_hunt.db and b/clue_hunt.db differ diff --git a/components/NavBar.js b/components/NavBar.js index eedb020..8238085 100644 --- a/components/NavBar.js +++ b/components/NavBar.js @@ -35,7 +35,7 @@ export default function NavBar({ activeLink, teamData }) { checkHuntStatus(); }, []); - // Handle map link click + // Handle map link click - only redirect for map page const handleMapClick = (e) => { if (huntActive === false) { e.preventDefault(); @@ -44,46 +44,38 @@ export default function NavBar({ activeLink, teamData }) { }; return ( -