From e47b160c032285799f7cfeca7a61a6668375442e Mon Sep 17 00:00:00 2001 From: DeveloperAmrit Date: Wed, 31 Dec 2025 11:52:36 +0530 Subject: [PATCH 1/2] Added gamification to celebrate new badges and top 10 appearance --- frontend/src/App.tsx | 4 + frontend/src/Pages/Leaderboard.tsx | 32 ------- frontend/src/components/BadgeUnlocked.tsx | 4 +- .../src/components/GamificationOverlay.tsx | 91 +++++++++++++++++++ 4 files changed, 98 insertions(+), 33 deletions(-) create mode 100644 frontend/src/components/GamificationOverlay.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2cbe4b1..8d2ea08 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -28,6 +28,8 @@ import CommunityFeed from './Pages/CommunityFeed'; import AdminSignup from './Pages/Admin/AdminSignup'; import AdminDashboard from './Pages/Admin/AdminDashboard'; import ViewDebate from './Pages/ViewDebate'; +import { Toaster } from '@/components/ui/toaster'; +import GamificationOverlay from './components/GamificationOverlay'; // Protects routes based on authentication status function ProtectedRoute() { @@ -110,6 +112,8 @@ function App() { + + ); diff --git a/frontend/src/Pages/Leaderboard.tsx b/frontend/src/Pages/Leaderboard.tsx index d4a4d4a..073bdf8 100644 --- a/frontend/src/Pages/Leaderboard.tsx +++ b/frontend/src/Pages/Leaderboard.tsx @@ -25,7 +25,6 @@ import { createGamificationWebSocket, GamificationEvent, } from "@/services/gamificationService"; -import BadgeUnlocked from "@/components/BadgeUnlocked"; import { useUser } from "@/hooks/useUser"; interface Debater { @@ -77,13 +76,6 @@ const Leaderboard: React.FC = () => { const [stats, setStats] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [badgeUnlocked, setBadgeUnlocked] = useState<{ - badgeName: string; - isOpen: boolean; - }>({ - badgeName: "", - isOpen: false, - }); const [sortCategory, setSortCategory] = useState("score"); const wsRef = useRef(null); const { user } = useUser(); @@ -134,25 +126,6 @@ const Leaderboard: React.FC = () => { (event: GamificationEvent) => { console.log("Gamification event received:", event); - if (event.type === "badge_awarded" && event.badgeName) { - // Show badge unlock notification if it's for the current user - setDebaters((currentDebaters) => { - const currentUserDebater = currentDebaters.find( - (d) => d.currentUser - ); - if ( - event.userId === currentUserDebater?.id || - event.userId === user?.id - ) { - setBadgeUnlocked({ - badgeName: event.badgeName!, - isOpen: true, - }); - } - return currentDebaters; - }); - } - if (event.type === "score_updated") { // Update the leaderboard when scores change setDebaters((prevDebaters) => { @@ -263,11 +236,6 @@ const Leaderboard: React.FC = () => { return (
- setBadgeUnlocked({ badgeName: "", isOpen: false })} - />

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; From d5e8bd87a29bf71faddd29aa156af8276cd88dff Mon Sep 17 00:00:00 2001 From: DeveloperAmrit Date: Sun, 11 Jan 2026 20:31:36 +0530 Subject: [PATCH 2/2] No reward on conceding --- backend/controllers/debatevsbot_controller.go | 11 ++++++++--- frontend/src/components/SavedTranscripts.tsx | 2 ++ frontend/src/services/transcriptService.ts | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/backend/controllers/debatevsbot_controller.go b/backend/controllers/debatevsbot_controller.go index 9571099..e2308bd 100644 --- a/backend/controllers/debatevsbot_controller.go +++ b/backend/controllers/debatevsbot_controller.go @@ -349,6 +349,9 @@ func updateGamificationAfterBotDebate(userID primitive.ObjectID, resultStatus, t case "loss": pointsToAdd = 10 // Participation points action = "debate_loss" + case "concede": + pointsToAdd = 0 // No points for conceding + action = "debate_concede" case "draw": pointsToAdd = 25 // Points for draw action = "debate_complete" @@ -448,7 +451,9 @@ func updateGamificationAfterBotDebate(userID primitive.ObjectID, resultStatus, t } // Check for automatic badges (Novice, Streak5, FactMaster, etc.) - checkAndAwardAutomaticBadges(ctx, userID, updatedUser) + if resultStatus != "concede" { + checkAndAwardAutomaticBadges(ctx, userID, updatedUser) + } // Broadcast score update via WebSocket websocket.BroadcastGamificationEvent(models.GamificationEvent{ @@ -589,7 +594,7 @@ func ConcedeDebate(c *gin.Context) { "user_vs_bot", debate.Topic, debate.BotName, - "loss", + "concede", historyToSave, nil, ) @@ -602,7 +607,7 @@ func ConcedeDebate(c *gin.Context) { log.Printf("Panic in updateGamificationAfterBotDebate (concede): %v", r) } }() - updateGamificationAfterBotDebate(user.ID, "loss", debate.Topic) + updateGamificationAfterBotDebate(user.ID, "concede", debate.Topic) }() c.JSON(200, gin.H{"message": "Debate conceded successfully"}) 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;