Skip to content

Commit 8604e3d

Browse files
authored
Merge pull request #99 from crux-bphc/feat/fix-wrapped-leaderboard
fix: updated wrapped leaderboard to use final rating
2 parents 4c07280 + 564b5b2 commit 8604e3d

6 files changed

Lines changed: 74 additions & 32 deletions

File tree

backend/src/controllers/wrapped.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { db } from "../drizzle/db";
2-
import { eq } from "drizzle-orm";
2+
import { eq, ne, asc } from "drizzle-orm";
33
import { wrapped25, users } from "../drizzle/schema";
44
import type { Request, Response } from "express";
55
import { generateAllWrappedStats } from "../scripts/generateWrappedStats";
@@ -32,6 +32,22 @@ export const getWrappedStats = async (req: Request, res: Response): Promise<void
3232
}
3333
};
3434

35+
export const getWrappedLeaderboard = async (req: Request, res: Response): Promise<void> => {
36+
try {
37+
const leaderboard = await db.select({
38+
id: users.id,
39+
name: users.name,
40+
campusRank: wrapped25.campusRank,
41+
finalRating: wrapped25.finalRating
42+
}).from(wrapped25).innerJoin(users, eq(wrapped25.userId, users.id)).orderBy(asc(wrapped25.campusRank)).where(ne(wrapped25.campusRank, 0));
43+
44+
res.status(200).json({ success: true, data: leaderboard });
45+
} catch (error) {
46+
console.error("Error fetching wrapped leaderboard:", error);
47+
res.status(500).json({ success: false, message: "Internal server error." });
48+
}
49+
};
50+
3551
export const triggerWrappedStatsGeneration = async (req: Request, res: Response): Promise<void> => {
3652
try {
3753
await generateAllWrappedStats();

backend/src/routes/wrapped.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import express from "express";
22
import { requireAuth, requireCruxMember } from "../middlewares/auth";
3-
import { getWrappedStats, triggerWrappedStatsGeneration } from "../controllers/wrapped";
3+
import { getWrappedLeaderboard, getWrappedStats, triggerWrappedStatsGeneration } from "../controllers/wrapped";
44

55
const router = express.Router();
66

7-
router.get("/:userId", requireAuth, getWrappedStats);
7+
router.get("/leaderboard", requireAuth, getWrappedLeaderboard);
88
router.post("/generate", requireAuth, requireCruxMember, triggerWrappedStatsGeneration);
9+
router.get("/:userId", requireAuth, getWrappedStats);
910

1011
export default router;

backend/src/scripts/generateWrappedStats.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,9 @@ async function calculateRanks() {
155155
}
156156

157157
const sortedByRating = combinedStats
158-
.filter(s => s.user.cfRating !== null)
159-
.sort((a, b) => b.user.cfRating! - a.user.cfRating!);
160-
158+
.filter(s => s.finalRating !== null && s.finalRating !== 0)
159+
.sort((a, b) => (b.finalRating ?? 0) - (a.finalRating ?? 0));
160+
161161
const rankUpdates = sortedByRating.map((stat, i) =>
162162
db.update(wrapped25).set({ campusRank: i + 1 }).where(eq(wrapped25.id, stat.id))
163163
);
@@ -178,8 +178,8 @@ async function calculateRanks() {
178178
const batchRankUpdates = [];
179179
for (const batch in batches) {
180180
const sortedBatch = batches[batch]
181-
.filter(s => s.user.cfRating !== null)
182-
.sort((a, b) => b.user.cfRating! - a.user.cfRating!);
181+
.filter(s => s.finalRating !== null && s.finalRating !== 0)
182+
.sort((a, b) => (b.finalRating ?? 0) - (a.finalRating ?? 0));
183183

184184
for (let i = 0; i < sortedBatch.length; i++) {
185185
const stat = sortedBatch[i];

frontend/src/components/Wrapped/CampusLeaderboardSlide.tsx

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,8 @@ import axios from "axios";
55
type LeaderboardUser = {
66
id: string;
77
name: string;
8-
email: string;
9-
cfHandle: string;
10-
cfRating: number;
11-
pfpUrl: string;
12-
batch: string | null;
13-
rank: number;
8+
campusRank: number;
9+
finalRating: number;
1410
};
1511

1612
interface CampusLeaderboardSlideProps {
@@ -61,14 +57,9 @@ const CampusLeaderboardSlide = ({ currentUser }: CampusLeaderboardSlideProps) =>
6157
const fetchLeaderboard = async () => {
6258
try {
6359
setIsLoading(true);
64-
const res = await axios.get("/api/leaderboard");
65-
66-
const rankedLeaderboard: LeaderboardUser[] = res.data.map((user: any, index: number) => ({
67-
...user,
68-
rank: index + 1,
69-
}));
70-
71-
setLeaderboard(rankedLeaderboard);
60+
const res = await axios.get("/api/wrapped/leaderboard", {withCredentials: true});
61+
62+
setLeaderboard(res.data.data);
7263
setIsError(false);
7364
} catch (error) {
7465
console.error("Error fetching leaderboard:", error);
@@ -142,33 +133,44 @@ const CampusLeaderboardSlide = ({ currentUser }: CampusLeaderboardSlideProps) =>
142133
transition={{ duration: 0.5 }}
143134
>
144135
<div className="flex items-center">
145-
<span className="text-xl font-bold w-8" style={{ color: user.id === currentUser.id ? 'white' : COLORS.muted }}>{user.rank}</span>
136+
<span className="text-xl font-bold w-8" style={{ color: user.id === currentUser.id ? 'white' : COLORS.muted }}>{user.campusRank}</span>
146137
<span className="font-semibold">{user.name}</span>
147138
</div>
148-
<span className="font-bold" style={{ color: COLORS.yellow }}>{user.cfRating}</span>
139+
<span className="font-bold" style={{ color: COLORS.yellow }}>{user.finalRating}</span>
149140
</motion.div>
150141
))}
151142
</div>
152143

153144
{!isUserInTop3 && currentUserRanked && (
154145
<motion.div
155146
key={currentUserRanked.id}
156-
className="flex flex-col items-center justify-between p-4 rounded-lg mt-6"
157-
style={{ backgroundColor: COLORS.userHighlight }}
147+
className="flex flex-col items-center justify-between mt-6"
158148
initial={{ opacity: 0, y: 20 }}
159149
animate={{ opacity: 1, y: 0 }}
160150
transition={{ delay: 0.6, duration: 0.5 }}
161151
>
162-
<p className="text-lg font-semibold mb-2">Your Rank</p>
163-
<div className="flex items-center justify-between w-full">
152+
<p className="text-xl font-semibold mb-2">Your Rank</p>
153+
<div className="flex items-center justify-between w-full p-4 rounded-lg" style={{backgroundColor: COLORS.userHighlight}}>
164154
<div className="flex items-center">
165-
<span className="text-2xl font-bold w-10 text-white">{currentUserRanked.rank}</span>
155+
<span className="text-2xl font-bold w-10 text-white">{currentUserRanked.campusRank}</span>
166156
<span className="text-lg font-semibold">{currentUserRanked.name}</span>
167157
</div>
168-
<span className="text-xl font-bold" style={{ color: COLORS.yellow }}>{currentUserRanked.cfRating}</span>
158+
<span className="text-xl font-bold" style={{ color: COLORS.yellow }}>{currentUserRanked.finalRating}</span>
169159
</div>
170160
</motion.div>
171161
)}
162+
163+
{!currentUserRanked && (
164+
<motion.div
165+
key={currentUser.id}
166+
className="flex flex-col items-center justify-between mt-6"
167+
initial={{ opacity: 0, y: 20 }}
168+
animate={{ opacity: 1, y: 0 }}
169+
transition={{ delay: 0.6, duration: 0.5 }}
170+
>
171+
<p className="text-xl font-semibold mb-2">2026 is the perfect year to start your leaderboard journey!</p>
172+
</motion.div>
173+
)}
172174
</motion.div>
173175
)}
174176
</AnimatePresence>

frontend/src/components/Wrapped/SummarySlide.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,11 @@ const SummarySlide = ({ wrappedData }: SummarySlideProps) => {
169169
>
170170
<div>
171171
<p className="text-sm" style={{ color: COLORS.subtext0 }}>Campus Rank</p>
172-
<p className="text-3xl font-bold" style={{ color: COLORS.mauve }}>{data.campusRank}</p>
172+
<p className="text-3xl font-bold" style={{ color: COLORS.mauve }}>{data.campusRank == 0 ? "-" : data.campusRank}</p>
173173
</div>
174174
<div>
175175
<p className="text-sm" style={{ color: COLORS.subtext0 }}>Batch Rank</p>
176-
<p className="text-3xl font-bold" style={{ color: COLORS.mauve }}>{data.batchRank}</p>
176+
<p className="text-3xl font-bold" style={{ color: COLORS.mauve }}>{data.batchRank == 0 ? "-" : data.batchRank}</p>
177177
</div>
178178
</div>
179179

frontend/src/routes/wrapped.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,27 @@ import dream4 from "../assets/wrapped/dream-4.mp3";
2525
import alive1 from "../assets/wrapped/alive-1.mp3";
2626
import alive2 from "../assets/wrapped/alive-2.mp3";
2727

28+
const AUDIO_ASSETS = [
29+
dancingQueen,
30+
franz1, franz2, franz3,
31+
dream1, dream2, dream3, dream4,
32+
alive1, alive2
33+
];
34+
35+
const preloadAudio = (src: string) => {
36+
return new Promise((resolve) => {
37+
const audio = new Audio(src);
38+
audio.preload = "auto";
39+
40+
audio.oncanplaythrough = () => resolve(src);
41+
42+
audio.onerror = () => {
43+
console.warn(`Failed to load audio: ${src}`);
44+
resolve(null);
45+
};
46+
});
47+
};
48+
2849
const fetchWrappedData = async (id: string) => {
2950
try {
3051
const res = await axios.get(
@@ -64,6 +85,8 @@ function RouteComponent() {
6485
setIsLoading(true);
6586
const data = await fetchWrappedData(user.id);
6687
setWrappedData(data);
88+
const audioPromises = AUDIO_ASSETS.map(src => preloadAudio(src));
89+
await Promise.all(audioPromises);
6790
} catch (error) {
6891
console.error("Error fetching wrapped data:", error);
6992
} finally {

0 commit comments

Comments
 (0)