mirror of
https://github.com/mudabbir-ahmad/peworkshop.git
synced 2026-10-08 03:50:21 +00:00
fixed user leaderboard. need to add features to admin leaderboard and fix timing issue on admin dashboard.
Grey = cant click Red = current Green = complete blue = skipped? Need to add Clue page Need to fix Leaderboard.
This commit is contained in:
1 parent
a762528494
commit
7d6b73ffb4
6 files changed
+437
-59
No files matched your search
+24
-52
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import {
|
import {
|
||||||
setHuntTimer,
|
setHuntTimer,
|
||||||
clearHuntTimer,
|
clearHuntTimer,
|
||||||
|
getHuntTimer,
|
||||||
formatElapsedTime,
|
formatElapsedTime,
|
||||||
} from "@/utils/timerService";
|
} from "@/utils/timerService";
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ export default function AdminDashboard() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadTimerStatus() {
|
async function loadTimerStatus() {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true);
|
||||||
const response = await fetch("/api/hunt/timer", {
|
const response = await fetch("/api/hunt/timer", {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -39,20 +41,15 @@ export default function AdminDashboard() {
|
|||||||
|
|
||||||
// Calculate elapsed time
|
// Calculate elapsed time
|
||||||
if (data.startTime) {
|
if (data.startTime) {
|
||||||
const startTime = new Date(data.startTime).getTime();
|
// Store the start time in the centralized service
|
||||||
const currentTime = Date.now();
|
|
||||||
const elapsed = Math.floor((currentTime - startTime) / 1000);
|
|
||||||
setElapsedTime(elapsed);
|
|
||||||
|
|
||||||
// Update global timer data for all clients
|
|
||||||
setHuntTimer(data.startTime, data.endTime);
|
setHuntTimer(data.startTime, data.endTime);
|
||||||
}
|
|
||||||
|
|
||||||
// If there's an end time, set it
|
// If there's an end time, set it
|
||||||
if (data.endTime) {
|
if (data.endTime) {
|
||||||
setTimerEndTime(new Date(data.endTime).getTime());
|
setTimerEndTime(new Date(data.endTime).getTime());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -78,62 +75,39 @@ export default function AdminDashboard() {
|
|||||||
}
|
}
|
||||||
}, [session, status, router]);
|
}, [session, status, router]);
|
||||||
|
|
||||||
// Timer update effect
|
// Timer update effect - use the centralized timer service
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (timerActive) {
|
let intervalId;
|
||||||
timerRef.current = setInterval(() => {
|
|
||||||
setElapsedTime((prev) => {
|
|
||||||
const newElapsed = prev + 1;
|
|
||||||
|
|
||||||
// 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) {
|
if (timerEndTime && Date.now() >= timerEndTime) {
|
||||||
stopTimer();
|
stopTimer();
|
||||||
return prev;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// Update the display
|
setTimerDisplay("--:--:--");
|
||||||
formatTimeDisplay(newElapsed);
|
}
|
||||||
return newElapsed;
|
|
||||||
});
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
if (timerRef.current) {
|
setTimerDisplay("--:--:--");
|
||||||
clearInterval(timerRef.current);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (timerRef.current) {
|
if (intervalId) {
|
||||||
clearInterval(timerRef.current);
|
clearInterval(intervalId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [timerActive, timerEndTime]);
|
}, [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) => {
|
const startTimer = async (preset = false) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -182,7 +156,6 @@ export default function AdminDashboard() {
|
|||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setTimerActive(true);
|
setTimerActive(true);
|
||||||
setElapsedTime(0);
|
|
||||||
|
|
||||||
if (endTime) {
|
if (endTime) {
|
||||||
setTimerEndTime(new Date(endTime).getTime());
|
setTimerEndTime(new Date(endTime).getTime());
|
||||||
@@ -221,8 +194,7 @@ export default function AdminDashboard() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
setTimerActive(false);
|
setTimerActive(false);
|
||||||
setTimerEndTime(null);
|
setTimerEndTime(null);
|
||||||
setElapsedTime(0);
|
setTimerDisplay("--:--:--");
|
||||||
formatTimeDisplay(0);
|
|
||||||
|
|
||||||
// Clear timer data for all clients
|
// Clear timer data for all clients
|
||||||
clearHuntTimer();
|
clearHuntTimer();
|
||||||
|
|||||||
@@ -1,15 +1,70 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useSearchParams } from "next/navigation";
|
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() {
|
export default function LeaderboardPage() {
|
||||||
const [teams, setTeams] = useState([]);
|
const [teams, setTeams] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState("");
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const huntJustEnded = searchParams.get("huntEnded") === "true";
|
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 (
|
||||||
|
<div className="leaderboard-page loading">
|
||||||
|
<div className="loading-spinner"></div>
|
||||||
|
<p>Loading leaderboard...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="leaderboard-page">
|
<div className="leaderboard-page">
|
||||||
@@ -29,11 +84,248 @@ export default function LeaderboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Rest of the leaderboard content */}
|
{error ? (
|
||||||
{/* ...existing code */}
|
<div className="error-message">{error}</div>
|
||||||
|
) : teams.length > 0 ? (
|
||||||
|
<div
|
||||||
|
className={`leaderboard-container ${
|
||||||
|
huntJustEnded ? "final-results" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<table className="leaderboard-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Rank</th>
|
||||||
|
<th>Team</th>
|
||||||
|
{!huntJustEnded && (
|
||||||
|
<>
|
||||||
|
<th>Progress</th>
|
||||||
|
<th>Last Activity</th>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{teams.map((team, index) => (
|
||||||
|
<tr
|
||||||
|
key={team.id}
|
||||||
|
className={index < 3 ? `rank-${index + 1}` : ""}
|
||||||
|
>
|
||||||
|
<td className="rank-cell">
|
||||||
|
{index < 3 ? (
|
||||||
|
<div className="medal">
|
||||||
|
{index === 0 && "🥇"}
|
||||||
|
{index === 1 && "🥈"}
|
||||||
|
{index === 2 && "🥉"}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
`${index + 1}`
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="team-name-cell">{team.name}</td>
|
||||||
|
{!huntJustEnded && (
|
||||||
|
<>
|
||||||
|
<td className="progress-cell">
|
||||||
|
<div className="progress-container">
|
||||||
|
<div
|
||||||
|
className="progress-bar"
|
||||||
|
style={{ width: `${team.progress}%` }}
|
||||||
|
></div>
|
||||||
|
<span className="progress-text">
|
||||||
|
{team.cluesFound} / {team.totalClues}
|
||||||
|
{team.progress > 0 && ` (${team.progress}%)`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="activity-cell">
|
||||||
|
{team.lastActivity
|
||||||
|
? new Date(team.lastActivity).toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})
|
||||||
|
: "Never"}
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="no-data">No teams on the leaderboard yet</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation Bar */}
|
||||||
|
<div className="nav-bar">
|
||||||
|
<Link href="/map" className="nav-item">
|
||||||
|
<div className="nav-icon">🗺️</div>
|
||||||
|
<div className="nav-label">Map</div>
|
||||||
|
</Link>
|
||||||
|
<Link href="/leaderboard" className="nav-item active">
|
||||||
|
<div className="nav-icon">📊</div>
|
||||||
|
<div className="nav-label">Stats</div>
|
||||||
|
</Link>
|
||||||
|
<Link href="/team-selection" className="nav-item">
|
||||||
|
<div className="nav-icon">👥</div>
|
||||||
|
<div className="nav-label">Team</div>
|
||||||
|
</Link>
|
||||||
|
<Link href="/profile" className="nav-item">
|
||||||
|
<div className="nav-icon">👤</div>
|
||||||
|
<div className="nav-label">Profile</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
/* ...existing styles */
|
.leaderboard-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
padding-bottom: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
background-color: #68543c;
|
||||||
|
color: white;
|
||||||
|
padding: 15px 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button,
|
||||||
|
.placeholder {
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-container {
|
||||||
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-results {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-table th,
|
||||||
|
.leaderboard-table td {
|
||||||
|
padding: 12px 15px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-table th {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-cell {
|
||||||
|
width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medal {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-1 {
|
||||||
|
background-color: rgba(255, 215, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-2 {
|
||||||
|
background-color: rgba(192, 192, 192, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-3 {
|
||||||
|
background-color: rgba(205, 127, 50, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-name-cell {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 20px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #4caf50;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 20px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #333;
|
||||||
|
text-shadow: 0 0 2px rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading,
|
||||||
|
.no-data {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 50px 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #68543c;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: red;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.hunt-ended-alert {
|
.hunt-ended-alert {
|
||||||
background-color: #f8d7da;
|
background-color: #f8d7da;
|
||||||
@@ -49,6 +341,42 @@ export default function LeaderboardPage() {
|
|||||||
font-size: 36px;
|
font-size: 36px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Navigation Bar Styles */
|
||||||
|
.nav-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
background-color: white;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
padding: 10px 0;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #666;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
color: #68543c;
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ export default function MapPage() {
|
|||||||
clearHuntTimer(); // Clean up all timer data
|
clearHuntTimer(); // Clean up all timer data
|
||||||
|
|
||||||
if (isMounted.current) {
|
if (isMounted.current) {
|
||||||
softRedirect("/leaderboard?huntEnded=true");
|
hardRedirect("/leaderboard?huntEnded=true");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-1
@@ -94,7 +94,12 @@ export default function WaitingPage() {
|
|||||||
// Clear any timer data
|
// Clear any timer data
|
||||||
clearHuntTimer();
|
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
|
// Save the current hunt data for next comparison
|
||||||
|
|||||||
Binary file not shown.
Reference in new issue
Block a user