Skip to content

Commit

Permalink
Merge pull request #8 from BYU-CPC/multiple-leaderboards
Browse files Browse the repository at this point in the history
multiple-leaderboards
  • Loading branch information
joshbtay authored Sep 15, 2024
2 parents 5f5d1c0 + ab488c2 commit 7b1d33c
Show file tree
Hide file tree
Showing 16 changed files with 435 additions and 261 deletions.
37 changes: 34 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@babel/runtime": "^7.25.6",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@tanstack/react-query": "^5.32.0",
"@tanstack/react-query-persist-client": "^5.51.15",
Expand Down
5 changes: 4 additions & 1 deletion src/components/Countdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ const formatTime = (time: number) => {
function Countdown({
leaderboard,
className,
fontSize,
}: {
leaderboard: string;
className?: string;
fontSize?: string;
}) {
const { data } = useLeaderboardIndex();
const board = data?.[leaderboard];
const board = data.combined?.[leaderboard];
const currentTime = useDebounce(new Date(), 500)[0];
if (!board) return null;
const timeDisplay = formatTime(board.end.getTime() - currentTime.getTime());
Expand All @@ -39,6 +41,7 @@ function Countdown({
progress={progress}
display={timeDisplay}
className={className}
fontSize={fontSize}
/>
);
}
Expand Down
115 changes: 62 additions & 53 deletions src/components/Leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useProblems } from "../hooks/UseProblem";
import { useQueries } from "@tanstack/react-query";
import { getStats } from "../score/score";
import {
staticLeaderboardDisplayNames,
useCurrentLeaderboard,
useLeaderboard,
useLeaderboardIndex,
Expand All @@ -25,11 +26,12 @@ function formatCodeforcesId(input: string) {
export function Leaderboard() {
const user = useUser();
const users = useUsers();
const leaderboard = useCurrentLeaderboard();
const [leaderboard] = useCurrentLeaderboard();

const { data } = useLeaderboard(leaderboard);
const thisWeek = data?.thisWeek;
const allStudyProblems = data?.allProblems;
const thisWeek = data && "thisWeek" in data ? data.thisWeek : undefined;
const allStudyProblems =
data && "allProblems" in data ? data.allProblems : undefined;
const allProblemsLength =
(thisWeek?.kattis?.length ?? 0) + (thisWeek?.codeforces?.length ?? 0);
const solvedProblems: {
Expand All @@ -50,7 +52,7 @@ export function Leaderboard() {
const links = thisWeek?.links ?? {};
const { data: allProblems } = useProblems();
const { data: leaderboardIndex } = useLeaderboardIndex();
const leaderboardData = leaderboardIndex?.[leaderboard];
const leaderboardData = leaderboardIndex?.combined?.[leaderboard];
useEffect(() => {
document.title = leaderboardData?.name ?? "Leaderboard";
}, [leaderboardData?.name]);
Expand All @@ -59,11 +61,11 @@ export function Leaderboard() {
queries: users.map((user) => ({
queryKey: [leaderboard, "score", user.id],
queryFn: async () => {
return allProblems && allStudyProblems && leaderboardData
? getStats(user, allProblems, allStudyProblems, leaderboardData)
return allProblems && leaderboardData
? getStats(user, allProblems, leaderboardData, allStudyProblems)
: undefined;
},
enabled: !!allProblems && !!allStudyProblems && !!leaderboardData,
enabled: !!allProblems && !!leaderboardData,
})),
combine: (results) => results.map((r) => r.data).filter((a) => !!a),
});
Expand All @@ -77,9 +79,14 @@ export function Leaderboard() {
}
return (
<div className="gap-6 flex flex-col w-full items-center overflow-y-scroll pt-6 px-6 md:pt-0">
{!Object.keys(staticLeaderboardDisplayNames).includes(leaderboard) && (
<Countdown
className="w-full bg-secondary z-[-1] rounded-lg"
leaderboard={leaderboard}
/>
)}
{thisWeek?.topic && (
<div className="w-full bg-secondary flex flex-col static md:sticky top-0 z-20">
<Countdown leaderboard={leaderboard} />
<div className="w-full bg-secondary flex flex-col static z-20 mb-[-1.5rem] rounded-t-md">
<h4 className="large text-center">Weekly Topic: {thisWeek?.topic}</h4>
<div className="items-center flex flex-col">
<div className="flex-row flex items-center gap-3 mb-3">
Expand All @@ -92,52 +99,54 @@ export function Leaderboard() {
))}
</div>
</div>
<div className="flex flex-row gap-2">
{!!thisWeek?.kattis &&
thisWeek.kattis.map((problemId: string) => (
<div
className={`center rounded-md truncate shrink-tiny ${
solvedProblems.kattis.get(problemId) === 1
? "outline-green"
: solvedProblems.kattis.get(problemId) === 2
? "bg-green"
: ""
}`}
style={{ width: `${100 / allProblemsLength}%` }}
key={problemId}
</div>
)}
{thisWeek && (
<div className="flex flex-row gap-2 md:sticky top-0 w-full z-20 bg-secondary rounded-b-md">
{!!thisWeek?.kattis &&
thisWeek.kattis.map((problemId: string) => (
<div
className={`center rounded-md truncate shrink-tiny ${
solvedProblems.kattis.get(problemId) === 1
? "outline-green"
: solvedProblems.kattis.get(problemId) === 2
? "bg-green"
: ""
}`}
style={{ width: `${100 / allProblemsLength}%` }}
key={problemId}
>
<a
href={`https://open.kattis.com/problems/${problemId}`}
className="fg-color underline "
>
<a
href={`https://open.kattis.com/problems/${problemId}`}
className="fg-color underline "
>
{problemId}
</a>
</div>
))}
{!!thisWeek?.codeforces &&
thisWeek.codeforces.map((problemId: string) => (
<div
className={`center rounded-md truncate shrink-tiny ${
solvedProblems.codeforces.get(problemId) === 1
? "outline-green"
: solvedProblems.codeforces.get(problemId) === 2
? "bg-green"
: ""
}`}
style={{ width: `${100 / allProblemsLength}%` }}
key={problemId}
{problemId}
</a>
</div>
))}
{!!thisWeek?.codeforces &&
thisWeek.codeforces.map((problemId: string) => (
<div
className={`center rounded-md truncate shrink-tiny ${
solvedProblems.codeforces.get(problemId) === 1
? "outline-green"
: solvedProblems.codeforces.get(problemId) === 2
? "bg-green"
: ""
}`}
style={{ width: `${100 / allProblemsLength}%` }}
key={problemId}
>
<a
href={`https://codeforces.com/problemset/problem/${formatCodeforcesId(
problemId
)}`}
className="fg-color underline "
>
<a
href={`https://codeforces.com/problemset/problem/${formatCodeforcesId(
problemId
)}`}
className="fg-color underline "
>
{problemId}
</a>
</div>
))}
</div>
{problemId}
</a>
</div>
))}
</div>
)}
{calculatedUsers
Expand Down
7 changes: 4 additions & 3 deletions src/components/LeaderboardRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ type LeaderboardRowProps = {

export function LeaderboardRow({ userStats, rank, isMe }: LeaderboardRowProps) {
const userId = userStats.user.id;
const leaderboard = useCurrentLeaderboard();
const [leaderboard] = useCurrentLeaderboard();
const { data } = useLeaderboard(leaderboard);
const thisWeek = data?.thisWeek;
const thisWeek = data && "thisWeek" in data ? data.thisWeek : undefined;
const allProblemsLength =
(thisWeek?.kattis?.length ?? 0) + (thisWeek?.codeforces?.length ?? 0);
const solvedKattis = new Set(Object.keys(userStats.user.kattis_submissions));
Expand All @@ -48,7 +48,7 @@ export function LeaderboardRow({ userStats, rank, isMe }: LeaderboardRowProps) {
);

return (
<div className="bg-secondary w-full">
<div className="bg-secondary w-full rounded-md overflow-hidden flex-none ">
<div className={"flex flex-col " + (isMe ? "highlight" : "")}>
<div className="flex flex-row gap-2">
{!!thisWeek?.kattis &&
Expand Down Expand Up @@ -157,6 +157,7 @@ export function LeaderboardRow({ userStats, rank, isMe }: LeaderboardRowProps) {
</div>
</div>
<ProgressBar
className="translate-y-[-1px]"
progress={
userStats.level.currentExp /
(userStats.level.currentExp + userStats.level.nextLevel)
Expand Down
99 changes: 99 additions & 0 deletions src/components/LeaderboardSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
useCurrentLeaderboard,
useLeaderboardIndex,
} from "../hooks/UseLeaderboard";
import React, { useEffect } from "react";
import Countdown from "./Countdown";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useIsMobile } from "../hooks/UseIsMobile";
import DownArrow from "../icons/DownArrow";

export default function LeaderboardSelector() {
const { data } = useLeaderboardIndex();
const [leaderboard, setLeaderboard] = useCurrentLeaderboard();
const isMobile = useIsMobile();
const [open, setOpen] = React.useState(!isMobile);
useEffect(() => {
setOpen(!isMobile);
}, [isMobile]);
const leaderboardItems = Object.entries(data.combined).map(([key, board]) => (
<LeaderboardSelectorItem
id={key}
key={key}
setLeaderboard={(leaderboard) => setLeaderboard({ leaderboard })}
isSelected={key === leaderboard}
isDynamic={data.dynamic.hasOwnProperty(key)}
name={board.name ?? key}
/>
));
return (
<Collapsible.Root open={open} onOpenChange={setOpen} className="w-full">
<Collapsible.Trigger className="w-full mb-2">
<div className="w-full flex flex-row items-center gap-2">
<LeaderboardSelectorItem
id={leaderboard}
isSelected={false}
isDynamic={false}
name={data.combined[leaderboard]?.name ?? leaderboard}
setLeaderboard={() => {}}
/>
<div
className={"transition-all "}
style={{
transform: open ? "scaleY(-1)" : "scaleY(1)",
}}
>
<DownArrow height="16" width="16" fill="var(--fg-color)" />
</div>
</div>
</Collapsible.Trigger>
<div className="flex flex-col w-full gap-2">
{leaderboardItems.map((item) => (
<Collapsible.Content
key={item.key}
className="CollapsibleContent w-full"
>
{item}
</Collapsible.Content>
))}
</div>
</Collapsible.Root>
);
}

const LeaderboardSelectorItem = ({
id,
isSelected,
isDynamic,
name,
setLeaderboard,
}: {
id: string;
isSelected: boolean;
isDynamic: boolean;
name: string;
setLeaderboard: (leaderboard: string) => void;
}) => {
return (
<button
style={{
background: isSelected
? `linear-gradient(90deg, var(--accent-color) 0%, var(--accent-color-two) 100%)`
: undefined,
}}
className="flex flex-col border-none gap-2 py-1 rounded-md bg-[var(--bg-tertiary)] hover:bg-[var(--accent-half-opacity)] transition-all w-full items-center"
onClick={() => setLeaderboard(id)}
>
<span className=" text-nowrap">{name}</span>
{isDynamic && (
<Countdown
className={
"w-full translate-y-[-7px] " + (isSelected ? "opacity-0" : "")
}
fontSize="7px"
leaderboard={id}
/>
)}
</button>
);
};
Loading

0 comments on commit 7b1d33c

Please sign in to comment.