feat: add AI options to landing mock sidebar and wire database-backed stats & leaderboard with fresh zero states
This commit is contained in:
144
backend/main.py
144
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)
|
query = select(models.Attempt).where(models.Attempt.user_id == user_id)
|
||||||
return session.exec(query).all()
|
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 ---
|
# --- ADMIN ROUTES ---
|
||||||
|
|
||||||
@app.get("/api/admin/settings", tags=["Admin"])
|
@app.get("/api/admin/settings", tags=["Admin"])
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ interface Doubt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CommunityPage() {
|
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 [activeFilter, setActiveFilter] = useState<'all' | 'my-doubts' | 'solved' | 'unsolved'>('all')
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [isAskModalOpen, setIsAskModalOpen] = useState(false)
|
const [isAskModalOpen, setIsAskModalOpen] = useState(false)
|
||||||
@@ -58,8 +60,8 @@ export default function CommunityPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "3",
|
id: "3",
|
||||||
author: "Demo Student",
|
author: currentUserName,
|
||||||
avatar: "DS",
|
avatar: currentUserInitials,
|
||||||
timeAgo: "1 day ago",
|
timeAgo: "1 day ago",
|
||||||
subject: "Mathematics",
|
subject: "Mathematics",
|
||||||
topic: "Definite Integration",
|
topic: "Definite Integration",
|
||||||
@@ -73,7 +75,7 @@ export default function CommunityPage() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const filteredDoubts = mockDoubts.filter(d => {
|
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 === 'solved' && !d.isSolved) return false
|
||||||
if (activeFilter === 'unsolved' && d.isSolved) return false
|
if (activeFilter === 'unsolved' && d.isSolved) return false
|
||||||
if (searchQuery && !d.title.toLowerCase().includes(searchQuery.toLowerCase())) return false
|
if (searchQuery && !d.title.toLowerCase().includes(searchQuery.toLowerCase())) return false
|
||||||
|
|||||||
@@ -1,28 +1,46 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Trophy, Medal, Star, Shield, Search } from "lucide-react"
|
import { Trophy, Medal, Star, Shield } from "lucide-react"
|
||||||
|
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
type LeaderboardTab = 'all-india' | 'state' | 'school'
|
type LeaderboardTab = 'all-india' | 'state' | 'school'
|
||||||
|
|
||||||
export default function LeaderboardPage() {
|
export default function LeaderboardPage() {
|
||||||
const [activeTab, setActiveTab] = useState<LeaderboardTab>('all-india')
|
const [activeTab, setActiveTab] = useState<LeaderboardTab>('all-india')
|
||||||
const [isAnonymous, setIsAnonymous] = useState(false)
|
const [isAnonymous, setIsAnonymous] = useState(false)
|
||||||
|
const { user } = useActiveUser()
|
||||||
|
const [rankings, setRankings] = useState<any[]>([])
|
||||||
|
const [loadingRankings, setLoadingRankings] = useState(true)
|
||||||
|
|
||||||
// Mock data representing F-07 requirements (Accuracy x Consistency)
|
useEffect(() => {
|
||||||
const rankings = [
|
async function fetchLeaderboard() {
|
||||||
{ rank: 1, name: "Rahul Verma", score: 9850, accuracy: "92%", streak: 45, isCurrentUser: false },
|
setLoadingRankings(true)
|
||||||
{ rank: 2, name: "Priya Sharma", score: 9420, accuracy: "89%", streak: 42, isCurrentUser: false },
|
try {
|
||||||
{ rank: 3, name: "Amit Kumar", score: 9100, accuracy: "87%", streak: 38, isCurrentUser: false },
|
const url = user?.id
|
||||||
{ rank: 4, name: "Sneha Patel", score: 8950, accuracy: "85%", streak: 30, isCurrentUser: false },
|
? `/api/leaderboard?tab=${activeTab}&user_id=${user.id}`
|
||||||
{ rank: 5, name: "Vikram Singh", score: 8800, accuracy: "84%", streak: 28, isCurrentUser: false },
|
: `/api/leaderboard?tab=${activeTab}`
|
||||||
{ rank: 6, name: "Demo Student", score: 8650, accuracy: "82%", streak: 25, isCurrentUser: true },
|
const res = await fetch(url)
|
||||||
{ rank: 7, name: "Ananya Desai", score: 8500, accuracy: "80%", streak: 20, isCurrentUser: false },
|
if (res.ok) {
|
||||||
{ rank: 8, name: "Karan Johar", score: 8450, accuracy: "79%", streak: 18, isCurrentUser: false },
|
const data = await res.json()
|
||||||
{ rank: 9, name: "Neha Gupta", score: 8300, accuracy: "78%", streak: 15, isCurrentUser: false },
|
setRankings(data.rankings || [])
|
||||||
{ rank: 10, name: "Rohan Das", score: 8100, accuracy: "75%", streak: 12, isCurrentUser: false },
|
}
|
||||||
]
|
} 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 (
|
return (
|
||||||
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-7xl mx-auto">
|
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-7xl mx-auto">
|
||||||
@@ -72,50 +90,57 @@ export default function LeaderboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
{loadingRankings ? (
|
||||||
{rankings.map((student) => (
|
<div className="flex flex-col items-center justify-center p-12 bg-card rounded-2xl border border-border">
|
||||||
<div
|
<div className="size-8 border-4 border-[#00F0FF] border-t-transparent rounded-full animate-spin mb-3"></div>
|
||||||
key={student.rank}
|
<p className="text-sm text-muted-foreground font-medium">Calculating live rankings...</p>
|
||||||
className={`flex justify-between items-center p-4 rounded-xl border transition-all ${
|
</div>
|
||||||
student.isCurrentUser
|
) : (
|
||||||
? 'border-[#00F0FF] bg-[#00F0FF]/5 shadow-[0_0_15px_-3px_rgba(0,240,255,0.2)]'
|
<div className="space-y-3">
|
||||||
: 'border-border bg-card hover:bg-secondary/20'
|
{rankings.map((student) => (
|
||||||
}`}
|
<div
|
||||||
>
|
key={student.rank}
|
||||||
<div className="w-12 flex justify-center">
|
className={`flex justify-between items-center p-4 rounded-xl border transition-all ${
|
||||||
{student.rank === 1 ? <Medal className="text-yellow-500" size={24} /> :
|
student.isCurrentUser
|
||||||
student.rank === 2 ? <Medal className="text-gray-400" size={24} /> :
|
? 'border-[#00F0FF] bg-[#00F0FF]/5 shadow-[0_0_15px_-3px_rgba(0,240,255,0.2)]'
|
||||||
student.rank === 3 ? <Medal className="text-amber-700" size={24} /> :
|
: 'border-border bg-card hover:bg-secondary/20'
|
||||||
<span className={`font-bold text-lg ${student.isCurrentUser ? 'text-[#00F0FF]' : 'text-muted-foreground'}`}>#{student.rank}</span>}
|
}`}
|
||||||
</div>
|
>
|
||||||
|
<div className="w-12 flex justify-center">
|
||||||
<div className="flex-1 px-4 flex items-center gap-3">
|
{student.rank === 1 ? <Medal className="text-yellow-500" size={24} /> :
|
||||||
<div className={`h-10 w-10 rounded-full flex items-center justify-center font-bold text-sm ${
|
student.rank === 2 ? <Medal className="text-gray-400" size={24} /> :
|
||||||
student.isCurrentUser ? 'bg-[#00F0FF]/20 text-[#00F0FF]' : 'bg-secondary text-muted-foreground'
|
student.rank === 3 ? <Medal className="text-amber-700" size={24} /> :
|
||||||
}`}>
|
<span className={`font-bold text-lg ${student.isCurrentUser ? 'text-[#00F0FF]' : 'text-muted-foreground'}`}>#{student.rank}</span>}
|
||||||
{student.name.substring(0, 2).toUpperCase()}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<p className={`font-bold ${student.isCurrentUser ? 'text-[#00F0FF]' : 'text-foreground'}`}>
|
<div className="flex-1 px-4 flex items-center gap-3">
|
||||||
{student.isCurrentUser ? (isAnonymous ? "Anonymous User" : student.name + " (You)") : student.name}
|
<div className={`h-10 w-10 rounded-full flex items-center justify-center font-bold text-sm ${
|
||||||
</p>
|
student.isCurrentUser ? 'bg-[#00F0FF]/20 text-[#00F0FF]' : 'bg-secondary text-muted-foreground'
|
||||||
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
}`}>
|
||||||
<Shield size={12} /> Accuracy: {student.accuracy}
|
{(student.name || "U").substring(0, 2).toUpperCase()}
|
||||||
</p>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className={`font-bold ${student.isCurrentUser ? 'text-[#00F0FF]' : 'text-foreground'}`}>
|
||||||
|
{student.isCurrentUser ? (isAnonymous ? "Anonymous User" : `${student.name} (You)`) : student.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
||||||
|
<Shield size={12} /> Accuracy: {student.accuracy}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-8 w-48 justify-end pr-4 items-center">
|
||||||
|
<div className="text-center font-mono font-bold text-foreground">
|
||||||
|
{student.score.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<div className="text-center flex items-center gap-1 font-bold text-orange-500">
|
||||||
|
<Star size={14} className="fill-orange-500" /> {student.streak}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
<div className="flex gap-8 w-48 justify-end pr-4 items-center">
|
</div>
|
||||||
<div className="text-center font-mono font-bold text-foreground">
|
)}
|
||||||
{student.score.toLocaleString()}
|
|
||||||
</div>
|
|
||||||
<div className="text-center flex items-center gap-1 font-bold text-orange-500">
|
|
||||||
<Star size={14} className="fill-orange-500" /> {student.streak}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -124,10 +149,14 @@ export default function LeaderboardPage() {
|
|||||||
<CardTitle className="text-xl flex items-center gap-2">Your Standing</CardTitle>
|
<CardTitle className="text-xl flex items-center gap-2">Your Standing</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-4xl font-black text-[#00F0FF] mb-1">#6</div>
|
<div className="text-4xl font-black text-[#00F0FF] mb-1">#{currentStudent.rank}</div>
|
||||||
<p className="text-sm font-medium text-foreground mb-4">Top 15% of users</p>
|
<p className="text-sm font-medium text-foreground mb-4">
|
||||||
|
{currentStudent.score === 0 ? "Fresh Start · Day 1 of your journey" : `Rank #${currentStudent.rank} overall`}
|
||||||
|
</p>
|
||||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||||
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!`}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-6 flex items-center justify-between border-t border-border pt-4">
|
<div className="mt-6 flex items-center justify-between border-t border-border pt-4">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useRef } from "react"
|
import { useRef, useState, useEffect } from "react"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -13,6 +13,24 @@ import { useActiveUser } from "@/hooks/useActiveUser"
|
|||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const { user, hasAccessTo, loading } = useActiveUser()
|
const { user, hasAccessTo, loading } = useActiveUser()
|
||||||
const progressRef = useRef<HTMLDivElement>(null)
|
const progressRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [userStats, setUserStats] = useState<any>(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 () => {
|
const handleExport = async () => {
|
||||||
if (progressRef.current) {
|
if (progressRef.current) {
|
||||||
@@ -37,6 +55,8 @@ export default function DashboardPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNew = userStats ? userStats.is_new_user : true
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 max-w-7xl mx-auto space-y-10 animate-in fade-in duration-500 slide-in-from-bottom-4">
|
<div className="p-8 max-w-7xl mx-auto space-y-10 animate-in fade-in duration-500 slide-in-from-bottom-4">
|
||||||
|
|
||||||
@@ -59,30 +79,49 @@ export default function DashboardPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div ref={progressRef} className="grid grid-cols-2 md:grid-cols-4 gap-4 p-4 -mx-4 bg-background rounded-2xl">
|
<div ref={progressRef} className="grid grid-cols-2 md:grid-cols-4 gap-4 p-4 -mx-4 bg-background rounded-2xl">
|
||||||
|
{/* Today's Score */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-5 flex flex-col items-center text-center">
|
<CardContent className="p-5 flex flex-col items-center text-center">
|
||||||
<div className="p-3 bg-secondary rounded-full mb-3 text-primary"><Target size={24} /></div>
|
<div className="p-3 bg-secondary rounded-full mb-3 text-primary"><Target size={24} /></div>
|
||||||
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Today's Score</p>
|
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Today's Score</p>
|
||||||
<h3 className="text-2xl font-bold">42 / 60</h3>
|
<h3 className="text-2xl font-bold">
|
||||||
<p className="text-xs text-green-500 font-bold mt-2 flex items-center gap-1"><TrendingUp size={12}/> +5 from yesterday</p>
|
{isNew ? "0 / 0" : `${userStats.today_score} / ${userStats.today_max_score || 60}`}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-muted-foreground font-medium mt-2">
|
||||||
|
{isNew ? "No tests taken today" : userStats.today_score > 0 ? "Daily session score" : "Start today's DPP"}
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Overall Accuracy */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-5 flex flex-col items-center text-center">
|
<CardContent className="p-5 flex flex-col items-center text-center">
|
||||||
<div className="p-3 bg-secondary rounded-full mb-3 text-amber-500"><Activity size={24} /></div>
|
<div className="p-3 bg-secondary rounded-full mb-3 text-amber-500"><Activity size={24} /></div>
|
||||||
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Overall Accuracy</p>
|
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Overall Accuracy</p>
|
||||||
<h3 className="text-2xl font-bold">68%</h3>
|
<h3 className="text-2xl font-bold">
|
||||||
<p className="text-xs text-amber-500 font-bold mt-2">Needs Improvement</p>
|
{isNew ? "0%" : `${userStats.overall_accuracy}%`}
|
||||||
|
</h3>
|
||||||
|
<p className={`text-xs font-bold mt-2 ${isNew ? 'text-muted-foreground font-normal' : userStats.overall_accuracy >= 70 ? 'text-green-500' : 'text-amber-500'}`}>
|
||||||
|
{isNew ? "Start a DPP to calculate" : userStats.overall_accuracy >= 70 ? "On Track" : "Needs Improvement"}
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Qs Attempted */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-5 flex flex-col items-center text-center">
|
<CardContent className="p-5 flex flex-col items-center text-center">
|
||||||
<div className="p-3 bg-secondary rounded-full mb-3 text-[#00F0FF]"><CheckCircle2 size={24} /></div>
|
<div className="p-3 bg-secondary rounded-full mb-3 text-[#00F0FF]"><CheckCircle2 size={24} /></div>
|
||||||
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Qs Attempted</p>
|
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Qs Attempted</p>
|
||||||
<h3 className="text-2xl font-bold">1,245</h3>
|
<h3 className="text-2xl font-bold">
|
||||||
<p className="text-xs text-muted-foreground font-medium mt-2">Lifetime total</p>
|
{isNew ? "0" : (userStats.total_questions_attempted || 0).toLocaleString()}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-muted-foreground font-medium mt-2">
|
||||||
|
{isNew ? "0 questions answered" : "Lifetime total"}
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* City Rank */}
|
||||||
<Card className="relative overflow-hidden group">
|
<Card className="relative overflow-hidden group">
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-background to-secondary/50 z-0"></div>
|
<div className="absolute inset-0 bg-gradient-to-br from-background to-secondary/50 z-0"></div>
|
||||||
<CardContent className="p-5 flex flex-col items-center text-center relative z-10">
|
<CardContent className="p-5 flex flex-col items-center text-center relative z-10">
|
||||||
@@ -90,8 +129,16 @@ export default function DashboardPage() {
|
|||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">City Rank</p>
|
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">City Rank</p>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-2xl font-bold">#42 <span className="text-base text-muted-foreground font-medium">/ 1,200</span></h3>
|
<h3 className="text-2xl font-bold">
|
||||||
<p className="text-xs text-muted-foreground font-medium mt-2 text-[#00F0FF]">Top 5% in Jaipur</p>
|
{isNew ? (
|
||||||
|
<span className="text-muted-foreground text-xl">#--</span>
|
||||||
|
) : (
|
||||||
|
<>#{userStats.city_rank || 1} <span className="text-base text-muted-foreground font-medium">/ {userStats.city_total || 50}</span></>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-muted-foreground font-medium mt-2 text-[#00F0FF]">
|
||||||
|
{isNew ? "Unranked · Take a test" : `Top rank in ${user?.city || "your city"}`}
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,17 +2,19 @@
|
|||||||
|
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { Flame, Gift, Star, Trophy, Target, ArrowRight, CheckCircle2 } from "lucide-react"
|
import { Flame, Gift, Star, Trophy, Target, ArrowRight, CheckCircle2 } from "lucide-react"
|
||||||
|
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
export default function StreaksPage() {
|
export default function StreaksPage() {
|
||||||
const currentStreak = 12;
|
const { user } = useActiveUser()
|
||||||
const bestStreak = 45;
|
const currentStreak = user?.streak_count ?? 1
|
||||||
|
const bestStreak = Math.max(currentStreak, user?.longest_streak ?? 1)
|
||||||
|
|
||||||
const rewards = [
|
const rewards = [
|
||||||
{ streak: 7, title: "Consistent Learner", reward: "1 Free Premium DPP", unlocked: true },
|
{ streak: 7, title: "Consistent Learner", reward: "1 Free Premium DPP", unlocked: currentStreak >= 7 },
|
||||||
{ streak: 14, title: "Dedicated Scholar", reward: "AI Performance Analysis Report", unlocked: false },
|
{ 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: false },
|
{ 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: false },
|
{ 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: false },
|
{ streak: 100, title: "Unstoppable", reward: "Merch + 50% Off Next Renewal", unlocked: currentStreak >= 100 },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -55,7 +57,7 @@ export default function StreaksPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground mb-1">Total Streak Points Earned</p>
|
<p className="text-sm text-muted-foreground mb-1">Total Streak Points Earned</p>
|
||||||
<p className="text-3xl font-bold text-[#00F0FF]">1,450</p>
|
<p className="text-3xl font-bold text-[#00F0FF]">{(currentStreak * 50).toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
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 Image from "next/image"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { motion, useScroll, useTransform, Variants } from "framer-motion"
|
import { motion, useScroll, useTransform, Variants } from "framer-motion"
|
||||||
@@ -185,13 +185,25 @@ export default function MarketingPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="pt-6 border-t border-border/50">
|
<div className="pt-6 border-t border-border/50">
|
||||||
<p className="px-3 text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-3">AI Intelligence</p>
|
<p className="px-3 text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-3">AI Intelligence</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-muted-foreground hover:bg-secondary/30 hover:text-foreground text-sm font-semibold transition-all">
|
<div className="flex items-center gap-3 px-3 py-2 rounded-xl text-muted-foreground hover:bg-secondary/30 hover:text-foreground text-sm font-semibold transition-all">
|
||||||
<LineChart size={18} />
|
<Zap size={17} className="text-yellow-500 shrink-0" />
|
||||||
|
Prep Pilot AI
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 px-3 py-2 rounded-xl text-muted-foreground hover:bg-secondary/30 hover:text-foreground text-sm font-semibold transition-all">
|
||||||
|
<Target size={17} className="text-[#00F0FF] shrink-0" />
|
||||||
|
Exam Strategy
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 px-3 py-2 rounded-xl text-muted-foreground hover:bg-secondary/30 hover:text-foreground text-sm font-semibold transition-all">
|
||||||
|
<Calendar size={17} className="text-primary shrink-0" />
|
||||||
|
AI Study Plan
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 px-3 py-2 rounded-xl text-muted-foreground hover:bg-secondary/30 hover:text-foreground text-sm font-semibold transition-all">
|
||||||
|
<LineChart size={17} className="shrink-0" />
|
||||||
Analytics
|
Analytics
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-muted-foreground hover:bg-secondary/30 hover:text-foreground text-sm font-semibold transition-all">
|
<div className="flex items-center gap-3 px-3 py-2 rounded-xl text-muted-foreground hover:bg-secondary/30 hover:text-foreground text-sm font-semibold transition-all">
|
||||||
<Trophy size={18} />
|
<Trophy size={17} className="shrink-0" />
|
||||||
Rank Predictor
|
Rank Predictor
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Card, CardContent } from "@/components/ui/card"
|
|||||||
import { Check, RefreshCw } from "lucide-react"
|
import { Check, RefreshCw } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
|
||||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
export function StudyPlanCard() {
|
export function StudyPlanCard() {
|
||||||
const [tasks, setTasks] = useState<any[]>([])
|
const [tasks, setTasks] = useState<any[]>([])
|
||||||
@@ -19,7 +19,7 @@ export function StudyPlanCard() {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/ai-coach/study-plan/today?user_id=${userId}`)
|
const res = await fetch(`/api/ai-coach/study-plan/today?user_id=${userId}`)
|
||||||
if (!res.ok) {
|
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")
|
throw new Error("silent_fallback")
|
||||||
}
|
}
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
@@ -28,14 +28,24 @@ export function StudyPlanCard() {
|
|||||||
if (err.message !== "silent_fallback") {
|
if (err.message !== "silent_fallback") {
|
||||||
console.error("Study plan fetch error:", err)
|
console.error("Study plan fetch error:", err)
|
||||||
}
|
}
|
||||||
// Mock fallback if celery hasn't seeded yet
|
if (isDemoUser(userId)) {
|
||||||
setTasks([
|
// Demo user gets showcase active tasks
|
||||||
{ type: "DPS", description: "Electrostatics + Kinematics", duration_minutes: 20, status: "Done" },
|
setTasks([
|
||||||
{ type: "Mistake_Review", description: "Thermodynamics error queue", duration_minutes: 10, status: "Done" },
|
{ type: "DPS", description: "Electrostatics + Kinematics", duration_minutes: 20, status: "Done" },
|
||||||
{ type: "Micro_test", description: "GOC — General Organic Chemistry", duration_minutes: 5, status: "Pending" },
|
{ type: "Mistake_Review", description: "Thermodynamics error queue", duration_minutes: 10, status: "Done" },
|
||||||
{ type: "Test", description: "Weekly Full-Syllabus Mock Test", duration_minutes: 180, status: "Pending" },
|
{ type: "Micro_test", description: "GOC — General Organic Chemistry", duration_minutes: 5, status: "Pending" },
|
||||||
{ type: "Revision", description: "Circles + Straight Lines (Maths)", duration_minutes: 15, 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 {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user