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 ( +
+
Loading clues...
+
+ ); + } + + if (error) { + return ( +
+
Error: {error}
+ +
+ ); + } + + if (clues.length === 0) { + return ( +
+
No clues available yet.
+ +
+ ); + } + + return ( +
+

Treasure Map

+
+ + {/* Map background with a treasure map style */} + + + + {/* Decorative map elements */} + + + + + + {/* Connection lines between clue points */} + {cluePositions.slice(0, -1).map((clue, index) => ( + + ))} + + {/* Clue points */} + {cluePositions.map((clue, index) => ( + + router.push(`/clue/${clue.id}`)} + style={{ cursor: "pointer" }} + /> + + {index + 1} + + + ))} + +
+ + {/* Clue list */} +
+

Clues

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