Skip to content

Commit 595260e

Browse files
committed
feat: add leaderboard
1 parent 1454354 commit 595260e

File tree

7 files changed

+410
-5
lines changed

7 files changed

+410
-5
lines changed

app/globals.css

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
@import "tailwindcss";
22

3-
html, body, #__next {
3+
html,
4+
body,
5+
#__next {
46
height: 100%;
57
}
8+
69
body {
710
position: relative;
811
min-height: 100%;
912
background-color: var(--bg-color, #0b0b0b);
10-
color: #ffffff; /* default text color set to white */
13+
color: #ffffff;
14+
/* default text color set to white */
1115
}
1216

1317
/* Use a fixed pseudo-element so we can control opacity/blur separately */
@@ -20,19 +24,45 @@ body::before {
2024
background-position: center;
2125
background-repeat: no-repeat;
2226
background-attachment: fixed;
23-
opacity: 0.45; /* lower this to hide more noise */
27+
opacity: 0.45;
28+
/* lower this to hide more noise */
2429
filter: blur(1.2px) saturate(0.8) contrast(0.95);
2530
z-index: -1;
2631
pointer-events: none;
2732
}
2833

2934
/* Ensure content sits above the background */
30-
main, #__next > div {
35+
main,
36+
#__next>div {
3137
position: relative;
3238
}
3339

3440
/* Optional: provide a subtle overlay class if pages need higher contrast */
35-
/* .bg-overlay {
41+
/* .bg-overlay {
3642
background-color: rgba(254, 12, 12, 0.35);
3743
} */
3844

45+
46+
@layer utilities {
47+
.text-shadow-neon {
48+
text-shadow: 0 0 5px rgba(132, 204, 22, 0.5), 0 0 10px rgba(132, 204, 22, 0.3);
49+
}
50+
51+
.custom-scrollbar::-webkit-scrollbar {
52+
width: 6px;
53+
height: 6px;
54+
}
55+
56+
.custom-scrollbar::-webkit-scrollbar-track {
57+
background: #1a1a1a;
58+
}
59+
60+
.custom-scrollbar::-webkit-scrollbar-thumb {
61+
background: #333;
62+
border-radius: 3px;
63+
}
64+
65+
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
66+
background: #444;
67+
}
68+
}

