Skip to content

Commit

Permalink
Added leaderboard
Browse files Browse the repository at this point in the history
  • Loading branch information
renatodellosso committed Dec 31, 2024
1 parent d8469b6 commit e66441c
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 0 deletions.
12 changes: 12 additions & 0 deletions components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import {
FaGithub,
FaInstagram,
FaList,
FaTrophy,
} from "react-icons/fa";
import { TbUfo } from "react-icons/tb";
import Link from "next/link";
import { MdAlternateEmail } from "react-icons/md";
import { HiStatusOnline } from "react-icons/hi";
import { useEffect, useState } from "react";
import Leaderboard from '../pages/leaderboard';

export default function Footer() {
const [swStatus, setSwStatus] = useState("Finding service worker...");
Expand Down Expand Up @@ -136,6 +138,16 @@ export default function Footer() {
/>
About Us
</Link>
<Link
className="link link-hover"
href="/leaderboard"
>
<FaTrophy
className="inline mr-1"
size={16}
/>
Leaderboard
</Link>
</nav>
<nav className="max-sm:pl-8">
<h6 className="footer-title">Debug</h6>
Expand Down
17 changes: 17 additions & 0 deletions lib/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,3 +578,20 @@ export class WebhookHolder {
this.url = url;
}
}

export type LeaderboardUser = {
_id: string;
name: string;
image: string;
xp: number;
level: number;
teams: string[];
};

export type LeaderboardTeam = {
_id: string;
name: string;
number: number;
league: League;
xp: number;
};
68 changes: 68 additions & 0 deletions lib/api/ClientApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
User,
Report,
WebhookHolder,
LeaderboardUser,
LeaderboardTeam,
} from "@/lib/Types";
import { NotLinkedToTba, removeDuplicates } from "../client/ClientUtils";
import {
Expand Down Expand Up @@ -2109,4 +2111,70 @@ export default class ClientApi extends NextApiTemplate<ApiDependencies> {
responseTime: Date.now() - times.responseTimestamp,
})),
);

getLeaderboard = createNextRoute<
[],
{ users: LeaderboardUser[]; teams: LeaderboardTeam[] },
ApiDependencies,
void
>({
isAuthorized: AccessLevels.AlwaysAuthorized,
handler: async (req, res, { db: dbPromise }, authData, args) => {
const db = await dbPromise;

const users = await db.findObjects<User>(CollectionId.Users, {
xp: { $gt: 0 },
});

const teams = await db.findObjects<Team>(CollectionId.Teams, {
_id: {
$in: users
.map((user) => user.teams.map((id) => new ObjectId(id)))
.flat(),
},
});

const leaderboardTeams = teams.reduce(
(acc, team) => {
acc[team._id!.toString()] = {
_id: team._id!.toString(),
name: team.name,
number: team.number,
league: team.league,
xp: 0,
};

return acc;
},
{} as { [_id: string]: LeaderboardTeam },
);

const leaderboardUsers = users
.map((user) => ({
_id: user._id!.toString(),
name: user.name?.split(" ")[0] ?? "Unknown",
image: user.image,
xp: user.xp,
level: user.level,
teams: user.teams
.map((id) => leaderboardTeams[id])
.map((team) => `${team.league} ${team.number}`),
}))
.sort((a, b) => b.xp - a.xp);

users.forEach((user) => {
user.teams.forEach((teamId) => {
leaderboardTeams[teamId].xp += user.xp;
});
});

const leaderboardTeamsArray = Object.values(leaderboardTeams).sort(
(a, b) => b.xp - a.xp,
);

return res
.status(200)
.send({ users: leaderboardUsers, teams: leaderboardTeamsArray });
},
});
}
101 changes: 101 additions & 0 deletions pages/leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import Card from "@/components/Card";
import Container from "@/components/Container";
import ClientApi from "@/lib/api/ClientApi";
import { useCurrentSession } from "@/lib/client/useCurrentSession";
import { LeaderboardTeam, LeaderboardUser } from "@/lib/Types";
import { useEffect, useState } from "react";

const api = new ClientApi();

export default function Leaderboard() {
const { session } = useCurrentSession();

const [usersOrTeam, setUsersOrTeam] = useState<"users" | "teams">("users");

const [userLeaderboard, setUserLeaderboard] = useState<LeaderboardUser[]>();
const [teamLeaderboard, setTeamLeaderboard] = useState<LeaderboardTeam[]>();

useEffect(() => {
api.getLeaderboard().then(({ users, teams }) => {
setUserLeaderboard(users);
setTeamLeaderboard(teams);
});
}, []);

return (
<Container
requireAuthentication={false}
title={"Scouting Leaderboard"}
>
<Card
title="Leaderboard"
coloredTop="bg-accent"
className="w-full m-6"
>
Here is every user/team ranked by their level and XP:
<div className="w-full">
<button
className={`w-1/2 btn btn-sm btn-${usersOrTeam == "users" ? "primary" : "ghost"}`}
onClick={() => setUsersOrTeam("users")}
>
Users
</button>
<button
className={`w-1/2 btn btn-sm btn-${usersOrTeam == "teams" ? "primary" : "ghost"}`}
onClick={() => setUsersOrTeam("teams")}
>
Teams
</button>
<table className="table">
<thead>
<tr>
<th>Rank</th>
<th>{usersOrTeam == "users" ? "User" : "Team"} </th>
{usersOrTeam == "users" && (
<>
<th>Teams</th>
<th>Level</th>
</>
)}
<th>XP</th>
</tr>
</thead>
{usersOrTeam == "users" &&
userLeaderboard &&
userLeaderboard.map((user, index) => (
<tr
key={user._id}
className={`${session?.user?._id == user._id && "text-primary"} hover:bg-base-100`}
>
<td>{index + 1}</td>
<td>
{user.name} {session?.user?._id == user._id ? "(You)" : ""}
</td>
<td>{user.teams.join(", ")}</td>
<td>{user.level}</td>
<td>{user.xp}</td>
</tr>
))}
{usersOrTeam == "teams" &&
teamLeaderboard &&
teamLeaderboard.map((team, index) => (
<tr
key={team._id}
className={`${session?.user?.teams.includes(team._id) && "text-primary"} hover:bg-base-100`}
>
<td>{index + 1}</td>
<td>
{team.name}{" "}
{session?.user?.teams.includes(team._id)
? "(Your Team)"
: ""}
</td>
<td>{team.xp}</td>
</tr>
))}
</table>
</div>
</Card>
</Container>
);
}

0 comments on commit e66441c

Please sign in to comment.