Hone your skills and see how you stack up against top debaters! 🏆
diff --git a/frontend/src/components/BadgeUnlocked.tsx b/frontend/src/components/BadgeUnlocked.tsx
index d74ed70..67bfb40 100644
--- a/frontend/src/components/BadgeUnlocked.tsx
+++ b/frontend/src/components/BadgeUnlocked.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import Confetti from "react-confetti";
-import { FaTrophy, FaMedal, FaAward } from "react-icons/fa";
+import { FaTrophy, FaMedal, FaAward, FaCrown } from "react-icons/fa";
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
@@ -16,6 +16,7 @@ const badgeIcons: Record = {
FactMaster: ,
FirstWin: ,
Debater10: ,
+ Top10: ,
};
const badgeDescriptions: Record = {
@@ -24,6 +25,7 @@ const badgeDescriptions: Record = {
FactMaster: "You're a master of facts! Keep up the great work!",
FirstWin: "Congratulations on your first victory!",
Debater10: "You've completed 10 debates! You're becoming a pro!",
+ Top10: "You've reached the Top 10! You are among the elite debaters!",
};
const BadgeUnlocked: React.FC = ({ badgeName, isOpen, onClose }) => {
diff --git a/frontend/src/components/GamificationOverlay.tsx b/frontend/src/components/GamificationOverlay.tsx
new file mode 100644
index 0000000..e144cb4
--- /dev/null
+++ b/frontend/src/components/GamificationOverlay.tsx
@@ -0,0 +1,91 @@
+import React, { useEffect, useState, useRef } from "react";
+import { useUser } from "@/hooks/useUser";
+import {
+ createGamificationWebSocket,
+ GamificationEvent,
+ fetchGamificationLeaderboard,
+} from "@/services/gamificationService";
+import BadgeUnlocked from "@/components/BadgeUnlocked";
+
+const GamificationOverlay: React.FC = () => {
+ const { user } = useUser();
+ const [badgeUnlocked, setBadgeUnlocked] = useState<{
+ badgeName: string;
+ isOpen: boolean;
+ }>({
+ badgeName: "",
+ isOpen: false,
+ });
+ const wsRef = useRef(null);
+ const previousRankRef = useRef(null);
+
+ useEffect(() => {
+ const token = localStorage.getItem("token");
+ if (!token || !user) return;
+
+ // Initial rank check
+ fetchGamificationLeaderboard(token).then((data) => {
+ const myEntry = data.debaters.find(d => d.id === user.id);
+ if (myEntry) {
+ previousRankRef.current = myEntry.rank;
+ }
+ }).catch(console.error);
+
+ if (wsRef.current) {
+ wsRef.current.close();
+ }
+
+ const ws = createGamificationWebSocket(
+ token,
+ async (event: GamificationEvent) => {
+ if (event.userId !== user.id) return;
+
+ if (event.type === "badge_awarded" && event.badgeName) {
+ setBadgeUnlocked({
+ badgeName: event.badgeName,
+ isOpen: true,
+ });
+ } else if (event.type === "score_updated") {
+ // Check for rank change
+ try {
+ const data = await fetchGamificationLeaderboard(token);
+ const myEntry = data.debaters.find(d => d.id === user.id);
+ if (myEntry) {
+ const newRank = myEntry.rank;
+ const oldRank = previousRankRef.current;
+
+ // Celebrate if entering top 10
+ if (newRank <= 10 && oldRank !== null && oldRank > 10) {
+ setBadgeUnlocked({
+ badgeName: "Top10",
+ isOpen: true,
+ });
+ }
+ previousRankRef.current = newRank;
+ }
+ } catch (e) {
+ console.error("Failed to check rank", e);
+ }
+ }
+ }
+ );
+
+ wsRef.current = ws;
+
+ return () => {
+ if (wsRef.current) {
+ wsRef.current.close();
+ }
+ };
+ }, [user]);
+
+ return (
+ setBadgeUnlocked({ ...badgeUnlocked, isOpen: false })}
+ />
+ );
+};
+
+export default GamificationOverlay;
diff --git a/frontend/src/components/SavedTranscripts.tsx b/frontend/src/components/SavedTranscripts.tsx
index 41bcd6f..1d3f856 100644
--- a/frontend/src/components/SavedTranscripts.tsx
+++ b/frontend/src/components/SavedTranscripts.tsx
@@ -141,6 +141,7 @@ const SavedTranscripts: React.FC = ({ className }) => {
case 'win':
return ;
case 'loss':
+ case 'concede':
return ;
case 'draw':
return ;
@@ -154,6 +155,7 @@ const SavedTranscripts: React.FC = ({ className }) => {
case 'win':
return 'bg-green-100 text-green-800 border-green-200';
case 'loss':
+ case 'concede':
return 'bg-red-100 text-red-800 border-red-200';
case 'draw':
return 'bg-gray-100 text-gray-800 border-gray-200';
diff --git a/frontend/src/services/transcriptService.ts b/frontend/src/services/transcriptService.ts
index 52d2d7a..1b2778c 100644
--- a/frontend/src/services/transcriptService.ts
+++ b/frontend/src/services/transcriptService.ts
@@ -9,7 +9,7 @@ export interface SavedDebateTranscript {
debateType: 'user_vs_bot' | 'user_vs_user';
topic: string;
opponent: string;
- result: 'win' | 'loss' | 'draw' | 'pending';
+ result: 'win' | 'loss' | 'draw' | 'pending' | 'concede';
messages: Array<{
sender: string;
text: string;