diff --git a/app/(main)/clue/[clueId]/page.js b/app/(main)/clue/[clueId]/page.js
new file mode 100644
index 0000000..698eb21
--- /dev/null
+++ b/app/(main)/clue/[clueId]/page.js
@@ -0,0 +1,298 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+
+export default function CluePage({ params }) {
+ const { clueId } = params;
+ const [clue, setClue] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const router = useRouter();
+
+ useEffect(() => {
+ async function checkHuntStatus() {
+ try {
+ const response = await fetch("/api/hunt-status", {
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ // If hunt is not active, redirect to the home page
+ if (!data.active) {
+ router.push("/");
+ return false;
+ }
+ return true;
+ } else {
+ console.error("Failed to check hunt status");
+ return false;
+ }
+ } catch (err) {
+ console.error("Error checking hunt status:", err);
+ return false;
+ }
+ }
+
+ async function fetchClueDetails() {
+ try {
+ // First verify the hunt is active
+ const huntIsActive = await checkHuntStatus();
+ if (!huntIsActive) return;
+
+ // Fetch the specific clue from the API with improved error handling
+ const response = await fetch(`/api/clues/${clueId}`, {
+ credentials: "include",
+ });
+
+ // Better error handling for non-JSON responses
+ if (!response.ok) {
+ const contentType = response.headers.get("content-type");
+ if (contentType && contentType.includes("application/json")) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || "Failed to fetch clue details");
+ } else {
+ throw new Error(
+ `Failed to fetch clue details: ${response.status} ${response.statusText}`
+ );
+ }
+ }
+
+ const data = await response.json();
+
+ // If we got back an error field, show it as an error
+ if (data.error) {
+ throw new Error(data.error);
+ }
+
+ setClue(data.clue);
+ } catch (err) {
+ console.error("Error fetching clue details:", err);
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ if (clueId) {
+ fetchClueDetails();
+ }
+ }, [clueId, router]);
+
+ if (loading) {
+ return (
+
+
Loading clue details...
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
Error: {error}
+
+
+
+
+
+ );
+ }
+
+ if (!clue) {
+ return (
+
+
Clue not found
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
{clue.title}
+
+
+
Description
+
{clue.description}
+
+ {clue.location && (
+
+
Location Hint
+
{clue.location}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/(main)/map/page.js b/app/(main)/map/page.js
new file mode 100644
index 0000000..e834029
--- /dev/null
+++ b/app/(main)/map/page.js
@@ -0,0 +1,358 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+
+export default function MapPage() {
+ const [clues, setClues] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [huntActive, setHuntActive] = useState(false);
+ const router = useRouter();
+
+ useEffect(() => {
+ async function checkHuntStatus() {
+ try {
+ const response = await fetch("/api/hunt-status", {
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setHuntActive(data.active || false);
+
+ // If hunt is not active, redirect to the home page
+ if (!data.active) {
+ router.push("/");
+ return false;
+ }
+ return true;
+ } else {
+ console.error("Failed to check hunt status");
+ return false;
+ }
+ } catch (err) {
+ console.error("Error checking hunt status:", err);
+ return false;
+ }
+ }
+
+ async function fetchClues() {
+ try {
+ // First verify the hunt is active
+ const huntIsActive = await checkHuntStatus();
+ if (!huntIsActive) return;
+
+ // Fetch the clues from the API with proper error handling
+ const response = await fetch("/api/clues", {
+ credentials: "include",
+ });
+
+ // Handle non-OK responses without throwing
+ if (!response.ok) {
+ const contentType = response.headers.get("content-type");
+ if (contentType && contentType.includes("application/json")) {
+ // If it's JSON, parse it for the error message
+ const errorData = await response.json();
+ throw new Error(errorData.error || "Failed to fetch clues");
+ } else {
+ // If it's not JSON, use the status text
+ throw new Error(
+ `Failed to fetch clues: ${response.status} ${response.statusText}`
+ );
+ }
+ }
+
+ // Parse the JSON response
+ const data = await response.json();
+
+ // If we got back an error field, show it as an error
+ if (data.error) {
+ throw new Error(data.error);
+ }
+
+ setClues(data.clues || []);
+ } catch (err) {
+ console.error("Error fetching clues:", err);
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ fetchClues();
+ }, [router]);
+
+ // Generate random x positions and sort by descending y positions
+ const generatePositions = (clues) => {
+ const positions = [];
+ const maxWidth = 800;
+ const maxHeight = 500;
+ const padding = 50;
+
+ for (let i = 0; i < clues.length; i++) {
+ positions.push({
+ id: clues[i].id,
+ title: clues[i].title || `Clue ${i + 1}`,
+ x: Math.random() * (maxWidth - 2 * padding) + padding,
+ y: (i + 1) * (maxHeight / (clues.length + 1)),
+ });
+ }
+
+ return positions;
+ };
+
+ const cluePositions = clues.length > 0 ? generatePositions(clues) : [];
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
Error: {error}
+
+
+ );
+ }
+
+ if (clues.length === 0) {
+ return (
+
+
No clues available yet.
+
+
+ );
+ }
+
+ return (
+
+
Treasure Map
+
+
+
+
+ {/* Clue list */}
+
+
Clues
+
+ {cluePositions.map((clue, index) => (
+ -
+
+ {index + 1}. {clue.title}
+
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/(main)/team/[teamId]/page.js b/app/(main)/team/[teamId]/page.js
index 10e4887..424459d 100644
--- a/app/(main)/team/[teamId]/page.js
+++ b/app/(main)/team/[teamId]/page.js
@@ -233,26 +233,25 @@ export default function TeamPage({ params }) {
onClick={async () => {
try {
setLoading(true);
- if (!huntActive) {
- // If hunt is not active, redirect to the "not started" page
- router.push(`/hunt-not-started?teamId=${teamId}`);
- } else {
- // If hunt is active, try to start the hunt
- const response = await fetch(
- `/api/teams/${teamId}/start-hunt`,
- {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- credentials: "include",
- }
- );
- if (response.ok) {
- fetchTeamData();
+ // Check hunt status first
+ const response = await fetch("/api/hunt-status", {
+ method: "GET",
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.active) {
+ // If hunt is active, navigate to map page
+ router.push("/map");
} else {
- const error = await response.json();
- throw new Error(error.message || "Failed to start hunt");
+ // If hunt is not active, show not started page
+ router.push(`/hunt-not-started?teamId=${teamId}`);
}
+ } else {
+ throw new Error("Failed to check hunt status");
}
} catch (err) {
setError(err.message);
diff --git a/app/api/clues/[clueId]/route.js b/app/api/clues/[clueId]/route.js
new file mode 100644
index 0000000..980f059
--- /dev/null
+++ b/app/api/clues/[clueId]/route.js
@@ -0,0 +1,65 @@
+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
+
+// GET endpoint to retrieve a specific clue
+export async function GET(request, { params }) {
+ const { clueId } = params;
+
+ try {
+ // Verify the user is authenticated
+ const cookieStore = await cookies();
+ const sessionToken = cookieStore.get("next-auth.session-token")?.value;
+
+ if (!sessionToken) {
+ return Response.json(
+ { error: "Unauthorized", clue: null },
+ { status: 401 }
+ );
+ }
+
+ const userData = verifyToken(sessionToken);
+ if (!userData) {
+ return Response.json(
+ { error: "Invalid session", clue: null },
+ { status: 401 }
+ );
+ }
+
+ // Check if the hunt is active
+ const huntActiveSetting = await queryOne(
+ "SELECT value FROM settings WHERE key = 'hunt_active'"
+ );
+
+ const huntActive = huntActiveSetting && huntActiveSetting.value === "true";
+
+ if (!huntActive) {
+ // Return a valid JSON response with clue set to null
+ return Response.json({
+ error: "Hunt is not active",
+ clue: null,
+ });
+ }
+
+ // Fetch the specific clue
+ const clue = await queryOne("SELECT * FROM clues WHERE id = ?", [clueId]);
+
+ if (!clue) {
+ return Response.json(
+ { error: "Clue not found", clue: null },
+ { status: 404 }
+ );
+ }
+
+ return Response.json({ clue });
+ } catch (error) {
+ console.error("Error fetching clue details:", error);
+ // Always return a valid JSON response even on error
+ return Response.json(
+ { error: "Failed to fetch clue details: " + error.message, clue: null },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/clues/route.js b/app/api/clues/route.js
new file mode 100644
index 0000000..23759c3
--- /dev/null
+++ b/app/api/clues/route.js
@@ -0,0 +1,104 @@
+import { query, 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
+
+// Helper function to ensure the clues table exists (if not already created elsewhere)
+async function ensureCluesTableExists() {
+ try {
+ // Create the clues table if it doesn't exist
+ await run(`
+ CREATE TABLE IF NOT EXISTS clues (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ title TEXT NOT NULL,
+ description TEXT NOT NULL,
+ location TEXT,
+ order_number INTEGER,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ `);
+
+ return true;
+ } catch (error) {
+ console.error("Error ensuring clues table exists:", error);
+ return false;
+ }
+}
+
+// GET endpoint to retrieve all clues
+export async function GET(request) {
+ try {
+ // Verify the user is authenticated
+ const cookieStore = await cookies();
+ const sessionToken = cookieStore.get("next-auth.session-token")?.value;
+
+ if (!sessionToken) {
+ return Response.json(
+ { error: "Unauthorized", clues: [] },
+ { status: 401 }
+ );
+ }
+
+ const userData = verifyToken(sessionToken);
+ if (!userData) {
+ return Response.json(
+ { error: "Invalid session", clues: [] },
+ { status: 401 }
+ );
+ }
+
+ // Ensure the clues table exists
+ await ensureCluesTableExists();
+
+ // Check if the hunt is active
+ const huntActiveSetting = await queryOne(
+ "SELECT value FROM settings WHERE key = 'hunt_active'"
+ );
+
+ const huntActive = huntActiveSetting && huntActiveSetting.value === "true";
+
+ if (!huntActive) {
+ // Return empty clues array instead of error status to avoid redirect issues
+ return Response.json({
+ error: "Hunt is not active",
+ clues: [],
+ });
+ }
+
+ // Fetch all clues
+ const clues = await query("SELECT * FROM clues ORDER BY order_number ASC");
+
+ // 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, order_number) VALUES (?, ?, ?, ?)",
+ ["First Clue", "This is the first clue description", "Location 1", 1]
+ );
+ await run(
+ "INSERT INTO clues (title, description, location, order_number) VALUES (?, ?, ?, ?)",
+ ["Second Clue", "This is the second clue description", "Location 2", 2]
+ );
+ await run(
+ "INSERT INTO clues (title, description, location, order_number) VALUES (?, ?, ?, ?)",
+ ["Final Clue", "This is the final clue description", "Location 3", 3]
+ );
+
+ // Fetch the inserted clues
+ const sampleClues = await query(
+ "SELECT * FROM clues ORDER BY order_number ASC"
+ );
+ return Response.json({ clues: sampleClues });
+ }
+
+ return Response.json({ clues });
+ } catch (error) {
+ console.error("Error fetching clues:", error);
+ // Always return a valid JSON response even on error
+ return Response.json(
+ { error: "Failed to fetch clues: " + error.message, clues: [] },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/hunt/timer/route.js b/app/api/hunt/timer/route.js
index 10a2717..6ce0820 100644
--- a/app/api/hunt/timer/route.js
+++ b/app/api/hunt/timer/route.js
@@ -7,18 +7,42 @@ export const dynamic = "force-dynamic"; // Disable caching
// Helper function to ensure the hunt_timer table exists
async function ensureTableExists() {
try {
- // Check if the table exists by querying it
- await queryOne("SELECT name FROM sqlite_master WHERE type='table' AND name='hunt_timer'");
-
- // If no error was thrown, the table exists
+ // Directly try to create the table (IF NOT EXISTS prevents errors if it already exists)
+ await run(`
+ CREATE TABLE IF NOT EXISTS hunt_timer (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ active INTEGER NOT NULL DEFAULT 0,
+ start_time TEXT NOT NULL,
+ end_time TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ `);
+
+ // Verify the table exists by inserting a dummy record and then deleting it
+ // This is a more reliable way to test if the table is usable
+ const tempTimestamp = new Date().toISOString();
+ const tempId = await run(
+ "INSERT INTO hunt_timer (active, start_time) VALUES (0, ?)",
+ [tempTimestamp]
+ );
+
+ if (tempId) {
+ // Delete the test record
+ await run("DELETE FROM hunt_timer WHERE id = ?", [tempId]);
+ }
+
+ console.log("hunt_timer table verified successfully");
return true;
} catch (error) {
- console.error("Error checking for hunt_timer table:", error);
-
- // Try to create the table
+ console.error("Error ensuring hunt_timer table:", error);
+
+ // Try with an alternative approach if the first method failed
try {
+ console.log("Attempting alternative table creation method...");
+ // Execute the CREATE TABLE statement directly
await run(`
- CREATE TABLE IF NOT EXISTS hunt_timer (
+ DROP TABLE IF EXISTS hunt_timer;
+ CREATE TABLE hunt_timer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
active INTEGER NOT NULL DEFAULT 0,
start_time TEXT NOT NULL,
@@ -26,7 +50,7 @@ async function ensureTableExists() {
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
- console.log("hunt_timer table created successfully");
+ console.log("hunt_timer table created with alternative method");
return true;
} catch (createError) {
console.error("Failed to create hunt_timer table:", createError);
@@ -35,11 +59,60 @@ async function ensureTableExists() {
}
}
+// Helper function to ensure settings table exists and manage hunt active status
+async function ensureSettingsTableExists() {
+ try {
+ // Create settings table if it doesn't exist
+ await run(`
+ CREATE TABLE IF NOT EXISTS settings (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL
+ )
+ `);
+
+ // Check if hunt_active setting exists
+ const huntActiveSetting = await queryOne(
+ "SELECT value FROM settings WHERE key = 'hunt_active'"
+ );
+
+ // If not, create it with default value "false"
+ if (!huntActiveSetting) {
+ await run("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", [
+ "hunt_active",
+ "false",
+ ]);
+ }
+
+ return true;
+ } catch (error) {
+ console.error("Error ensuring settings table:", error);
+ return false;
+ }
+}
+
+// Helper function to update hunt active status
+async function updateHuntActiveStatus(isActive) {
+ try {
+ await ensureSettingsTableExists();
+
+ await run("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", [
+ "hunt_active",
+ isActive ? "true" : "false",
+ ]);
+
+ console.log("Hunt active status updated:", isActive);
+ return true;
+ } catch (error) {
+ console.error("Failed to update hunt active status:", error);
+ return false;
+ }
+}
+
// GET endpoint to get current timer status
export async function GET(request) {
try {
// Verify the user is authenticated
- const cookieStore = cookies();
+ const cookieStore = await cookies(); // Add await here
const sessionToken = cookieStore.get("next-auth.session-token")?.value;
if (!sessionToken) {
@@ -51,8 +124,10 @@ export async function GET(request) {
return Response.json({ error: "Invalid session" }, { status: 401 });
}
- // Ensure the table exists
+ // Ensure both tables exist
const tableExists = await ensureTableExists();
+ await ensureSettingsTableExists();
+
if (!tableExists) {
return Response.json(
{ error: "Database table not found" },
@@ -65,16 +140,26 @@ export async function GET(request) {
"SELECT active, start_time, end_time FROM hunt_timer ORDER BY id DESC LIMIT 1"
);
+ // Get the hunt active status from settings
+ const huntActiveSetting = await queryOne(
+ "SELECT value FROM settings WHERE key = 'hunt_active'"
+ );
+
+ const huntActive = huntActiveSetting
+ ? huntActiveSetting.value === "true"
+ : false;
+
if (!timerStatus) {
return Response.json({
- active: false,
+ active: huntActive, // Use settings value if timer not found
startTime: null,
endTime: null,
});
}
return Response.json({
- active: !!timerStatus.active,
+ active: huntActive, // Return settings value for consistency
+ timerActive: !!timerStatus.active, // Also return timer status for admin panel
startTime: timerStatus.start_time,
endTime: timerStatus.end_time,
});
@@ -91,7 +176,7 @@ export async function GET(request) {
export async function POST(request) {
try {
// Verify the user is authenticated and is an admin
- const cookieStore = cookies();
+ const cookieStore = await cookies(); // Add await here
const sessionToken = cookieStore.get("next-auth.session-token")?.value;
if (!sessionToken) {
@@ -102,22 +187,30 @@ export async function POST(request) {
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 (
+ !(
+ userData.role === "admin" ||
+ userData.isAdmin === true ||
+ userData.is_admin === 1
+ )
+ ) {
return Response.json(
{ error: "Unauthorized: Admin access required" },
{ status: 403 }
);
}
- // Ensure the table exists
+ // Create required tables if they don't exist
const tableExists = await ensureTableExists();
+ await ensureSettingsTableExists();
+
if (!tableExists) {
return Response.json(
- { error: "Database table not found" },
+ { error: "Failed to create hunt_timer database table" },
{ status: 500 }
);
}
@@ -141,6 +234,9 @@ export async function POST(request) {
[1, startTime, endTime || null]
);
+ // Update hunt active status to true
+ await updateHuntActiveStatus(true);
+
return Response.json({
success: true,
message: "Timer started successfully",
@@ -162,6 +258,9 @@ export async function POST(request) {
[new Date().toISOString()]
);
+ // Update hunt active status to false
+ await updateHuntActiveStatus(false);
+
return Response.json({
success: true,
message: "Timer stopped successfully",
@@ -181,8 +280,11 @@ export async function POST(request) {
}
} catch (error) {
console.error("Error managing timer:", error);
- return Response.json({
- error: "Failed to manage timer: " + error.message
- }, { status: 500 });
+ return Response.json(
+ {
+ error: "Failed to manage timer: " + error.message,
+ },
+ { status: 500 }
+ );
}
}
diff --git a/clue_hunt.db b/clue_hunt.db
index 8baf3d0..cc18ffd 100644
Binary files a/clue_hunt.db and b/clue_hunt.db differ