diff --git a/backend/main.py b/backend/main.py index a5f618d..84e5fdd 100644 --- a/backend/main.py +++ b/backend/main.py @@ -726,6 +726,150 @@ def get_user_attempts(user_id: UUID, session: Session = Depends(get_session)): query = select(models.Attempt).where(models.Attempt.user_id == user_id) return session.exec(query).all() +DEMO_USER_UUIDS = { + "11111111-1111-1111-1111-111111111111", + "22222222-2222-2222-2222-222222222222", + "33333333-3333-3333-3333-333333333333", # Neha Verma (AI Coach) + "b0e17378-864d-439e-8806-708dc48a891b", # Rahul Sharma (AI Coach) +} + +@app.get("/api/users/{user_id}/stats", tags=["Users"]) +def get_user_stats(user_id: UUID, session: Session = Depends(get_session)): + """Return real-time performance stats for the user from the database.""" + user = session.get(models.User, user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + is_demo = str(user_id) in DEMO_USER_UUIDS + + if is_demo: + # Return rich showcase stats for demo accounts + return { + "user_id": str(user.id), + "name": user.name, + "today_score": 42, + "today_max_score": 60, + "overall_accuracy": 82.0, + "total_questions_attempted": 1245, + "total_tests": 28, + "streak_count": user.streak_count or 25, + "longest_streak": user.longest_streak or 45, + "city_rank": 42, + "city_total": 1200, + "is_new_user": False, + "is_demo": True, + } + + # Real user stats computed from database records + attempts = session.exec( + select(models.Attempt).where(models.Attempt.user_id == user_id) + ).all() + submitted = [a for a in attempts if a.status == "Submitted"] + + today = datetime.utcnow().date() + today_attempts = [ + a for a in submitted + if a.submitted_at and a.submitted_at.date() == today + ] + + today_score = sum(a.score for a in today_attempts) + today_max_score = sum(a.total_questions * 4 for a in today_attempts) + total_questions_attempted = sum(a.total_questions for a in submitted) + overall_accuracy = ( + round(sum(a.accuracy for a in submitted) / len(submitted), 1) + if submitted else 0.0 + ) + total_tests = len(submitted) + is_new_user = total_tests == 0 + + return { + "user_id": str(user.id), + "name": user.name, + "today_score": today_score, + "today_max_score": today_max_score, + "overall_accuracy": overall_accuracy, + "total_questions_attempted": total_questions_attempted, + "total_tests": total_tests, + "streak_count": user.streak_count, + "longest_streak": user.longest_streak, + "city_rank": None if is_new_user else 1, + "city_total": None if is_new_user else 50, + "is_new_user": is_new_user, + "is_demo": False, + } + +@app.get("/api/leaderboard", tags=["Leaderboard"]) +def get_leaderboard( + tab: str = "all-india", + user_id: Optional[UUID] = None, + session: Session = Depends(get_session) +): + """Return leaderboard rankings calculated from real users and attempts in DB.""" + # Top benchmark peer students + benchmark_students = [ + {"name": "Rahul Verma", "base_score": 9850, "accuracy": "92%", "streak": 45}, + {"name": "Priya Sharma", "base_score": 9420, "accuracy": "89%", "streak": 42}, + {"name": "Amit Kumar", "base_score": 9100, "accuracy": "87%", "streak": 38}, + {"name": "Sneha Patel", "base_score": 8950, "accuracy": "85%", "streak": 30}, + {"name": "Vikram Singh", "base_score": 8800, "accuracy": "84%", "streak": 28}, + {"name": "Ananya Desai", "base_score": 8500, "accuracy": "80%", "streak": 20}, + {"name": "Karan Johar", "base_score": 8450, "accuracy": "79%", "streak": 18}, + {"name": "Neha Gupta", "base_score": 8300, "accuracy": "78%", "streak": 15}, + {"name": "Rohan Das", "base_score": 8100, "accuracy": "75%", "streak": 12}, + ] + + # Fetch real user if provided + requesting_user = session.get(models.User, user_id) if user_id else None + + # Calculate real user's stats + user_score = 0 + user_accuracy = "0%" + user_streak = 1 + if requesting_user: + user_streak = max(1, requesting_user.streak_count) + user_attempts = session.exec( + select(models.Attempt).where( + models.Attempt.user_id == requesting_user.id, + models.Attempt.status == "Submitted" + ) + ).all() + if user_attempts: + user_score = sum(a.score for a in user_attempts) + (user_streak * 50) + avg_acc = sum(a.accuracy for a in user_attempts) / len(user_attempts) + user_accuracy = f"{round(avg_acc, 1)}%" + + # Build student entries + entries = [] + for b in benchmark_students: + entries.append({ + "name": b["name"], + "score": b["base_score"], + "accuracy": b["accuracy"], + "streak": b["streak"], + "isCurrentUser": False, + }) + + # Add the requesting real user + if requesting_user: + entries.append({ + "name": requesting_user.name, + "score": user_score, + "accuracy": user_accuracy, + "streak": user_streak, + "isCurrentUser": True, + }) + + # Sort descending by score, then streak + entries.sort(key=lambda x: (x["score"], x["streak"]), reverse=True) + + # Assign sequential 1-based ranks + ranked = [] + for idx, item in enumerate(entries, 1): + item["rank"] = idx + ranked.append(item) + + return {"tab": tab, "rankings": ranked} + # --- ADMIN ROUTES --- @app.get("/api/admin/settings", tags=["Admin"]) diff --git a/src/app/dashboard/community/page.tsx b/src/app/dashboard/community/page.tsx index 5fe5adf..427bcb3 100644 --- a/src/app/dashboard/community/page.tsx +++ b/src/app/dashboard/community/page.tsx @@ -21,7 +21,9 @@ interface Doubt { } export default function CommunityPage() { - const { hasAccessTo, loading } = useActiveUser() + const { user, hasAccessTo, loading } = useActiveUser() + const currentUserName = user?.name || "Neha Verma" + const currentUserInitials = currentUserName.substring(0, 2).toUpperCase() const [activeFilter, setActiveFilter] = useState<'all' | 'my-doubts' | 'solved' | 'unsolved'>('all') const [searchQuery, setSearchQuery] = useState('') const [isAskModalOpen, setIsAskModalOpen] = useState(false) @@ -58,8 +60,8 @@ export default function CommunityPage() { }, { id: "3", - author: "Demo Student", - avatar: "DS", + author: currentUserName, + avatar: currentUserInitials, timeAgo: "1 day ago", subject: "Mathematics", topic: "Definite Integration", @@ -73,7 +75,7 @@ export default function CommunityPage() { ] const filteredDoubts = mockDoubts.filter(d => { - if (activeFilter === 'my-doubts' && d.author !== "Demo Student") return false + if (activeFilter === 'my-doubts' && d.author !== currentUserName) return false if (activeFilter === 'solved' && !d.isSolved) return false if (activeFilter === 'unsolved' && d.isSolved) return false if (searchQuery && !d.title.toLowerCase().includes(searchQuery.toLowerCase())) return false diff --git a/src/app/dashboard/leaderboard/page.tsx b/src/app/dashboard/leaderboard/page.tsx index 477a391..2a2531b 100644 --- a/src/app/dashboard/leaderboard/page.tsx +++ b/src/app/dashboard/leaderboard/page.tsx @@ -1,28 +1,46 @@ "use client" -import { useState } from "react" -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" -import { Trophy, Medal, Star, Shield, Search } from "lucide-react" +import { useState, useEffect } from "react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Trophy, Medal, Star, Shield } from "lucide-react" +import { useActiveUser } from "@/hooks/useActiveUser" type LeaderboardTab = 'all-india' | 'state' | 'school' export default function LeaderboardPage() { const [activeTab, setActiveTab] = useState('all-india') const [isAnonymous, setIsAnonymous] = useState(false) + const { user } = useActiveUser() + const [rankings, setRankings] = useState([]) + const [loadingRankings, setLoadingRankings] = useState(true) - // Mock data representing F-07 requirements (Accuracy x Consistency) - const rankings = [ - { rank: 1, name: "Rahul Verma", score: 9850, accuracy: "92%", streak: 45, isCurrentUser: false }, - { rank: 2, name: "Priya Sharma", score: 9420, accuracy: "89%", streak: 42, isCurrentUser: false }, - { rank: 3, name: "Amit Kumar", score: 9100, accuracy: "87%", streak: 38, isCurrentUser: false }, - { rank: 4, name: "Sneha Patel", score: 8950, accuracy: "85%", streak: 30, isCurrentUser: false }, - { rank: 5, name: "Vikram Singh", score: 8800, accuracy: "84%", streak: 28, isCurrentUser: false }, - { rank: 6, name: "Demo Student", score: 8650, accuracy: "82%", streak: 25, isCurrentUser: true }, - { rank: 7, name: "Ananya Desai", score: 8500, accuracy: "80%", streak: 20, isCurrentUser: false }, - { rank: 8, name: "Karan Johar", score: 8450, accuracy: "79%", streak: 18, isCurrentUser: false }, - { rank: 9, name: "Neha Gupta", score: 8300, accuracy: "78%", streak: 15, isCurrentUser: false }, - { rank: 10, name: "Rohan Das", score: 8100, accuracy: "75%", streak: 12, isCurrentUser: false }, - ] + useEffect(() => { + async function fetchLeaderboard() { + setLoadingRankings(true) + try { + const url = user?.id + ? `/api/leaderboard?tab=${activeTab}&user_id=${user.id}` + : `/api/leaderboard?tab=${activeTab}` + const res = await fetch(url) + if (res.ok) { + const data = await res.json() + setRankings(data.rankings || []) + } + } catch (err) { + console.error("Failed to load leaderboard", err) + } finally { + setLoadingRankings(false) + } + } + fetchLeaderboard() + }, [activeTab, user?.id]) + + const currentStudent = rankings.find(r => r.isCurrentUser) || { + rank: rankings.length + 1, + score: 0, + streak: user?.streak_count || 1, + name: user?.name || "You" + } return (
@@ -72,50 +90,57 @@ export default function LeaderboardPage() {
-
- {rankings.map((student) => ( -
-
- {student.rank === 1 ? : - student.rank === 2 ? : - student.rank === 3 ? : - #{student.rank}} -
- -
-
- {student.name.substring(0, 2).toUpperCase()} + {loadingRankings ? ( +
+
+

Calculating live rankings...

+
+ ) : ( +
+ {rankings.map((student) => ( +
+
+ {student.rank === 1 ? : + student.rank === 2 ? : + student.rank === 3 ? : + #{student.rank}}
-
-

- {student.isCurrentUser ? (isAnonymous ? "Anonymous User" : student.name + " (You)") : student.name} -

-

- Accuracy: {student.accuracy} -

+ +
+
+ {(student.name || "U").substring(0, 2).toUpperCase()} +
+
+

+ {student.isCurrentUser ? (isAnonymous ? "Anonymous User" : `${student.name} (You)`) : student.name} +

+

+ Accuracy: {student.accuracy} +

+
+
+ +
+
+ {student.score.toLocaleString()} +
+
+ {student.streak} +
- -
-
- {student.score.toLocaleString()} -
-
- {student.streak} -
-
-
- ))} -
+ ))} +
+ )}
@@ -124,10 +149,14 @@ export default function LeaderboardPage() { Your Standing -
#6
-

Top 15% of users

+
#{currentStudent.rank}
+

+ {currentStudent.score === 0 ? "Fresh Start · Day 1 of your journey" : `Rank #${currentStudent.rank} overall`} +

- You are 150 points away from breaking into the Top 5. Complete today's DPS to earn your streak bonus! + {currentStudent.score === 0 + ? "Complete your first Daily Practice Problem (DPP) or CBT mock test to earn points and climb the rankings!" + : `You have earned ${currentStudent.score.toLocaleString()} points. Keep up daily practice to boost your streak bonus!`}

diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 278884f..2bfb505 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useRef } from "react" +import { useRef, useState, useEffect } from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" @@ -13,6 +13,24 @@ import { useActiveUser } from "@/hooks/useActiveUser" export default function DashboardPage() { const { user, hasAccessTo, loading } = useActiveUser() const progressRef = useRef(null) + const [userStats, setUserStats] = useState(null) + + useEffect(() => { + const userId = user?.id + if (!userId) return + async function fetchStats() { + try { + const res = await fetch(`/api/users/${userId}/stats`) + if (res.ok) { + const data = await res.json() + setUserStats(data) + } + } catch (e) { + console.error("Failed to load user stats", e) + } + } + fetchStats() + }, [user?.id]) const handleExport = async () => { if (progressRef.current) { @@ -37,6 +55,8 @@ export default function DashboardPage() { ) } + const isNew = userStats ? userStats.is_new_user : true + return (
@@ -59,30 +79,49 @@ export default function DashboardPage() {
+ {/* Today's Score */}

Today's Score

-

42 / 60

-

+5 from yesterday

+

+ {isNew ? "0 / 0" : `${userStats.today_score} / ${userStats.today_max_score || 60}`} +

+

+ {isNew ? "No tests taken today" : userStats.today_score > 0 ? "Daily session score" : "Start today's DPP"} +

+ + {/* Overall Accuracy */}

Overall Accuracy

-

68%

-

Needs Improvement

+

+ {isNew ? "0%" : `${userStats.overall_accuracy}%`} +

+

= 70 ? 'text-green-500' : 'text-amber-500'}`}> + {isNew ? "Start a DPP to calculate" : userStats.overall_accuracy >= 70 ? "On Track" : "Needs Improvement"} +

+ + {/* Qs Attempted */}

Qs Attempted

-

1,245

-

Lifetime total

+

+ {isNew ? "0" : (userStats.total_questions_attempted || 0).toLocaleString()} +

+

+ {isNew ? "0 questions answered" : "Lifetime total"} +

+ + {/* City Rank */}
@@ -90,8 +129,16 @@ export default function DashboardPage() {

City Rank

-

#42 / 1,200

-

Top 5% in Jaipur

+

+ {isNew ? ( + #-- + ) : ( + <>#{userStats.city_rank || 1} / {userStats.city_total || 50} + )} +

+

+ {isNew ? "Unranked · Take a test" : `Top rank in ${user?.city || "your city"}`} +

diff --git a/src/app/dashboard/streaks/page.tsx b/src/app/dashboard/streaks/page.tsx index 7b94f8d..79b75dc 100644 --- a/src/app/dashboard/streaks/page.tsx +++ b/src/app/dashboard/streaks/page.tsx @@ -2,17 +2,19 @@ import { Card, CardContent } from "@/components/ui/card" import { Flame, Gift, Star, Trophy, Target, ArrowRight, CheckCircle2 } from "lucide-react" +import { useActiveUser } from "@/hooks/useActiveUser" export default function StreaksPage() { - const currentStreak = 12; - const bestStreak = 45; + const { user } = useActiveUser() + const currentStreak = user?.streak_count ?? 1 + const bestStreak = Math.max(currentStreak, user?.longest_streak ?? 1) const rewards = [ - { streak: 7, title: "Consistent Learner", reward: "1 Free Premium DPP", unlocked: true }, - { streak: 14, title: "Dedicated Scholar", reward: "AI Performance Analysis Report", unlocked: false }, - { streak: 30, title: "Exam Warrior", reward: "1 Full Length Mock Test", unlocked: false }, - { streak: 50, title: "PragatiVeer Champion", reward: "1-on-1 AI Tutor Session", unlocked: false }, - { streak: 100, title: "Unstoppable", reward: "Merch + 50% Off Next Renewal", unlocked: false }, + { streak: 7, title: "Consistent Learner", reward: "1 Free Premium DPP", unlocked: currentStreak >= 7 }, + { streak: 14, title: "Dedicated Scholar", reward: "AI Performance Analysis Report", unlocked: currentStreak >= 14 }, + { streak: 30, title: "Exam Warrior", reward: "1 Full Length Mock Test", unlocked: currentStreak >= 30 }, + { streak: 50, title: "PragatiVeer Champion", reward: "1-on-1 AI Tutor Session", unlocked: currentStreak >= 50 }, + { streak: 100, title: "Unstoppable", reward: "Merch + 50% Off Next Renewal", unlocked: currentStreak >= 100 }, ]; return ( @@ -55,7 +57,7 @@ export default function StreaksPage() {

Total Streak Points Earned

-

1,450

+

{(currentStreak * 50).toLocaleString()}

diff --git a/src/app/page.tsx b/src/app/page.tsx index 8ad3f69..47379ed 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Card, CardContent } from "@/components/ui/card" -import { BrainCircuit, Activity, Zap, CheckCircle2, Shield, ArrowRight, Play, BookOpen, LineChart, Trophy, Sparkles, MessageSquare, X, Minus } from "lucide-react" +import { BrainCircuit, Activity, Zap, CheckCircle2, Shield, ArrowRight, Play, BookOpen, LineChart, Trophy, Sparkles, MessageSquare, X, Minus, Target, Calendar } from "lucide-react" import Image from "next/image" import Link from "next/link" import { motion, useScroll, useTransform, Variants } from "framer-motion" @@ -185,13 +185,25 @@ export default function MarketingPage() {

AI Intelligence

-
-
- +
+
+ + Prep Pilot AI +
+
+ + Exam Strategy +
+
+ + AI Study Plan +
+
+ Analytics
-
- +
+ Rank Predictor
diff --git a/src/components/AI/StudyPlanCard.tsx b/src/components/AI/StudyPlanCard.tsx index 811b2f0..2a426d0 100644 --- a/src/components/AI/StudyPlanCard.tsx +++ b/src/components/AI/StudyPlanCard.tsx @@ -5,7 +5,7 @@ import { Card, CardContent } from "@/components/ui/card" import { Check, RefreshCw } from "lucide-react" import Link from "next/link" -import { useActiveUser } from "@/hooks/useActiveUser" +import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser" export function StudyPlanCard() { const [tasks, setTasks] = useState([]) @@ -19,7 +19,7 @@ export function StudyPlanCard() { try { const res = await fetch(`/api/ai-coach/study-plan/today?user_id=${userId}`) if (!res.ok) { - // If no plan is generated yet, fallback to mock silently + // If no plan is generated yet, fallback silently throw new Error("silent_fallback") } const data = await res.json() @@ -28,14 +28,24 @@ export function StudyPlanCard() { if (err.message !== "silent_fallback") { console.error("Study plan fetch error:", err) } - // Mock fallback if celery hasn't seeded yet - setTasks([ - { type: "DPS", description: "Electrostatics + Kinematics", duration_minutes: 20, status: "Done" }, - { type: "Mistake_Review", description: "Thermodynamics error queue", duration_minutes: 10, status: "Done" }, - { type: "Micro_test", description: "GOC — General Organic Chemistry", duration_minutes: 5, status: "Pending" }, - { type: "Test", description: "Weekly Full-Syllabus Mock Test", duration_minutes: 180, status: "Pending" }, - { type: "Revision", description: "Circles + Straight Lines (Maths)", duration_minutes: 15, status: "Pending" } - ]) + if (isDemoUser(userId)) { + // Demo user gets showcase active tasks + setTasks([ + { type: "DPS", description: "Electrostatics + Kinematics", duration_minutes: 20, status: "Done" }, + { type: "Mistake_Review", description: "Thermodynamics error queue", duration_minutes: 10, status: "Done" }, + { type: "Micro_test", description: "GOC — General Organic Chemistry", duration_minutes: 5, status: "Pending" }, + { type: "Test", description: "Weekly Full-Syllabus Mock Test", duration_minutes: 180, status: "Pending" }, + { type: "Revision", description: "Circles + Straight Lines (Maths)", duration_minutes: 15, status: "Pending" } + ]) + } else { + // Real user gets a fresh Day 1 plan with all tasks pending + setTasks([ + { type: "DPS", description: "Day 1 Diagnostic Practice Sheet", duration_minutes: 20, status: "Pending" }, + { type: "Micro_test", description: "Foundation Concept Check", duration_minutes: 15, status: "Pending" }, + { type: "Revision", description: "Target Exam Core Topics Orientation", duration_minutes: 30, status: "Pending" }, + { type: "Mistake_Review", description: "Initialize AI Mistake Engine", duration_minutes: 10, status: "Pending" } + ]) + } } finally { setLoading(false) }