mirror of
https://github.com/mudabbir-ahmad/peworkshop.git
synced 2026-10-07 19:50:20 +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
+440
-62
No files matched your search
+27
-55
@@ -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();
|
||||
|
||||
@@ -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 (
|
||||
<div className="leaderboard-page loading">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>Loading leaderboard...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="leaderboard-page">
|
||||
@@ -29,11 +84,248 @@ export default function LeaderboardPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Rest of the leaderboard content */}
|
||||
{/* ...existing code */}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* 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>{`
|
||||
/* ...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 {
|
||||
background-color: #f8d7da;
|
||||
@@ -49,6 +341,42 @@ export default function LeaderboardPage() {
|
||||
font-size: 36px;
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user