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 }
);
}
}