diff --git a/app/(main)/admin/clues/page.js b/app/(main)/admin/clues/page.js
new file mode 100644
index 0000000..24137e4
--- /dev/null
+++ b/app/(main)/admin/clues/page.js
@@ -0,0 +1,468 @@
+"use client";
+import { useState, useEffect } from "react";
+import { useSession } from "next-auth/react";
+import { useRouter } from "next/navigation";
+
+export default function AdminCluesPage() {
+ const { data: session, status } = useSession();
+ const [clues, setClues] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const router = useRouter();
+
+ // Form state for adding new clues - removed coordinates
+ const [newClue, setNewClue] = useState({
+ title: "",
+ description: "",
+ location: "",
+ });
+
+ // Check if user is admin
+ useEffect(() => {
+ if (status === "authenticated") {
+ if (session?.user?.role !== "admin") {
+ router.push("/team-selection");
+ } else {
+ fetchClues();
+ }
+ } else if (status === "unauthenticated") {
+ router.push("/login");
+ }
+ }, [session, status, router]);
+
+ // Fetch all clues
+ const fetchClues = async () => {
+ try {
+ setLoading(true);
+ const response = await fetch("/api/admin/clues", {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "Cache-Control": "no-cache, no-store, must-revalidate",
+ },
+ credentials: "include",
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.error || `Failed to fetch clues (${response.status})`
+ );
+ }
+
+ const data = await response.json();
+
+ // Validate the response data
+ if (!Array.isArray(data)) {
+ throw new Error("Invalid response format from server");
+ }
+
+ setClues(data);
+ setError("");
+ } catch (err) {
+ console.error("Error fetching clues:", err);
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Handle input change for new clue form
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ setNewClue((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ };
+
+ // Handle form submission to add new clue
+ const handleAddClue = async (e) => {
+ e.preventDefault();
+ try {
+ setLoading(true);
+ const response = await fetch("/api/admin/clues", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(newClue),
+ credentials: "include",
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to add clue");
+ }
+
+ // Refresh the clue list
+ fetchClues();
+
+ // Reset the form - removed coordinates
+ setNewClue({
+ title: "",
+ description: "",
+ location: "",
+ });
+
+ setError("");
+ } catch (err) {
+ console.error("Error adding clue:", err);
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Handle deleting a clue
+ const handleDeleteClue = async (clueId) => {
+ if (!confirm("Are you sure you want to delete this clue?")) return;
+
+ try {
+ setLoading(true);
+ const response = await fetch(`/api/admin/clues/${clueId}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ credentials: "include",
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to delete clue");
+ }
+
+ // Refresh the clue list
+ fetchClues();
+ setError("");
+ } catch (err) {
+ console.error("Error deleting clue:", err);
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (status === "loading" || loading) {
+ return
Loading...
;
+ }
+
+ if (status === "unauthenticated") {
+ return null; // Will redirect via useEffect
+ }
+
+ return (
+
+
+
+
+ {error &&
{error}
}
+
+ {/* Add New Clue Form - removed coordinate fields */}
+
+
+ {/* Existing Clues List - updated to remove coordinates */}
+
+
Existing Clues
+ {clues.length === 0 ? (
+
No clues found.
+ ) : (
+
+ {clues.map((clue) => (
+
+
+
{clue.title}
+
+
+
{clue.description}
+ {clue.location && (
+
+ Location: {clue.location}
+
+ )}
+
+ Position: Clue #{clue.id} in sequence
+
+
+ ))}
+
+ )}
+
+
+
+ {/* Admin Navigation Bar */}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/(main)/admin/leaderboard/page.js b/app/(main)/admin/leaderboard/page.js
new file mode 100644
index 0000000..e1042b6
--- /dev/null
+++ b/app/(main)/admin/leaderboard/page.js
@@ -0,0 +1,360 @@
+"use client";
+import { useState, useEffect } from "react";
+import { useSession } from "next-auth/react";
+import { useRouter } from "next/navigation";
+
+export default function AdminLeaderboardPage() {
+ const { data: session, status } = useSession();
+ const [teams, setTeams] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const router = useRouter();
+
+ // Check if user is admin
+ useEffect(() => {
+ if (status === "authenticated") {
+ if (session?.user?.role !== "admin") {
+ router.push("/team-selection");
+ } else {
+ fetchLeaderboardData();
+ }
+ } else if (status === "unauthenticated") {
+ router.push("/login");
+ }
+ }, [session, status, router]);
+
+ // Fetch leaderboard data
+ const fetchLeaderboardData = async () => {
+ try {
+ setLoading(true);
+ setError("");
+
+ // This is a placeholder - you'll need to implement the actual API
+ // For now, we'll just set dummy data
+ const dummyTeams = [
+ {
+ id: 1,
+ name: "Team Alpha",
+ code: "ALPHA1",
+ memberCount: 4,
+ cluesFound: 5,
+ totalClues: 10,
+ lastActivity: new Date().toISOString(),
+ },
+ {
+ id: 2,
+ name: "Team Beta",
+ code: "BETA12",
+ memberCount: 3,
+ cluesFound: 8,
+ totalClues: 10,
+ lastActivity: new Date().toISOString(),
+ },
+ {
+ id: 3,
+ name: "Team Gamma",
+ code: "GAMMA3",
+ memberCount: 5,
+ cluesFound: 2,
+ totalClues: 10,
+ lastActivity: new Date().toISOString(),
+ },
+ ];
+
+ setTeams(dummyTeams);
+
+ // Uncomment this when API is ready
+ /*
+ const response = await fetch("/api/admin/leaderboard", {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "Cache-Control": "no-cache, no-store, must-revalidate"
+ },
+ credentials: "include"
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch leaderboard data");
+ }
+
+ const data = await response.json();
+ setTeams(data);
+ */
+ } catch (err) {
+ console.error("Error fetching leaderboard data:", err);
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (status === "loading" || loading) {
+ return Loading...
;
+ }
+
+ if (status === "unauthenticated") {
+ return null; // Will redirect via useEffect
+ }
+
+ return (
+
+
+
+
+ {error &&
{error}
}
+
+
+
Team Progress
+
+
+
+ {teams.length === 0 ? (
+
No team data available.
+ ) : (
+
+
+
+
+ | Rank |
+ Team Name |
+ Members |
+ Progress |
+ Last Activity |
+ Actions |
+
+
+
+ {teams.map((team, index) => (
+
+ | {index + 1} |
+
+ {team.name} ({team.code})
+ |
+ {team.memberCount} |
+
+ {team.cluesFound} / {team.totalClues}
+ |
+
+ {team.lastActivity
+ ? new Date(team.lastActivity).toLocaleString()
+ : "Never"}
+ |
+
+
+ |
+
+ ))}
+
+
+
+ )}
+
+
+
+ {/* Admin Navigation Bar */}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/(main)/admin/page.js b/app/(main)/admin/page.js
index 9056d33..59e66fc 100644
--- a/app/(main)/admin/page.js
+++ b/app/(main)/admin/page.js
@@ -336,6 +336,31 @@ export default function AdminDashboard() {
+ {/* Admin Navigation Bar */}
+
+
+
+
+
+
);
diff --git a/app/(main)/map/page.js b/app/(main)/map/page.js
index c48a356..bc02ae3 100644
--- a/app/(main)/map/page.js
+++ b/app/(main)/map/page.js
@@ -200,17 +200,20 @@ export default function MapPage() {
// Convert NodeList to Array for easier manipulation
Array.from(circleSvgElements).forEach((circle, index) => {
- // Only process if we have a clue for this index
- if (clues[index]) {
+ // Only process if we have a clue for this index and it's within bounds
+ if (index < clues.length) {
+ // Map SVG circle at position index to the clue at index
+ // This uses sequential order rather than coordinates
+ const clue = clues[index];
+
// Check if this clue is available (either found or next in sequence)
- const isAvailable =
- clues[index].found || index <= nextAvailableClueIndex;
+ const isAvailable = clue.found || index <= nextAvailableClueIndex;
// Make circle interactive
circle.style.cursor = isAvailable ? "pointer" : "not-allowed";
// Set appropriate colors
- if (clues[index].found) {
+ if (clue.found) {
// Found clues are green
circle.style.fill = "#4caf50";
circle.style.stroke = "#2e7d32";
@@ -232,8 +235,8 @@ export default function MapPage() {
e.stopPropagation();
if (isAvailable) {
- console.log(`Navigating to clue ${clues[index].id}`);
- router.push(`/clue/${clues[index].id}`);
+ console.log(`Navigating to clue ${clue.id}`);
+ router.push(`/clue/${clue.id}`);
} else {
alert("Complete previous clues first to unlock this one!");
}
@@ -247,7 +250,7 @@ export default function MapPage() {
};
circle.onmouseleave = () => {
- if (clues[index].found) {
+ if (clue.found) {
circle.style.fill = "#4caf50"; // Back to green for found clues
} else if (isAvailable) {
circle.style.fill = "#ff9800"; // Back to orange for available clues
@@ -256,13 +259,13 @@ export default function MapPage() {
}
};
- // Add tooltip/title
+ // Add tooltip/title with actual clue ID and position information
const label = document.createElementNS(
"http://www.w3.org/2000/svg",
"title"
);
label.textContent = `Clue #${index + 1}: ${
- clues[index].title || "Mystery clue"
+ clue.title || "Mystery clue"
}${!isAvailable ? " (Locked)" : ""}`;
circle.appendChild(label);
}
@@ -409,26 +412,26 @@ export default function MapPage() {
try {
// First try to get timer data from the centralized service
let timerData = getHuntTimer();
-
+
// If no timer data yet, fetch it from API
if (!timerData || !timerData.startTime) {
console.log("No timer data found, fetching from API...");
const startTimeStr = await fetchTeamHuntStartTime();
console.log("Got start time:", startTimeStr);
-
+
if (startTimeStr) {
setHuntTimer(startTimeStr);
timerData = getHuntTimer();
}
}
-
+
// Set up interval for timer updates
const timerInterval = setInterval(() => {
if (!isMounted.current) {
clearInterval(timerInterval);
return;
}
-
+
const currentTimerData = getHuntTimer();
if (currentTimerData && currentTimerData.startTime) {
const formattedTime = formatElapsedTime(currentTimerData.startTime);
@@ -438,12 +441,12 @@ export default function MapPage() {
setElapsedTime("00:00:00");
}
}, 1000);
-
+
// Initial timer update
if (timerData && timerData.startTime) {
setElapsedTime(formatElapsedTime(timerData.startTime));
}
-
+
return () => {
clearInterval(timerInterval);
};
@@ -451,7 +454,7 @@ export default function MapPage() {
console.error("Error initializing timer:", error);
}
};
-
+
initializeTimer();
}, []); // Empty dependency array since fetchTeamHuntStartTime is defined inside the component
@@ -459,7 +462,7 @@ export default function MapPage() {
useEffect(() => {
const checkHuntStatus = async () => {
if (!isMounted.current) return;
-
+
try {
const timestamp = Date.now(); // Add cache-busting timestamp
const response = await fetch(`/api/hunt-status?t=${timestamp}`, {
@@ -471,15 +474,15 @@ export default function MapPage() {
},
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");
@@ -488,7 +491,7 @@ export default function MapPage() {
}
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");
@@ -497,7 +500,7 @@ export default function MapPage() {
}
return;
}
-
+
// Otherwise, hunt is active and timer is running
if (isMounted.current) {
setHuntActive(true);
@@ -506,11 +509,11 @@ export default function MapPage() {
console.error("Failed to check hunt status:", err);
}
};
-
+
// Check immediately and every second
checkHuntStatus();
const intervalId = setInterval(checkHuntStatus, 1000);
-
+
return () => clearInterval(intervalId);
}, [softRedirect]);
diff --git a/app/api/admin/clues/[clueId]/route.js b/app/api/admin/clues/[clueId]/route.js
new file mode 100644
index 0000000..8759173
--- /dev/null
+++ b/app/api/admin/clues/[clueId]/route.js
@@ -0,0 +1,167 @@
+import { getToken } from "next-auth/jwt";
+import { queryOne, run } from "@/lib/db";
+
+export const dynamic = "force-dynamic"; // Disable caching
+
+// Ensure the user is an admin
+async function verifyAdmin(request) {
+ const token = await getToken({
+ req: request,
+ secret:
+ process.env.NEXTAUTH_SECRET ||
+ "your-fallback-secret-should-be-at-least-32-chars",
+ });
+
+ if (!token) {
+ return { authorized: false, error: "Unauthorized", status: 401 };
+ }
+
+ if (token.role !== "admin") {
+ return { authorized: false, error: "Admin access required", status: 403 };
+ }
+
+ return { authorized: true, token };
+}
+
+// DELETE - Remove a specific clue
+export async function DELETE(request, context) {
+ try {
+ // Verify admin access
+ const { authorized, error, status } = await verifyAdmin(request);
+ if (!authorized) {
+ return Response.json({ error }, { status });
+ }
+
+ const { clueId } = context.params;
+
+ // Validate clueId is a number
+ if (isNaN(parseInt(clueId))) {
+ return Response.json({ error: "Invalid clue ID" }, { status: 400 });
+ }
+
+ // Check if clue exists
+ const clue = await queryOne("SELECT id FROM clues WHERE id = ?", [clueId]);
+ if (!clue) {
+ return Response.json({ error: "Clue not found" }, { status: 404 });
+ }
+
+ // Delete clue record
+ await run("DELETE FROM clues WHERE id = ?", [clueId]);
+
+ // Also delete any references in team_clues table
+ await run(`
+ 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)
+ )
+ `);
+ await run("DELETE FROM team_clues WHERE clue_id = ?", [clueId]);
+
+ return Response.json({
+ success: true,
+ message: "Clue deleted successfully",
+ });
+ } catch (error) {
+ console.error("Admin clue deletion error:", error);
+ return Response.json(
+ { error: "Failed to delete clue: " + error.message },
+ { status: 500 }
+ );
+ }
+}
+
+// GET - Get a specific clue
+export async function GET(request, context) {
+ try {
+ // Verify admin access
+ const { authorized, error, status } = await verifyAdmin(request);
+ if (!authorized) {
+ return Response.json({ error }, { status });
+ }
+
+ 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 - removed coordinates
+ const clue = await queryOne(
+ "SELECT id, title, description, location FROM clues WHERE id = ?",
+ [clueId]
+ );
+
+ if (!clue) {
+ return Response.json({ error: "Clue not found" }, { status: 404 });
+ }
+
+ return Response.json(clue);
+ } catch (error) {
+ console.error("Admin clue fetch error:", error);
+ return Response.json(
+ { error: "Failed to fetch clue: " + error.message },
+ { status: 500 }
+ );
+ }
+}
+
+// PUT - Update a specific clue
+export async function PUT(request, context) {
+ try {
+ // Verify admin access
+ const { authorized, error, status } = await verifyAdmin(request);
+ if (!authorized) {
+ return Response.json({ error }, { status });
+ }
+
+ const { clueId } = context.params;
+
+ // Validate clueId is a number
+ if (isNaN(parseInt(clueId))) {
+ return Response.json({ error: "Invalid clue ID" }, { status: 400 });
+ }
+
+ // Parse the request body - removed coordinates
+ const { title, description, location } = await request.json();
+
+ // Validate required fields
+ if (!title || !description) {
+ return Response.json(
+ { error: "Title and description are required" },
+ { status: 400 }
+ );
+ }
+
+ // Check if clue exists
+ const clue = await queryOne("SELECT id FROM clues WHERE id = ?", [clueId]);
+ if (!clue) {
+ return Response.json({ error: "Clue not found" }, { status: 404 });
+ }
+
+ // Update the clue - removed coordinates
+ await run(
+ `
+ UPDATE clues
+ SET title = ?, description = ?, location = ?
+ WHERE id = ?
+ `,
+ [title, description, location || "", clueId]
+ );
+
+ return Response.json({
+ success: true,
+ message: "Clue updated successfully",
+ });
+ } catch (error) {
+ console.error("Admin clue update error:", error);
+ return Response.json(
+ { error: "Failed to update clue: " + error.message },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/admin/clues/route.js b/app/api/admin/clues/route.js
new file mode 100644
index 0000000..a867380
--- /dev/null
+++ b/app/api/admin/clues/route.js
@@ -0,0 +1,118 @@
+import { getToken } from "next-auth/jwt";
+import { query, queryOne, run } from "@/lib/db";
+
+export const dynamic = "force-dynamic"; // Disable caching
+
+// Ensure the user is an admin
+async function verifyAdmin(request) {
+ const token = await getToken({
+ req: request,
+ secret:
+ process.env.NEXTAUTH_SECRET ||
+ "your-fallback-secret-should-be-at-least-32-chars",
+ });
+
+ if (!token) {
+ return { authorized: false, error: "Unauthorized", status: 401 };
+ }
+
+ if (token.role !== "admin") {
+ return { authorized: false, error: "Admin access required", status: 403 };
+ }
+
+ return { authorized: true, token };
+}
+
+// GET - Fetch all clues for admin
+export async function GET(request) {
+ try {
+ // Verify admin access
+ const { authorized, error, status, token } = await verifyAdmin(request);
+ if (!authorized) {
+ return Response.json({ error }, { status });
+ }
+
+ // Ensure clues table exists with simplified structure (no coordinates)
+ 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
+ )
+ `);
+
+ // Fetch all clues with sequential ordering
+ const clues = await query(`
+ SELECT id, title, description, location, created_at
+ FROM clues
+ ORDER BY id ASC
+ `);
+
+ console.log(`Admin API: Returning ${clues.length} clues`);
+
+ return Response.json(clues, {
+ headers: {
+ "Cache-Control": "no-store, no-cache, must-revalidate",
+ Pragma: "no-cache",
+ Expires: "0",
+ },
+ });
+ } catch (error) {
+ console.error("Admin clue fetch error:", error);
+ return Response.json(
+ { error: "Failed to fetch clues: " + error.message },
+ { status: 500 }
+ );
+ }
+}
+
+// POST - Add a new clue
+export async function POST(request) {
+ try {
+ // Verify admin access
+ const { authorized, error, status } = await verifyAdmin(request);
+ if (!authorized) {
+ return Response.json({ error }, { status });
+ }
+
+ // Parse the request body - simplified without coordinates
+ const { title, description, location = "" } = await request.json();
+
+ // Validate required fields
+ if (!title || !description) {
+ return Response.json(
+ { error: "Title and description are required" },
+ { status: 400 }
+ );
+ }
+
+ // Insert the new clue - simplified without coordinates
+ await run(
+ `
+ INSERT INTO clues (title, description, location)
+ VALUES (?, ?, ?)
+ `,
+ [title, description, location]
+ );
+
+ // Get the inserted clue ID
+ const newClue = await queryOne("SELECT last_insert_rowid() as id");
+
+ return Response.json(
+ {
+ success: true,
+ message: "Clue added successfully",
+ clueId: newClue.id,
+ },
+ { status: 201 }
+ );
+ } catch (error) {
+ console.error("Admin clue creation error:", error);
+ return Response.json(
+ { error: "Failed to add clue: " + error.message },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/clues/[clueId]/route.js b/app/api/clues/[clueId]/route.js
index cf464a7..e8ffef4 100644
--- a/app/api/clues/[clueId]/route.js
+++ b/app/api/clues/[clueId]/route.js
@@ -31,9 +31,9 @@ export async function GET(request, context) {
return Response.json({ error: "Invalid clue ID" }, { status: 400 });
}
- // Get the specific clue
+ // Get the specific clue - removed coordinates
const clue = await queryOne(
- "SELECT id, title, description, location, x_coord, y_coord FROM clues WHERE id = ?",
+ "SELECT id, title, description, location FROM clues WHERE id = ?",
[clueId]
);
diff --git a/app/api/clues/route.js b/app/api/clues/route.js
index bfbe844..034058c 100644
--- a/app/api/clues/route.js
+++ b/app/api/clues/route.js
@@ -24,9 +24,9 @@ export async function GET(request) {
);
}
- // Get all clues with explicit ordering to ensure consistency
+ // Get all clues with explicit ordering - removed coordinates
const clues = await query(`
- SELECT id, title, description, location, x_coord, y_coord
+ SELECT id, title, description, location
FROM clues
ORDER BY id ASC
`);
diff --git a/clue_hunt.db b/clue_hunt.db
index 003581c..20c6409 100644
Binary files a/clue_hunt.db and b/clue_hunt.db differ