app/leaderboard/page.tsx

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"use client";
2+
3+
import React, { useEffect, useState } from 'react';
4+
import Navbar from "@/components/Navbar";
5+
import { useAuth } from "@/hooks/useAuth";
6+
import TopThree from '@/components/leaderboard/TopThree';
7+
import LeaderboardTable from '@/components/leaderboard/LeaderboardTable';
8+
import { Team } from '@/components/leaderboard/data';
9+
10+
export default function LeaderboardPage() {
11+
const [teams, setTeams] = useState<Team[]>([]);
12+
const [loading, setLoading] = useState(true);
13+
const [error, setError] = useState<string | null>(null);
14+
15+
const { isAuthenticated, isLoading: authLoading, login } = useAuth();
16+
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
17+
18+
useEffect(() => {
19+
if (authLoading) return;
20+
21+
if (!isAuthenticated) {
22+
setLoading(false);
23+
return;
24+
}
25+
26+
const fetchLeaderboard = async () => {
27+
try {
28+
const res = await fetch(`${API_URL}/leaderboard`);
29+
if (!res.ok) {
30+
throw new Error('Failed to fetch leaderboard data');
31+
}
32+
const data = await res.json();
33+
34+
if (Array.isArray(data)) {
35+
setTeams(data);
36+
} else if (Array.isArray(data.data)) {
37+
setTeams(data.data);
38+
} else {
39+
setTeams([]);
40+
console.error("Unexpected data format", data);
41+
}
42+
} catch (err) {
43+
console.error("Error fetching leaderboard:", err);
44+
setError("Failed to load leaderboard data.");
45+
} finally {
46+
setLoading(false);
47+
}
48+
};
49+
50+
fetchLeaderboard();
51+
}, [API_URL, isAuthenticated, authLoading]);
52+
53+
if (authLoading) {
54+
return <div className="min-h-screen bg-[#0b0b0b] flex items-center justify-center text-white font-mono">[ AUTH_CHECK... ]</div>;
55+
}
56+
57+
if (!isAuthenticated) {
58+
return (
59+
<>
60+
<Navbar />
61+
<main className="min-h-screen w-full bg-[#0b0b0b] text-white pt-24 pb-12 px-4 md:px-8 relative overflow-hidden flex flex-col items-center justify-center">
62+
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 pointer-events-none"></div>
63+
<div className="z-10 text-center border border-red-500/30 p-8 rounded-lg bg-red-900/10 backdrop-blur">
64+
<h1 className="text-2xl font-bold font-mono text-red-500 mb-4">[ ACCESS_DENIED ]</h1>
65+
<p className="text-gray-400 font-mono mb-6">Restricted Area // Authorization Required</p>
66+
<button
67+
onClick={login}
68+
className="bg-[#CCFF00] text-black px-6 py-2 rounded font-bold tracking-widest hover:bg-[#b8e600] transition"
69+
>
70+
LOGIN_TO_ACCESS
71+
</button>
72+
</div>
73+
</main>
74+
</>
75+
);
76+
}
77+
78+
const topTeams = teams.slice(0, 3);
79+
const restTeams = teams.slice(3);
80+
81+
return (
82+
<>
83+
<Navbar />
84+
<main className="min-h-screen w-full bg-[#0b0b0b] text-white pt-24 pb-12 px-4 md:px-8 relative overflow-hidden">
85+
{/* Background Noise/Grid Overlay */}
86+
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 pointer-events-none"></div>
87+
88+
<div className="max-w-7xl mx-auto relative z-10 flex flex-col items-center">
89+
90+
{/* Header Section */}
91+
<div className="text-center mb-12 w-full">
92+
<h1 className="text-4xl md:text-6xl font-black tracking-tighter uppercase mb-4 font-mono">
93+
LEADERBOARD<span className="text-gray-600 mx-2">//</span>
94+
<span className="text-lime-400 text-shadow-neon">GLOBAL_NET_STATS</span>
95+
</h1>
96+
97+
<div className="flex justify-between items-center max-w-4xl mx-auto text-xs md:text-sm font-mono text-gray-400 border-t border-b border-gray-800 py-2 mt-4">
98+
<span>[ STATUS=<span className="text-lime-500">LIVE_EVALUATION</span> ]</span>
99+
<div className="flex items-center">
100+
<span>TOTAL_CLUSTERS= {teams.length}</span>
101+
<div className={`w-2 h-2 ${loading ? 'bg-yellow-500' : 'bg-lime-500'} rounded-full ml-2 animate-pulse`}></div>
102+
</div>
103+
</div>
104+
</div>
105+
106+
{loading ? (
107+
<div className="flex flex-col items-center justify-center h-64 font-mono text-lime-400">
108+
<div className="w-16 h-16 border-4 border-lime-400 border-t-transparent rounded-full animate-spin mb-4"></div>
109+
<div>[ LOADING_DATA_STREAM... ]</div>
110+
</div>
111+
) : error ? (
112+
<div className="text-red-500 font-mono text-center border border-red-900/50 bg-red-900/10 p-4 rounded">
113+
[ ERROR: {error} ]
114+
</div>
115+
) : (
116+
<>
117+
{/* Top 3 Section */}
118+
{topTeams.length > 0 && <TopThree teams={topTeams} />}
119+
120+
{/* Data Grid Section */}
121+
<LeaderboardTable teams={restTeams} />
122+
</>
123+
)}
124+
125+
</div>
126+
</main>
127+
</>
128+
);
129+
}

components/Hero3D.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ export default function Hero3D() {
235235
}, []);
236236

237237
// Reset error boundary periodically to recover from WebGL context loss
238+
// Reset error boundary periodically to recover from WebGL context loss
239+
// REMOVED: Aggressive reset on visibility change caused random disappearances.
240+
// Instead we rely on standard WebGL context restoration events handled below.
241+
/*
238242
useEffect(() => {
239243
const handleVisibilityChange = () => {
240244
if (document.visibilityState === 'visible') {
@@ -245,6 +249,7 @@ export default function Hero3D() {
245249
document.addEventListener('visibilitychange', handleVisibilityChange);
246250
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
247251
}, []);
252+
*/
248253

249254
// Handle WebGL context loss/restore
250255
const handleCreated = ({ gl }: { gl: THREE.WebGLRenderer }) => {

components/Navbar.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export default function Navbar() {
1717
setActive("home");
1818
} else if (pathname.includes("dashboard")) {
1919
setActive("dashboard");
20+
} else if (pathname.includes("leaderboard")) {
21+
setActive("leaderboard");
2022
} else {
2123
setActive("");
2224
}
@@ -123,6 +125,22 @@ export default function Navbar() {
123125
</button>
124126
)}
125127

