Skip to content

Commit 9594c59

Browse files
authored
Merge pull request #78 from crux-bphc/feat/potd-stats
Potd Hall of Fame and solve count stats
2 parents aeb9d44 + 4032204 commit 9594c59

3 files changed

Lines changed: 143 additions & 21 deletions

File tree

backend/src/controllers/potd.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
users,
99
} from "../drizzle/schema";
1010
import { getRandomProblem, verifySubmission } from "../codeforces_api";
11-
import { eq, gte, desc, and } from "drizzle-orm";
11+
import { eq, gte, desc, and, sql } from "drizzle-orm";
1212

1313
// GET /potd/current
1414
export async function getCurrentPotd(
@@ -51,7 +51,19 @@ export async function getCurrentPotd(
5151
.where(and(eq(potdSolves.userId, userId), eq(potdSolves.potdId, potdId)))
5252
.limit(1);
5353

54-
res.json({ problem: problemRow, solved: solve.length > 0 });
54+
// how many total solves for this POTD?
55+
const [countRow] = await db
56+
.select({
57+
solveCount: sql<number>`COUNT(${potdSolves.id})`,
58+
})
59+
.from(potdSolves)
60+
.where(eq(potdSolves.potdId, potdId));
61+
62+
res.json({
63+
problem: problemRow,
64+
solved: solve.length > 0,
65+
solveCount: countRow.solveCount,
66+
});
5567
} catch (err) {
5668
next(err);
5769
}
@@ -266,3 +278,32 @@ export async function getSolveHistory(
266278
next(err);
267279
}
268280
}
281+
282+
export async function getPotdStats(
283+
req: Request,
284+
res: Response,
285+
next: NextFunction
286+
) {
287+
try {
288+
const topSolvers = await db
289+
.select({
290+
user: users,
291+
solveCount: sql<number>`COUNT(${potdSolves.id})`,
292+
})
293+
.from(potdSolves)
294+
.innerJoin(users, eq(users.id, potdSolves.userId))
295+
.groupBy(
296+
users.id,
297+
users.name,
298+
users.cfHandle,
299+
users.email,
300+
users.createdAt
301+
)
302+
.orderBy(desc(sql<number>`COUNT(${potdSolves.id})`))
303+
.limit(3);
304+
305+
res.json({ topSolvers });
306+
} catch (err) {
307+
next(err);
308+
}
309+
}

backend/src/routes/potd.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
getScheduledPotd,
66
verifyPotdSolve,
77
getSolveHistory,
8+
getPotdStats,
89
} from "../controllers/potd";
910
import { requireAuth, requireCruxMember } from "../middlewares/auth";
1011

@@ -17,5 +18,5 @@ router.get("/schedule", requireCruxMember, getScheduledPotd);
1718
router.get("/current", requireAuth, getCurrentPotd);
1819
router.post("/verify-solve", requireAuth, verifyPotdSolve);
1920
router.get("/solve-history", requireAuth, getSolveHistory);
20-
21+
router.get("/stats", requireAuth, getPotdStats);
2122
export default router;

frontend/src/routes/potd.tsx

Lines changed: 98 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import {
99
Info,
1010
AlertCircle,
1111
Lightbulb,
12+
Trophy,
1213
} from "lucide-react";
1314
import ProfileHeader from "../components/ProfileHeader";
1415
import { useAuth } from "../context/AuthContext";
1516
import POTDStreakHeatmap from "../components/POTDStreakHeatmap";
1617
import LoadingIndicator from "../components/LoadingIndicator";
18+
import type { User } from "../types/User";
1719

