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 */}
-