128+
{isAuthenticated && (
129+
<button
130+
onClick={() => {
131+
if (pathname === '/leaderboard') return;
132+
window.location.href = "/leaderboard";
133+
}}
134+
className={`relative text-sm md:text-base tracking-wide md:tracking-wider px-2 md:px-3 py-1 font-medium transition ${pathname === "/leaderboard" ? "text-[#CCFF00]" : "text-white/80 hover:text-white"
135+
}`}
136+
>
137+
LEADERBOARD
138+
{pathname === "/leaderboard" && (
139+
<span className="absolute -bottom-2 left-0 h-1 w-full bg-[#CCFF00] rounded" />
140+
)}
141+
</button>
142+
)}
143+
126144
<button
127145
onClick={() => scrollTo("timeline")}
128146
className={`relative text-sm md:text-base tracking-wide md:tracking-wider px-2 md:px-3 py-1 font-medium transition ${active === "timeline" ? "text-[#CCFF00]" : "text-white/80 hover:text-white"
@@ -235,6 +253,21 @@ export default function Navbar() {
235253
</button>
236254
)}
237255

256+
{isAuthenticated && (
257+
<button
258+
onClick={() => {
259+
if (pathname === '/leaderboard') {
260+
setIsMenuOpen(false);
261+
return;
262+
}
263+
window.location.href = "/leaderboard";
264+
}}
265+
className={`text-lg font-bold tracking-widest hover:text-[#CCFF00] transition-colors ${pathname === "/leaderboard" ? "text-[#CCFF00]" : "text-white"}`}
266+
>
267+
LEADERBOARD
268+
</button>
269+
)}
270+
238271
<button
239272
onClick={() => scrollTo("timeline")}
240273
className={`text-lg font-bold tracking-widest hover:text-[#CCFF00] transition-colors ${active === "timeline" ? "text-[#CCFF00]" : "text-white"}`}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"use client";
2+
3+
import React, { useState } from 'react';
4+
import { Team } from './data';
5+
import { FaCaretUp, FaCaretDown, FaMinus } from 'react-icons/fa';
6+
7+
export default function LeaderboardTable({ teams }: { teams: Team[] }) {
8+
const [searchTerm, setSearchTerm] = useState('');
9+
10+
const filteredTeams = teams.filter(team =>
11+
team.teamName.toLowerCase().includes(searchTerm.toLowerCase())
12+
);
13+
14+
return (
15+
<div className="w-full max-w-4xl border border-gray-700 rounded-lg p-6 bg-black/50 backdrop-blur-md">
16+
{/* Header / Search Bar */}
17+
<div className="flex flex-col md:flex-row justify-between items-center mb-6 gap-4">
18+
<h2 className="text-xl font-mono text-white tracking-widest">DATA_GRID</h2>
19+
<div className="flex items-center w-full md:w-auto">
20+
<span className="text-lime-400 font-mono mr-2 bg-lime-900/20 px-1 border border-lime-800 text-sm">&gt;_SEARCH_FILTER:</span>
21+
<input
22+
type="text"
23+
placeholder="[ Enter_Team_Name ]"
24+
className="bg-transparent border-b border-gray-600 text-gray-300 font-mono focus:outline-none focus:border-lime-500 w-full md:w-64 px-2 py-1 placeholder-gray-600"
25+
value={searchTerm}
26+
onChange={(e) => setSearchTerm(e.target.value)}
27+
/>
28+
</div>
29+
</div>
30+
31+
{/* Table Header */}
32+
<div className="grid grid-cols-12 gap-2 text-gray-500 font-mono text-xs uppercase tracking-wider mb-2 px-4">
33+
<div className="col-span-2">Rank <span className="mx-1 text-gray-700">//</span></div>
34+
<div className="col-span-4">Team_Name <span className="mx-1 text-gray-700">//</span></div>
35+
<div className="col-span-3 text-right">Score <span className="mx-1 text-gray-700">//</span></div>
36+
<div className="col-span-3 text-right">Submitted_At <span className="mx-1 text-gray-700">//</span></div>
37+
</div>
38+
39+
<div className="border-b border-gray-800 mb-4"></div>
40+
41+
{/* Table Rows */}
42+
<div className="space-y-2 max-h-[500px] overflow-y-auto pr-2 custom-scrollbar">
43+
{filteredTeams.map((team) => (
44+
<div key={team.rank} className="group relative">
45+
{/* Row Background & Border */}
46+
<div className="absolute inset-0 border border-gray-700 rounded bg-gray-900/30 group-hover:bg-gray-800/50 transition-colors pointer-events-none"></div>
47+
48+
{/* Content */}
49+
<div className="grid grid-cols-12 gap-2 items-center px-4 py-3 font-mono text-sm text-gray-300 relative z-10">
50+
<div className="col-span-2 text-gray-400">#{String(team.rank).padStart(3, '0')}</div>
51+
<div className="col-span-4 text-white font-semibold">{team.teamName}</div>
52+
<div className="col-span-3 text-right text-lime-400 font-bold">{team.score ? team.score.toFixed(4) : "0"}</div>
53+
<div className="col-span-3 text-right text-gray-500 text-xs">
54+
{team.submittedAt ? new Date(team.submittedAt).toLocaleString() : 'Not Submitted'}
55+
</div>
56+
</div>
57+
58+
{/* Hover Highlight (Left Bar) */}
59+
<div className="absolute left-0 top-0 bottom-0 w-1 bg-lime-500 opacity-0 group-hover:opacity-100 transition-opacity rounded-l"></div>
60+
</div>
61+
))}
62+
63+
{filteredTeams.length === 0 && (
64+
<div className="text-center py-8 text-gray-500 font-mono">
65+
[ NO_DATA_FOUND ]
66+
</div>
67+
)}
68+
</div>
69+
70+
</div>
71+
);
72+
}

0 commit comments

Comments
 (0)