1820
export const Route = createFileRoute("/potd")({
1921
component: RouteComponent,
@@ -28,21 +30,33 @@ interface Problem {
2830
tags: string[];
2931
}
3032

33+
interface TopSolver {
34+
user: User;
35+
solveCount: number;
36+
}
37+
3138
export function RouteComponent() {
3239
const [problem, setProblem] = useState<Problem | null>(null);
3340
const [solved, setSolved] = useState(false);
41+
const [solveCount, setSolveCount] = useState<number>(0);
3442
const [showTags, setShowTags] = useState(false);
3543
const [checking, setChecking] = useState(false);
3644
const [message, setMessage] = useState<string | null>(null);
45+
const [showTop, setShowTop] = useState(true);
46+
const [topSolver, setTopSolver] = useState<TopSolver | null>(null);
47+
const [loadingTop, setLoadingTop] = useState(false);
3748

3849
const { user } = useAuth();
3950

4051
useEffect(() => {
52+
fetchTopSolver();
53+
4154
fetch(`${import.meta.env.VITE_API_BASE_URL}/potd/current`)
4255
.then((res) => res.json())
4356
.then((data) => {
4457
setProblem(data.problem);
4558
setSolved(data.solved);
59+
setSolveCount(data.solveCount);
4660
})
4761
.catch(() => setProblem(null));
4862
}, []);
@@ -67,6 +81,7 @@ export function RouteComponent() {
6781
setMessage(json.message);
6882
if (res.ok) {
6983
setSolved(true);
84+
fetchTopSolver();
7085
}
7186
} catch {
7287
setMessage("Error checking solve");
@@ -75,6 +90,27 @@ export function RouteComponent() {
7590
}
7691
};
7792

93+
const fetchTopSolver = async () => {
94+
setLoadingTop(true);
95+
try {
96+
const res = await fetch(
97+
`${import.meta.env.VITE_API_BASE_URL}/potd/stats`
98+
);
99+
const { topSolvers } = await res.json();
100+
if (topSolvers.length) setTopSolver(topSolvers[0]);
101+
} catch {
102+
// ignore
103+
} finally {
104+
setLoadingTop(false);
105+
}
106+
};
107+
108+
const toggleTop = () => {
109+
const next = !showTop;
110+
setShowTop(next);
111+
if (next && !topSolver) fetchTopSolver();
112+
};
113+
78114
if (!problem) {
79115
return <LoadingIndicator />;
80116
}
@@ -101,12 +137,15 @@ export function RouteComponent() {
101137
<div className="mx-auto">
102138
<div className="flex flex-col lg:flex-row gap-6">
103139
{/* POTD Card */}
104-
<div className="flex-1 bg-highlight-dark rounded-lg p-4 lg:p-6">
140+
<div className="flex-1 bg-highlight-dark rounded-lg p-4 lg:p-6 flex flex-col justify-between">
105141
<div className="flex items-start justify-between mb-4">
106142
<div>
107143
<h2 className="text-3xl text-white mb-1">{problem.name}</h2>
108144
<div className="text-muted text-sm">
109-
Contest {problem.contestId} • Problem {problem.index}
145+
Contest {problem.contestId} • Problem {problem.index} • Solved
146+
by <span className="text-white">{solveCount} </span>{" "}
147+
{solveCount == 1 ? "person " : "people "}
148+
today
110149
</div>
111150
</div>
112151
<button
@@ -194,23 +233,64 @@ export function RouteComponent() {
194233

195234
{/* How it works */}
196235
<div className="lg:w-1/3 bg-highlight-dark rounded-lg p-4 lg:p-6">
197-
<h3 className="text-lg text-white mb-3 flex items-center gap-1">
198-
<Lightbulb className="w-4 h-4" /> How it works
199-
</h3>
200-
<div className="space-y-2 text-muted text-xs">
201-
<div className="flex items-start gap-2">
202-
<span className="text-accent-purple">1.</span>
203-
<span>Click "Solve" to open the problem</span>
204-
</div>
205-
<div className="flex items-start gap-2">
206-
<span className="text-accent-purple">2.</span>
207-
<span>Submit your solution on Codeforces</span>
208-
</div>
209-
<div className="flex items-start gap-2">
210-
<span className="text-accent-purple">3.</span>
211-
<span>Return and click "Verify" to check your answer</span>
212-
</div>
236+
<div className="flex items-center justify-between mb-3">
237+
<h3 className="text-lg text-white flex items-center gap-1">
238+
{showTop ? (
239+
<Trophy className="w-5 h-5 mr-2 text-accent-yellow" />
240+
) : (
241+
<Lightbulb className="w-5 h-5 " />
242+
)}{" "}
243+
{showTop ? "Hall of Fame" : "How it works"}
244+
</h3>
245+
<button
246+
onClick={toggleTop}
247+
className="text-xs text-accent-yellow hover:underline hover:cursor-pointer"
248+
>
249+
{showTop ? "How to solve?" : "See Top"}
250+
</button>
213251
</div>
252+
253+
{showTop ? (
254+
loadingTop ? (
255+
<LoadingIndicator />
256+
) : topSolver ? (
257+
<div className="p-2 flex flex-col items-center gap-3 max-w-xs mx-auto">
258+
<img
259+
src={topSolver.user.pfpUrl}
260+
className="w-12 h-12 rounded-full"
261+
/>
262+
<div className="flex items-center gap-1">
263+
<h4 className="text-lg font-semibold text-white">
264+
{topSolver.user.name}
265+
</h4>
266+
</div>
267+
<p className="text-base text-muted">
268+
has solved{" "}
269+
<span className="font-bold text-accent-yellow">
270+
{topSolver.solveCount}
271+
</span>{" "}
272+
POTD{topSolver.solveCount > 1 ? "s" : ""}
273+
</p>
274+
</div>
275+
) : (
276+
<div className="text-muted text-sm">No solves yet.</div>
277+
)
278+
) : (
279+
<div className="space-y-2 text-muted text-xs">
280+
<div className="flex items-start gap-2">
281+
<span className="text-accent-purple">1.</span>
282+
<span>Click "Solve" to open the problem</span>
283+
</div>
284+
<div className="flex items-start gap-2">
285+
<span className="text-accent-purple">2.</span>
286+
<span>Submit your solution on Codeforces</span>
287+
</div>
288+
<div className="flex items-start gap-2">
289+
<span className="text-accent-purple">3.</span>
290+
<span>Return and click "Verify" to check your answer</span>
291+
</div>
292+
</div>
293+
)}
214294
</div>
215295
</div>
216296
</div>

0 commit comments

Comments
 (0)