diff --git a/app/(auth)/login/page.js b/app/(auth)/login/page.js index 2af7fda..a03cb51 100644 --- a/app/(auth)/login/page.js +++ b/app/(auth)/login/page.js @@ -90,7 +90,6 @@ export default function Login() { return (
-
←
Login
diff --git a/app/(auth)/register/page.js b/app/(auth)/register/page.js index cbf9ffe..b3709c4 100644 --- a/app/(auth)/register/page.js +++ b/app/(auth)/register/page.js @@ -64,7 +64,6 @@ export default function Register() { return (
-
←
Register
diff --git a/app/(main)/[teamId]/page.js b/app/(main)/[teamId]/page.js index b3362b0..f9b6e6e 100644 --- a/app/(main)/[teamId]/page.js +++ b/app/(main)/[teamId]/page.js @@ -334,6 +334,10 @@ export default function TeamPage({ params }) { min-height: 100vh; background-color: #f4f4f4; font-family: Arial, sans-serif; + position: relative; + overflow-x: hidden; /* Prevent horizontal scroll */ + width: 100%; + padding-bottom: 70px; /* Add padding to accommodate fixed navbar */ } .team-banner { @@ -449,6 +453,12 @@ export default function TeamPage({ params }) { background-color: white; border-top: 1px solid #ddd; padding: 10px 0; + position: fixed; /* Fix the navbar at the bottom */ + bottom: 0; + left: 0; + width: 100%; /* Use 100% instead of 100vw to prevent overflow issues */ + box-sizing: border-box; /* Include padding in width calculation */ + z-index: 100; /* Ensure navbar is above other content */ } .nav-item { @@ -458,7 +468,8 @@ export default function TeamPage({ params }) { text-decoration: none; color: #666; flex: 1; - padding: 10px 0; + padding: 8px 0; + max-width: 25%; /* Ensure each item takes up equal space */ } .nav-item.active { @@ -466,12 +477,27 @@ export default function TeamPage({ params }) { } .nav-icon { - font-size: 24px; - margin-bottom: 5px; + font-size: 22px; + margin-bottom: 3px; } .nav-label { font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + text-align: center; + } + + /* Responsive adjustments for smaller screens */ + @media (max-width: 375px) { + .nav-icon { + font-size: 20px; + } + .nav-label { + font-size: 10px; + } } .loading-container, diff --git a/app/(main)/admin/page.js b/app/(main)/admin/page.js index 44dbc42..3f6d17e 100644 --- a/app/(main)/admin/page.js +++ b/app/(main)/admin/page.js @@ -1,124 +1,444 @@ -'use client'; // Mark as a Client Component -import { useState, useEffect } from 'react'; -import { useSession } from 'next-auth/react'; -import { useRouter } from 'next/navigation'; -import { startHuntTimer, getHuntStatus } from '../actions/auth'; +"use client"; +import { useState, useEffect, useRef } from "react"; +import { useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; -export default function Admin() { - const { data: session, status } = useSession(); - const [timerActive, setTimerActive] = useState(false); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - const router = useRouter(); +export default function AdminDashboard() { + const { data: session, status } = useSession(); + const [timerActive, setTimerActive] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [elapsedTime, setElapsedTime] = useState(0); // in seconds + const [timerDisplay, setTimerDisplay] = useState("--:--:--"); + const [presetHours, setPresetHours] = useState(""); + const [presetMinutes, setPresetMinutes] = useState(""); + const [presetSeconds, setPresetSeconds] = useState(""); + const [timerEndTime, setTimerEndTime] = useState(null); + const timerRef = useRef(null); + const router = useRouter(); - // Load timer status on page load - useEffect(() => { - async function loadTimerStatus() { - try { - const status = await getHuntStatus(); - setTimerActive(status.active); - } catch (err) { - console.error('Failed to load timer status:', err); - setError('Failed to load timer status'); - } finally { - setLoading(false); - } + // Load timer status on page load + useEffect(() => { + async function loadTimerStatus() { + try { + const response = await fetch("/api/hunt/timer", { + credentials: "include", + headers: { + "Cache-Control": "no-cache", + }, + }); + const data = await response.json(); + + if (data.active) { + setTimerActive(true); + + // Calculate elapsed time + if (data.startTime) { + const startTime = new Date(data.startTime).getTime(); + const currentTime = Date.now(); + const elapsed = Math.floor((currentTime - startTime) / 1000); + setElapsedTime(elapsed); + } + + // If there's an end time, set it + if (data.endTime) { + setTimerEndTime(new Date(data.endTime).getTime()); + } } - if (status === 'authenticated') { - loadTimerStatus(); - } - }, [status]); - - // Check if user is admin - useEffect(() => { - if (status === 'authenticated' && session?.user?.role !== 'admin') { - router.push('/'); - } - }, [session, status, router]); - - // Handle access denied and loading states - if (status === 'loading' || loading) { - return
Loading...
; + setLoading(false); + } catch (err) { + console.error("Failed to load timer status:", err); + setError("Failed to load timer status"); + setLoading(false); + } } - if (status === 'unauthenticated') { - router.push('/login'); - return null; + if (status === "authenticated") { + loadTimerStatus(); + } + }, [status]); + + // Check if user is admin + useEffect(() => { + if (status === "authenticated") { + if (session?.user?.role !== "admin") { + router.push("/team-selection"); + } + } else if (status === "unauthenticated") { + router.push("/login"); + } + }, [session, status, router]); + + // Timer update effect + useEffect(() => { + if (timerActive) { + timerRef.current = setInterval(() => { + setElapsedTime((prev) => { + const newElapsed = prev + 1; + + // If we have an end time and we've reached it, stop the timer + if (timerEndTime && Date.now() >= timerEndTime) { + stopTimer(); + return prev; + } + + // Update the display + formatTimeDisplay(newElapsed); + return newElapsed; + }); + }, 1000); + } else { + if (timerRef.current) { + clearInterval(timerRef.current); + } } - if (session?.user?.role !== 'admin') { - return null; // Already redirecting via useEffect - } - - const toggleTimer = async () => { - setLoading(true); - try { - const newStatus = !timerActive; - const result = await startHuntTimer(newStatus); - - if (result.success) { - setTimerActive(newStatus); - } else { - setError('Failed to update timer status'); - } - } catch (err) { - console.error('Failed to toggle timer:', err); - setError('Failed to toggle timer'); - } finally { - setLoading(false); - } + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + } }; + }, [timerActive, timerEndTime]); - return ( -
-
-
←
-
Admin Controls
-
-
+ // Format time for display whenever elapsed time changes + useEffect(() => { + formatTimeDisplay(elapsedTime); + }, [elapsedTime]); -
-
- - -
+ // Format seconds into HH:MM:SS + const formatTimeDisplay = (totalSeconds) => { + if (!timerActive) { + setTimerDisplay("--:--:--"); + return; + } -
- - -
+ const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; -
- - -
+ const display = [ + hours.toString().padStart(2, "0"), + minutes.toString().padStart(2, "0"), + seconds.toString().padStart(2, "0"), + ].join(":"); - + setTimerDisplay(display); + }; - {error &&

{error}

} -
+ const startTimer = async (preset = false) => { + setLoading(true); + try { + let endTime = null; -
-
-
🏠
-
-
-
📊
-
-
-
🏆
-
-
-
👤
-
-
+ // If using preset, calculate the end time + if (preset) { + const hours = parseInt(presetHours || "0"); + const minutes = parseInt(presetMinutes || "0"); + const seconds = parseInt(presetSeconds || "0"); + + // Validate input + if (isNaN(hours) || isNaN(minutes) || isNaN(seconds)) { + setError( + "Please enter valid numbers for hours, minutes, and seconds" + ); + setLoading(false); + return; + } + + // Calculate total duration in milliseconds + const durationMs = (hours * 3600 + minutes * 60 + seconds) * 1000; + if (durationMs <= 0) { + setError("Please enter a duration greater than zero"); + setLoading(false); + return; + } + + // Set end time + endTime = new Date(Date.now() + durationMs).toISOString(); + } + + // API call to start timer + const response = await fetch("/api/hunt/timer", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + action: "start", + endTime: endTime, + }), + }); + + const data = await response.json(); + + if (data.success) { + setTimerActive(true); + setElapsedTime(0); + + if (endTime) { + setTimerEndTime(new Date(endTime).getTime()); + } else { + setTimerEndTime(null); + } + + setError(""); + } else { + setError(data.error || "Failed to start timer"); + } + } catch (err) { + console.error("Failed to start timer:", err); + setError("Failed to start timer"); + } finally { + setLoading(false); + } + }; + + const stopTimer = async () => { + setLoading(true); + try { + const response = await fetch("/api/hunt/timer", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ action: "stop" }), + }); + + const data = await response.json(); + + if (data.success) { + setTimerActive(false); + setTimerEndTime(null); + setError(""); + } else { + setError(data.error || "Failed to stop timer"); + } + } catch (err) { + console.error("Failed to stop timer:", err); + setError("Failed to stop timer"); + } finally { + setLoading(false); + } + }; + + const startPresetTimer = () => { + startTimer(true); + }; + + // Handle access denied and loading states + if (status === "loading" || loading) { + return
Loading...
; + } + + if (status === "unauthenticated") { + return null; // Will redirect via useEffect + } + + if (session?.user?.role !== "admin") { + return null; // Will redirect via useEffect + } + + return ( +
+
+
router.push("/team-selection")} + > + ←
- ); -} \ No newline at end of file +
Admin Dashboard
+
+
+ +
+
+

