Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion backend/src/controllers/wrapped.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { db } from "../drizzle/db";
import { eq } from "drizzle-orm";
import { eq, ne, asc } from "drizzle-orm";
import { wrapped25, users } from "../drizzle/schema";
import type { Request, Response } from "express";
import { generateAllWrappedStats } from "../scripts/generateWrappedStats";
Expand Down Expand Up @@ -32,6 +32,22 @@ export const getWrappedStats = async (req: Request, res: Response): Promise<void
}
};

export const getWrappedLeaderboard = async (req: Request, res: Response): Promise<void> => {
try {
const leaderboard = await db.select({
id: users.id,
name: users.name,
campusRank: wrapped25.campusRank,
finalRating: wrapped25.finalRating
}).from(wrapped25).innerJoin(users, eq(wrapped25.userId, users.id)).orderBy(asc(wrapped25.campusRank)).where(ne(wrapped25.campusRank, 0));

res.status(200).json({ success: true, data: leaderboard });
} catch (error) {
console.error("Error fetching wrapped leaderboard:", error);
res.status(500).json({ success: false, message: "Internal server error." });
}
};

export const triggerWrappedStatsGeneration = async (req: Request, res: Response): Promise<void> => {
try {
await generateAllWrappedStats();
Expand Down
5 changes: 3 additions & 2 deletions backend/src/routes/wrapped.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import express from "express";
import { requireAuth, requireCruxMember } from "../middlewares/auth";
import { getWrappedStats, triggerWrappedStatsGeneration } from "../controllers/wrapped";
import { getWrappedLeaderboard, getWrappedStats, triggerWrappedStatsGeneration } from "../controllers/wrapped";

const router = express.Router();

router.get("/:userId", requireAuth, getWrappedStats);
router.get("/leaderboard", requireAuth, getWrappedLeaderboard);
router.post("/generate", requireAuth, requireCruxMember, triggerWrappedStatsGeneration);
router.get("/:userId", requireAuth, getWrappedStats);

export default router;
10 changes: 5 additions & 5 deletions backend/src/scripts/generateWrappedStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,9 @@ async function calculateRanks() {
}

const sortedByRating = combinedStats
.filter(s => s.user.cfRating !== null)
.sort((a, b) => b.user.cfRating! - a.user.cfRating!);

.filter(s => s.finalRating !== null && s.finalRating !== 0)
.sort((a, b) => (b.finalRating ?? 0) - (a.finalRating ?? 0));
const rankUpdates = sortedByRating.map((stat, i) =>
db.update(wrapped25).set({ campusRank: i + 1 }).where(eq(wrapped25.id, stat.id))
);
Expand All @@ -178,8 +178,8 @@ async function calculateRanks() {
const batchRankUpdates = [];
for (const batch in batches) {
const sortedBatch = batches[batch]
.filter(s => s.user.cfRating !== null)
.sort((a, b) => b.user.cfRating! - a.user.cfRating!);
.filter(s => s.finalRating !== null && s.finalRating !== 0)
.sort((a, b) => (b.finalRating ?? 0) - (a.finalRating ?? 0));

for (let i = 0; i < sortedBatch.length; i++) {
const stat = sortedBatch[i];
Expand Down
46 changes: 24 additions & 22 deletions frontend/src/components/Wrapped/CampusLeaderboardSlide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@ import axios from "axios";
type LeaderboardUser = {
id: string;
name: string;
email: string;
cfHandle: string;
cfRating: number;
pfpUrl: string;
batch: string | null;
rank: number;
campusRank: number;
finalRating: number;
};

interface CampusLeaderboardSlideProps {
Expand Down Expand Up @@ -61,14 +57,9 @@ const CampusLeaderboardSlide = ({ currentUser }: CampusLeaderboardSlideProps) =>
const fetchLeaderboard = async () => {
try {
setIsLoading(true);
const res = await axios.get("/api/leaderboard");

const rankedLeaderboard: LeaderboardUser[] = res.data.map((user: any, index: number) => ({
...user,
rank: index + 1,
}));

setLeaderboard(rankedLeaderboard);
const res = await axios.get("/api/wrapped/leaderboard", {withCredentials: true});

setLeaderboard(res.data.data);
setIsError(false);
} catch (error) {
console.error("Error fetching leaderboard:", error);
Expand Down Expand Up @@ -142,33 +133,44 @@ const CampusLeaderboardSlide = ({ currentUser }: CampusLeaderboardSlideProps) =>
transition={{ duration: 0.5 }}
>
<div className="flex items-center">
<span className="text-xl font-bold w-8" style={{ color: user.id === currentUser.id ? 'white' : COLORS.muted }}>{user.rank}</span>
<span className="text-xl font-bold w-8" style={{ color: user.id === currentUser.id ? 'white' : COLORS.muted }}>{user.campusRank}</span>
<span className="font-semibold">{user.name}</span>
</div>
<span className="font-bold" style={{ color: COLORS.yellow }}>{user.cfRating}</span>
<span className="font-bold" style={{ color: COLORS.yellow }}>{user.finalRating}</span>
</motion.div>
))}
</div>

{!isUserInTop3 && currentUserRanked && (
<motion.div
key={currentUserRanked.id}
className="flex flex-col items-center justify-between p-4 rounded-lg mt-6"
style={{ backgroundColor: COLORS.userHighlight }}
className="flex flex-col items-center justify-between mt-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6, duration: 0.5 }}
>
<p className="text-lg font-semibold mb-2">Your Rank</p>
<div className="flex items-center justify-between w-full">
<p className="text-xl font-semibold mb-2">Your Rank</p>
<div className="flex items-center justify-between w-full p-4 rounded-lg" style={{backgroundColor: COLORS.userHighlight}}>
<div className="flex items-center">
<span className="text-2xl font-bold w-10 text-white">{currentUserRanked.rank}</span>
<span className="text-2xl font-bold w-10 text-white">{currentUserRanked.campusRank}</span>
<span className="text-lg font-semibold">{currentUserRanked.name}</span>
</div>
<span className="text-xl font-bold" style={{ color: COLORS.yellow }}>{currentUserRanked.cfRating}</span>
<span className="text-xl font-bold" style={{ color: COLORS.yellow }}>{currentUserRanked.finalRating}</span>
</div>
</motion.div>
)}

{!currentUserRanked && (
<motion.div
key={currentUser.id}
className="flex flex-col items-center justify-between mt-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6, duration: 0.5 }}
>
<p className="text-xl font-semibold mb-2">2026 is the perfect year to start your leaderboard journey!</p>
</motion.div>
)}
</motion.div>
)}
</AnimatePresence>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Wrapped/SummarySlide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,11 @@ const SummarySlide = ({ wrappedData }: SummarySlideProps) => {
>
<div>
<p className="text-sm" style={{ color: COLORS.subtext0 }}>Campus Rank</p>
<p className="text-3xl font-bold" style={{ color: COLORS.mauve }}>{data.campusRank}</p>
<p className="text-3xl font-bold" style={{ color: COLORS.mauve }}>{data.campusRank == 0 ? "-" : data.campusRank}</p>
</div>
<div>
<p className="text-sm" style={{ color: COLORS.subtext0 }}>Batch Rank</p>
<p className="text-3xl font-bold" style={{ color: COLORS.mauve }}>{data.batchRank}</p>
<p className="text-3xl font-bold" style={{ color: COLORS.mauve }}>{data.batchRank == 0 ? "-" : data.batchRank}</p>
</div>
</div>

Expand Down
23 changes: 23 additions & 0 deletions frontend/src/routes/wrapped.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,27 @@ import dream4 from "../assets/wrapped/dream-4.mp3";
import alive1 from "../assets/wrapped/alive-1.mp3";
import alive2 from "../assets/wrapped/alive-2.mp3";

const AUDIO_ASSETS = [
dancingQueen,
franz1, franz2, franz3,
dream1, dream2, dream3, dream4,
alive1, alive2
];

const preloadAudio = (src: string) => {
return new Promise((resolve) => {
const audio = new Audio(src);
audio.preload = "auto";

audio.oncanplaythrough = () => resolve(src);

audio.onerror = () => {
console.warn(`Failed to load audio: ${src}`);
resolve(null);
};
});
};

const fetchWrappedData = async (id: string) => {
try {
const res = await axios.get(
Expand Down Expand Up @@ -64,6 +85,8 @@ function RouteComponent() {
setIsLoading(true);
const data = await fetchWrappedData(user.id);
setWrappedData(data);
const audioPromises = AUDIO_ASSETS.map(src => preloadAudio(src));
await Promise.all(audioPromises);
} catch (error) {
console.error("Error fetching wrapped data:", error);
} finally {
Expand Down