Files
peworkshop/app/(main)/leaderboard/page.js
T
bobbert 8d0bca1d87 fixed a bunch of styling and component usage
made sure almost all viewer pages use the navbar component properly instead of using their hardcoded version, started to match the styling of the design for a lot of pages
2025-03-23 14:33:53 +00:00

411 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";
import NavBar from "@/components/NavBar";
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 && (
<NavBar
activeLink="leaderboard"
teamData={{ id: session?.user?.teamId }}
/>
)}
{/* 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-image: url("/images/Bottom-Bar.svg");
background-repeat: no-repeat;
background-size: cover;
background-position: center;
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>
);
}