fixed the map, made it so the map is scrollable and synced the timing between the admin page and map page. They now share the same elapsed time. need to fix map css for altering the svg.

Grey = cant click
Red = current
Green = complete
blue = skipped?

Need to add Clue page Need to fix Leaderboard.
This commit is contained in:
bobbert committed 2025-03-15 16:30:04 +00:00
1 parent e7ad53643c
commit 16319f1333
14 files changed
+791 -472

No files matched your search

+79 -52
View File
@@ -1,70 +1,97 @@
import { queryOne, run } from "@/lib/db";
import { cookies } from "next/headers";
import { verifyToken } from "@/app/api/auth/[...nextauth]/route";
import { queryOne } from "@/lib/db";
import { getToken } from "next-auth/jwt";
export const dynamic = "force-dynamic"; // Disable caching
// GET endpoint to retrieve a specific clue
export async function GET(request, { params }) {
const { clueId } = params;
export async function GET(request, context) {
try {
// Check if the hunt is active first
const huntActiveSetting = await queryOne(
"SELECT value FROM settings WHERE key = 'hunt_active'"
);
const huntActive = huntActiveSetting && huntActiveSetting.value === "true";
const token = await getToken({
req: request,
secret:
process.env.NEXTAUTH_SECRET ||
"your-fallback-secret-should-be-at-least-32-chars",
});
// Only verify authentication when hunt is not active
if (!huntActive) {
const cookieStore = await cookies();
const sessionToken =
cookieStore.get("next-auth.session-token")?.value ||
cookieStore.get("__Secure-next-auth.session-token")?.value;
if (!sessionToken) {
return Response.json(
{ error: "Unauthorized", clue: null },
{ status: 401, headers: { "Content-Type": "application/json" } }
);
}
const userData = verifyToken(sessionToken);
if (!userData) {
return Response.json(
{ error: "Invalid session", clue: null },
{ status: 401, headers: { "Content-Type": "application/json" } }
);
}
// Admin can see clues even if hunt is not active
if (userData.role !== "admin") {
return Response.json(
{ error: "Hunt is not active", clue: null },
{ headers: { "Content-Type": "application/json" } }
);
}
if (!token) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
// Fetch the specific clue
const clue = await queryOne("SELECT * FROM clues WHERE id = ?", [clueId]);
if (!clue) {
// Check if user has a team
if (!token.teamId) {
return Response.json(
{ error: "Clue not found", clue: null },
{ status: 404, headers: { "Content-Type": "application/json" } }
{ error: "You must be part of a team to view clues" },
{ status: 403 }
);
}
const { clueId } = context.params;
// Validate clueId is a number
if (isNaN(parseInt(clueId))) {
return Response.json({ error: "Invalid clue ID" }, { status: 400 });
}
// Get the specific clue
const clue = await queryOne(
"SELECT id, title, description, location, x_coord, y_coord FROM clues WHERE id = ?",
[clueId]
);
if (!clue) {
return Response.json({ error: "Clue not found" }, { status: 404 });
}
// Check if this team has found this clue before
const found = await queryOne(
"SELECT * FROM team_clues WHERE team_id = ? AND clue_id = ?",
[token.teamId, clueId]
);
// If not found yet, mark it as found now
if (!found) {
try {
// First ensure the team_clues table exists
await queryOne(`
CREATE TABLE IF NOT EXISTS team_clues (
team_id INTEGER NOT NULL,
clue_id INTEGER NOT NULL,
found_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (team_id, clue_id),
FOREIGN KEY (team_id) REFERENCES teams(id),
FOREIGN KEY (clue_id) REFERENCES clues(id)
)
`);
// Record that the team found this clue
await queryOne(
"INSERT INTO team_clues (team_id, clue_id) VALUES (?, ?)",
[token.teamId, clueId]
);
} catch (err) {
console.error("Error marking clue as found:", err);
// Continue even if there's an error recording the find
}
}
return Response.json(
{ clue },
{ headers: { "Content-Type": "application/json" } }
{
...clue,
found: !!found,
foundNow: !found,
},
{
headers: {
"Cache-Control": "no-store, no-cache, must-revalidate",
Pragma: "no-cache",
Expires: "0",
},
}
);
} catch (error) {
console.error("Error fetching clue details:", error);
console.error("Error fetching clue:", error);
return Response.json(
{ error: "Failed to fetch clue details: " + error.message, clue: null },
{ status: 500, headers: { "Content-Type": "application/json" } }
{ error: "Failed to fetch clue: " + error.message },
{ status: 500 }
);
}
}
+43 -107
View File
@@ -1,127 +1,63 @@
import { query, queryOne, run } from "@/lib/db";
import { cookies } from "next/headers";
import { verifyToken } from "@/app/api/auth/[...nextauth]/route";
import { query, queryOne } from "@/lib/db";
import { getToken } from "next-auth/jwt";
export const dynamic = "force-dynamic"; // Disable caching
// Helper function to ensure the clues table exists
async function ensureCluesTableExists() {
try {
await run(`
CREATE TABLE IF NOT EXISTS clues (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL,
location TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
return true;
} catch (error) {
console.error("Error ensuring clues table exists:", error);
return false;
}
}
// Helper function to extract numeric value from clue title
function extractClueNumber(title) {
// Look for patterns like "Clue 1", "Clue 10", etc.
const match = title.match(/Clue\s+(\d+)/i);
// If we find a match, return the number, otherwise return Infinity (to sort to the end)
return match ? parseInt(match[1], 10) : Infinity;
}
// GET endpoint to retrieve all clues
export async function GET(request) {
try {
// First check if the hunt is active
const huntActiveSetting = await queryOne(
"SELECT value FROM settings WHERE key = 'hunt_active'"
);
const huntActive = huntActiveSetting && huntActiveSetting.value === "true";
const token = await getToken({
req: request,
secret:
process.env.NEXTAUTH_SECRET ||
"your-fallback-secret-should-be-at-least-32-chars",
});
// If hunt is active, allow access without authentication
if (!huntActive) {
// Only verify authentication when hunt is not active
const cookieStore = await cookies();
const sessionToken =
cookieStore.get("next-auth.session-token")?.value ||
cookieStore.get("__Secure-next-auth.session-token")?.value;
if (!sessionToken) {
return Response.json(
{ error: "Unauthorized", clues: [] },
{ status: 401, headers: { "Content-Type": "application/json" } }
);
}
const userData = verifyToken(sessionToken);
if (!userData) {
return Response.json(
{ error: "Invalid session", clues: [] },
{ status: 401, headers: { "Content-Type": "application/json" } }
);
}
// Admin can see clues even if hunt is not active
if (userData.role !== "admin") {
return Response.json(
{ error: "Hunt is not active", clues: [] },
{ headers: { "Content-Type": "application/json" } }
);
}
if (!token) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
// Ensure the clues table exists
await ensureCluesTableExists();
// Fetch all clues without sorting in SQL
const clues = await query("SELECT * FROM clues");
// If no clues are found, insert sample clues (for testing purposes)
if (!clues || clues.length === 0) {
// Insert sample clues
await run(
"INSERT INTO clues (title, description, location) VALUES (?, ?, ?)",
["Clue 1", "This is the first clue description", "Location 1"]
);
await run(
"INSERT INTO clues (title, description, location) VALUES (?, ?, ?)",
["Clue 2", "This is the second clue description", "Location 2"]
);
await run(
"INSERT INTO clues (title, description, location) VALUES (?, ?, ?)",
["Clue 3", "This is the final clue description", "Location 3"]
);
// Fetch the inserted clues
const sampleClues = await query("SELECT * FROM clues");
// Sort clues based on numbering in title
sampleClues.sort(
(a, b) => extractClueNumber(a.title) - extractClueNumber(b.title)
);
// Check if user has a team
if (!token.teamId) {
return Response.json(
{ clues: sampleClues },
{ headers: { "Content-Type": "application/json" } }
{ error: "You must be part of a team to view clues" },
{ status: 403 }
);
}
// Sort clues based on numbering in title
clues.sort(
(a, b) => extractClueNumber(a.title) - extractClueNumber(b.title)
// Get all clues
const clues = await query(`
SELECT id, title, description, location, x_coord, y_coord
FROM clues
ORDER BY id ASC
`);
// For each clue, check if the team has found it
const cluesWithStatus = await Promise.all(
clues.map(async (clue) => {
const found = await queryOne(
"SELECT 1 FROM team_clues WHERE team_id = ? AND clue_id = ?",
[token.teamId, clue.id]
);
return {
...clue,
found: !!found,
};
})
);
return Response.json(
{ clues },
{ headers: { "Content-Type": "application/json" } }
);
return Response.json(cluesWithStatus, {
headers: {
"Cache-Control": "no-store, no-cache, must-revalidate",
Pragma: "no-cache",
Expires: "0",
},
});
} catch (error) {
console.error("Error fetching clues:", error);
return Response.json(
{ error: "Failed to fetch clues: " + error.message, clues: [] },
{ status: 500, headers: { "Content-Type": "application/json" } }
{ error: "Failed to fetch clues: " + error.message },
{ status: 500 }
);
}
}
+15 -2
View File
@@ -62,6 +62,7 @@ export async function GET(request) {
let isActive = false;
let startTime = null;
let endTime = null;
let source = "none";
// Check all possible active indicators in priority order
if (timerStatus && Number(timerStatus.active) === 1) {
@@ -69,27 +70,39 @@ export async function GET(request) {
isActive = true;
startTime = timerStatus.start_time;
endTime = timerStatus.end_time;
source = "timer";
} else if (huntStatus && huntStatus.is_active) {
console.log("Hunt active from hunt_status table");
isActive = true;
startTime = huntStatus.start_time;
endTime = huntStatus.end_time;
source = "hunt_status";
} else if (huntActiveSetting && huntActiveSetting.value === "true") {
console.log("Hunt active from settings table");
isActive = true;
// If we have no start time but hunt is active, use current time
if (!startTime) {
startTime = new Date().toISOString();
}
source = "settings";
} else if (oldHuntRecord && oldHuntRecord.active) {
console.log("Hunt active from hunts table");
isActive = true;
startTime = oldHuntRecord.start_time;
endTime = oldHuntRecord.end_time;
source = "hunts";
}
console.log("Hunt status check:", { isActive, startTime, endTime });
console.log("Hunt status check:", { isActive, startTime, endTime, source });
// Set extremely aggressive cache control headers
// Always include all timing data for consistency
return new Response(
JSON.stringify({
active: isActive,
startTime: startTime || null,
endTime: endTime || null,
source, // For debugging
timestamp: new Date().toISOString(), // For cache-busting
}),
{
status: 200,
+18 -9
View File
@@ -90,10 +90,12 @@ async function ensureSettingsTableExists() {
}
// Helper function to update hunt active status - ensure it updates all tables
async function updateHuntActiveStatus(isActive) {
async function updateHuntActiveStatus(isActive, startTime = null) {
try {
await ensureSettingsTableExists();
const now = startTime || new Date().toISOString();
// Update settings table - this is used by many endpoints to check status
await run("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", [
"hunt_active",
@@ -102,7 +104,6 @@ async function updateHuntActiveStatus(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),
@@ -121,11 +122,15 @@ async function updateHuntActiveStatus(isActive) {
[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]
);
// Update existing record - but preserve start_time when stopping
if (isActive) {
await run(
"UPDATE hunt_status SET is_active = ?, start_time = ? WHERE id = 1",
[1, now]
);
} else {
await run("UPDATE hunt_status SET is_active = ? WHERE id = 1", [0]);
}
}
} catch (err) {
console.error("Error updating hunt_status table:", err);
@@ -289,7 +294,7 @@ export async function POST(request) {
);
// Update hunt active status to true
await updateHuntActiveStatus(true);
await updateHuntActiveStatus(true, startTime);
return Response.json({
success: true,
@@ -306,10 +311,13 @@ export async function POST(request) {
}
} else if (action === "stop") {
try {
// Get current time for the end timestamp
const endTime = new Date().toISOString();
// Update the most recent timer to inactive
await run(
"UPDATE hunt_timer SET active = 0, end_time = ? WHERE active = 1",
[new Date().toISOString()]
[endTime]
);
// Update hunt active status to false
@@ -318,6 +326,7 @@ export async function POST(request) {
return Response.json({
success: true,
message: "Timer stopped successfully",
endTime: endTime,
});
} catch (dbError) {
console.error("Database error stopping timer:", dbError);
+14 -2
View File
@@ -1,5 +1,5 @@
import { getToken } from "next-auth/jwt";
import { queryOne } from "@/lib/db";
import { queryOne, run } from "@/lib/db";
export async function GET(request, context) {
try {
@@ -14,7 +14,8 @@ export async function GET(request, context) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const { teamId } = context.params;
// Properly await context.params
const { teamId } = await context.params;
// Validate that user belongs to this team
if (!token.teamId || parseInt(token.teamId, 10) !== parseInt(teamId, 10)) {
@@ -24,6 +25,17 @@ export async function GET(request, context) {
);
}
// First ensure the team_hunt_sessions table exists
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)
)
`);
// 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 = ?",