Skip to content
Open
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
47 changes: 44 additions & 3 deletions frontend/src/Pages/Leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
GamificationEvent,
} from "@/services/gamificationService";
import BadgeUnlocked from "@/components/BadgeUnlocked";
import Top10Celebration from "@/components/Top10Celebration";
import { useUser } from "@/hooks/useUser";

interface Debater {
Expand Down Expand Up @@ -86,7 +87,16 @@ const Leaderboard: React.FC = () => {
});
const [sortCategory, setSortCategory] = useState<SortCategory>("score");
const wsRef = useRef<WebSocket | null>(null);
const previousRankRef = useRef<number | null>(null);
const isInitialRenderRef = useRef(true);
const { user } = useUser();
const [top10Celebration, setTop10Celebration] = useState<{
rank: number;
isOpen: boolean;
}>({
rank: 0,
isOpen: false,
});

// Load initial leaderboard data
useEffect(() => {
Expand Down Expand Up @@ -226,6 +236,33 @@ const Leaderboard: React.FC = () => {
(debater) => debater.currentUser
);

// Check if user entered top 10 and trigger celebration
useEffect(() => {
// Skip celebration on initial page load
if (isInitialRenderRef.current) {
if (currentUserIndex !== -1) {
previousRankRef.current = currentUserIndex + 1;
}
isInitialRenderRef.current = false;
return;
}

if (currentUserIndex !== -1 && currentUserIndex < 10) {
const currentRank = currentUserIndex + 1;
// Only celebrate if user wasn't in top 10 before or improved their rank
if (previousRankRef.current !== null && previousRankRef.current > 10) {
// User just entered top 10!
setTop10Celebration({ rank: currentRank, isOpen: true });
} else if (previousRankRef.current !== null && previousRankRef.current > currentRank && currentRank <= 3) {
// User improved to top 3!
setTop10Celebration({ rank: currentRank, isOpen: true });
}
previousRankRef.current = currentRank;
} else if (currentUserIndex >= 10) {
previousRankRef.current = currentUserIndex + 1;
}
}, [currentUserIndex, sortedDebaters]);

const getVisibleDebaters = () => {
if (!sortedDebaters.length) return [];
const initialList = sortedDebaters
Expand Down Expand Up @@ -268,6 +305,11 @@ const Leaderboard: React.FC = () => {
isOpen={badgeUnlocked.isOpen}
onClose={() => setBadgeUnlocked({ badgeName: "", isOpen: false })}
/>
<Top10Celebration
rank={top10Celebration.rank}
isOpen={top10Celebration.isOpen}
onClose={() => setTop10Celebration({ rank: 0, isOpen: false })}
/>
<div className="max-w-7xl mx-auto">
<p className="text-center text-muted-foreground mb-8 text-lg">
Hone your skills and see how you stack up against top debaters! 🏆
Expand Down Expand Up @@ -315,9 +357,8 @@ const Leaderboard: React.FC = () => {
{visibleDebaters.map((debater) => (
<TableRow
key={debater.id}
className={`group hover:bg-accent/30 ${
debater.currentUser ? "bg-primary/10" : ""
}`}
className={`group hover:bg-accent/30 ${debater.currentUser ? "bg-primary/10" : ""
}`}
>
<TableCell className="pl-6">
<div
Expand Down
108 changes: 108 additions & 0 deletions frontend/src/components/Top10Celebration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useEffect, useState } from "react";
import Confetti from "react-confetti";
import { FaTrophy, FaStar } from "react-icons/fa";
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

interface Top10CelebrationProps {
rank: number;
isOpen: boolean;
onClose: () => void;
}

const Top10Celebration: React.FC<Top10CelebrationProps> = ({ rank, isOpen, onClose }) => {
const [showConfetti, setShowConfetti] = useState(false);
const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });

useEffect(() => {
if (isOpen) {
setShowConfetti(true);
setWindowSize({ width: window.innerWidth, height: window.innerHeight });

// Hide confetti after 6 seconds
const timer = setTimeout(() => {
setShowConfetti(false);
}, 6000);

return () => clearTimeout(timer);
}
}, [isOpen]);

useEffect(() => {
const handleResize = () => {
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
};

window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);

const getRankContent = () => {
if (rank === 1) {
return {
icon: <FaTrophy className="w-20 h-20 text-amber-500" />,
title: "🥇 You're #1!",
message: "You've reached the top of the leaderboard! You're the champion!",
confettiColors: ['#FFD700', '#FFA500', '#FF8C00', '#FF6347'],
};
} else if (rank === 2) {
return {
icon: <FaTrophy className="w-20 h-20 text-slate-400" />,
title: "🥈 Silver Position!",
message: "Amazing! You're in 2nd place on the leaderboard!",
confettiColors: ['#C0C0C0', '#A8A8A8', '#909090', '#B8B8B8'],
};
} else if (rank === 3) {
return {
icon: <FaTrophy className="w-20 h-20 text-orange-500" />,
title: "🥉 Bronze Achievement!",
message: "Fantastic! You've claimed 3rd place on the leaderboard!",
confettiColors: ['#CD7F32', '#B87333', '#A0522D', '#D2691E'],
};
} else {
return {
icon: <FaStar className="w-20 h-20 text-primary" />,
title: `🌟 Top 10! Rank #${rank}`,
message: "Congratulations! You've made it to the Top 10!",
confettiColors: ['#6366F1', '#8B5CF6', '#A855F7', '#D946EF'],
};
}
};

const content = getRankContent();

return (
<>
{showConfetti && (
<Confetti
width={windowSize.width}
height={windowSize.height}
recycle={false}
numberOfPieces={600}
gravity={0.25}
colors={content.confettiColors}
/>
)}
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-md">
<DialogTitle className="text-2xl font-bold text-center mb-4">
{content.title}
</DialogTitle>
<DialogDescription className="text-center">
<div className="flex flex-col items-center justify-center space-y-4 py-4">
<div className="animate-bounce">{content.icon}</div>
<p className="text-muted-foreground text-lg">{content.message}</p>
</div>
</DialogDescription>
<div className="flex justify-center mt-4">
<Button onClick={onClose} className="px-8 py-2">
Keep Going! 🚀
</Button>
</div>
</DialogContent>
</Dialog>
</>
);
};

export default Top10Celebration;