From e7ad53643cf089e9733e1626dd2f6d60f6b60e98 Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Sat, 15 Mar 2025 01:10:05 +0000 Subject: [PATCH] issues regarding the map and hunt stopping have been fixed. leaderboard page needs to be integrated and a clues should be added to be properly processed. after that the css for the application should be developed. --- app/(main)/hunt-not-started/page.js | 43 +++ app/(main)/map/map.module.css | 23 +- app/(main)/map/page.js | 258 ++++++++++++++---- app/(main)/team/[teamId]/page.js | 75 +++-- app/api/hunt-status/route.js | 88 +++--- app/api/hunt/timer/route.js | 120 +++++--- app/api/teams/[teamId]/hunt-session/route.js | 74 +++++ app/api/teams/[teamId]/route.js | 9 +- .../teams/[teamId]/start-team-hunt/route.js | 114 ++++++++ app/waiting/page.js | 150 ++++++++-- app/waiting/waiting.module.css | 88 ++++++ clue_hunt.db | Bin 69632 -> 69632 bytes 12 files changed, 855 insertions(+), 187 deletions(-) create mode 100644 app/api/teams/[teamId]/hunt-session/route.js create mode 100644 app/api/teams/[teamId]/start-team-hunt/route.js create mode 100644 app/waiting/waiting.module.css 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 */} - Treasure Map { - // Fallback to a static image if SVG fails to load - setMapError(true); - }} - /> + {/* Using background-image for the map SVG */} +
- {/* Navigation bar - now sticky */} + {/* Navigation bar - Map as first button */} ); diff --git a/app/(main)/team/[teamId]/page.js b/app/(main)/team/[teamId]/page.js index c8e6a17..44409da 100644 --- a/app/(main)/team/[teamId]/page.js +++ b/app/(main)/team/[teamId]/page.js @@ -243,39 +243,64 @@ export default function TeamPage({ params }) { try { setLoading(true); - // Use try-catch with robust error handling - const response = await fetch("/api/hunt-status", { - method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - credentials: "include", - }); + // First, immediately check hunt status + const timestamp = Date.now(); + const huntStatusResponse = 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", + }, + } + ); + + const huntData = await huntStatusResponse.json(); + + // If hunt is active, immediately redirect to map + if (huntData.active) { + console.log("Hunt already active! Redirecting to map..."); + document.body.innerHTML = + '
Loading Map...
Please wait...
'; + 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. +

+
⏳
+ {huntActive && ( +
+

The hunt has started!

+ +
+ )} + )}
); diff --git a/app/waiting/waiting.module.css b/app/waiting/waiting.module.css new file mode 100644 index 0000000..579d1de --- /dev/null +++ b/app/waiting/waiting.module.css @@ -0,0 +1,88 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: 20px; + text-align: center; + background-color: #f4f4f4; +} + +.spinner { + border: 4px solid #f3f3f3; + border-top: 4px solid #68543c; + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; + margin-bottom: 20px; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.waitingAnimation { + font-size: 48px; + margin: 20px 0; + animation: pulse 2s infinite ease-in-out; +} + +@keyframes pulse { + 0% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.1); + opacity: 0.8; + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +.startedMessage { + background-color: #4caf50; + color: white; + padding: 15px; + border-radius: 8px; + margin-top: 20px; +} + +.startedMessage button { + background-color: white; + color: #4caf50; + border: none; + padding: 8px 15px; + margin-top: 10px; + border-radius: 4px; + cursor: pointer; + font-weight: bold; +} + +.endedMessage { + background-color: #f44336; + color: white; + padding: 15px; + border-radius: 8px; + margin-top: 20px; +} + +.endedMessage button { + background-color: white; + color: #f44336; + border: none; + padding: 8px 15px; + margin-top: 10px; + border-radius: 4px; + cursor: pointer; + font-weight: bold; +} diff --git a/clue_hunt.db b/clue_hunt.db index 14aa51cc35f8e14ea7813c0b7a02d8bbe682cd61..f78e873bf6d6fa4f3bb194019d39db4e30c5e951 100644 GIT binary patch delta 2679 zcmb7`O^g&p6vwNlXL@G1^y-2{F>yC~rM6yu^hd>w=&jL%hdme%!m#YZ2(07o0De+T z2saLF=H^8rT#dpeCdP{<-iQ|uazu{?W9GzxkYH8yEa|PT2{D<>;qCjq_kSPtb}rv& zUcS-1{!$}Y*?##g{eAoRH%rg|grazi;)n6w_^1| zx3-C{9*DxFz#0a<&0&AFKj>`?x8~x1&|GP@ero6L8;y4tAc#E z$zRm=4nl1F%PaGDu19mt2Pl59^W69M;};N`KN`Mz&}QhZ4*M6@)_Z5SZa(>LE;xGU z=YT}P0KJJO#7(vwXWsmnqEl+W}Lt8 z)*&IoB26py1zVr?hf&MKs@HCrB(F1|vT%U7w_$YwPZJd*)6BYPB6*5=Mk0V0R2j)1c_j-;63n6cE{x=| z6rj}=vx<#B41DQJgyim8XuHyP;p5qiaE|6~V>Y}aFlFvstUsLbrsv8s)R!LMte>%pox7|XV-=tBDo!N@44FGh ztxR#f7l}ITm>$4c$vD961+nA!~RV>U)7FqT9nD;`3Z9cE1H~yebc+-~*Z_*j`0A;Kk@xv53*n@`&_1x#EZG5P6ysm=fU*#%hM zFz~2ba#=yWJ&G~&Y`v>#Q kh5|b|Sq<{9