Skip to content

Commit 4147dc0

Browse files
committed
Created Leaderboard Page
1 parent f1ece45 commit 4147dc0

5 files changed

Lines changed: 157 additions & 3 deletions

File tree

src/App.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Profile } from './pages/profile.js'
66
import { Register } from './pages/register.js'
77
import { Challenges } from './pages/challenges.js'
88
import { AdminPanel } from './pages/admin_panel.js'
9+
import { Leaderboard } from './pages/leaderboard.js'
910
import { RatingPage } from './pages/rate_challenge.js'
1011
import { ChallengeDetail } from './pages/challenge_template.js'
1112

@@ -17,11 +18,12 @@ function App() {
1718
<Routes>
1819
<Route path="/" element={<Home />} />
1920
<Route path="/admin" element={<Admin />} />
20-
<Route path="/admin/panel" element={<AdminPanel />} />
2121
<Route path="/login" element={<Login />} />
2222
<Route path="/profile" element={<Profile />} />
2323
<Route path="/register" element={<Register />} />
2424
<Route path="/compete" element={<Challenges />} />
25+
<Route path="/admin/panel" element={<AdminPanel />} />
26+
<Route path="/leaderboard" element={<Leaderboard />} />
2527
<Route path="/challenge" element={<ChallengeDetail />} />
2628
<Route path="/rate-challenge" element={<RatingPage />} />
2729
</Routes>

src/backend/db.js

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,74 @@ async function GetChallengeInfo(data) {
320320
}
321321
}
322322

323+
// if a player is not in a team calculate collected points and display their username
324+
// otherwise calculate points collected by the team and display the team name
325+
async function GetLeaderboardData() {
326+
// { name: STRING, points: NUMBER }
327+
// sort by point desending and if points match
328+
// sort based on time of recent completion (time attribute (time | timestamp) on last completion)
329+
let leaderboardData = []
330+
331+
const soloUsers = await UserCollection.find({ team_id: "None" }, { username: 1, completions: 1 })
332+
const teams = await TeamCollection.find({}, { name: 1, completions: 1 })
333+
334+
let readableSoloUsers = [];
335+
let readableTeams = [];
336+
337+
// for each user we need to calculate accumulated points
338+
for (let userDoc of soloUsers) {
339+
const user = userDoc.toObject(); // Make it modifiable
340+
341+
user.points = 0;
342+
user.name = user.username;
343+
344+
for (const completion of user.completions) {
345+
const challengeProfile = await ChallengeCollection.findOne({ _id: completion.id });
346+
if (challengeProfile) {
347+
user.points += challengeProfile.points;
348+
}
349+
}
350+
351+
user.recent = user.completions.at(-1)?.time ?? 0;
352+
delete user.completions;
353+
delete user.username;
354+
delete user._id;
355+
356+
readableSoloUsers.push(user); // Save modified copy
357+
}
358+
359+
// for each tean we need to calculate accumulated points
360+
for (let teamDoc of teams) {
361+
const team = teamDoc.toObject(); // Convert Mongoose document to plain object
362+
363+
team.points = 0;
364+
365+
for (const completion of team.completions) {
366+
team.points += completion.points;
367+
}
368+
369+
team.recent = team.completions.at(-1)?.timestamp ?? 0;
370+
delete team.completions;
371+
delete team._id;
372+
373+
readableTeams.push(team); // Store modified team object
374+
}
375+
376+
// merge both lists (readableSoloUsers & readableTeams) in sorted fashion
377+
const merged = [...readableSoloUsers, ...readableTeams];
378+
merged.sort((a, b) => {
379+
if (b.points !== a.points) {
380+
// Sort by points descending
381+
return b.points - a.points;
382+
} else {
383+
// If points are equal, sort by recent (epoch) ascending
384+
return a.recent - b.recent;
385+
}
386+
});
387+
388+
return merged;
389+
}
390+
323391
async function DoesExist(username, email) {
324392
username = SanitizeString(username);
325393
email = SanitizeString(email);
@@ -1719,4 +1787,4 @@ export { LoginUser, LoginAdmin, RegisterUser, GetUserProfile, UpdateUserProfile,
17191787
GetChallengeInfo, GetAllUsers, GetAllTeams, RemoveTeam,
17201788
RemoveUser, UpdateChallenge, AdminGetChallenges,
17211789
CreateChallenge, DeleteChallenge, RegisterAdmin, RemoveAdmin,
1722-
GetAdmins, ValidateAdmin };
1790+
GetAdmins, ValidateAdmin, GetLeaderboardData };

