diff --git a/app/(main)/admin/page.js b/app/(main)/admin/page.js index 59e66fc..9f9bcbf 100644 --- a/app/(main)/admin/page.js +++ b/app/(main)/admin/page.js @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import { setHuntTimer, clearHuntTimer, + getHuntTimer, formatElapsedTime, } from "@/utils/timerService"; @@ -26,6 +27,7 @@ export default function AdminDashboard() { useEffect(() => { async function loadTimerStatus() { try { + setLoading(true); const response = await fetch("/api/hunt/timer", { credentials: "include", headers: { @@ -39,18 +41,13 @@ export default function AdminDashboard() { // 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); - - // Update global timer data for all clients + // Store the start time in the centralized service setHuntTimer(data.startTime, data.endTime); - } - // If there's an end time, set it - if (data.endTime) { - setTimerEndTime(new Date(data.endTime).getTime()); + // If there's an end time, set it + if (data.endTime) { + setTimerEndTime(new Date(data.endTime).getTime()); + } } } @@ -78,62 +75,39 @@ export default function AdminDashboard() { } }, [session, status, router]); - // Timer update effect + // Timer update effect - use the centralized timer service useEffect(() => { - if (timerActive) { - timerRef.current = setInterval(() => { - setElapsedTime((prev) => { - const newElapsed = prev + 1; + let intervalId; - // If we have an end time and we've reached it, stop the timer + if (timerActive) { + // Use the existing timer data from the centralized service + intervalId = setInterval(() => { + const timerData = getHuntTimer(); + + if (timerData && timerData.startTime) { + // Format the elapsed time using the centralized service + const formattedTime = formatElapsedTime(timerData.startTime); + setTimerDisplay(formattedTime); + + // If there's an end time, check if we've reached it if (timerEndTime && Date.now() >= timerEndTime) { stopTimer(); - return prev; } - - // Update the display - formatTimeDisplay(newElapsed); - return newElapsed; - }); + } else { + setTimerDisplay("--:--:--"); + } }, 1000); } else { - if (timerRef.current) { - clearInterval(timerRef.current); - } + setTimerDisplay("--:--:--"); } return () => { - if (timerRef.current) { - clearInterval(timerRef.current); + if (intervalId) { + clearInterval(intervalId); } }; }, [timerActive, timerEndTime]); - // 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); - }; - const startTimer = async (preset = false) => { setLoading(true); try { @@ -182,7 +156,6 @@ export default function AdminDashboard() { if (data.success) { setTimerActive(true); - setElapsedTime(0); if (endTime) { setTimerEndTime(new Date(endTime).getTime()); @@ -221,8 +194,7 @@ export default function AdminDashboard() { if (data.success) { setTimerActive(false); setTimerEndTime(null); - setElapsedTime(0); - formatTimeDisplay(0); + setTimerDisplay("--:--:--"); // Clear timer data for all clients clearHuntTimer(); diff --git a/app/(main)/leaderboard/page.js b/app/(main)/leaderboard/page.js index 906a9d8..c7870a1 100644 --- a/app/(main)/leaderboard/page.js +++ b/app/(main)/leaderboard/page.js @@ -1,15 +1,70 @@ "use client"; import { useState, useEffect } from "react"; import { useSearchParams } from "next/navigation"; -// ...other imports +import { useRouter } from "next/navigation"; +import { useSession } from "next-auth/react"; +import Link from "next/link"; export default function LeaderboardPage() { const [teams, setTeams] = useState([]); const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); const searchParams = useSearchParams(); const huntJustEnded = searchParams.get("huntEnded") === "true"; + const router = useRouter(); + const { status } = useSession(); - // ...existing fetch logic + // Function to fetch leaderboard data + const fetchLeaderboard = async () => { + try { + setLoading(true); + + // Fetch from the public leaderboard endpoint + const response = await fetch("/api/leaderboard", { + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate", + Pragma: "no-cache", + }, + credentials: "include", + }); + + if (!response.ok) { + throw new Error("Failed to fetch leaderboard data"); + } + + const data = await response.json(); + setTeams(data); + setError(""); + } catch (err) { + console.error("Error fetching leaderboard:", err); + setError(err.message); + } finally { + setLoading(false); + } + }; + + // Fetch data on mount and set up refresh interval + useEffect(() => { + // Only proceed if authenticated + if (status === "authenticated") { + fetchLeaderboard(); + + // Refresh data every 30 seconds + const interval = setInterval(fetchLeaderboard, 30000); + return () => clearInterval(interval); + } else if (status === "unauthenticated") { + router.push("/login"); + } + }, [status, router]); + + if (loading) { + return ( +
+
+

Loading leaderboard...

+
+ ); + } return (
@@ -29,11 +84,248 @@ export default function LeaderboardPage() {
)} - {/* Rest of the leaderboard content */} - {/* ...existing code */} + {error ? ( +
{error}
+ ) : teams.length > 0 ? ( +
+ + + + + + {!huntJustEnded && ( + <> + + + + )} + + + + {teams.map((team, index) => ( + + + + {!huntJustEnded && ( + <> + + + + )} + + ))} + +
RankTeamProgressLast Activity
+ {index < 3 ? ( +
+ {index === 0 && "πŸ₯‡"} + {index === 1 && "πŸ₯ˆ"} + {index === 2 && "πŸ₯‰"} +
+ ) : ( + `${index + 1}` + )} +
{team.name} +
+
+ + {team.cluesFound} / {team.totalClues} + {team.progress > 0 && ` (${team.progress}%)`} + +
+
+ {team.lastActivity + ? new Date(team.lastActivity).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }) + : "Never"} +
+
+ ) : ( +
No teams on the leaderboard yet
+ )} + + {/* Navigation Bar */} +
+ +
πŸ—ΊοΈ
+
Map
+ + +
πŸ“Š
+
Stats
+ + +
πŸ‘₯
+
Team
+ + +
πŸ‘€
+
Profile
+ +
); diff --git a/app/(main)/map/page.js b/app/(main)/map/page.js index bc02ae3..afc6e34 100644 --- a/app/(main)/map/page.js +++ b/app/(main)/map/page.js @@ -384,7 +384,7 @@ export default function MapPage() { clearHuntTimer(); // Clean up all timer data if (isMounted.current) { - softRedirect("/leaderboard?huntEnded=true"); + hardRedirect("/leaderboard?huntEnded=true"); } return; } diff --git a/app/api/leaderboard/route.js b/app/api/leaderboard/route.js new file mode 100644 index 0000000..7f6948b --- /dev/null +++ b/app/api/leaderboard/route.js @@ -0,0 +1,73 @@ +import { query, queryOne } from "@/lib/db"; +import { getToken } from "next-auth/jwt"; + +export const dynamic = "force-dynamic"; // Disable caching + +export async function GET(request) { + try { + // Verify authentication - users need to be logged in to see leaderboard + const token = await getToken({ + req: request, + secret: + process.env.NEXTAUTH_SECRET || + "your-fallback-secret-should-be-at-least-32-chars", + }); + + if (!token) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Get the total number of clues for calculating progress + const clueCountResult = await queryOne( + "SELECT COUNT(*) as total FROM clues" + ); + const totalClues = clueCountResult?.total || 0; + + // Get all teams with progress info and member count + // Only include teams that have at least one member + const teams = await query(` + SELECT + t.id, + t.name, + t.code, + COUNT(DISTINCT tc.clue_id) as cluesFound, + MAX(tc.found_at) as lastActivity, + COUNT(DISTINCT u.id) as memberCount + FROM + teams t + LEFT JOIN + users u ON t.id = u.team_id + LEFT JOIN + team_clues tc ON t.id = tc.team_id + GROUP BY + t.id + HAVING + memberCount > 0 + ORDER BY + cluesFound DESC, + lastActivity ASC + `); + + // Add progress percentage and total clues to each team + const teamsWithProgress = teams.map((team) => ({ + ...team, + totalClues, + progress: + totalClues > 0 ? Math.round((team.cluesFound / totalClues) * 100) : 0, + })); + + return Response.json(teamsWithProgress, { + headers: { + "Cache-Control": "no-store, no-cache, must-revalidate", + Pragma: "no-cache", + Expires: "0", + }, + }); + } catch (error) { + console.error("Leaderboard error:", error); + return Response.json( + { error: "Failed to fetch leaderboard data" }, + { status: 500 } + ); + } +} diff --git a/app/waiting/page.js b/app/waiting/page.js index ee5e844..03529bd 100644 --- a/app/waiting/page.js +++ b/app/waiting/page.js @@ -94,7 +94,12 @@ export default function WaitingPage() { // Clear any timer data clearHuntTimer(); - // Don't redirect immediately to allow user to read the message + // Redirect to leaderboard after a brief delay + setTimeout(() => { + window.location.href = "/leaderboard?huntEnded=true"; + }, 2000); + + return; } // Save the current hunt data for next comparison diff --git a/clue_hunt.db b/clue_hunt.db index a3a3931..6ca0835 100644 Binary files a/clue_hunt.db and b/clue_hunt.db differ