mirror of
https://github.com/mudabbir-ahmad/peworkshop.git
synced 2026-10-07 19:50:20 +00:00
428 lines
11 KiB
JavaScript
428 lines
11 KiB
JavaScript
"use client";
|
|
import { useState, useEffect } from "react";
|
|
import { useSearchParams } from "next/navigation";
|
|
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 { data: session, status } = useSession(); // Fix: Properly destructure session from useSession()
|
|
|
|
// 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 (
|
|
<div className="leaderboard-page">
|
|
<div className="page-header">
|
|
<div className="back-button"></div>
|
|
<div className="page-title">
|
|
{huntJustEnded ? "Final Results" : "Leaderboard"}
|
|
</div>
|
|
</div>
|
|
|
|
{huntJustEnded && (
|
|
<div className="hunt-ended-alert">
|
|
<div className="hunt-ended-icon">🎯</div>
|
|
<h2>The Hunt Has Ended!</h2>
|
|
<p>
|
|
The timer has been stopped by an administrator. Here are the final
|
|
results.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{error ? (
|
|
<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>
|
|
)}
|
|
|
|
{/* Only show navigation bar when the hunt hasn't ended */}
|
|
{!huntJustEnded && (
|
|
<div className="nav-bar">
|
|
<Link href="/map" className="nav-item">
|
|
<div className="nav-icon">🗺️</div>
|
|
<div className="nav-label">Map</div>
|
|
</Link>
|
|
<Link href="/stats" className="nav-item">
|
|
<div className="nav-icon">📊</div>
|
|
<div className="nav-label">Stats</div>
|
|
</Link>
|
|
<Link
|
|
href={
|
|
session?.user?.teamId
|
|
? `/${session.user.teamId}`
|
|
: "/team-selection"
|
|
}
|
|
className="nav-item"
|
|
>
|
|
<div className="nav-icon">👥</div>
|
|
<div className="nav-label">Team</div>
|
|
</Link>
|
|
<Link href="/leaderboard" className="nav-item active">
|
|
<div className="nav-icon">🏆</div>
|
|
<div className="nav-label">Leaderboard</div>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
|
|
{/* Add a "back to home" button when the hunt has ended */}
|
|
{huntJustEnded && (
|
|
<div className="hunt-ended-actions">
|
|
<button
|
|
onClick={() => (window.location.href = "/team-selection")}
|
|
className="back-to-home-button"
|
|
>
|
|
Back to Home
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<style jsx>{`
|
|
.leaderboard-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100vh;
|
|
background-color: #f4f4f4;
|
|
padding-bottom: ${huntJustEnded ? "0" : "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 {
|
|
background-color: #f8d7da;
|
|
color: #721c24;
|
|
border: 1px solid #f5c6cb;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.hunt-ended-icon {
|
|
font-size: 46px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.hunt-ended-actions {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin: 30px 0;
|
|
}
|
|
|
|
.back-to-home-button {
|
|
background-color: #68543c;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
padding: 12px 24px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
transition: background-color 0.3s;
|
|
}
|
|
|
|
.back-to-home-button:hover {
|
|
background-color: #53402d;
|
|
}
|
|
|
|
/* 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>
|
|
</div>
|
|
);
|
|
}
|