diff --git a/app/(main)/hunt-not-started/page.js b/app/(main)/hunt-not-started/page.js
index 58f8f66..c910bdd 100644
--- a/app/(main)/hunt-not-started/page.js
+++ b/app/(main)/hunt-not-started/page.js
@@ -9,6 +9,49 @@ export default function HuntNotStarted() {
const searchParams = useSearchParams();
const teamId = searchParams.get("teamId");
+ // Add aggressive hunt status checking
+ useEffect(() => {
+ let redirectAttempted = false;
+
+ // Function to check hunt status
+ async function checkHuntStatus() {
+ if (redirectAttempted) return;
+
+ try {
+ const timestamp = new Date().getTime();
+ 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 hunt becomes active while on this page, immediately redirect to map
+ if (data.active) {
+ console.log("Hunt now active! Redirecting to map page");
+ redirectAttempted = true;
+ window.location.replace("/map");
+ }
+ }
+ } catch (err) {
+ console.error("Error checking hunt status:", err);
+ }
+ }
+
+ // Check immediately and every 500ms
+ checkHuntStatus();
+ const intervalId = setInterval(checkHuntStatus, 500);
+
+ return () => clearInterval(intervalId);
+ }, []);
+
+ // Existing countdown effect
useEffect(() => {
// Countdown timer
if (countdown > 0) {
diff --git a/app/(main)/map/map.module.css b/app/(main)/map/map.module.css
index 8d5bcc3..07ab6af 100644
--- a/app/(main)/map/map.module.css
+++ b/app/(main)/map/map.module.css
@@ -5,7 +5,7 @@
height: 100vh; /* Full viewport height */
overflow: hidden; /* Hide overflow at page level */
position: relative;
- background-color: #69b5a8;
+ background-color: #69B5A8;
}
.map-banner {
@@ -63,17 +63,36 @@
position: relative;
width: 100%;
height: 100%;
+ min-height: calc(100vh - 120px); /* Adjust for header and navbar height */
+ background-color: #69B5A8; /* Updated background color */
}
.treasure-map {
width: 100%;
height: 100%;
- min-height: 100%;
+ position: relative;
display: flex;
justify-content: center;
align-items: center;
}
+/* New class for the background map image */
+.map-background {
+ width: 100%;
+ height: 100%;
+ min-height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-image: url("/images/map.svg"); /* Updated path to the correct SVG file */
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ z-index: 5; /* Ensure the map is above the background */
+}
+
.map-svg {
width: 100% !important; /* Override Next.js Image default width */
height: auto !important; /* Override Next.js Image default height */
diff --git a/app/(main)/map/page.js b/app/(main)/map/page.js
index bb993dd..89ba2e9 100644
--- a/app/(main)/map/page.js
+++ b/app/(main)/map/page.js
@@ -3,9 +3,7 @@ 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 { useHuntStatusCheck } from "@/hooks/useHuntStatusCheck";
export default function MapPage() {
const { data: session, status } = useSession();
@@ -17,14 +15,63 @@ export default function MapPage() {
const mapContainerRef = useRef(null);
const router = useRouter();
- // Use the custom hook to check if hunt is still active
- const huntActive = useHuntStatusCheck();
+ // Add local state for hunt active status
+ const [huntActive, setHuntActive] = useState(null);
- // Debug log for hunt status
+ // Add function to fetch team hunt start time
+ const fetchTeamHuntStartTime = async () => {
+ try {
+ // First check localStorage for cached start time
+ const cachedStartTime = localStorage.getItem("teamHuntStartTime");
+
+ // If we have a cached time, use it
+ if (cachedStartTime) {
+ return cachedStartTime;
+ }
+
+ // Otherwise fetch from the server
+ if (session?.user?.teamId) {
+ const response = await fetch(
+ `/api/teams/${session.user.teamId}/hunt-session`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "Cache-Control": "no-cache, no-store, must-revalidate",
+ },
+ credentials: "include",
+ }
+ );
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.startTime) {
+ // Cache the start time for future use
+ localStorage.setItem("teamHuntStartTime", data.startTime);
+ return data.startTime;
+ }
+ }
+ }
+
+ // Fallback to current time if we couldn't get a start time
+ return new Date().toISOString();
+ } catch (error) {
+ console.error("Failed to fetch team hunt start time:", error);
+ return new Date().toISOString();
+ }
+ };
+
+ // Initial hunt status check
useEffect(() => {
- console.log("Hunt active status in map page:", huntActive);
- }, [huntActive]);
+ // Check if hunt is active
+ checkHuntAndTimer();
+ // Set up periodic checks
+ const interval = setInterval(checkHuntAndTimer, 3000);
+ return () => clearInterval(interval);
+ }, []);
+
+ // Handle authentication and hunt status
useEffect(() => {
// Only redirect if explicitly not authenticated
if (status === "unauthenticated") {
@@ -45,31 +92,89 @@ export default function MapPage() {
}
}, [status, huntActive, router]);
- // Update elapsed time every second
+ // 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",
+ },
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ console.log("Hunt status check from map:", data);
+
+ // If hunt is not active, check if it has ended
+ if (!data.active) {
+ console.log("Hunt not active, checking if it ended...");
+
+ // Check if we had an active hunt before (using localStorage)
+ const wasActive = localStorage.getItem("teamHuntStartTime");
+
+ if (wasActive) {
+ // Hunt was active before but not now - it ended
+ console.log("Hunt has ended, redirecting to leaderboard");
+ localStorage.removeItem("teamHuntStartTime"); // Clean up
+ window.location.href = "/leaderboard?huntEnded=true";
+ return;
+ }
+
+ // Otherwise just show the waiting page
+ setHuntActive(false);
+ router.push("/waiting");
+ return;
+ }
+
+ // If timer endTime exists and has passed, redirect to leaderboard
+ if (data.endTime && new Date(data.endTime) <= new Date()) {
+ console.log("Timer expired, redirecting to leaderboard");
+ // Clean up the storage
+ localStorage.removeItem("teamHuntStartTime");
+ router.push("/leaderboard?huntEnded=true");
+ return;
+ }
+
+ // Otherwise, hunt is active and timer is running
+ setHuntActive(true);
+ }
+ } catch (err) {
+ console.error("Failed to check hunt status:", err);
+ }
+ }
+
+ // Use team hunt start time for elapsed time calculation
useEffect(() => {
if (!teamData?.team) return;
- const startTime = new Date(teamData?.huntStartTime || Date.now()).getTime();
+ let startTimePromise = fetchTeamHuntStartTime();
+ startTimePromise.then((startTimeStr) => {
+ // Parse the ISO string to milliseconds
+ const startTime = new Date(startTimeStr).getTime();
- const timer = setInterval(() => {
- const now = Date.now();
- const elapsed = Math.floor((now - startTime) / 1000);
+ const timer = setInterval(() => {
+ const now = Date.now();
+ const elapsed = Math.floor((now - startTime) / 1000);
- const hours = Math.floor(elapsed / 3600)
- .toString()
- .padStart(2, "0");
- const minutes = Math.floor((elapsed % 3600) / 60)
- .toString()
- .padStart(2, "0");
- const seconds = Math.floor(elapsed % 60)
- .toString()
- .padStart(2, "0");
+ const hours = Math.floor(elapsed / 3600)
+ .toString()
+ .padStart(2, "0");
+ const minutes = Math.floor((elapsed % 3600) / 60)
+ .toString()
+ .padStart(2, "0");
+ const seconds = Math.floor(elapsed % 60)
+ .toString()
+ .padStart(2, "0");
- setElapsedTime(`${hours}:${minutes}:${seconds}`);
- }, 1000);
+ setElapsedTime(`${hours}:${minutes}:${seconds}`);
+ }, 1000);
- return () => clearInterval(timer);
- }, [teamData]);
+ return () => clearInterval(timer);
+ });
+ }, [teamData, session]);
const fetchTeamData = async () => {
try {
@@ -119,6 +224,56 @@ export default function MapPage() {
}
};
+ // Check hunt status every second to ensure immediate updates
+ useEffect(() => {
+ const checkHuntStatus = async () => {
+ try {
+ const timestamp = Date.now(); // Add cache-busting timestamp
+ 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) {
+ console.error("Failed to fetch hunt status:", response.statusText);
+ return;
+ }
+
+ const data = await response.json();
+ console.log("Map page hunt status check:", data);
+
+ // If hunt is not active, force redirect to waiting page
+ if (!data.active) {
+ console.log("Hunt not active, redirecting to waiting page");
+ window.location.href = "/waiting";
+ return;
+ }
+
+ // If timer endTime exists and has passed, redirect to leaderboard
+ if (data.endTime && new Date(data.endTime) <= new Date()) {
+ console.log("Timer expired, redirecting to leaderboard");
+ window.location.href = "/leaderboard?huntEnded=true";
+ return;
+ }
+
+ // Otherwise, hunt is active and timer is running
+ setHuntActive(true);
+ } catch (err) {
+ console.error("Failed to check hunt status:", err);
+ }
+ };
+
+ // Check immediately and every second
+ checkHuntStatus();
+ const intervalId = setInterval(checkHuntStatus, 1000);
+ return () => clearInterval(intervalId);
+ }, []);
+
if (status === "loading" || loading) {
return (
@@ -174,42 +329,20 @@ export default function MapPage() {
{teamData?.team?.code || ""}
- {/* Main map content area */}
+ {/* Main map content area - showing map as background */}
- {/* Use Image component with fill layout for better SVG handling */}
- {
- // Fallback to a static image if SVG fails to load
- setMapError(true);
- }}
- />
+ {/* Using background-image for the map SVG */}
+
';
+ window.location.href = "/map";
+ return;
+ }
+
+ // If hunt isn't active, trigger it for the entire team
+ const response = await fetch(
+ `/api/teams/${teamId}/start-team-hunt`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "Cache-Control": "no-store, no-cache, must-revalidate",
+ },
+ credentials: "include",
+ }
+ );
if (!response.ok) {
- const errorText = await response.text();
- console.error("Hunt status error response:", errorText);
- throw new Error(
- `Failed to check hunt status: ${response.status}`
- );
+ const errorData = await response.json();
+ throw new Error(errorData.error || "Failed to start hunt");
}
const data = await response.json();
- console.log("Hunt status response:", data);
+ console.log("Hunt started response:", data);
- // If hunt is active, go directly to the map page using window.location
- if (data.active) {
- // Use window.location.href for a full page reload to avoid caching issues
- window.location.href = "/map";
- } else {
- // If hunt is not active, show the not started page
- router.push(`/hunt-not-started?teamId=${teamId}`);
+ // Store hunt start time in localStorage for persistence
+ if (data.startTime) {
+ localStorage.setItem("teamHuntStartTime", data.startTime);
}
+
+ // Force redirect to map page to ensure immediate navigation
+ document.body.innerHTML =
+ '
Loading Map...
Please wait...
';
+ window.location.href = "/map";
} catch (err) {
- console.error("Error checking hunt status:", err);
- setError(`Failed to check hunt status: ${err.message}`);
- } finally {
+ console.error("Error starting hunt:", err);
+ setError(err.message);
setLoading(false);
}
}}
diff --git a/app/api/hunt-status/route.js b/app/api/hunt-status/route.js
index a014e7d..9042901 100644
--- a/app/api/hunt-status/route.js
+++ b/app/api/hunt-status/route.js
@@ -1,6 +1,4 @@
import { queryOne, run } from "@/lib/db";
-import { cookies } from "next/headers";
-import { verifyToken } from "@/app/api/auth/[...nextauth]/route";
export const dynamic = "force-dynamic"; // Disable caching
@@ -33,68 +31,76 @@ async function ensureHuntStatusTableExists() {
}
}
-// GET endpoint to check if the hunt is active
+// GET endpoint to check if the hunt is active - allow unauthenticated access for easier status checks
export async function GET(request) {
try {
// First ensure the table exists
await ensureHuntStatusTableExists();
- // Get the token from cookies - properly awaited
- const cookieStore = await cookies();
- const sessionToken = cookieStore.get("next-auth.session-token")?.value;
+ // NO TOKEN CHECK - Allow anonymous status checks
+ // This is critical for the waiting page to work reliably
- // Check hunt status even if user is not authenticated
- // to prevent auth errors from interfering with status checks
-
- // First check the hunt_status table for direct status
+ // Query all possible sources
const huntStatus = await queryOne(
"SELECT is_active, start_time FROM hunt_status LIMIT 1"
- ).catch((err) => {
- console.warn("Error querying hunt_status:", err.message);
- return null;
- });
+ ).catch(() => null);
+
+ const timerStatus = await queryOne(
+ "SELECT active, start_time, end_time FROM hunt_timer WHERE active = 1 ORDER BY id DESC LIMIT 1"
+ ).catch(() => null);
- // Then check the settings table as a backup
const huntActiveSetting = await queryOne(
"SELECT value FROM settings WHERE key = 'hunt_active'"
- ).catch((err) => {
- console.warn("Error querying settings:", err.message);
- return null;
- });
+ ).catch(() => null);
- // Also check the hunts table if it exists (for backward compatibility)
+ // Also check the traditional hunts table
const oldHuntRecord = await queryOne(
"SELECT active, start_time FROM hunts ORDER BY id DESC LIMIT 1"
- ).catch((err) => {
- // Ignore errors if table doesn't exist
- return null;
- });
+ ).catch(() => null);
- // Determine active status from all available sources
+ // Determine active status from all possible sources
let isActive = false;
let startTime = null;
+ let endTime = null;
- // First priority: hunt_status.is_active
- if (huntStatus && huntStatus.is_active !== null) {
- isActive = Boolean(huntStatus.is_active);
+ // Check all possible active indicators in priority order
+ if (timerStatus && Number(timerStatus.active) === 1) {
+ console.log("Hunt active from timer table");
+ isActive = true;
+ startTime = timerStatus.start_time;
+ endTime = timerStatus.end_time;
+ } else if (huntStatus && huntStatus.is_active) {
+ console.log("Hunt active from hunt_status table");
+ isActive = true;
startTime = huntStatus.start_time;
- }
- // Second priority: settings table
- else if (huntActiveSetting) {
- isActive = huntActiveSetting.value === "true";
- }
- // Third priority: old hunts table
- else if (oldHuntRecord) {
- isActive = Boolean(oldHuntRecord.active);
+ } else if (huntActiveSetting && huntActiveSetting.value === "true") {
+ console.log("Hunt active from settings table");
+ isActive = true;
+ } else if (oldHuntRecord && oldHuntRecord.active) {
+ console.log("Hunt active from hunts table");
+ isActive = true;
startTime = oldHuntRecord.start_time;
}
- console.log("Hunt status check:", { isActive, startTime });
+ console.log("Hunt status check:", { isActive, startTime, endTime });
- return Response.json({
- active: isActive,
- startTime: startTime || null,
- });
+ // Set extremely aggressive cache control headers
+ return new Response(
+ JSON.stringify({
+ active: isActive,
+ startTime: startTime || null,
+ endTime: endTime || null,
+ }),
+ {
+ status: 200,
+ headers: {
+ "Content-Type": "application/json",
+ "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
+ Pragma: "no-cache",
+ Expires: "0",
+ },
+ }
+ );
} catch (error) {
console.error("Error checking hunt status:", error);
return Response.json(
diff --git a/app/api/hunt/timer/route.js b/app/api/hunt/timer/route.js
index 6ce0820..c3605da 100644
--- a/app/api/hunt/timer/route.js
+++ b/app/api/hunt/timer/route.js
@@ -1,6 +1,5 @@
import { query, queryOne, run } from "@/lib/db";
-import { cookies } from "next/headers";
-import { verifyToken } from "@/app/api/auth/[...nextauth]/route";
+import { getToken } from "next-auth/jwt";
export const dynamic = "force-dynamic"; // Disable caching
@@ -90,17 +89,83 @@ async function ensureSettingsTableExists() {
}
}
-// Helper function to update hunt active status
+// Helper function to update hunt active status - ensure it updates all tables
async function updateHuntActiveStatus(isActive) {
try {
await ensureSettingsTableExists();
+ // Update settings table - this is used by many endpoints to check status
await run("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", [
"hunt_active",
isActive ? "true" : "false",
]);
- console.log("Hunt active status updated:", isActive);
+ // Update hunt_status table for consistency
+ try {
+ const now = new Date().toISOString();
+ await run(`
+ CREATE TABLE IF NOT EXISTS hunt_status (
+ id INTEGER PRIMARY KEY CHECK (id = 1),
+ is_active BOOLEAN DEFAULT 0,
+ start_time TEXT,
+ end_time TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ `);
+
+ // Check if we need to insert the initial record
+ const status = await queryOne("SELECT * FROM hunt_status LIMIT 1");
+ if (!status) {
+ await run(
+ "INSERT INTO hunt_status (id, is_active, start_time) VALUES (1, ?, ?)",
+ [isActive ? 1 : 0, isActive ? now : null]
+ );
+ } else {
+ // Update existing record
+ await run(
+ "UPDATE hunt_status SET is_active = ?, start_time = ? WHERE id = 1",
+ [isActive ? 1 : 0, isActive ? now : null]
+ );
+ }
+ } catch (err) {
+ console.error("Error updating hunt_status table:", err);
+ }
+
+ // Also try to update the traditional hunts table for backwards compatibility
+ try {
+ const now = new Date().toISOString();
+ await run(`
+ CREATE TABLE IF NOT EXISTS hunts (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ active BOOLEAN DEFAULT 0,
+ start_time DATETIME DEFAULT NULL,
+ end_time DATETIME DEFAULT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ `);
+
+ // Check if we need to insert a new record or update existing
+ const hunt = await queryOne(
+ "SELECT * FROM hunts ORDER BY id DESC LIMIT 1"
+ );
+ if (!hunt) {
+ await run("INSERT INTO hunts (active, start_time) VALUES (?, ?)", [
+ isActive ? 1 : 0,
+ isActive ? now : null,
+ ]);
+ } else {
+ // Update most recent hunt record
+ await run("UPDATE hunts SET active = ?, start_time = ? WHERE id = ?", [
+ isActive ? 1 : 0,
+ isActive ? now : null,
+ hunt.id,
+ ]);
+ }
+ } catch (err) {
+ console.error("Error updating hunts table:", err);
+ }
+
+ console.log("Hunt active status updated across ALL tables:", isActive);
return true;
} catch (error) {
console.error("Failed to update hunt active status:", error);
@@ -111,19 +176,18 @@ async function updateHuntActiveStatus(isActive) {
// GET endpoint to get current timer status
export async function GET(request) {
try {
- // Verify the user is authenticated
- const cookieStore = await cookies(); // Add await here
- const sessionToken = cookieStore.get("next-auth.session-token")?.value;
+ // Use proper next-auth token validation
+ const token = await getToken({
+ req: request,
+ secret:
+ process.env.NEXTAUTH_SECRET ||
+ "your-fallback-secret-should-be-at-least-32-chars",
+ });
- if (!sessionToken) {
+ if (!token) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
- const userData = verifyToken(sessionToken);
- if (!userData) {
- return Response.json({ error: "Invalid session" }, { status: 401 });
- }
-
// Ensure both tables exist
const tableExists = await ensureTableExists();
await ensureSettingsTableExists();
@@ -175,31 +239,21 @@ export async function GET(request) {
// POST endpoint to start/stop timer
export async function POST(request) {
try {
- // Verify the user is authenticated and is an admin
- const cookieStore = await cookies(); // Add await here
- const sessionToken = cookieStore.get("next-auth.session-token")?.value;
-
- if (!sessionToken) {
+ // Use next-auth's getToken to properly decode the token
+ const token = await getToken({
+ req: request,
+ secret:
+ process.env.NEXTAUTH_SECRET ||
+ "your-fallback-secret-should-be-at-least-32-chars",
+ });
+ if (!token) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
- const userData = verifyToken(sessionToken);
- if (!userData) {
- return Response.json({ error: "Invalid session" }, { status: 401 });
- }
-
- console.log("User role:", userData.role);
-
// Check if user is admin (more lenient check)
- if (
- !(
- userData.role === "admin" ||
- userData.isAdmin === true ||
- userData.is_admin === 1
- )
- ) {
+ if (token.role !== "admin") {
return Response.json(
- { error: "Unauthorized: Admin access required" },
+ { error: "Only admin can start the timer" },
{ status: 403 }
);
}
diff --git a/app/api/teams/[teamId]/hunt-session/route.js b/app/api/teams/[teamId]/hunt-session/route.js
new file mode 100644
index 0000000..3eb2c93
--- /dev/null
+++ b/app/api/teams/[teamId]/hunt-session/route.js
@@ -0,0 +1,74 @@
+import { getToken } from "next-auth/jwt";
+import { queryOne } from "@/lib/db";
+
+export async function GET(request, context) {
+ try {
+ const token = await getToken({
+ req: request,
+ secret:
+ process.env.NEXTAUTH_SECRET ||
+ "your-fallback-secret-should-be-at-least-32-chars",
+ });
+
+ if (!token) {
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { teamId } = context.params;
+
+ // Validate that user belongs to this team
+ if (!token.teamId || parseInt(token.teamId, 10) !== parseInt(teamId, 10)) {
+ return Response.json(
+ { error: "You are not a member of this team" },
+ { status: 403 }
+ );
+ }
+
+ // Get the team's hunt session from the database
+ const session = await queryOne(
+ "SELECT team_id, start_time FROM team_hunt_sessions WHERE team_id = ?",
+ [teamId]
+ );
+
+ if (!session) {
+ // Try to get the global hunt start time as fallback
+ const huntStatus = await queryOne(
+ "SELECT start_time FROM hunt_status WHERE is_active = 1 LIMIT 1"
+ );
+ if (huntStatus && huntStatus.start_time) {
+ return Response.json({
+ startTime: huntStatus.start_time,
+ source: "global_hunt_status",
+ });
+ }
+
+ // Final fallback to settings table
+ const huntActive = await queryOne(
+ "SELECT value FROM settings WHERE key = 'hunt_active'"
+ );
+ if (huntActive && huntActive.value === "true") {
+ return Response.json({
+ startTime: new Date().toISOString(),
+ source: "fallback_current_time",
+ });
+ }
+
+ return Response.json(
+ { error: "No hunt session found for this team" },
+ { status: 404 }
+ );
+ }
+
+ return Response.json({
+ teamId: session.team_id,
+ startTime: session.start_time,
+ source: "team_session",
+ });
+ } catch (error) {
+ console.error("Get team hunt session error:", error);
+ return Response.json(
+ { error: "Failed to get hunt session: " + error.message },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/teams/[teamId]/route.js b/app/api/teams/[teamId]/route.js
index d4fad35..fddfd19 100644
--- a/app/api/teams/[teamId]/route.js
+++ b/app/api/teams/[teamId]/route.js
@@ -14,11 +14,10 @@ export async function GET(request, context) {
if (!token) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
- // Use token.teamId for validation
- if (
- !token.teamId ||
- parseInt(token.teamId, 10) !== parseInt(context.params.teamId, 10)
- ) {
+ // Await context.params so its properties can be safely used
+ const { teamId } = await context.params;
+ // Validate that the token's teamId matches the requested teamId
+ if (!token.teamId || parseInt(token.teamId, 10) !== parseInt(teamId, 10)) {
return Response.json(
{ error: "Not authorized to view this team" },
{ status: 403 }
diff --git a/app/api/teams/[teamId]/start-team-hunt/route.js b/app/api/teams/[teamId]/start-team-hunt/route.js
new file mode 100644
index 0000000..3748f2e
--- /dev/null
+++ b/app/api/teams/[teamId]/start-team-hunt/route.js
@@ -0,0 +1,114 @@
+import { getToken } from "next-auth/jwt";
+import { query, queryOne, run } from "@/lib/db";
+
+export async function POST(request, context) {
+ try {
+ const token = await getToken({
+ req: request,
+ secret:
+ process.env.NEXTAUTH_SECRET ||
+ "your-fallback-secret-should-be-at-least-32-chars",
+ });
+
+ if (!token) {
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { teamId } = context.params;
+
+ // Validate that user belongs to this team
+ if (!token.teamId || parseInt(token.teamId, 10) !== parseInt(teamId, 10)) {
+ return Response.json(
+ { error: "You are not a member of this team" },
+ { status: 403 }
+ );
+ }
+
+ // Get current time in ISO format for consistent timing across team members
+ const startTime = new Date().toISOString();
+
+ // Store the hunt start time for this team in the database
+ await run(`
+ CREATE TABLE IF NOT EXISTS team_hunt_sessions (
+ team_id INTEGER PRIMARY KEY,
+ start_time TEXT NOT NULL,
+ initiated_by INTEGER,
+ FOREIGN KEY (team_id) REFERENCES teams(id),
+ FOREIGN KEY (initiated_by) REFERENCES users(id)
+ )
+ `);
+
+ // Insert or replace the team's hunt session
+ await run(
+ "INSERT OR REPLACE INTO team_hunt_sessions (team_id, start_time, initiated_by) VALUES (?, ?, ?)",
+ [teamId, startTime, token.id]
+ );
+
+ // Update hunt_status and settings tables to activate the hunt
+ await run(`
+ CREATE TABLE IF NOT EXISTS hunt_status (
+ id INTEGER PRIMARY KEY CHECK (id = 1),
+ is_active BOOLEAN DEFAULT 0,
+ start_time TEXT,
+ end_time TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ `);
+
+ // Check if record exists
+ const statusExists = await queryOne(
+ "SELECT 1 FROM hunt_status WHERE id = 1"
+ );
+ if (statusExists) {
+ await run(
+ "UPDATE hunt_status SET is_active = 1, start_time = ? WHERE id = 1",
+ [startTime]
+ );
+ } else {
+ await run(
+ "INSERT INTO hunt_status (id, is_active, start_time) VALUES (1, 1, ?)",
+ [startTime]
+ );
+ }
+
+ // Update settings table
+ await run(`
+ CREATE TABLE IF NOT EXISTS settings (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL
+ )
+ `);
+ await run("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", [
+ "hunt_active",
+ "true",
+ ]);
+
+ // Get the team members for potential notifications
+ const teamMembers = await query(
+ "SELECT id, username FROM users WHERE team_id = ?",
+ [teamId]
+ );
+
+ return Response.json(
+ {
+ success: true,
+ startTime,
+ message: "Hunt started for the entire team",
+ teamMembers: teamMembers.map((m) => m.username),
+ },
+ {
+ status: 200,
+ headers: {
+ "Cache-Control": "no-store, no-cache, must-revalidate",
+ Pragma: "no-cache",
+ },
+ }
+ );
+ } catch (error) {
+ console.error("Start team hunt error:", error);
+ return Response.json(
+ { error: "Failed to start hunt: " + error.message },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/waiting/page.js b/app/waiting/page.js
index c7b2f7f..a7ecd52 100644
--- a/app/waiting/page.js
+++ b/app/waiting/page.js
@@ -8,33 +8,113 @@ export default function WaitingPage() {
const router = useRouter();
const { status } = useSession();
const [huntActive, setHuntActive] = useState(false);
+ const [huntEnded, setHuntEnded] = useState(false);
const [loading, setLoading] = useState(true);
+ const [lastHuntData, setLastHuntData] = useState(null);
- // Check hunt status
+ // Check hunt status VERY aggressively
useEffect(() => {
- // Function to check hunt status
+ let redirectAttempted = false;
+ let lastKnownHuntStatus = false;
+
async function checkHuntStatus() {
+ if (redirectAttempted) return;
+
try {
- const response = await fetch("/api/hunt-status", {
+ // First check if we have a team ID from the session
+ const userResponse = await fetch("/api/user", {
method: "GET",
headers: {
- "Content-Type": "application/json",
- "Cache-Control": "no-store, no-store, must-revalidate",
+ "Cache-Control": "no-store, no-cache, must-revalidate",
+ Pragma: "no-cache",
},
credentials: "include",
});
- if (response.ok) {
- const data = await response.json();
- console.log("Hunt status from waiting page:", data);
+ if (userResponse.ok) {
+ const userData = await userResponse.json();
- // If hunt is active, redirect to map page
+ // If user has a team, check if that team has an active hunt session
+ if (userData.teamId) {
+ const teamHuntResponse = await fetch(
+ `/api/teams/${userData.teamId}/hunt-session`,
+ {
+ method: "GET",
+ headers: {
+ "Cache-Control": "no-store, no-cache, must-revalidate",
+ Pragma: "no-cache",
+ },
+ credentials: "include",
+ }
+ );
+
+ // If team has an active hunt session, redirect to map
+ if (teamHuntResponse.ok) {
+ const teamHuntData = await teamHuntResponse.json();
+ if (teamHuntData.startTime) {
+ // Store the start time for the map page
+ localStorage.setItem(
+ "teamHuntStartTime",
+ teamHuntData.startTime
+ );
+
+ console.log("Team hunt is active! Redirecting to map...");
+ redirectAttempted = true;
+ lastKnownHuntStatus = true;
+
+ // Show loading screen and redirect
+ document.body.innerHTML =
+ '
Loading Map...
Please wait...
';
+ window.location.href = "/map";
+ return;
+ }
+ }
+ }
+ }
+
+ // Fall back to checking the global hunt status
+ const timestamp = Date.now();
+ const globalResponse = 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",
+ },
+ });
+
+ if (globalResponse.ok) {
+ const data = await globalResponse.json();
+ console.log("Hunt status check result:", data);
+
+ // Store the most recent hunt data to determine if hunt ended
+ if (lastHuntData && lastHuntData.active && !data.active) {
+ // Hunt was active before but now it's not - it has ended
+ console.log("Hunt has ended! Showing leaderboard...");
+ setHuntEnded(true);
+ // Don't redirect immediately to allow user to read the message
+ }
+
+ // Save the current hunt data for next comparison
+ setLastHuntData(data);
+
if (data.active) {
- console.log("Hunt is active! Redirecting to map page");
- router.push("/map");
+ // Store the global start time
+ if (data.startTime) {
+ localStorage.setItem("teamHuntStartTime", data.startTime);
+ }
+
+ console.log("Hunt is active! Redirecting immediately...");
+ redirectAttempted = true;
+ lastKnownHuntStatus = true;
+
+ // Show loading screen during redirect
+ document.body.innerHTML =
+ '
Loading Map...
Please wait...
';
+ window.location.href = "/map";
+ return;
}
- // Update local state
setHuntActive(data.active);
}
@@ -45,12 +125,12 @@ export default function WaitingPage() {
}
}
- // Check on load and every 5 seconds
+ // Check immediately and very frequently
checkHuntStatus();
- const intervalId = setInterval(checkHuntStatus, 5000);
+ const intervalId = setInterval(checkHuntStatus, 500); // Check every 500ms
return () => clearInterval(intervalId);
- }, [router]);
+ }, []);
// Redirect to login if not authenticated
useEffect(() => {
@@ -70,17 +150,35 @@ export default function WaitingPage() {
return (
-
Waiting for Hunt to Start
-
- The treasure hunt has not started yet. Please wait for the administrator
- to start the hunt.
-
-
β³
- {huntActive && (
-
-
The hunt has started!
-
-
+ {huntEnded ? (
+ <>
+
Hunt Has Ended
+
+ The treasure hunt has ended. You can view the results on the
+ leaderboard.
+
+
+
Hunt Complete!
+
+
+ >
+ ) : (
+ <>
+
Waiting for Hunt to Start
+
+ The treasure hunt has not started yet. Please wait for the
+ administrator to start the hunt.
+