src/backend/server.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import GetChallenges, { LoginUser, LoginAdmin, RegisterUser,
66
GetAllUsers, GetAllTeams, RemoveTeam, RemoveUser,
77
UpdateChallenge, AdminGetChallenges, CreateChallenge,
88
DeleteChallenge, RegisterAdmin, RemoveAdmin, GetAdmins,
9-
ValidateAdmin } from './db.js';
9+
ValidateAdmin, GetLeaderboardData } from './db.js';
1010
import rateLimit from 'express-rate-limit';
1111
import cookieParser from 'cookie-parser';
1212
import sanitize from 'sanitize-filename';
@@ -484,6 +484,13 @@ app.post('/data/get-completions', async (req, res) => {
484484
}
485485
});
486486

487+
// leaderboard is publically accessible
488+
app.get('/data/leaderboard', async (req, res) => {
489+
const data = await GetLeaderboardData();
490+
console.log(data);
491+
return res.json(data)
492+
});
493+
487494
app.post('/rate-challenge', async (req, res) => {
488495
const token = req.cookies.khi_token;
489496
const data = req.body;

src/components/navbar.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ function Navbar() {
7676
<li className="nav-item">
7777
<Link className="nav-link" to="/compete">Compete</Link>
7878
</li>
79+
<li className="nav-item">
80+
<Link className="nav-link" to="/leaderboard">Leaderboard</Link>
81+
</li>
7982

8083
{authenticated ? (
8184
<>

src/pages/leaderboard.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React, { useState, useEffect } from 'react';
2+
import Navbar, { GetBackendHost } from '../components/navbar.js';
3+
import '../App.css';
4+
5+
export function Leaderboard() {
6+
const [leaderboard, setLeaderboard] = useState([]);
7+
8+
async function FetchLeaderboard() {
9+
try {
10+
const response = await fetch(`http://${GetBackendHost()}/data/leaderboard`, {
11+
method: "GET",
12+
credentials: 'include' // ensures cookies are sent
13+
});
14+
15+
const data = await response.json();
16+
setLeaderboard(data);
17+
} catch (error) {
18+
console.error("Error sending request:", error);
19+
}
20+
}
21+
22+
// runs periodically
23+
useEffect(() => {
24+
FetchLeaderboard()
25+
}, []); // [] means execute this once on page-load
26+
27+
return (
28+
<div className="App">
29+
<Navbar />
30+
<header
31+
className="App-header"
32+
style={{
33+
display: 'block',
34+
height: 'auto',
35+
paddingTop: '15px',
36+
}}>
37+
<h1>KHI Leaderboard</h1>
38+
39+
<div className="container mt-4 d-flex justify-content-center">
40+
<div className="card shadow-sm" style={{ minWidth: '200px', maxWidth: '500px', width: '100%' }}>
41+
<div className="card-header bg-primary text-white text-center fw-bold">
42+
Leaderboard
43+
</div>
44+
<ul className="list-group list-group-flush">
45+
{leaderboard.map((item, index) => {
46+
let badgeColor = "secondary";
47+
if (index === 0) badgeColor = "warning"; // Gold
48+
else if (index === 1) badgeColor = "secondary"; // Silver
49+
else if (index === 2) badgeColor = "danger"; // Bronze
50+
51+
return (
52+
<li
53+
key={index}
54+
className="list-group-item d-flex justify-content-between align-items-center px-4 py-2"
55+
style={{ fontSize: "0.95rem" }}
56+
>
57+
<span>
58+
<span className={`badge bg-${badgeColor} me-2`}>
59+
{index + 1}
60+
</span>
61+
{item.name}
62+
</span>
63+
<span className="text-muted fw-bold">{item.points} pts</span>
64+
</li>
65+
);
66+
})}
67+
</ul>
68+
</div>
69+
</div>
70+
71+
</header>
72+
</div>
73+
);
74+
}

0 commit comments

Comments
 (0)