Hunt Timer

+
{timerDisplay}
+ +
+ + +
+ +
+

Set Timer Duration

+
+
+ + setPresetHours(e.target.value)} + disabled={timerActive || loading} + className="admin-input" + /> +
+
+ + setPresetMinutes(e.target.value)} + disabled={timerActive || loading} + className="admin-input" + /> +
+
+ + setPresetSeconds(e.target.value)} + disabled={timerActive || loading} + className="admin-input" + /> +
+
+ + +
+ + {error &&

{error}

} +
+
+ + +
+ ); +} diff --git a/app/(main)/create-team/page.js b/app/(main)/create-team/page.js index df50595..0d8b408 100644 --- a/app/(main)/create-team/page.js +++ b/app/(main)/create-team/page.js @@ -4,17 +4,7 @@ import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import Link from "next/link"; -const styles = { - navbar: { - position: "fixed", - bottom: 0, - width: "100%", - backgroundColor: "#fff", - borderTop: "1px solid #ccc", - padding: "10px 0", - textAlign: "center", - }, -}; +// Removed the styles object for navbar export default function CreateTeam() { const { data: session, status } = useSession(); @@ -106,20 +96,7 @@ export default function CreateTeam() {
-
-
-
🏠
-
-
-
📊
-
-
-
🏆
-
-
-
👤
-
-
+ {/* Navbar removed from here */}
); diff --git a/app/(main)/team-selection/page.js b/app/(main)/team-selection/page.js index c7bc71a..8347866 100644 --- a/app/(main)/team-selection/page.js +++ b/app/(main)/team-selection/page.js @@ -28,8 +28,20 @@ export default function TeamSelection() { }) .then((res) => res.json()) .then((userData) => { - if (userData.teamId) { - window.location.href = `/${userData.teamId}`; + console.log("User data:", userData); // Debug user data + + // Check if user is an admin first + if (userData.role === "admin") { + // Redirect admin to admin dashboard with correct path + router.push("/admin"); // Changed from /admin-dashboard to /admin + } else if (userData.teamId) { + // Only attempt to construct URL if teamId is valid + if (userData.teamId && !isNaN(parseInt(userData.teamId))) { + window.location.href = `/${userData.teamId}`; + } else { + console.error("Invalid teamId received:", userData.teamId); + setIsLoading(false); + } } else { setIsLoading(false); } @@ -64,6 +76,11 @@ export default function TeamSelection() { throw new Error(data.error || "Failed to join team"); } + // Add validation before constructing the URL + if (!data.teamId || isNaN(parseInt(data.teamId))) { + throw new Error("Invalid team ID received"); + } + // Navigate to the team page using the correct URL format window.location.href = `/${data.teamId}`; } catch (err) { @@ -111,20 +128,7 @@ export default function TeamSelection() { -
-
-
🏠
-
-
-
📊
-
-
-
🏆
-
-
-
👤
-
-
+ {/* Navbar removed from here */} ); diff --git a/app/api/hunt/timer/route.js b/app/api/hunt/timer/route.js new file mode 100644 index 0000000..0eec5a3 --- /dev/null +++ b/app/api/hunt/timer/route.js @@ -0,0 +1,109 @@ +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 + +// GET endpoint to get current timer status +export async function GET(request) { + try { + // Verify the user is authenticated + const cookieStore = cookies(); + const sessionToken = cookieStore.get("next-auth.session-token")?.value; + + if (!sessionToken) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const userData = verifyToken(sessionToken); + if (!userData) { + return Response.json({ error: "Invalid session" }, { status: 401 }); + } + + // Get the current hunt timer status + const timerStatus = await queryOne( + "SELECT active, start_time, end_time FROM hunt_timer ORDER BY id DESC LIMIT 1" + ); + + if (!timerStatus) { + return Response.json({ + active: false, + startTime: null, + endTime: null, + }); + } + + return Response.json({ + active: !!timerStatus.active, + startTime: timerStatus.start_time, + endTime: timerStatus.end_time, + }); + } catch (error) { + console.error("Error getting timer status:", error); + return Response.json( + { error: "Failed to get timer status" }, + { status: 500 } + ); + } +} + +// POST endpoint to start/stop timer +export async function POST(request) { + try { + // Verify the user is authenticated and is an admin + const cookieStore = cookies(); + const sessionToken = cookieStore.get("next-auth.session-token")?.value; + + if (!sessionToken) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const userData = verifyToken(sessionToken); + if (!userData || userData.role !== "admin") { + return Response.json( + { error: "Unauthorized: Admin access required" }, + { status: 403 } + ); + } + + const body = await request.json(); + const { action, endTime } = body; + + if (action === "start") { + // Get current time in ISO format + const startTime = new Date().toISOString(); + + // Insert new timer record + await run( + "INSERT INTO hunt_timer (active, start_time, end_time) VALUES (?, ?, ?)", + [1, startTime, endTime || null] + ); + + return Response.json({ + success: true, + message: "Timer started successfully", + startTime, + endTime: endTime || null, + }); + } else if (action === "stop") { + // Update the most recent timer to inactive + await run( + "UPDATE hunt_timer SET active = 0, end_time = ? WHERE id = (SELECT id FROM hunt_timer ORDER BY id DESC LIMIT 1)", + [new Date().toISOString()] + ); + + return Response.json({ + success: true, + message: "Timer stopped successfully", + }); + } else { + return Response.json( + { error: "Invalid action. Use 'start' or 'stop'." }, + { status: 400 } + ); + } + } catch (error) { + console.error("Error managing timer:", error); + return Response.json({ error: "Failed to manage timer" }, { status: 500 }); + } +} diff --git a/app/api/users/admin/route.js b/app/api/users/admin/route.js new file mode 100644 index 0000000..3081cd0 --- /dev/null +++ b/app/api/users/admin/route.js @@ -0,0 +1,76 @@ +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 + +// GET to list admin users +export async function GET(request) { + try { + // Verify the user is authenticated + const cookieStore = cookies(); + const sessionToken = cookieStore.get("next-auth.session-token")?.value; + + if (!sessionToken) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const userData = verifyToken(sessionToken); + if (!userData) { + return Response.json({ error: "Invalid session" }, { status: 401 }); + } + + // Get all admin users + const adminUsers = await query( + "SELECT id, username, email FROM users WHERE is_admin = 1" + ); + + return Response.json({ users: adminUsers }); + } catch (error) { + console.error("Error fetching admin users:", error); + return Response.json( + { error: "Failed to fetch admin users" }, + { status: 500 } + ); + } +} + +// DELETE to remove admin privileges from a user +export async function DELETE(request) { + try { + // Verify the user is authenticated + const cookieStore = cookies(); + const sessionToken = cookieStore.get("next-auth.session-token")?.value; + + if (!sessionToken) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const userData = verifyToken(sessionToken); + if (!userData) { + return Response.json({ error: "Invalid session" }, { status: 401 }); + } + + // Get the user ID from the URL or request body + const { searchParams } = new URL(request.url); + const userId = searchParams.get("userId"); + + if (!userId) { + return Response.json({ error: "User ID is required" }, { status: 400 }); + } + + // Remove admin privileges by setting is_admin to 0 + await run("UPDATE users SET is_admin = 0 WHERE id = ?", [userId]); + + return Response.json({ + success: true, + message: "Admin privileges removed successfully", + }); + } catch (error) { + console.error("Error removing admin privileges:", error); + return Response.json( + { error: "Failed to remove admin privileges" }, + { status: 500 } + ); + } +} diff --git a/clue_hunt.db b/clue_hunt.db index f1a283f..f09b7c5 100644 Binary files a/clue_hunt.db and b/clue_hunt.db differ diff --git a/migrations/create_hunt_timer_table.js b/migrations/create_hunt_timer_table.js new file mode 100644 index 0000000..69260e8 --- /dev/null +++ b/migrations/create_hunt_timer_table.js @@ -0,0 +1,26 @@ +import { run } from "@/lib/db"; + +export async function createHuntTimerTable() { + console.log("Creating hunt_timer table..."); + + try { + 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 + ) + `); + + console.log("hunt_timer table created successfully"); + return { success: true }; + } catch (error) { + console.error("Error creating hunt_timer table:", error); + return { success: false, error }; + } +} + +// Run this migration if you want to execute it directly: +// createHuntTimerTable().then(console.log).catch(console.error); diff --git a/sqlite.db b/sqlite.db deleted file mode 100644 index 62f33d6..0000000 Binary files a/sqlite.db and /dev/null differ