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.

This commit is contained in:
bobbert committed 2025-03-15 01:10:05 +00:00
1 parent c79a9636a7
commit e7ad53643c
12 files changed
+824 -156

No files matched your search

+43
View File
@@ -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) {
+21 -2
View File
@@ -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 */
+188 -40
View File
@@ -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,11 +92,68 @@ 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();
@@ -69,7 +173,8 @@ export default function MapPage() {
}, 1000);
return () => clearInterval(timer);
}, [teamData]);
});
}, [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 (
<div className={styles["loading-container"]}>
@@ -174,42 +329,20 @@ export default function MapPage() {
<div className={styles["team-code"]}>{teamData?.team?.code || ""}</div>
</div>
{/* Main map content area */}
{/* Main map content area - showing map as background */}
<div className={styles["map-container"]} ref={mapContainerRef}>
<div className={styles["treasure-map"]}>
{/* Use Image component with fill layout for better SVG handling */}
<Image
src="/images/treasure-map.svg"
alt="Treasure Map"
className={styles["map-svg"]}
width={1000}
height={1000}
priority
onError={() => {
// Fallback to a static image if SVG fails to load
setMapError(true);
}}
/>
{/* Using background-image for the map SVG */}
<div
className={styles["map-background"]}
role="img"
aria-label="Treasure Map"
></div>
</div>
</div>
{/* Navigation bar - now sticky */}
{/* Navigation bar - Map as first button */}
<nav className={styles["nav-bar"]}>
<Link
href={teamData?.teamId ? `/${teamData.teamId}` : "/"}
className={styles["nav-item"]}
>
<div className={styles["nav-icon"]}>🏠</div>
<div className={styles["nav-label"]}>Home</div>
</Link>
<Link href="/leaderboard" className={styles["nav-item"]}>
<div className={styles["nav-icon"]}>📊</div>
<div className={styles["nav-label"]}>Stats</div>
</Link>
<Link href="/team-selection" className={styles["nav-item"]}>
<div className={styles["nav-icon"]}>👥</div>
<div className={styles["nav-label"]}>Team</div>
</Link>
<Link
href="/map"
className={`${styles["nav-item"]} ${styles["active"]}`}
@@ -217,6 +350,21 @@ export default function MapPage() {
<div className={styles["nav-icon"]}>🗺️</div>
<div className={styles["nav-label"]}>Map</div>
</Link>
<Link href="/leaderboard" className={styles["nav-item"]}>
<div className={styles["nav-icon"]}>📊</div>
<div className={styles["nav-label"]}>Stats</div>
</Link>
<Link
href={teamData?.team ? `/${teamData.team.id}` : "/team-selection"}
className={styles["nav-item"]}
>
<div className={styles["nav-icon"]}>👥</div>
<div className={styles["nav-label"]}>Team</div>
</Link>
<Link href="/team-selection" className={styles["nav-item"]}>
<div className={styles["nav-icon"]}>🔙</div>
<div className={styles["nav-label"]}>Back</div>
</Link>
</nav>
</div>
);
+45 -20
View File
@@ -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", {
// 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",
Accept: "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 =
'<div style="display:flex;justify-content:center;align-items:center;height:100vh;font-family:sans-serif;"><div style="text-align:center"><div style="font-size:24px">Loading Map...</div><div style="margin-top:20px">Please wait...</div></div></div>';
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 =
'<div style="display:flex;justify-content:center;align-items:center;height:100vh;font-family:sans-serif;"><div style="text-align:center"><div style="font-size:24px">Loading Map...</div><div style="margin-top:20px">Please wait...</div></div></div>';
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);
}
}}
+45 -39
View File
@@ -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({
// 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(
+87 -33
View File
@@ -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 }
);
}
@@ -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 }
);
}
}
+4 -5
View File
@@ -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 }
@@ -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 }
);
}
}
+115 -17
View File
@@ -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 =
'<div style="display:flex;justify-content:center;align-items:center;height:100vh;font-family:sans-serif;"><div style="text-align:center"><div style="font-size:24px">Loading Map...</div><div style="margin-top:20px">Please wait...</div></div></div>';
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 =
'<div style="display:flex;justify-content:center;align-items:center;height:100vh;font-family:sans-serif;"><div style="text-align:center"><div style="font-size:24px">Loading Map...</div><div style="margin-top:20px">Please wait...</div></div></div>';
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,10 +150,26 @@ export default function WaitingPage() {
return (
<div className={styles.container}>
{huntEnded ? (
<>
<h1>Hunt Has Ended</h1>
<p>
The treasure hunt has ended. You can view the results on the
leaderboard.
</p>
<div className={styles.endedMessage}>
<p>Hunt Complete!</p>
<button onClick={() => router.push("/leaderboard?huntEnded=true")}>
View Results
</button>
</div>
</>
) : (
<>
<h1>Waiting for Hunt to Start</h1>
<p>
The treasure hunt has not started yet. Please wait for the administrator
to start the hunt.
The treasure hunt has not started yet. Please wait for the
administrator to start the hunt.
</p>
<div className={styles.waitingAnimation}>⏳</div>
{huntActive && (
@@ -82,6 +178,8 @@ export default function WaitingPage() {
<button onClick={() => router.push("/map")}>Go to Map</button>
</div>
)}
</>
)}
</div>
);
}
+88
View File
@@ -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;
}
BIN
View File
Binary file not shown.