mirror of
https://github.com/mudabbir-ahmad/peworkshop.git
synced 2026-10-07 19:50:20 +00:00
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:
1 parent
e7ad53643c
commit
16319f1333
14 files changed
+791
-472
No files matched your search
@@ -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
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = ?",
|
||||
|
||||
Reference in new issue
Block a user