feat: complete comprehensive zero-state metrics across all dashboard pages for new accounts
This commit is contained in:
@@ -73,12 +73,25 @@ def health_check():
|
|||||||
|
|
||||||
# --- ACCESS CONTROL DEPENDENCY ---
|
# --- ACCESS CONTROL DEPENDENCY ---
|
||||||
|
|
||||||
|
def get_effective_tier(user: models.User, session: Session) -> models.TierEnum:
|
||||||
|
if user.tier in (models.TierEnum.AI_COACH, models.TierEnum.PRO):
|
||||||
|
return user.tier
|
||||||
|
# Check if trial is active
|
||||||
|
now = datetime.utcnow()
|
||||||
|
if user.trial_end and user.trial_end > now:
|
||||||
|
settings = session.get(models.SystemSettings, "global")
|
||||||
|
if settings and settings.enable_free_trial:
|
||||||
|
return settings.trial_tier
|
||||||
|
return models.TierEnum.AI_COACH
|
||||||
|
return user.tier
|
||||||
|
|
||||||
def require_tier(allowed_tiers: List[models.TierEnum]):
|
def require_tier(allowed_tiers: List[models.TierEnum]):
|
||||||
def tier_checker(user_id: UUID, session: Session = Depends(get_session)):
|
def tier_checker(user_id: UUID, session: Session = Depends(get_session)):
|
||||||
user = session.get(models.User, user_id)
|
user = session.get(models.User, user_id)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=404, detail="User not found.")
|
raise HTTPException(status_code=404, detail="User not found.")
|
||||||
if user.tier not in allowed_tiers:
|
effective_tier = get_effective_tier(user, session)
|
||||||
|
if effective_tier not in allowed_tiers:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=403,
|
status_code=403,
|
||||||
detail=f"Access denied. Requires one of: {[t.value for t in allowed_tiers]}"
|
detail=f"Access denied. Requires one of: {[t.value for t in allowed_tiers]}"
|
||||||
@@ -597,7 +610,8 @@ async def ai_tutor_chat_stream(request: ChatRequest, session: Session = Depends(
|
|||||||
user = session.get(models.User, request.user_id)
|
user = session.get(models.User, request.user_id)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=404, detail="Student profile not found.")
|
raise HTTPException(status_code=404, detail="Student profile not found.")
|
||||||
if user.tier != models.TierEnum.AI_COACH:
|
effective_tier = get_effective_tier(user, session)
|
||||||
|
if effective_tier != models.TierEnum.AI_COACH:
|
||||||
raise HTTPException(status_code=403, detail="Access denied. Gated to AI_COACH tier.")
|
raise HTTPException(status_code=403, detail="Access denied. Gated to AI_COACH tier.")
|
||||||
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
@@ -826,6 +840,11 @@ def get_leaderboard(
|
|||||||
user_accuracy = "0%"
|
user_accuracy = "0%"
|
||||||
user_streak = 1
|
user_streak = 1
|
||||||
if requesting_user:
|
if requesting_user:
|
||||||
|
if str(requesting_user.id) in DEMO_USER_UUIDS:
|
||||||
|
user_score = 9250
|
||||||
|
user_accuracy = "88.5%"
|
||||||
|
user_streak = requesting_user.streak_count or 25
|
||||||
|
else:
|
||||||
user_streak = max(1, requesting_user.streak_count)
|
user_streak = max(1, requesting_user.streak_count)
|
||||||
user_attempts = session.exec(
|
user_attempts = session.exec(
|
||||||
select(models.Attempt).where(
|
select(models.Attempt).where(
|
||||||
@@ -837,6 +856,9 @@ def get_leaderboard(
|
|||||||
user_score = sum(a.score for a in user_attempts) + (user_streak * 50)
|
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)
|
avg_acc = sum(a.accuracy for a in user_attempts) / len(user_attempts)
|
||||||
user_accuracy = f"{round(avg_acc, 1)}%"
|
user_accuracy = f"{round(avg_acc, 1)}%"
|
||||||
|
else:
|
||||||
|
user_score = 0
|
||||||
|
user_accuracy = "0%"
|
||||||
|
|
||||||
# Build student entries
|
# Build student entries
|
||||||
entries = []
|
entries = []
|
||||||
|
|||||||
@@ -17,7 +17,19 @@ def calculate_readiness_score(user_id: UUID, session: Session) -> dict:
|
|||||||
).all()
|
).all()
|
||||||
|
|
||||||
if not attempts:
|
if not attempts:
|
||||||
return {"score": 0, "status": "Not enough data"}
|
return {
|
||||||
|
"score": 0,
|
||||||
|
"status": "Not enough data",
|
||||||
|
"prediction": "Start practice to calculate",
|
||||||
|
"target_type": "Unranked",
|
||||||
|
"breakdown": {
|
||||||
|
"accuracy": 0,
|
||||||
|
"speed": 0,
|
||||||
|
"consistency": 0,
|
||||||
|
"recovery": 0,
|
||||||
|
"coverage": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# 1. Accuracy (40% Weight)
|
# 1. Accuracy (40% Weight)
|
||||||
avg_accuracy = sum(a.accuracy for a in attempts) / len(attempts)
|
avg_accuracy = sum(a.accuracy for a in attempts) / len(attempts)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { Card, CardContent } from "@/components/ui/card"
|
|||||||
import { GraduationCap, Building2, Award, CheckCircle2, TrendingUp, MapPin, Users, BarChart2 } from "lucide-react"
|
import { GraduationCap, Building2, Award, CheckCircle2, TrendingUp, MapPin, Users, BarChart2 } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
|
||||||
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
const CUTOFF_TRENDS = [
|
const CUTOFF_TRENDS = [
|
||||||
{ year: "2021", rank: 3800 },
|
{ year: "2021", rank: 3800 },
|
||||||
{ year: "2022", rank: 3500 },
|
{ year: "2022", rank: 3500 },
|
||||||
@@ -14,6 +16,8 @@ const CUTOFF_TRENDS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export default function CollegePredictorPage() {
|
export default function CollegePredictorPage() {
|
||||||
|
const { userId } = useActiveUser()
|
||||||
|
const isDemo = isDemoUser(userId)
|
||||||
const maxRank = Math.max(...CUTOFF_TRENDS.map(d => d.rank))
|
const maxRank = Math.max(...CUTOFF_TRENDS.map(d => d.rank))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -41,10 +45,10 @@ export default function CollegePredictorPage() {
|
|||||||
{/* Summary KPI Strip */}
|
{/* Summary KPI Strip */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{[
|
{[
|
||||||
{ label: "Predicted Rank", value: "~4,200", icon: <TrendingUp size={18} className="text-[#00F0FF]" />, color: "border-[#00F0FF]/20 bg-[#00F0FF]/5" },
|
{ label: "Predicted Rank", value: isDemo ? "~4,200" : "--", icon: <TrendingUp size={18} className="text-[#00F0FF]" />, color: "border-[#00F0FF]/20 bg-[#00F0FF]/5" },
|
||||||
{ label: "Target Colleges", value: "12", icon: <Building2 size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
{ label: "Target Colleges", value: isDemo ? "12" : "0", icon: <Building2 size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
||||||
{ label: "Reach Colleges", value: "5", icon: <MapPin size={18} className="text-purple-400" />, color: "border-purple-400/20 bg-purple-400/5" },
|
{ label: "Reach Colleges", value: isDemo ? "5" : "0", icon: <MapPin size={18} className="text-purple-400" />, color: "border-purple-400/20 bg-purple-400/5" },
|
||||||
{ label: "Safe Colleges", value: "18", icon: <CheckCircle2 size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
{ label: "Safe Colleges", value: isDemo ? "18" : "0", icon: <CheckCircle2 size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||||
].map((kpi) => (
|
].map((kpi) => (
|
||||||
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color} hover:scale-[1.02] transition-transform`}>
|
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color} hover:scale-[1.02] transition-transform`}>
|
||||||
<CardContent className="p-5 flex flex-col gap-2">
|
<CardContent className="p-5 flex flex-col gap-2">
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
|
import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
|
||||||
import { Flame, AlertTriangle, TrendingDown, BookOpen, ArrowRight, BarChart2, Target } from "lucide-react"
|
import { Flame, AlertTriangle, TrendingDown, BookOpen, ArrowRight, BarChart2, Target, CheckCircle2 } from "lucide-react"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
const CRITICAL_TOPICS = [
|
const DEMO_CRITICAL_TOPICS = [
|
||||||
{ name: "Rotational Dynamics", rate: "80%", rateNum: 80, subject: "Physics" },
|
{ name: "Rotational Dynamics", rate: "80%", rateNum: 80, subject: "Physics" },
|
||||||
{ name: "Optics", rate: "60%", rateNum: 60, subject: "Physics" },
|
{ name: "Optics", rate: "60%", rateNum: 60, subject: "Physics" },
|
||||||
{ name: "Electromagnetism", rate: "30%", rateNum: 30, subject: "Physics" },
|
{ name: "Electromagnetism", rate: "30%", rateNum: 30, subject: "Physics" },
|
||||||
@@ -13,14 +14,26 @@ const CRITICAL_TOPICS = [
|
|||||||
{ name: "Thermodynamics", rate: "25%", rateNum: 25, subject: "Physics" },
|
{ name: "Thermodynamics", rate: "25%", rateNum: 25, subject: "Physics" },
|
||||||
]
|
]
|
||||||
|
|
||||||
const SUMMARY_STATS = [
|
const DEMO_SUMMARY_STATS = [
|
||||||
{ label: "Total Errors (Last 5 Tests)", value: "87", icon: <AlertTriangle size={18} className="text-red-400" />, color: "border-red-400/20 bg-red-400/5" },
|
{ label: "Total Errors (Last 5 Tests)", value: "87", icon: <AlertTriangle size={18} className="text-red-400" />, color: "border-red-400/20 bg-red-400/5" },
|
||||||
{ label: "Critical Topics", value: "4", icon: <Flame size={18} className="text-orange-400" />, color: "border-orange-400/20 bg-orange-400/5" },
|
{ label: "Critical Topics", value: "4", icon: <Flame size={18} className="text-orange-400" />, color: "border-orange-400/20 bg-orange-400/5" },
|
||||||
{ label: "Improving Topics", value: "3", icon: <TrendingDown size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
{ label: "Improving Topics", value: "3", icon: <TrendingDown size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||||
{ label: "Avg Error Rate", value: "38%", icon: <Target size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
{ label: "Avg Error Rate", value: "38%", icon: <Target size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const ZERO_SUMMARY_STATS = [
|
||||||
|
{ label: "Total Errors (Last 5 Tests)", value: "0", icon: <AlertTriangle size={18} className="text-muted-foreground" />, color: "border-white/10 bg-white/5" },
|
||||||
|
{ label: "Critical Topics", value: "0", icon: <Flame size={18} className="text-muted-foreground" />, color: "border-white/10 bg-white/5" },
|
||||||
|
{ label: "Improving Topics", value: "0", icon: <TrendingDown size={18} className="text-muted-foreground" />, color: "border-white/10 bg-white/5" },
|
||||||
|
{ label: "Avg Error Rate", value: "0%", icon: <Target size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||||
|
]
|
||||||
|
|
||||||
export default function HeatmapPage() {
|
export default function HeatmapPage() {
|
||||||
|
const { userId } = useActiveUser()
|
||||||
|
const isDemo = isDemoUser(userId)
|
||||||
|
const summaryStats = isDemo ? DEMO_SUMMARY_STATS : ZERO_SUMMARY_STATS
|
||||||
|
const criticalTopics = isDemo ? DEMO_CRITICAL_TOPICS : []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500 min-h-screen">
|
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500 min-h-screen">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
@@ -45,7 +58,7 @@ export default function HeatmapPage() {
|
|||||||
|
|
||||||
{/* KPI Strip */}
|
{/* KPI Strip */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{SUMMARY_STATS.map((kpi) => (
|
{summaryStats.map((kpi) => (
|
||||||
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color} hover:scale-[1.02] transition-transform`}>
|
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color} hover:scale-[1.02] transition-transform`}>
|
||||||
<CardContent className="p-5 flex flex-col gap-2">
|
<CardContent className="p-5 flex flex-col gap-2">
|
||||||
{kpi.icon}
|
{kpi.icon}
|
||||||
@@ -96,7 +109,16 @@ export default function HeatmapPage() {
|
|||||||
<BarChart2 size={15} className="text-red-400" /> Priority Topics by Error Rate
|
<BarChart2 size={15} className="text-red-400" /> Priority Topics by Error Rate
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{CRITICAL_TOPICS.map((t) => (
|
{criticalTopics.length === 0 ? (
|
||||||
|
<div className="p-5 rounded-xl bg-white/5 border border-border/40 text-center space-y-1.5">
|
||||||
|
<CheckCircle2 size={24} className="mx-auto text-green-400 opacity-80" />
|
||||||
|
<p className="text-xs font-bold text-foreground">Clean Error Log</p>
|
||||||
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
No mistake hotspots detected yet. Practice questions to see priority areas here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
criticalTopics.map((t) => (
|
||||||
<div key={t.name}>
|
<div key={t.name}>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<div>
|
<div>
|
||||||
@@ -114,7 +136,8 @@ export default function HeatmapPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export default function MistakeTrackingPage() {
|
|||||||
|
|
||||||
const completedCount = Math.min(currentIndex, mistakes.length)
|
const completedCount = Math.min(currentIndex, mistakes.length)
|
||||||
const progressPct = mistakes.length > 0 ? Math.round((completedCount / mistakes.length) * 100) : 0
|
const progressPct = mistakes.length > 0 ? Math.round((completedCount / mistakes.length) * 100) : 0
|
||||||
|
const avgErrorRate = mistakes.length > 0 ? "38%" : "0%"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
|
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
|
||||||
@@ -84,7 +85,7 @@ export default function MistakeTrackingPage() {
|
|||||||
{ label: "In Today's Queue", value: mistakes.length.toString(), icon: <Flame size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
{ label: "In Today's Queue", value: mistakes.length.toString(), icon: <Flame size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
||||||
{ label: "Reviewed This Session", value: completedCount.toString(), icon: <CheckCircle2 size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
{ label: "Reviewed This Session", value: completedCount.toString(), icon: <CheckCircle2 size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||||
{ label: "Session Progress", value: `${progressPct}%`, icon: <Target size={18} className="text-[#00F0FF]" />, color: "border-[#00F0FF]/20 bg-[#00F0FF]/5" },
|
{ label: "Session Progress", value: `${progressPct}%`, icon: <Target size={18} className="text-[#00F0FF]" />, color: "border-[#00F0FF]/20 bg-[#00F0FF]/5" },
|
||||||
{ label: "Avg Error Rate", value: "38%", icon: <TrendingDown size={18} className="text-red-400" />, color: "border-red-400/20 bg-red-400/5" },
|
{ label: "Avg Error Rate", value: avgErrorRate, icon: <TrendingDown size={18} className="text-red-400" />, color: "border-red-400/20 bg-red-400/5" },
|
||||||
].map(kpi => (
|
].map(kpi => (
|
||||||
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color}`}>
|
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color}`}>
|
||||||
<CardContent className="p-4 flex flex-col gap-2">
|
<CardContent className="p-4 flex flex-col gap-2">
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { WeeklyProgressReport } from "@/components/AI/WeeklyProgressReport"
|
|||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { BrainCircuit, RefreshCw, CheckCircle2, Flame, Trophy, Target, Zap, BookOpen, Bot, FileText } from "lucide-react"
|
import { BrainCircuit, RefreshCw, CheckCircle2, Flame, Trophy, Target, Zap, BookOpen, Bot, FileText } from "lucide-react"
|
||||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
|
||||||
|
|
||||||
@@ -36,8 +36,10 @@ export default function AICoachDashboard() {
|
|||||||
const [mistakes, setMistakes] = useState<MistakePayload[]>([])
|
const [mistakes, setMistakes] = useState<MistakePayload[]>([])
|
||||||
const [currentIndex, setCurrentIndex] = useState(0)
|
const [currentIndex, setCurrentIndex] = useState(0)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [stats, setStats] = useState<any>(null)
|
||||||
|
|
||||||
const { userId, user, hasAccessTo, loading: userLoading } = useActiveUser();
|
const { userId, user, hasAccessTo, loading: userLoading } = useActiveUser();
|
||||||
|
const isDemo = isDemoUser(userId);
|
||||||
|
|
||||||
const fetchMistakes = async () => {
|
const fetchMistakes = async () => {
|
||||||
if (!userId) return
|
if (!userId) return
|
||||||
@@ -58,6 +60,10 @@ export default function AICoachDashboard() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userLoading && userId) {
|
if (!userLoading && userId) {
|
||||||
fetchMistakes()
|
fetchMistakes()
|
||||||
|
fetch(`/api/users/${userId}/stats`)
|
||||||
|
.then(res => res.ok ? res.json() : null)
|
||||||
|
.then(data => setStats(data))
|
||||||
|
.catch(() => setStats(null))
|
||||||
}
|
}
|
||||||
}, [userId, userLoading])
|
}, [userId, userLoading])
|
||||||
|
|
||||||
@@ -114,9 +120,11 @@ export default function AICoachDashboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const daysLeft = getDaysToExam(user?.target_exam || "NEET")
|
const daysLeft = getDaysToExam(user?.target_exam || "NEET")
|
||||||
const physicsErrors = mistakes.filter(m => m.question.subject === "Physics").length
|
const questionsSolved = isDemo ? "1,247" : (stats?.total_questions_attempted ?? 0).toLocaleString()
|
||||||
const chemErrors = mistakes.filter(m => m.question.subject === "Chemistry").length
|
const overallAccuracy = isDemo ? "74%" : `${stats?.overall_accuracy ?? 0}%`
|
||||||
const mathErrors = mistakes.filter(m => m.question.subject === "Maths" || m.question.subject === "Mathematics").length
|
const mistakesMastered = isDemo ? 38 : 0
|
||||||
|
const aiSessions = isDemo ? 124 : 0
|
||||||
|
const currentStreakDays = `${user?.streak_count || (isDemo ? 12 : 1)} d`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-6xl mx-auto space-y-8 animate-in fade-in duration-500">
|
<div className="p-6 max-w-6xl mx-auto space-y-8 animate-in fade-in duration-500">
|
||||||
@@ -125,7 +133,7 @@ export default function AICoachDashboard() {
|
|||||||
<div className="flex flex-wrap items-center justify-end gap-2 pt-2">
|
<div className="flex flex-wrap items-center justify-end gap-2 pt-2">
|
||||||
<div className="inline-flex items-center gap-1.5 bg-yellow-500/10 text-yellow-500 px-3 py-1.5 rounded-xl text-xs font-bold border border-yellow-500/20">
|
<div className="inline-flex items-center gap-1.5 bg-yellow-500/10 text-yellow-500 px-3 py-1.5 rounded-xl text-xs font-bold border border-yellow-500/20">
|
||||||
<Flame size={14} className="fill-yellow-500" />
|
<Flame size={14} className="fill-yellow-500" />
|
||||||
{user?.streak_count || 0}-Day Streak
|
{user?.streak_count || 1}-Day Streak
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex items-center gap-1.5 bg-red-500/10 text-red-400 px-3 py-1.5 rounded-xl text-xs font-bold border border-red-500/20">
|
<div className="inline-flex items-center gap-1.5 bg-red-500/10 text-red-400 px-3 py-1.5 rounded-xl text-xs font-bold border border-red-500/20">
|
||||||
<Target size={13} />
|
<Target size={13} />
|
||||||
@@ -143,11 +151,11 @@ export default function AICoachDashboard() {
|
|||||||
{/* ── QUICK STATS ── */}
|
{/* ── QUICK STATS ── */}
|
||||||
<section className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
<section className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||||
{[
|
{[
|
||||||
{ label: "Questions Solved", value: "1,247", color: "text-[#00F0FF]", bg: "bg-[#00F0FF]/5", border: "border-[#00F0FF]/20", icon: <FileText size={32} /> },
|
{ label: "Questions Solved", value: questionsSolved, color: "text-[#00F0FF]", bg: "bg-[#00F0FF]/5", border: "border-[#00F0FF]/20", icon: <FileText size={32} /> },
|
||||||
{ label: "Overall Accuracy", value: "74%", color: "text-amber-500", bg: "bg-amber-500/5", border: "border-amber-500/20", icon: <Target size={32} /> },
|
{ label: "Overall Accuracy", value: overallAccuracy, color: "text-amber-500", bg: "bg-amber-500/5", border: "border-amber-500/20", icon: <Target size={32} /> },
|
||||||
{ label: "Current Streak", value: `${user?.streak_count || 0} d`, color: "text-yellow-400", bg: "bg-yellow-400/5", border: "border-yellow-400/20", icon: <Flame size={32} className="fill-yellow-400" /> },
|
{ label: "Current Streak", value: currentStreakDays, color: "text-yellow-400", bg: "bg-yellow-400/5", border: "border-yellow-400/20", icon: <Flame size={32} className="fill-yellow-400" /> },
|
||||||
{ label: "Mistakes Mastered", value: 38, color: "text-green-400", bg: "bg-green-400/5", border: "border-green-400/20", icon: <CheckCircle2 size={32} /> },
|
{ label: "Mistakes Mastered", value: mistakesMastered, color: "text-green-400", bg: "bg-green-400/5", border: "border-green-400/20", icon: <CheckCircle2 size={32} /> },
|
||||||
{ label: "AI Sessions", value: 124, color: "text-purple-400", bg: "bg-purple-400/5", border: "border-purple-400/20", icon: <Bot size={32} /> },
|
{ label: "AI Sessions", value: aiSessions, color: "text-purple-400", bg: "bg-purple-400/5", border: "border-purple-400/20", icon: <Bot size={32} /> },
|
||||||
].map((stat) => (
|
].map((stat) => (
|
||||||
<Card key={stat.label} className={`border ${stat.border} ${stat.bg} backdrop-blur-sm transition-all duration-300 transform hover:-translate-y-1 rounded-2xl`}>
|
<Card key={stat.label} className={`border ${stat.border} ${stat.bg} backdrop-blur-sm transition-all duration-300 transform hover:-translate-y-1 rounded-2xl`}>
|
||||||
<CardContent className="p-6 flex flex-col items-center justify-center text-center">
|
<CardContent className="p-6 flex flex-col items-center justify-center text-center">
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { Card, CardContent } from "@/components/ui/card"
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Gauge, TrendingUp, Target, Award, ShieldAlert, CheckCircle2, BookOpen, Zap, BarChart2, Clock } from "lucide-react"
|
import { Gauge, TrendingUp, Target, Award, ShieldAlert, CheckCircle2, BookOpen, Zap, BarChart2, Clock } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
const SCORE_HISTORY = [
|
const DEMO_SCORE_HISTORY = [
|
||||||
{ week: "Wk 1", score: 54 },
|
{ week: "Wk 1", score: 54 },
|
||||||
{ week: "Wk 2", score: 60 },
|
{ week: "Wk 2", score: 60 },
|
||||||
{ week: "Wk 3", score: 63 },
|
{ week: "Wk 3", score: 63 },
|
||||||
@@ -15,8 +16,20 @@ const SCORE_HISTORY = [
|
|||||||
{ week: "Wk 6", score: 78 },
|
{ week: "Wk 6", score: 78 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const ZERO_SCORE_HISTORY = [
|
||||||
|
{ week: "Wk 1", score: 0 },
|
||||||
|
{ week: "Wk 2", score: 0 },
|
||||||
|
{ week: "Wk 3", score: 0 },
|
||||||
|
{ week: "Wk 4", score: 0 },
|
||||||
|
{ week: "Wk 5", score: 0 },
|
||||||
|
{ week: "Wk 6", score: 0 },
|
||||||
|
]
|
||||||
|
|
||||||
export default function ReadinessPage() {
|
export default function ReadinessPage() {
|
||||||
const maxScore = Math.max(...SCORE_HISTORY.map(d => d.score))
|
const { userId } = useActiveUser()
|
||||||
|
const isDemo = isDemoUser(userId)
|
||||||
|
const scoreHistory = isDemo ? DEMO_SCORE_HISTORY : ZERO_SCORE_HISTORY
|
||||||
|
const maxScore = Math.max(1, ...scoreHistory.map(d => d.score))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
|
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
|
||||||
@@ -43,16 +56,16 @@ export default function ReadinessPage() {
|
|||||||
{/* KPI Strip */}
|
{/* KPI Strip */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{[
|
{[
|
||||||
{ label: "Overall Score", value: "72 / 100", trend: "+5 this week", trendUp: true, icon: <Gauge size={20} className="text-[#00F0FF]" /> },
|
{ label: "Overall Score", value: isDemo ? "72 / 100" : "0 / 100", trend: isDemo ? "+5 this week" : "Baseline", trendUp: true, icon: <Gauge size={20} className="text-[#00F0FF]" /> },
|
||||||
{ label: "Accuracy Rate", value: "88%", trend: "+3% vs last week", trendUp: true, icon: <Target size={20} className="text-amber-400" /> },
|
{ label: "Accuracy Rate", value: isDemo ? "88%" : "0%", trend: isDemo ? "+3% vs last week" : "0 attempts", trendUp: isDemo, icon: <Target size={20} className="text-amber-400" /> },
|
||||||
{ label: "Avg Speed / Q", value: "45 sec", trend: "↓ Needs work", trendUp: false, icon: <Clock size={20} className="text-red-400" /> },
|
{ label: "Avg Speed / Q", value: isDemo ? "45 sec" : "-- sec", trend: isDemo ? "↓ Needs work" : "Not tested", trendUp: false, icon: <Clock size={20} className="text-red-400" /> },
|
||||||
{ label: "Syllabus Covered", value: "64%", trend: "+8% this month", trendUp: true, icon: <BookOpen size={20} className="text-emerald-400" /> },
|
{ label: "Syllabus Covered", value: isDemo ? "64%" : "0%", trend: isDemo ? "+8% this month" : "Day 1", trendUp: true, icon: <BookOpen size={20} className="text-emerald-400" /> },
|
||||||
].map((kpi) => (
|
].map((kpi) => (
|
||||||
<Card key={kpi.label} className="border-border/60 bg-secondary/10 rounded-2xl hover:border-[#00F0FF]/30 transition-colors">
|
<Card key={kpi.label} className="border-border/60 bg-secondary/10 rounded-2xl hover:border-[#00F0FF]/30 transition-colors">
|
||||||
<CardContent className="p-5">
|
<CardContent className="p-5">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
{kpi.icon}
|
{kpi.icon}
|
||||||
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${kpi.trendUp ? "bg-green-500/10 text-green-400" : "bg-red-500/10 text-red-400"}`}>
|
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${kpi.trendUp ? "bg-green-500/10 text-green-400" : "bg-white/5 text-muted-foreground"}`}>
|
||||||
{kpi.trend}
|
{kpi.trend}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +83,9 @@ export default function ReadinessPage() {
|
|||||||
<div className="flex items-center gap-2 mb-5">
|
<div className="flex items-center gap-2 mb-5">
|
||||||
<Award className="text-amber-400" size={22} />
|
<Award className="text-amber-400" size={22} />
|
||||||
<h2 className="text-xl font-extrabold tracking-tight">Composite Readiness Index</h2>
|
<h2 className="text-xl font-extrabold tracking-tight">Composite Readiness Index</h2>
|
||||||
<span className="ml-auto text-xs font-bold text-[#00F0FF] bg-cyan-50 dark:bg-[#00F0FF]/10 border border-cyan-300/60 dark:border-[#00F0FF]/20 px-2.5 py-1 rounded-full">Updated Daily</span>
|
<span className="ml-auto text-xs font-bold text-[#00F0FF] bg-cyan-50 dark:bg-[#00F0FF]/10 border border-cyan-300/60 dark:border-[#00F0FF]/20 px-2.5 py-1 rounded-full">
|
||||||
|
{isDemo ? "Updated Daily" : "Day 1 Initializing"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ReadinessGauge />
|
<ReadinessGauge />
|
||||||
</Card>
|
</Card>
|
||||||
@@ -83,9 +98,9 @@ export default function ReadinessPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{[
|
{[
|
||||||
{ subject: "Biology / Maths", score: 82, color: "bg-[#00F0FF]", status: "High Readiness", statusColor: "text-[#00F0FF]" },
|
{ subject: "Biology / Maths", score: isDemo ? 82 : 0, color: "bg-[#00F0FF]", status: isDemo ? "High Readiness" : "Not Started", statusColor: isDemo ? "text-[#00F0FF]" : "text-muted-foreground" },
|
||||||
{ subject: "Chemistry", score: 74, color: "bg-amber-400", status: "Moderate Readiness", statusColor: "text-amber-400" },
|
{ subject: "Chemistry", score: isDemo ? 74 : 0, color: "bg-amber-400", status: isDemo ? "Moderate Readiness" : "Not Started", statusColor: isDemo ? "text-amber-400" : "text-muted-foreground" },
|
||||||
{ subject: "Physics", score: 61, color: "bg-red-400", status: "Needs Intensive Drill", statusColor: "text-red-400" },
|
{ subject: "Physics", score: isDemo ? 61 : 0, color: "bg-red-400", status: isDemo ? "Needs Intensive Drill" : "Not Started", statusColor: isDemo ? "text-red-400" : "text-muted-foreground" },
|
||||||
].map((sub) => (
|
].map((sub) => (
|
||||||
<div key={sub.subject} className="space-y-2">
|
<div key={sub.subject} className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
@@ -107,12 +122,18 @@ export default function ReadinessPage() {
|
|||||||
<h3 className="text-lg font-extrabold tracking-tight">6-Week Readiness Trend</h3>
|
<h3 className="text-lg font-extrabold tracking-tight">6-Week Readiness Trend</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end gap-3 h-28">
|
<div className="flex items-end gap-3 h-28">
|
||||||
{SCORE_HISTORY.map((d, i) => (
|
{scoreHistory.map((d, i) => (
|
||||||
<div key={d.week} className="flex flex-col items-center gap-1 flex-1">
|
<div key={d.week} className="flex flex-col items-center gap-1 flex-1">
|
||||||
<span className="text-xs font-black text-muted-foreground">{d.score}</span>
|
<span className="text-xs font-black text-muted-foreground">{d.score}</span>
|
||||||
<div
|
<div
|
||||||
className={`w-full rounded-t-md transition-all duration-700 ${i === SCORE_HISTORY.length - 1 ? "bg-gradient-to-t from-[#00F0FF]/60 to-[#00F0FF]" : "bg-gradient-to-t from-white/10 to-white/25"}`}
|
className={`w-full rounded-t-md transition-all duration-700 ${
|
||||||
style={{ height: `${(d.score / maxScore) * 80}px` }}
|
!isDemo
|
||||||
|
? "bg-white/5 border border-white/10"
|
||||||
|
: i === scoreHistory.length - 1
|
||||||
|
? "bg-gradient-to-t from-[#00F0FF]/60 to-[#00F0FF]"
|
||||||
|
: "bg-gradient-to-t from-white/10 to-white/25"
|
||||||
|
}`}
|
||||||
|
style={{ height: isDemo ? `${(d.score / maxScore) * 80}px` : "4px" }}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-muted-foreground font-semibold">{d.week}</span>
|
<span className="text-xs text-muted-foreground font-semibold">{d.week}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,19 +151,19 @@ export default function ReadinessPage() {
|
|||||||
<ul className="space-y-3.5 text-sm font-semibold text-muted-foreground">
|
<ul className="space-y-3.5 text-sm font-semibold text-muted-foreground">
|
||||||
<li className="flex items-start gap-2.5">
|
<li className="flex items-start gap-2.5">
|
||||||
<CheckCircle2 size={16} className="text-amber-400 shrink-0 mt-0.5" />
|
<CheckCircle2 size={16} className="text-amber-400 shrink-0 mt-0.5" />
|
||||||
<span>Complete 2 full-length mock tests this week to boost consistency score.</span>
|
<span>{isDemo ? "Complete 2 full-length mock tests this week to boost consistency score." : "Complete your first diagnostic test to calculate your initial readiness baseline."}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<CheckCircle2 size={16} className="text-amber-400 shrink-0 mt-0.5" />
|
<CheckCircle2 size={16} className="text-amber-400 shrink-0 mt-0.5" />
|
||||||
<span>Clear 15 pending errors in your Physics queue to gain ~4 readiness points.</span>
|
<span>{isDemo ? "Clear 15 pending errors in your Physics queue to gain ~4 readiness points." : "Attempt daily DPPs to populate your question error log."}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<CheckCircle2 size={16} className="text-amber-400 shrink-0 mt-0.5" />
|
<CheckCircle2 size={16} className="text-amber-400 shrink-0 mt-0.5" />
|
||||||
<span>Maintain a 7+ day streak — consistency adds 6% to your readiness index.</span>
|
<span>Maintain daily streak — consistency adds directly to your readiness algorithm.</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<CheckCircle2 size={16} className="text-amber-400 shrink-0 mt-0.5" />
|
<CheckCircle2 size={16} className="text-amber-400 shrink-0 mt-0.5" />
|
||||||
<span>Reduce avg time-per-question below 40s with daily timed DPP sessions.</span>
|
<span>Target under 45s per question in timed test series practice.</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -4,14 +4,25 @@ import { ExamStrategyCard } from "@/components/AI/ExamStrategyCard"
|
|||||||
import { StudyTimeOptimizationCard } from "@/components/AI/StudyTimeOptimizationCard"
|
import { StudyTimeOptimizationCard } from "@/components/AI/StudyTimeOptimizationCard"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { Zap, Clock, Target, Lightbulb, CheckCircle2, AlertTriangle, TrendingUp } from "lucide-react"
|
import { Zap, Clock, Target, Lightbulb, CheckCircle2, AlertTriangle, TrendingUp } from "lucide-react"
|
||||||
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
const NEG_MARKING_DATA = [
|
const DEMO_NEG_MARKING_DATA = [
|
||||||
{ subject: "Physics", attempts: 30, negMarks: 12, accuracy: 60 },
|
{ subject: "Physics", attempts: 30, negMarks: 12, accuracy: 60 },
|
||||||
{ subject: "Chemistry", attempts: 35, negMarks: 7, accuracy: 80 },
|
{ subject: "Chemistry", attempts: 35, negMarks: 7, accuracy: 80 },
|
||||||
{ subject: "Maths", attempts: 28, negMarks: 10, accuracy: 64 },
|
{ subject: "Maths / Biology", attempts: 28, negMarks: 10, accuracy: 64 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const ZERO_NEG_MARKING_DATA = [
|
||||||
|
{ subject: "Physics", attempts: 0, negMarks: 0, accuracy: 0 },
|
||||||
|
{ subject: "Chemistry", attempts: 0, negMarks: 0, accuracy: 0 },
|
||||||
|
{ subject: "Maths / Biology", attempts: 0, negMarks: 0, accuracy: 0 },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function StrategyPage() {
|
export default function StrategyPage() {
|
||||||
|
const { userId } = useActiveUser()
|
||||||
|
const isDemo = isDemoUser(userId)
|
||||||
|
const negMarkingData = isDemo ? DEMO_NEG_MARKING_DATA : ZERO_NEG_MARKING_DATA
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
|
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
@@ -37,10 +48,10 @@ export default function StrategyPage() {
|
|||||||
{/* KPI Strip */}
|
{/* KPI Strip */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{[
|
{[
|
||||||
{ label: "Time Per Question", value: "45 sec", note: "Target: 38 sec", icon: <Clock size={18} className="text-red-400" />, color: "border-red-400/20 bg-red-400/5" },
|
{ label: "Time Per Question", value: isDemo ? "45 sec" : "-- sec", note: isDemo ? "Target: 38 sec" : "Not calibrated", icon: <Clock size={18} className="text-red-400" />, color: "border-red-400/20 bg-red-400/5" },
|
||||||
{ label: "Negative Marks Lost", value: "−29", note: "Last mock test", icon: <AlertTriangle size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
{ label: "Negative Marks Lost", value: isDemo ? "−29" : "0", note: isDemo ? "Last mock test" : "0 negative marks", icon: <AlertTriangle size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
||||||
{ label: "Attempted Questions", value: "93 / 120", note: "+7 vs last test", icon: <Target size={18} className="text-[#00F0FF]" />, color: "border-[#00F0FF]/20 bg-[#00F0FF]/5" },
|
{ label: "Attempted Questions", value: isDemo ? "93 / 120" : "0 / 120", note: isDemo ? "+7 vs last test" : "0 attempted", icon: <Target size={18} className="text-[#00F0FF]" />, color: "border-[#00F0FF]/20 bg-[#00F0FF]/5" },
|
||||||
{ label: "Score Yield", value: "214", note: "Predicted score", icon: <TrendingUp size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
{ label: "Score Yield", value: isDemo ? "214" : "0", note: isDemo ? "Predicted score" : "Baseline", icon: <TrendingUp size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||||
].map((kpi) => (
|
].map((kpi) => (
|
||||||
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color} hover:scale-[1.02] transition-transform`}>
|
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color} hover:scale-[1.02] transition-transform`}>
|
||||||
<CardContent className="p-5 flex flex-col gap-2">
|
<CardContent className="p-5 flex flex-col gap-2">
|
||||||
@@ -77,10 +88,12 @@ export default function StrategyPage() {
|
|||||||
<div className="flex items-center gap-2 mb-5">
|
<div className="flex items-center gap-2 mb-5">
|
||||||
<AlertTriangle size={20} className="text-amber-400" />
|
<AlertTriangle size={20} className="text-amber-400" />
|
||||||
<h3 className="text-lg font-extrabold tracking-tight">Negative Marking Analysis</h3>
|
<h3 className="text-lg font-extrabold tracking-tight">Negative Marking Analysis</h3>
|
||||||
<span className="ml-auto text-xs text-muted-foreground font-semibold">Last Mock Test · Aug 1</span>
|
<span className="ml-auto text-xs text-muted-foreground font-semibold">
|
||||||
|
{isDemo ? "Last Mock Test · Aug 1" : "Day 1 Initializing"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{NEG_MARKING_DATA.map((row) => (
|
{negMarkingData.map((row) => (
|
||||||
<div key={row.subject} className="grid grid-cols-4 gap-4 items-center p-3 rounded-xl bg-white/5 border border-border/40">
|
<div key={row.subject} className="grid grid-cols-4 gap-4 items-center p-3 rounded-xl bg-white/5 border border-border/40">
|
||||||
<div className="font-black text-sm">{row.subject}</div>
|
<div className="font-black text-sm">{row.subject}</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@@ -88,17 +101,19 @@ export default function StrategyPage() {
|
|||||||
<div className="text-[11px] text-muted-foreground font-semibold">Attempted</div>
|
<div className="text-[11px] text-muted-foreground font-semibold">Attempted</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-sm font-black text-red-400">−{row.negMarks}</div>
|
<div className="text-sm font-black text-red-400">{row.negMarks === 0 ? "0" : `−${row.negMarks}`}</div>
|
||||||
<div className="text-[11px] text-muted-foreground font-semibold">Neg. Marks</div>
|
<div className="text-[11px] text-muted-foreground font-semibold">Neg. Marks</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between text-xs font-bold mb-1">
|
<div className="flex justify-between text-xs font-bold mb-1">
|
||||||
<span className="text-muted-foreground">Accuracy</span>
|
<span className="text-muted-foreground">Accuracy</span>
|
||||||
<span className={row.accuracy >= 75 ? "text-green-400" : row.accuracy >= 60 ? "text-amber-400" : "text-red-400"}>{row.accuracy}%</span>
|
<span className={row.accuracy >= 75 ? "text-green-400" : row.accuracy >= 60 ? "text-amber-400" : row.accuracy === 0 ? "text-muted-foreground" : "text-red-400"}>
|
||||||
|
{row.accuracy}%
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden">
|
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={`h-full rounded-full ${row.accuracy >= 75 ? "bg-green-400" : row.accuracy >= 60 ? "bg-amber-400" : "bg-red-400"}`}
|
className={`h-full rounded-full ${row.accuracy >= 75 ? "bg-green-400" : row.accuracy >= 60 ? "bg-amber-400" : isDemo ? "bg-red-400" : "bg-white/10"}`}
|
||||||
style={{ width: `${row.accuracy}%` }}
|
style={{ width: `${row.accuracy}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,7 +122,11 @@ export default function StrategyPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-4 font-medium pl-1">
|
<p className="text-xs text-muted-foreground mt-4 font-medium pl-1">
|
||||||
<span className="font-black text-amber-400">Insight:</span> You're losing ~29 marks to wrong answers. Switching to "skip and revisit" for uncertain Physics questions could recover 15–20 marks.
|
<span className="font-black text-amber-400">Insight:</span> {isDemo ? (
|
||||||
|
<>You're losing ~29 marks to wrong answers. Switching to "skip and revisit" for uncertain Physics questions could recover 15–20 marks.</>
|
||||||
|
) : (
|
||||||
|
<>Complete your first mock test or timed DPP to calculate your negative marking profile and fine-tune your subject pacing.</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,13 @@ import { StudyPlanCard } from "@/components/AI/StudyPlanCard"
|
|||||||
import { WeeklyProgressReport } from "@/components/AI/WeeklyProgressReport"
|
import { WeeklyProgressReport } from "@/components/AI/WeeklyProgressReport"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { Calendar, BookOpen, Clock, Target, CheckCircle2, TrendingUp, Flame } from "lucide-react"
|
import { Calendar, BookOpen, Clock, Target, CheckCircle2, TrendingUp, Flame } from "lucide-react"
|
||||||
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
export default function StudyPlanPage() {
|
export default function StudyPlanPage() {
|
||||||
|
const { user, userId } = useActiveUser()
|
||||||
|
const isDemo = isDemoUser(userId)
|
||||||
|
const streakCount = user?.streak_count || (isDemo ? 12 : 1)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
|
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
@@ -31,10 +36,10 @@ export default function StudyPlanPage() {
|
|||||||
{/* KPI Strip */}
|
{/* KPI Strip */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{[
|
{[
|
||||||
{ label: "Tasks Today", value: "5", sub: "2 completed", icon: <CheckCircle2 size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
{ label: "Tasks Today", value: isDemo ? "5" : "3", sub: isDemo ? "2 completed" : "0 completed", icon: <CheckCircle2 size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||||
{ label: "Study Hours Today", value: "3.8 hrs", sub: "Target: 5 hrs", icon: <Clock size={18} className="text-[#00F0FF]" />, color: "border-[#00F0FF]/20 bg-[#00F0FF]/5" },
|
{ label: "Study Hours Today", value: isDemo ? "3.8 hrs" : "0.0 hrs", sub: "Target: 5 hrs", icon: <Clock size={18} className="text-[#00F0FF]" />, color: "border-[#00F0FF]/20 bg-[#00F0FF]/5" },
|
||||||
{ label: "Syllabus Coverage", value: "64%", sub: "+8% this week", icon: <Target size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
{ label: "Syllabus Coverage", value: isDemo ? "64%" : "0%", sub: isDemo ? "+8% this week" : "Day 1 Baseline", icon: <Target size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
||||||
{ label: "Current Streak", value: "12 Days", sub: "Keep it up!", icon: <Flame size={18} className="text-orange-400" />, color: "border-orange-400/20 bg-orange-400/5" },
|
{ label: "Current Streak", value: `${streakCount} Day${streakCount === 1 ? '' : 's'}`, sub: isDemo ? "Keep it up!" : "Day 1 active", icon: <Flame size={18} className="text-orange-400" />, color: "border-orange-400/20 bg-orange-400/5" },
|
||||||
].map((kpi) => (
|
].map((kpi) => (
|
||||||
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color} hover:scale-[1.02] transition-transform`}>
|
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color} hover:scale-[1.02] transition-transform`}>
|
||||||
<CardContent className="p-5 flex flex-col gap-2">
|
<CardContent className="p-5 flex flex-col gap-2">
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import { StudyTimeOptimizationCard } from "@/components/AI/StudyTimeOptimization
|
|||||||
import { Clock, Zap, Moon, Sun, AlertCircle, ArrowRight, BarChart2 } from "lucide-react"
|
import { Clock, Zap, Moon, Sun, AlertCircle, ArrowRight, BarChart2 } from "lucide-react"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
const INSIGHTS = [
|
const DEMO_INSIGHTS = [
|
||||||
{
|
{
|
||||||
icon: <Sun size={16} className="text-yellow-400" />,
|
icon: <Sun size={16} className="text-yellow-400" />,
|
||||||
title: "Peak Cognitive Window",
|
title: "Peak Cognitive Window",
|
||||||
@@ -26,7 +27,7 @@ const INSIGHTS = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const HOURLY_EFFICIENCY = [
|
const DEMO_HOURLY_EFFICIENCY = [
|
||||||
{ slot: "6–9 AM", pct: 60, label: "Morning" },
|
{ slot: "6–9 AM", pct: 60, label: "Morning" },
|
||||||
{ slot: "9–12 PM", pct: 72, label: "Mid-Morning" },
|
{ slot: "9–12 PM", pct: 72, label: "Mid-Morning" },
|
||||||
{ slot: "12–2 PM", pct: 55, label: "Post-Lunch dip" },
|
{ slot: "12–2 PM", pct: 55, label: "Post-Lunch dip" },
|
||||||
@@ -37,7 +38,36 @@ const HOURLY_EFFICIENCY = [
|
|||||||
{ slot: "11 PM+", pct: 32, label: "Avoid" },
|
{ slot: "11 PM+", pct: 32, label: "Avoid" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const ZERO_HOURLY_EFFICIENCY = [
|
||||||
|
{ slot: "6–9 AM", pct: 0, label: "Pending" },
|
||||||
|
{ slot: "9–12 PM", pct: 0, label: "Pending" },
|
||||||
|
{ slot: "12–2 PM", pct: 0, label: "Pending" },
|
||||||
|
{ slot: "2–5 PM", pct: 0, label: "Calibrating", highlight: false },
|
||||||
|
{ slot: "5–7 PM", pct: 0, label: "Pending" },
|
||||||
|
{ slot: "7–9 PM", pct: 0, label: "Pending" },
|
||||||
|
{ slot: "9–11 PM", pct: 0, label: "Pending" },
|
||||||
|
{ slot: "11 PM+", pct: 0, label: "Pending" },
|
||||||
|
]
|
||||||
|
|
||||||
export default function TimeOptimizationPage() {
|
export default function TimeOptimizationPage() {
|
||||||
|
const { userId } = useActiveUser()
|
||||||
|
const isDemo = isDemoUser(userId)
|
||||||
|
const hourlySlots = isDemo ? DEMO_HOURLY_EFFICIENCY : ZERO_HOURLY_EFFICIENCY
|
||||||
|
const insights = isDemo ? DEMO_INSIGHTS : [
|
||||||
|
{
|
||||||
|
icon: <Sun size={16} className="text-yellow-400" />,
|
||||||
|
title: "Peak Window Calibrating",
|
||||||
|
desc: "Complete 3 test sessions at different times of day to establish your circadian peak focus hours.",
|
||||||
|
color: "border-yellow-400/20 bg-yellow-400/5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Moon size={16} className="text-indigo-400" />,
|
||||||
|
title: "Consistency Booster",
|
||||||
|
desc: "Start with a 30-minute daily review routine to jumpstart your spaced repetition learning schedule.",
|
||||||
|
color: "border-indigo-400/20 bg-indigo-400/5",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500 min-h-screen">
|
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500 min-h-screen">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
@@ -63,10 +93,10 @@ export default function TimeOptimizationPage() {
|
|||||||
{/* KPI Strip */}
|
{/* KPI Strip */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{[
|
{[
|
||||||
{ label: "Peak Efficiency", value: "95%", sub: "2–5 PM window", color: "border-yellow-400/20 bg-yellow-400/5", icon: <Sun size={18} className="text-yellow-400" /> },
|
{ label: "Peak Efficiency", value: isDemo ? "95%" : "--", sub: isDemo ? "2–5 PM window" : "Calibrating", color: "border-yellow-400/20 bg-yellow-400/5", icon: <Sun size={18} className="text-yellow-400" /> },
|
||||||
{ label: "Study Hrs Today", value: "3.8 h", sub: "Target: 6 h", color: "border-[#00F0FF]/20 bg-[#00F0FF]/5", icon: <Clock size={18} className="text-[#00F0FF]" /> },
|
{ label: "Study Hrs Today", value: isDemo ? "3.8 h" : "0.0 h", sub: "Target: 6 h", color: "border-[#00F0FF]/20 bg-[#00F0FF]/5", icon: <Clock size={18} className="text-[#00F0FF]" /> },
|
||||||
{ label: "Morning Block", value: "60%", sub: "6–9 AM", color: "border-indigo-400/20 bg-indigo-400/5", icon: <Moon size={18} className="text-indigo-400" /> },
|
{ label: "Morning Block", value: isDemo ? "60%" : "0%", sub: "6–9 AM", color: "border-indigo-400/20 bg-indigo-400/5", icon: <Moon size={18} className="text-indigo-400" /> },
|
||||||
{ label: "Evening Block", value: "74%", sub: "7–9 PM", color: "border-green-400/20 bg-green-400/5", icon: <BarChart2 size={18} className="text-green-400" /> },
|
{ label: "Evening Block", value: isDemo ? "74%" : "0%", sub: "7–9 PM", color: "border-green-400/20 bg-green-400/5", icon: <BarChart2 size={18} className="text-green-400" /> },
|
||||||
].map(kpi => (
|
].map(kpi => (
|
||||||
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color}`}>
|
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color}`}>
|
||||||
<CardContent className="p-4 flex flex-col gap-2">
|
<CardContent className="p-4 flex flex-col gap-2">
|
||||||
@@ -96,12 +126,12 @@ export default function TimeOptimizationPage() {
|
|||||||
<CardContent className="p-5 space-y-3">
|
<CardContent className="p-5 space-y-3">
|
||||||
<h3 className="text-sm font-black uppercase tracking-wider text-muted-foreground">Time Slot Breakdown</h3>
|
<h3 className="text-sm font-black uppercase tracking-wider text-muted-foreground">Time Slot Breakdown</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{HOURLY_EFFICIENCY.map(slot => (
|
{hourlySlots.map(slot => (
|
||||||
<div key={slot.slot} className={`flex items-center gap-3 p-2 rounded-lg ${slot.highlight ? "bg-yellow-400/10 border border-yellow-400/20" : "bg-white/5"}`}>
|
<div key={slot.slot} className={`flex items-center gap-3 p-2 rounded-lg ${slot.highlight ? "bg-yellow-400/10 border border-yellow-400/20" : "bg-white/5"}`}>
|
||||||
<div className="w-16 text-xs font-bold text-muted-foreground shrink-0">{slot.slot}</div>
|
<div className="w-16 text-xs font-bold text-muted-foreground shrink-0">{slot.slot}</div>
|
||||||
<div className="flex-1 h-1.5 bg-secondary rounded-full overflow-hidden">
|
<div className="flex-1 h-1.5 bg-secondary rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={`h-full rounded-full ${slot.pct >= 80 ? (slot.highlight ? "bg-yellow-400" : "bg-green-400") : slot.pct >= 60 ? "bg-[#00F0FF]" : "bg-red-400"}`}
|
className={`h-full rounded-full ${slot.pct >= 80 ? (slot.highlight ? "bg-yellow-400" : "bg-green-400") : slot.pct >= 60 ? "bg-[#00F0FF]" : isDemo ? "bg-red-400" : "bg-white/10"}`}
|
||||||
style={{ width: `${slot.pct}%` }}
|
style={{ width: `${slot.pct}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,7 +145,7 @@ export default function TimeOptimizationPage() {
|
|||||||
|
|
||||||
{/* AI Insights */}
|
{/* AI Insights */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{INSIGHTS.map(insight => (
|
{insights.map(insight => (
|
||||||
<Card key={insight.title} className={`rounded-2xl border ${insight.color}`}>
|
<Card key={insight.title} className={`rounded-2xl border ${insight.color}`}>
|
||||||
<CardContent className="p-4 flex items-start gap-3">
|
<CardContent className="p-4 flex items-start gap-3">
|
||||||
<div className="mt-0.5 shrink-0">{insight.icon}</div>
|
<div className="mt-0.5 shrink-0">{insight.icon}</div>
|
||||||
|
|||||||
@@ -4,22 +4,42 @@ import { WeeklyProgressReport } from "@/components/AI/WeeklyProgressReport"
|
|||||||
import { Target, TrendingUp, Calendar, Zap, ArrowRight, BookOpen, CheckCircle2 } from "lucide-react"
|
import { Target, TrendingUp, Calendar, Zap, ArrowRight, BookOpen, CheckCircle2 } from "lucide-react"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
const STATS = [
|
const DEMO_STATS = [
|
||||||
{ label: "Avg. Accuracy", value: "85%", delta: "+6%", positive: true },
|
{ label: "Avg. Accuracy", value: "85%", delta: "+6%", positive: true },
|
||||||
{ label: "Days Active", value: "7 / 7", delta: "Perfect week", positive: true },
|
{ label: "Days Active", value: "7 / 7", delta: "Perfect week", positive: true },
|
||||||
{ label: "Questions Solved", value: "214", delta: "+38 vs last week", positive: true },
|
{ label: "Questions Solved", value: "214", delta: "+38 vs last week", positive: true },
|
||||||
{ label: "Topics Covered", value: "12", delta: "3 remaining", positive: false },
|
{ label: "Topics Covered", value: "12", delta: "3 remaining", positive: false },
|
||||||
]
|
]
|
||||||
|
|
||||||
const SYLLABUS_PROGRESS = [
|
const ZERO_STATS = [
|
||||||
|
{ label: "Avg. Accuracy", value: "0%", delta: "Baseline", positive: true },
|
||||||
|
{ label: "Days Active", value: "0 / 7", delta: "Day 1 Active", positive: true },
|
||||||
|
{ label: "Questions Solved", value: "0", delta: "0 vs last week", positive: true },
|
||||||
|
{ label: "Topics Covered", value: "0", delta: "0 completed", positive: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
const DEMO_SYLLABUS_PROGRESS = [
|
||||||
{ subject: "Physics", completed: 42, total: 60, percentage: 70, color: "bg-blue-500", text: "text-blue-500" },
|
{ subject: "Physics", completed: 42, total: 60, percentage: 70, color: "bg-blue-500", text: "text-blue-500" },
|
||||||
{ subject: "Chemistry", completed: 35, total: 55, percentage: 63, color: "bg-amber-500", text: "text-amber-500" },
|
{ subject: "Chemistry", completed: 35, total: 55, percentage: 63, color: "bg-amber-500", text: "text-amber-500" },
|
||||||
{ subject: "Mathematics", completed: 50, total: 65, percentage: 76, color: "bg-purple-500", text: "text-purple-500" },
|
{ subject: "Mathematics", completed: 50, total: 65, percentage: 76, color: "bg-purple-500", text: "text-purple-500" },
|
||||||
{ subject: "Biology", completed: 20, total: 40, percentage: 50, color: "bg-emerald-500", text: "text-emerald-500" },
|
{ subject: "Biology", completed: 20, total: 40, percentage: 50, color: "bg-emerald-500", text: "text-emerald-500" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const ZERO_SYLLABUS_PROGRESS = [
|
||||||
|
{ subject: "Physics", completed: 0, total: 60, percentage: 0, color: "bg-blue-500", text: "text-blue-500" },
|
||||||
|
{ subject: "Chemistry", completed: 0, total: 55, percentage: 0, color: "bg-amber-500", text: "text-amber-500" },
|
||||||
|
{ subject: "Mathematics", completed: 0, total: 65, percentage: 0, color: "bg-purple-500", text: "text-purple-500" },
|
||||||
|
{ subject: "Biology", completed: 0, total: 40, percentage: 0, color: "bg-emerald-500", text: "text-emerald-500" },
|
||||||
|
]
|
||||||
|
|
||||||
export default function WeeklyProgressPage() {
|
export default function WeeklyProgressPage() {
|
||||||
|
const { userId } = useActiveUser()
|
||||||
|
const isDemo = isDemoUser(userId)
|
||||||
|
const stats = isDemo ? DEMO_STATS : ZERO_STATS
|
||||||
|
const syllabusProgress = isDemo ? DEMO_SYLLABUS_PROGRESS : ZERO_SYLLABUS_PROGRESS
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500 min-h-screen">
|
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500 min-h-screen">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
@@ -46,7 +66,7 @@ export default function WeeklyProgressPage() {
|
|||||||
|
|
||||||
{/* KPI Stats Row */}
|
{/* KPI Stats Row */}
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{STATS.map((stat) => (
|
{stats.map((stat) => (
|
||||||
<Card key={stat.label} className="bg-secondary/20 border-border/60 rounded-2xl">
|
<Card key={stat.label} className="bg-secondary/20 border-border/60 rounded-2xl">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<p className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-1">{stat.label}</p>
|
<p className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-1">{stat.label}</p>
|
||||||
@@ -80,7 +100,7 @@ export default function WeeklyProgressPage() {
|
|||||||
</h2>
|
</h2>
|
||||||
<Card className="bg-secondary/10 border-border/60 rounded-2xl">
|
<Card className="bg-secondary/10 border-border/60 rounded-2xl">
|
||||||
<CardContent className="p-5 space-y-4">
|
<CardContent className="p-5 space-y-4">
|
||||||
{SYLLABUS_PROGRESS.map((item) => (
|
{syllabusProgress.map((item) => (
|
||||||
<div key={item.subject} className="space-y-1.5">
|
<div key={item.subject} className="space-y-1.5">
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="font-bold">{item.subject}</span>
|
<span className="font-bold">{item.subject}</span>
|
||||||
@@ -107,10 +127,14 @@ export default function WeeklyProgressPage() {
|
|||||||
<h3 className="text-sm font-black uppercase tracking-wider text-[#00F0FF]">Weekly Goals</h3>
|
<h3 className="text-sm font-black uppercase tracking-wider text-[#00F0FF]">Weekly Goals</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-start gap-2 p-2 rounded-lg bg-white/5">
|
<div className="flex items-start gap-2 p-2 rounded-lg bg-white/5">
|
||||||
|
{isDemo ? (
|
||||||
<CheckCircle2 size={16} className="text-green-400 mt-0.5 shrink-0" />
|
<CheckCircle2 size={16} className="text-green-400 mt-0.5 shrink-0" />
|
||||||
|
) : (
|
||||||
|
<div className="w-4 h-4 rounded-full border-2 border-muted-foreground mt-0.5 shrink-0" />
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-bold">Complete 200 Questions</div>
|
<div className="text-xs font-bold">Complete 200 Questions</div>
|
||||||
<div className="text-[10px] text-muted-foreground">Goal Met (214/200)</div>
|
<div className="text-[10px] text-muted-foreground">{isDemo ? "Goal Met (214/200)" : "Pending (0/200)"}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-2 p-2 rounded-lg bg-white/5 opacity-70">
|
<div className="flex items-start gap-2 p-2 rounded-lg bg-white/5 opacity-70">
|
||||||
|
|||||||
@@ -3,28 +3,52 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||||
import { LineChart as LineChartIcon, Activity, BrainCircuit } from "lucide-react"
|
import { LineChart as LineChartIcon, Activity, BrainCircuit } from "lucide-react"
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, Cell } from 'recharts'
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, Cell } from 'recharts'
|
||||||
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
export default function AnalyticsPage() {
|
const DEMO_TREND_DATA = [
|
||||||
const trendData = [
|
|
||||||
{ name: 'Week 1', score: 120 },
|
{ name: 'Week 1', score: 120 },
|
||||||
{ name: 'Week 2', score: 135 },
|
{ name: 'Week 2', score: 135 },
|
||||||
{ name: 'Week 3', score: 130 },
|
{ name: 'Week 3', score: 130 },
|
||||||
{ name: 'Week 4', score: 145 },
|
{ name: 'Week 4', score: 145 },
|
||||||
{ name: 'Week 5', score: 160 },
|
{ name: 'Week 5', score: 160 },
|
||||||
{ name: 'Week 6', score: 185 },
|
{ name: 'Week 6', score: 185 },
|
||||||
]
|
]
|
||||||
|
|
||||||
// For heatmap-like visualization (using a barchart with different colors)
|
const ZERO_TREND_DATA = [
|
||||||
const heatmapData = [
|
{ name: 'Week 1', score: 0 },
|
||||||
|
{ name: 'Week 2', score: 0 },
|
||||||
|
{ name: 'Week 3', score: 0 },
|
||||||
|
{ name: 'Week 4', score: 0 },
|
||||||
|
{ name: 'Week 5', score: 0 },
|
||||||
|
{ name: 'Week 6', score: 0 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const DEMO_HEATMAP_DATA = [
|
||||||
{ topic: 'Kinematics', accuracy: 95 },
|
{ topic: 'Kinematics', accuracy: 95 },
|
||||||
{ topic: 'Optics', accuracy: 72 },
|
{ topic: 'Optics', accuracy: 72 },
|
||||||
{ topic: 'Thermodynamics', accuracy: 45 },
|
{ topic: 'Thermodynamics', accuracy: 45 },
|
||||||
{ topic: 'Calculus', accuracy: 38 },
|
{ topic: 'Calculus', accuracy: 38 },
|
||||||
{ topic: 'Organic Chem', accuracy: 88 },
|
{ topic: 'Organic Chem', accuracy: 88 },
|
||||||
{ topic: 'Inorganic', accuracy: 65 },
|
{ topic: 'Inorganic', accuracy: 65 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const ZERO_HEATMAP_DATA = [
|
||||||
|
{ topic: 'Kinematics', accuracy: 0 },
|
||||||
|
{ topic: 'Optics', accuracy: 0 },
|
||||||
|
{ topic: 'Thermodynamics', accuracy: 0 },
|
||||||
|
{ topic: 'Calculus', accuracy: 0 },
|
||||||
|
{ topic: 'Organic Chem', accuracy: 0 },
|
||||||
|
{ topic: 'Inorganic', accuracy: 0 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function AnalyticsPage() {
|
||||||
|
const { userId } = useActiveUser()
|
||||||
|
const isDemo = isDemoUser(userId)
|
||||||
|
const trendData = isDemo ? DEMO_TREND_DATA : ZERO_TREND_DATA
|
||||||
|
const heatmapData = isDemo ? DEMO_HEATMAP_DATA : ZERO_HEATMAP_DATA
|
||||||
|
|
||||||
const getColor = (accuracy: number) => {
|
const getColor = (accuracy: number) => {
|
||||||
|
if (accuracy === 0) return '#374151' // neutral gray
|
||||||
if (accuracy >= 80) return '#10b981' // emerald-500
|
if (accuracy >= 80) return '#10b981' // emerald-500
|
||||||
if (accuracy >= 60) return '#f59e0b' // amber-500
|
if (accuracy >= 60) return '#f59e0b' // amber-500
|
||||||
return '#ef4444' // red-500
|
return '#ef4444' // red-500
|
||||||
@@ -50,7 +74,9 @@ export default function AnalyticsPage() {
|
|||||||
<Card className="col-span-1 lg:col-span-2 border-border shadow-sm bg-background">
|
<Card className="col-span-1 lg:col-span-2 border-border shadow-sm bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2"><LineChartIcon className="text-[#00F0FF]" size={20} /> Projected Score Trajectory</CardTitle>
|
<CardTitle className="flex items-center gap-2"><LineChartIcon className="text-[#00F0FF]" size={20} /> Projected Score Trajectory</CardTitle>
|
||||||
<CardDescription>Your mock scores over the last 6 months projected towards the final exam.</CardDescription>
|
<CardDescription>
|
||||||
|
{isDemo ? "Your mock scores over the last 6 months projected towards the final exam." : "Your mock test trajectory will plot here as you complete tests."}
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="h-[350px] mt-4">
|
<CardContent className="h-[350px] mt-4">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
@@ -75,7 +101,11 @@ export default function AnalyticsPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-sm text-foreground/80 leading-relaxed font-medium">
|
<p className="text-sm text-foreground/80 leading-relaxed font-medium">
|
||||||
Our models detect a consistent <strong>14% accuracy drop</strong> in physics questions involving Calculus. We are adjusting your upcoming DPPs to focus heavily on integration applications in kinematics.
|
{isDemo ? (
|
||||||
|
<>Our models detect a consistent <strong>14% accuracy drop</strong> in physics questions involving Calculus. We are adjusting your upcoming DPPs to focus heavily on integration applications in kinematics.</>
|
||||||
|
) : (
|
||||||
|
<>Complete your first diagnostic test or DPP to unlock personalized AI diagnoses and focus area recommendations. Our neural models will begin analyzing your accuracy curves as soon as you attempt questions.</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
|
||||||
import { MessageSquare, ThumbsUp, ChevronUp, Search, Image as ImageIcon, CheckCircle, Lock, X, Send } from "lucide-react"
|
import { MessageSquare, ThumbsUp, ChevronUp, Search, Image as ImageIcon, CheckCircle, Lock, X, Send } from "lucide-react"
|
||||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
interface Doubt {
|
interface Doubt {
|
||||||
id: string
|
id: string
|
||||||
@@ -22,7 +22,8 @@ interface Doubt {
|
|||||||
|
|
||||||
export default function CommunityPage() {
|
export default function CommunityPage() {
|
||||||
const { user, hasAccessTo, loading } = useActiveUser()
|
const { user, hasAccessTo, loading } = useActiveUser()
|
||||||
const currentUserName = user?.name || "Neha Verma"
|
const isDemo = isDemoUser(user?.id)
|
||||||
|
const currentUserName = user?.name || (isDemo ? "Neha Verma" : "Student")
|
||||||
const currentUserInitials = currentUserName.substring(0, 2).toUpperCase()
|
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('')
|
||||||
@@ -58,7 +59,7 @@ export default function CommunityPage() {
|
|||||||
isSolved: false,
|
isSolved: false,
|
||||||
hasModeratorReply: false
|
hasModeratorReply: false
|
||||||
},
|
},
|
||||||
{
|
...(isDemo ? [{
|
||||||
id: "3",
|
id: "3",
|
||||||
author: currentUserName,
|
author: currentUserName,
|
||||||
avatar: currentUserInitials,
|
avatar: currentUserInitials,
|
||||||
@@ -71,7 +72,7 @@ export default function CommunityPage() {
|
|||||||
replies: 8,
|
replies: 8,
|
||||||
isSolved: true,
|
isSolved: true,
|
||||||
hasModeratorReply: true
|
hasModeratorReply: true
|
||||||
}
|
}] : [])
|
||||||
]
|
]
|
||||||
|
|
||||||
const filteredDoubts = mockDoubts.filter(d => {
|
const filteredDoubts = mockDoubts.filter(d => {
|
||||||
@@ -244,9 +245,16 @@ export default function CommunityPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
{filteredDoubts.length === 0 && (
|
{filteredDoubts.length === 0 && (
|
||||||
<div className="text-center py-12 text-muted-foreground">
|
<div className="text-center py-12 text-muted-foreground bg-secondary/10 border border-dashed border-border rounded-2xl p-6">
|
||||||
<MessageSquare size={48} className="mx-auto mb-4 opacity-20" />
|
<MessageSquare size={48} className="mx-auto mb-3 opacity-30 text-[#00F0FF]" />
|
||||||
<p className="font-medium">No doubts found matching your criteria.</p>
|
<p className="font-bold text-foreground mb-1">
|
||||||
|
{activeFilter === 'my-doubts' ? "No Doubts Asked Yet" : "No doubts found matching your criteria."}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground max-w-sm mx-auto">
|
||||||
|
{activeFilter === 'my-doubts'
|
||||||
|
? "You haven't asked any doubts yet. Click 'Ask a Doubt' above to get answers from mentors and peers."
|
||||||
|
: "Try selecting another filter or clearing your search query."}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"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, CardDescription } from "@/components/ui/card"
|
||||||
import { ChevronDown, ChevronRight, CheckCircle2, Circle, Clock, Activity, Target } from "lucide-react"
|
import { ChevronDown, ChevronRight, CheckCircle2, Circle, Clock, Activity, Target } from "lucide-react"
|
||||||
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
type ChapterStatus = 'Mastered' | 'Practiced' | 'In Progress' | 'Not Started'
|
type ChapterStatus = 'Mastered' | 'Practiced' | 'In Progress' | 'Not Started'
|
||||||
|
|
||||||
@@ -34,8 +35,7 @@ const getStatusIcon = (status: ChapterStatus) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProgressTrackerPage() {
|
const DEMO_SUBJECTS: Subject[] = [
|
||||||
const [subjects, setSubjects] = useState<Subject[]>([
|
|
||||||
{
|
{
|
||||||
name: "Physics",
|
name: "Physics",
|
||||||
completionPct: 42,
|
completionPct: 42,
|
||||||
@@ -82,7 +82,7 @@ export default function ProgressTrackerPage() {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Mathematics",
|
name: "Mathematics / Biology",
|
||||||
completionPct: 22,
|
completionPct: 22,
|
||||||
chapters: [
|
chapters: [
|
||||||
{
|
{
|
||||||
@@ -95,7 +95,78 @@ export default function ProgressTrackerPage() {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
])
|
]
|
||||||
|
|
||||||
|
const ZERO_SUBJECTS: Subject[] = [
|
||||||
|
{
|
||||||
|
name: "Physics",
|
||||||
|
completionPct: 0,
|
||||||
|
chapters: [
|
||||||
|
{
|
||||||
|
name: "Kinematics",
|
||||||
|
isExpanded: true,
|
||||||
|
topics: [
|
||||||
|
{ name: "Motion in One Dimension", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
{ name: "Projectile Motion", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
{ name: "Relative Velocity", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Laws of Motion",
|
||||||
|
isExpanded: false,
|
||||||
|
topics: [
|
||||||
|
{ name: "Newton's First Law", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
{ name: "Friction", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Thermodynamics",
|
||||||
|
isExpanded: false,
|
||||||
|
topics: [
|
||||||
|
{ name: "First Law", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
{ name: "Carnot Engine", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Chemistry",
|
||||||
|
completionPct: 0,
|
||||||
|
chapters: [
|
||||||
|
{
|
||||||
|
name: "Atomic Structure",
|
||||||
|
isExpanded: false,
|
||||||
|
topics: [
|
||||||
|
{ name: "Bohr Model", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
{ name: "Quantum Numbers", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mathematics / Biology",
|
||||||
|
completionPct: 0,
|
||||||
|
chapters: [
|
||||||
|
{
|
||||||
|
name: "Core Foundation",
|
||||||
|
isExpanded: false,
|
||||||
|
topics: [
|
||||||
|
{ name: "Limits / Cell Structure", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
{ name: "Derivatives / Genetics", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function ProgressTrackerPage() {
|
||||||
|
const { userId } = useActiveUser()
|
||||||
|
const isDemo = isDemoUser(userId)
|
||||||
|
const [subjects, setSubjects] = useState<Subject[]>(isDemo ? DEMO_SUBJECTS : ZERO_SUBJECTS)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSubjects(isDemo ? DEMO_SUBJECTS : ZERO_SUBJECTS)
|
||||||
|
}, [isDemo])
|
||||||
|
|
||||||
const toggleChapter = (subjectIndex: number, chapterIndex: number) => {
|
const toggleChapter = (subjectIndex: number, chapterIndex: number) => {
|
||||||
const newSubjects = [...subjects]
|
const newSubjects = [...subjects]
|
||||||
|
|||||||
@@ -4,31 +4,40 @@ import { useState, useEffect } from "react"
|
|||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Trophy, TrendingUp, AlertTriangle, Sparkles, Building2, Target, Sliders, CheckCircle2, ChevronRight, UserCheck } from "lucide-react"
|
import { Trophy, TrendingUp, AlertTriangle, Sparkles, Building2, Target, Sliders, CheckCircle2, ChevronRight, UserCheck } from "lucide-react"
|
||||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
export default function RankPredictorPage() {
|
export default function RankPredictorPage() {
|
||||||
const { user } = useActiveUser()
|
const { user } = useActiveUser()
|
||||||
|
const isDemo = isDemoUser(user?.id)
|
||||||
const [exam, setExam] = useState<"NEET" | "JEE">("NEET")
|
const [exam, setExam] = useState<"NEET" | "JEE">("NEET")
|
||||||
const [physics, setPhysics] = useState(135)
|
const [physics, setPhysics] = useState(0)
|
||||||
const [chemistry, setChemistry] = useState(155)
|
const [chemistry, setChemistry] = useState(0)
|
||||||
const [bioOrMath, setBioOrMath] = useState(330) // Bio out of 360 or Math out of 100
|
const [bioOrMath, setBioOrMath] = useState(0)
|
||||||
const [saved, setSaved] = useState(false)
|
const [saved, setSaved] = useState(false)
|
||||||
|
const [initialized, setInitialized] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user?.target_exam) {
|
if (!user || initialized) return
|
||||||
if (user.target_exam.includes("NEET")) {
|
const isNeet = user.target_exam ? user.target_exam.includes("NEET") : true
|
||||||
setExam("NEET")
|
setExam(isNeet ? "NEET" : "JEE")
|
||||||
|
|
||||||
|
if (isDemo) {
|
||||||
|
if (isNeet) {
|
||||||
setPhysics(140)
|
setPhysics(140)
|
||||||
setChemistry(150)
|
setChemistry(150)
|
||||||
setBioOrMath(320)
|
setBioOrMath(320)
|
||||||
} else {
|
} else {
|
||||||
setExam("JEE")
|
|
||||||
setPhysics(65)
|
setPhysics(65)
|
||||||
setChemistry(70)
|
setChemistry(70)
|
||||||
setBioOrMath(75)
|
setBioOrMath(75)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setPhysics(0)
|
||||||
|
setChemistry(0)
|
||||||
|
setBioOrMath(0)
|
||||||
}
|
}
|
||||||
}, [user])
|
setInitialized(true)
|
||||||
|
}, [user, isDemo, initialized])
|
||||||
|
|
||||||
const maxTotal = exam === "NEET" ? 720 : 300
|
const maxTotal = exam === "NEET" ? 720 : 300
|
||||||
const maxPhysics = exam === "NEET" ? 180 : 100
|
const maxPhysics = exam === "NEET" ? 180 : 100
|
||||||
@@ -83,7 +92,18 @@ export default function RankPredictorPage() {
|
|||||||
{/* Exam Switcher */}
|
{/* Exam Switcher */}
|
||||||
<div className="inline-flex p-1.5 bg-secondary/40 border border-border rounded-2xl gap-2 mt-4">
|
<div className="inline-flex p-1.5 bg-secondary/40 border border-border rounded-2xl gap-2 mt-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => { setExam("NEET"); setPhysics(135); setChemistry(155); setBioOrMath(330) }}
|
onClick={() => {
|
||||||
|
setExam("NEET")
|
||||||
|
if (isDemo) {
|
||||||
|
setPhysics(135)
|
||||||
|
setChemistry(155)
|
||||||
|
setBioOrMath(330)
|
||||||
|
} else {
|
||||||
|
setPhysics(0)
|
||||||
|
setChemistry(0)
|
||||||
|
setBioOrMath(0)
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={`px-6 py-2 rounded-xl text-sm font-extrabold transition-all ${
|
className={`px-6 py-2 rounded-xl text-sm font-extrabold transition-all ${
|
||||||
exam === "NEET" ? "bg-[#00F0FF] text-black shadow-[0_0_15px_#00F0FF]" : "text-muted-foreground hover:text-foreground"
|
exam === "NEET" ? "bg-[#00F0FF] text-black shadow-[0_0_15px_#00F0FF]" : "text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
@@ -91,7 +111,18 @@ export default function RankPredictorPage() {
|
|||||||
NEET (UG) 720 Marks
|
NEET (UG) 720 Marks
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setExam("JEE"); setPhysics(65); setChemistry(70); setBioOrMath(75) }}
|
onClick={() => {
|
||||||
|
setExam("JEE")
|
||||||
|
if (isDemo) {
|
||||||
|
setPhysics(65)
|
||||||
|
setChemistry(70)
|
||||||
|
setBioOrMath(75)
|
||||||
|
} else {
|
||||||
|
setPhysics(0)
|
||||||
|
setChemistry(0)
|
||||||
|
setBioOrMath(0)
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={`px-6 py-2 rounded-xl text-sm font-extrabold transition-all ${
|
className={`px-6 py-2 rounded-xl text-sm font-extrabold transition-all ${
|
||||||
exam === "JEE" ? "bg-[#00F0FF] text-black shadow-[0_0_15px_#00F0FF]" : "text-muted-foreground hover:text-foreground"
|
exam === "JEE" ? "bg-[#00F0FF] text-black shadow-[0_0_15px_#00F0FF]" : "text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
@@ -175,7 +206,9 @@ export default function RankPredictorPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h4 className="font-extrabold text-emerald-400">Strongest Domain</h4>
|
<h4 className="font-extrabold text-emerald-400">Strongest Domain</h4>
|
||||||
<p className="text-xs text-muted-foreground font-medium mt-1">
|
<p className="text-xs text-muted-foreground font-medium mt-1">
|
||||||
Your Biology/Math score ({bioOrMath}) provides solid rank leverage. Keep maintaining your revision momentum.
|
{totalScore === 0
|
||||||
|
? "Baseline Calibrating: Solve your first DPP or CBT to benchmark your subject strengths."
|
||||||
|
: `Your Biology/Math score (${bioOrMath}) provides solid rank leverage. Keep maintaining your revision momentum.`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -185,7 +218,9 @@ export default function RankPredictorPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h4 className="font-extrabold text-amber-400">High-Yield Growth Zone</h4>
|
<h4 className="font-extrabold text-amber-400">High-Yield Growth Zone</h4>
|
||||||
<p className="text-xs text-muted-foreground font-medium mt-1">
|
<p className="text-xs text-muted-foreground font-medium mt-1">
|
||||||
Increasing Physics by just 25 marks will boost your rank by approximately ~4,800 positions.
|
{totalScore === 0
|
||||||
|
? "Zero Baseline: Adjust the sliders above or take practice tests to see rank impact in real time."
|
||||||
|
: "Increasing Physics by just 25 marks will boost your rank by approximately ~4,800 positions."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -202,10 +237,10 @@ export default function RankPredictorPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-xs font-black uppercase tracking-widest text-[#00F0FF] mb-2">Estimated All India Rank</div>
|
<div className="text-xs font-black uppercase tracking-widest text-[#00F0FF] mb-2">Estimated All India Rank</div>
|
||||||
<div className="text-5xl md:text-6xl font-black tracking-tighter text-foreground my-3 filter drop-shadow-md">
|
<div className="text-5xl md:text-6xl font-black tracking-tighter text-foreground my-3 filter drop-shadow-md">
|
||||||
AIR ~{predictedRank.toLocaleString()}
|
{totalScore === 0 ? "AIR --" : `AIR ~${predictedRank.toLocaleString()}`}
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-[#00F0FF]/20 text-[#00F0FF] text-xs font-bold border border-[#00F0FF]/40">
|
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-[#00F0FF]/20 text-[#00F0FF] text-xs font-bold border border-[#00F0FF]/40">
|
||||||
<CheckCircle2 size={13} /> High Accuracy Confidence
|
<CheckCircle2 size={13} /> {totalScore === 0 ? "Calibrating · Adjust Sliders" : "High Accuracy Confidence"}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -215,6 +250,12 @@ export default function RankPredictorPage() {
|
|||||||
<Building2 className="text-[#00F0FF]" size={20} /> College Admission Chances
|
<Building2 className="text-[#00F0FF]" size={20} /> College Admission Chances
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
{totalScore === 0 ? (
|
||||||
|
<div className="p-4 rounded-2xl bg-secondary/20 border border-dashed border-border text-center text-xs text-muted-foreground space-y-1">
|
||||||
|
<p className="font-bold text-foreground">No Marks Simulated</p>
|
||||||
|
<p>Drag the subject sliders on the left or complete mock tests to view college probability predictions.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{colleges.map((c) => {
|
{colleges.map((c) => {
|
||||||
const isLikely = totalScore >= c.minScore
|
const isLikely = totalScore >= c.minScore
|
||||||
@@ -235,6 +276,7 @@ export default function RankPredictorPage() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,40 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
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"
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
export default function StreaksPage() {
|
export default function StreaksPage() {
|
||||||
const { user } = useActiveUser()
|
const { user } = useActiveUser()
|
||||||
|
const isDemo = isDemoUser(user?.id)
|
||||||
const currentStreak = user?.streak_count ?? 1
|
const currentStreak = user?.streak_count ?? 1
|
||||||
const bestStreak = Math.max(currentStreak, user?.longest_streak ?? 1)
|
const bestStreak = isDemo ? (user?.longest_streak ?? 45) : Math.max(currentStreak, user?.longest_streak ?? 1)
|
||||||
|
const [streakPoints, setStreakPoints] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user?.id) return
|
||||||
|
if (isDemo) {
|
||||||
|
setStreakPoints(currentStreak * 50)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
async function loadStats() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/users/${user?.id}/stats`)
|
||||||
|
if (res.ok) {
|
||||||
|
const stats = await res.json()
|
||||||
|
if (!stats.is_new_user) {
|
||||||
|
setStreakPoints(currentStreak * 50)
|
||||||
|
} else {
|
||||||
|
setStreakPoints(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setStreakPoints(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadStats()
|
||||||
|
}, [user?.id, isDemo, currentStreak])
|
||||||
|
|
||||||
const rewards = [
|
const rewards = [
|
||||||
{ streak: 7, title: "Consistent Learner", reward: "1 Free Premium DPP", unlocked: currentStreak >= 7 },
|
{ streak: 7, title: "Consistent Learner", reward: "1 Free Premium DPP", unlocked: currentStreak >= 7 },
|
||||||
@@ -39,7 +66,11 @@ export default function StreaksPage() {
|
|||||||
<Flame size={64} className="text-orange-500 mb-4 animate-pulse" />
|
<Flame size={64} className="text-orange-500 mb-4 animate-pulse" />
|
||||||
<h2 className="text-6xl font-black text-foreground mb-2">{currentStreak} <span className="text-2xl text-muted-foreground">Days</span></h2>
|
<h2 className="text-6xl font-black text-foreground mb-2">{currentStreak} <span className="text-2xl text-muted-foreground">Days</span></h2>
|
||||||
<p className="text-sm font-bold text-orange-500">CURRENT STREAK</p>
|
<p className="text-sm font-bold text-orange-500">CURRENT STREAK</p>
|
||||||
<p className="text-xs text-muted-foreground mt-4 max-w-xs">Complete a Daily Practice Problem (DPP) or CBT Exam every day to keep your streak alive.</p>
|
<p className="text-xs text-muted-foreground mt-4 max-w-xs">
|
||||||
|
{streakPoints === 0
|
||||||
|
? "Complete your first Daily Practice Problem (DPP) or CBT Exam today to start earning streak points."
|
||||||
|
: "Complete a Daily Practice Problem (DPP) or CBT Exam every day to keep your streak alive."}
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -57,7 +88,10 @@ 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]">{(currentStreak * 50).toLocaleString()}</p>
|
<p className="text-3xl font-bold text-[#00F0FF]">{streakPoints.toLocaleString()}</p>
|
||||||
|
{streakPoints === 0 && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Take a test to activate consistency bonus points</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, 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"
|
||||||
import { CheckCircle2, Clock, SkipForward, BrainCircuit } from "lucide-react"
|
import { CheckCircle2, Clock, SkipForward, BrainCircuit } from "lucide-react"
|
||||||
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
type PlanState = 'PENDING' | 'DONE' | 'SKIPPED';
|
type PlanState = 'PENDING' | 'DONE' | 'SKIPPED';
|
||||||
|
|
||||||
@@ -16,14 +17,30 @@ interface PlanItem {
|
|||||||
status: PlanState;
|
status: PlanState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL_PLAN: PlanItem[] = [
|
const DEMO_PLAN: PlanItem[] = [
|
||||||
{ id: '1', title: 'Revise Thermodynamics Concepts', duration: '45 min', type: 'Theory', status: 'PENDING' },
|
{ id: '1', title: 'Revise Thermodynamics Concepts', duration: '45 min', type: 'Theory', status: 'PENDING' },
|
||||||
{ id: '2', title: 'Solve 15 Rotational Dynamics DPPs', duration: '30 min', type: 'Practice', status: 'PENDING' },
|
{ id: '2', title: 'Solve 15 Rotational Dynamics DPPs', duration: '30 min', type: 'Practice', status: 'PENDING' },
|
||||||
{ id: '3', title: 'Review Mistakes in Optics (T-3)', duration: '20 min', type: 'Review', status: 'PENDING' },
|
{ id: '3', title: 'Review Mistakes in Optics (T-3)', duration: '20 min', type: 'Review', status: 'PENDING' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const NEW_USER_PLAN: PlanItem[] = [
|
||||||
|
{ id: '1', title: 'Take Baseline Diagnostic CBT Test', duration: '45 min', type: 'Diagnostic', status: 'PENDING' },
|
||||||
|
{ id: '2', title: 'Attempt Today\'s 15 DPP Practice Questions', duration: '30 min', type: 'Practice', status: 'PENDING' },
|
||||||
|
{ id: '3', title: 'Calibrate Target Exam & Daily Time Slot', duration: '15 min', type: 'Setup', status: 'PENDING' },
|
||||||
|
];
|
||||||
|
|
||||||
export function AdaptiveStudyPlan() {
|
export function AdaptiveStudyPlan() {
|
||||||
const [items, setItems] = useState<PlanItem[]>(INITIAL_PLAN);
|
const { user } = useActiveUser();
|
||||||
|
const isDemo = isDemoUser(user?.id);
|
||||||
|
const [items, setItems] = useState<PlanItem[]>(DEMO_PLAN);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDemo) {
|
||||||
|
setItems(DEMO_PLAN);
|
||||||
|
} else {
|
||||||
|
setItems(NEW_USER_PLAN);
|
||||||
|
}
|
||||||
|
}, [isDemo]);
|
||||||
|
|
||||||
const updateStatus = (id: string, newStatus: PlanState) => {
|
const updateStatus = (id: string, newStatus: PlanState) => {
|
||||||
setItems(items.map(item => item.id === id ? { ...item, status: newStatus } : item));
|
setItems(items.map(item => item.id === id ? { ...item, status: newStatus } : item));
|
||||||
@@ -48,7 +65,9 @@ export function AdaptiveStudyPlan() {
|
|||||||
<Clock size={14} className="text-primary" /> Today: {totalTime} min
|
<Clock size={14} className="text-primary" /> Today: {totalTime} min
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">Dynamically generated based on your weak areas.</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{isDemo ? "Dynamically generated based on your weak areas." : "Day 1 Onboarding Plan · Calibrating AI recommendations based on your initial performance."}
|
||||||
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex-1 p-6">
|
<CardContent className="flex-1 p-6">
|
||||||
<div className="relative space-y-6 before:absolute before:inset-0 before:ml-5 before:-translate-x-px md:before:mx-auto md:before:translate-x-0 before:h-full before:w-0.5 before:bg-gradient-to-b before:from-transparent before:via-border before:to-transparent">
|
<div className="relative space-y-6 before:absolute before:inset-0 before:ml-5 before:-translate-x-px md:before:mx-auto md:before:translate-x-0 before:h-full before:w-0.5 before:bg-gradient-to-b before:from-transparent before:via-border before:to-transparent">
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Flame, Info } from "lucide-react"
|
import { Flame, Info, CheckCircle2 } from "lucide-react"
|
||||||
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
const HEATMAP_DATA = [
|
const HEATMAP_DATA = [
|
||||||
{ topic: "Rotational Dynamics", attempts: [80, 90, 40, 20, 10] }, // 10 is critical error rate
|
{ topic: "Rotational Dynamics", attempts: [80, 90, 40, 20, 10] }, // 10 is critical error rate
|
||||||
@@ -10,14 +13,26 @@ const HEATMAP_DATA = [
|
|||||||
{ topic: "Modern Physics", attempts: [10, 5, 2, 0, 0] }
|
{ topic: "Modern Physics", attempts: [10, 5, 2, 0, 0] }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const ZERO_HEATMAP_DATA = [
|
||||||
|
{ topic: "Physics (Core Mechanics)", attempts: [0, 0, 0, 0, 0] },
|
||||||
|
{ topic: "Chemistry (Physical & Organic)", attempts: [0, 0, 0, 0, 0] },
|
||||||
|
{ topic: "Mathematics / Biology", attempts: [0, 0, 0, 0, 0] },
|
||||||
|
];
|
||||||
|
|
||||||
export function MistakeHeatmap() {
|
export function MistakeHeatmap() {
|
||||||
|
const { userId } = useActiveUser();
|
||||||
|
const isDemo = isDemoUser(userId);
|
||||||
|
|
||||||
const getColor = (errorRate: number) => {
|
const getColor = (errorRate: number) => {
|
||||||
|
if (errorRate === 0) return 'bg-white/5 border-white/10 text-muted-foreground';
|
||||||
if (errorRate >= 50) return 'bg-red-500/80 border-red-500 shadow-[0_0_10px_-2px_rgba(239,68,68,0.5)]'; // Critical
|
if (errorRate >= 50) return 'bg-red-500/80 border-red-500 shadow-[0_0_10px_-2px_rgba(239,68,68,0.5)]'; // Critical
|
||||||
if (errorRate >= 30) return 'bg-amber-500/60 border-amber-500'; // Warning
|
if (errorRate >= 30) return 'bg-amber-500/60 border-amber-500'; // Warning
|
||||||
if (errorRate >= 10) return 'bg-yellow-500/30 border-yellow-500/50'; // Moderate
|
if (errorRate >= 10) return 'bg-yellow-500/30 border-yellow-500/50'; // Moderate
|
||||||
return 'bg-green-500/20 border-green-500/30'; // Good
|
return 'bg-green-500/20 border-green-500/30'; // Good
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rows = isDemo ? HEATMAP_DATA : ZERO_HEATMAP_DATA;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border-border bg-background shadow-sm h-full min-h-[540px] flex flex-col justify-between">
|
<Card className="border-border bg-background shadow-sm h-full min-h-[540px] flex flex-col justify-between">
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-4">
|
||||||
@@ -28,7 +43,9 @@ export function MistakeHeatmap() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Info size={16} className="text-muted-foreground" />
|
<Info size={16} className="text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">Topic-wise error rate across last 5 tests.</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{isDemo ? "Topic-wise error rate across last 5 tests." : "Topic-wise error rate across your recent test sessions."}
|
||||||
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex-1 p-4 md:p-6 flex flex-col justify-between">
|
<CardContent className="flex-1 p-4 md:p-6 flex flex-col justify-between">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -46,7 +63,7 @@ export function MistakeHeatmap() {
|
|||||||
|
|
||||||
{/* Data rows */}
|
{/* Data rows */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{HEATMAP_DATA.map((row, idx) => (
|
{rows.map((row, idx) => (
|
||||||
<div key={idx} className="flex items-center group">
|
<div key={idx} className="flex items-center group">
|
||||||
<div className="w-32 sm:w-36 md:w-40 shrink-0 text-xs sm:text-sm md:text-base font-bold truncate pr-2 group-hover:text-primary transition-colors" title={row.topic}>
|
<div className="w-32 sm:w-36 md:w-40 shrink-0 text-xs sm:text-sm md:text-base font-bold truncate pr-2 group-hover:text-primary transition-colors" title={row.topic}>
|
||||||
{row.topic}
|
{row.topic}
|
||||||
@@ -65,11 +82,22 @@ export function MistakeHeatmap() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!isDemo && (
|
||||||
|
<div className="mt-6 p-4 rounded-xl bg-secondary/30 border border-border/50 text-center space-y-1">
|
||||||
|
<div className="flex items-center justify-center gap-1.5 text-xs font-bold text-green-400">
|
||||||
|
<CheckCircle2 size={14} /> Clean Slate · 0 Errors Recorded
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
As you attempt DPPs and CBT mock tests, questions you get wrong will automatically be tracked and heatmapped here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Legend */}
|
{/* Legend */}
|
||||||
<div className="mt-6 pt-4 border-t border-border/50 flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-xs font-bold text-muted-foreground">
|
<div className="mt-6 pt-4 border-t border-border/50 flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-xs font-bold text-muted-foreground">
|
||||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-green-500/20 border border-green-500/30"></div> {"< 10%"}</div>
|
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-green-500/20 border border-green-500/30"></div> {"0-10% (Optimal)"}</div>
|
||||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-yellow-500/30 border border-yellow-500/50"></div> {"10-30%"}</div>
|
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-yellow-500/30 border border-yellow-500/50"></div> {"10-30%"}</div>
|
||||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-amber-500/60 border border-amber-500"></div> {"30-50%"}</div>
|
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-amber-500/60 border border-amber-500"></div> {"30-50%"}</div>
|
||||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-red-500/80 border border-red-500"></div> {"> 50%"}</div>
|
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-red-500/80 border border-red-500"></div> {"> 50%"}</div>
|
||||||
@@ -78,3 +106,4 @@ export function MistakeHeatmap() {
|
|||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import { Card, CardContent } from "@/components/ui/card"
|
|||||||
import { TrendingUp, Flame, RefreshCw } from "lucide-react"
|
import { TrendingUp, Flame, 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 ReadinessGauge() {
|
export function ReadinessGauge() {
|
||||||
const [data, setData] = useState<any>(null)
|
const [data, setData] = useState<any>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const { userId, loading: userLoading } = useActiveUser();
|
const { userId, loading: userLoading } = useActiveUser();
|
||||||
|
const isDemo = isDemoUser(userId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userLoading || !userId) return;
|
if (userLoading || !userId) return;
|
||||||
@@ -22,7 +23,7 @@ export function ReadinessGauge() {
|
|||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
setData(json)
|
setData(json)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("Using mock fallback data for Readiness Gauge.")
|
if (isDemo) {
|
||||||
setData({
|
setData({
|
||||||
score: 72,
|
score: 72,
|
||||||
breakdown: {
|
breakdown: {
|
||||||
@@ -33,12 +34,24 @@ export function ReadinessGauge() {
|
|||||||
coverage: 14
|
coverage: 14
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
setData({
|
||||||
|
score: 0,
|
||||||
|
breakdown: {
|
||||||
|
accuracy: 0,
|
||||||
|
speed: 0,
|
||||||
|
consistency: 0,
|
||||||
|
recovery: 0,
|
||||||
|
coverage: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchReadiness()
|
fetchReadiness()
|
||||||
}, [userId, userLoading])
|
}, [userId, userLoading, isDemo])
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -49,16 +62,10 @@ export function ReadinessGauge() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const score = data?.score || 0
|
const score = data?.score || 0
|
||||||
const breakdown = data?.breakdown || {}
|
const isZero = score === 0 && !isDemo
|
||||||
const circumference = 2 * Math.PI * 45 // r=45
|
const circumference = 2 * Math.PI * 45 // r=45
|
||||||
const strokeDashoffset = circumference - (score / 100) * circumference
|
const strokeDashoffset = circumference - (score / 100) * circumference
|
||||||
|
|
||||||
const getColor = (s: number) => {
|
|
||||||
if (s >= 75) return "text-green-500"
|
|
||||||
if (s >= 50) return "text-yellow-500"
|
|
||||||
return "text-red-500"
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
|
|
||||||
@@ -67,10 +74,12 @@ export function ReadinessGauge() {
|
|||||||
<div className="absolute -top-1/2 -right-1/4 w-48 h-48 bg-[radial-gradient(circle,rgba(94,196,184,0.06)_0%,transparent_70%)] rounded-full pointer-events-none" />
|
<div className="absolute -top-1/2 -right-1/4 w-48 h-48 bg-[radial-gradient(circle,rgba(94,196,184,0.06)_0%,transparent_70%)] rounded-full pointer-events-none" />
|
||||||
|
|
||||||
<div className="flex justify-between items-start mb-4 relative z-10">
|
<div className="flex justify-between items-start mb-4 relative z-10">
|
||||||
<span className="text-[11px] font-bold text-muted-foreground uppercase tracking-widest">Updated daily • Today</span>
|
<span className="text-[11px] font-bold text-muted-foreground uppercase tracking-widest">
|
||||||
<span className="flex items-center gap-1 text-xs font-bold text-green-500 bg-green-500/10 px-2.5 py-0.5 rounded-full">
|
{isZero ? "Algorithmic Baseline" : "Updated daily • Today"}
|
||||||
|
</span>
|
||||||
|
<span className={`flex items-center gap-1 text-xs font-bold px-2.5 py-0.5 rounded-full ${isZero ? "text-muted-foreground bg-white/5" : "text-green-500 bg-green-500/10"}`}>
|
||||||
<TrendingUp size={14} className="stroke-[2.5px]" />
|
<TrendingUp size={14} className="stroke-[2.5px]" />
|
||||||
+5 this week
|
{isZero ? "Day 1 Baseline" : "+5 this week"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -91,7 +100,7 @@ export function ReadinessGauge() {
|
|||||||
strokeWidth="6"
|
strokeWidth="6"
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
strokeDasharray={circumference}
|
strokeDasharray={circumference}
|
||||||
strokeDashoffset={strokeDashoffset}
|
strokeDashoffset={isZero ? circumference : strokeDashoffset}
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
className="transition-all duration-1500 ease-out"
|
className="transition-all duration-1500 ease-out"
|
||||||
/>
|
/>
|
||||||
@@ -105,19 +114,31 @@ export function ReadinessGauge() {
|
|||||||
{/* Quick metric grid beside ring */}
|
{/* Quick metric grid beside ring */}
|
||||||
<div className="flex-1 grid grid-cols-2 gap-2.5 w-full">
|
<div className="flex-1 grid grid-cols-2 gap-2.5 w-full">
|
||||||
<div className="bg-white/5 rounded-lg p-2.5">
|
<div className="bg-white/5 rounded-lg p-2.5">
|
||||||
<div className="text-base font-black flex items-baseline gap-1">88% <span className="text-xs text-green-500">↑</span></div>
|
<div className="text-base font-black flex items-baseline gap-1">
|
||||||
|
{isZero ? "0%" : "88%"}
|
||||||
|
{!isZero && <span className="text-xs text-green-500">↑</span>}
|
||||||
|
</div>
|
||||||
<div className="text-xs font-bold text-muted-foreground">Accuracy</div>
|
<div className="text-xs font-bold text-muted-foreground">Accuracy</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white/5 rounded-lg p-2.5">
|
<div className="bg-white/5 rounded-lg p-2.5">
|
||||||
<div className="text-base font-black flex items-baseline gap-1">45s <span className="text-xs text-red-500">↓</span></div>
|
<div className="text-base font-black flex items-baseline gap-1">
|
||||||
|
{isZero ? "-- s" : "45s"}
|
||||||
|
{!isZero && <span className="text-xs text-red-500">↓</span>}
|
||||||
|
</div>
|
||||||
<div className="text-xs font-bold text-muted-foreground">Speed/Q</div>
|
<div className="text-xs font-bold text-muted-foreground">Speed/Q</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white/5 rounded-lg p-2.5">
|
<div className="bg-white/5 rounded-lg p-2.5">
|
||||||
<div className="text-base font-black flex items-baseline gap-1">High <span className="text-xs text-green-500">↑</span></div>
|
<div className="text-base font-black flex items-baseline gap-1">
|
||||||
|
{isZero ? "0%" : "High"}
|
||||||
|
{!isZero && <span className="text-xs text-green-500">↑</span>}
|
||||||
|
</div>
|
||||||
<div className="text-xs font-bold text-muted-foreground">Consistency</div>
|
<div className="text-xs font-bold text-muted-foreground">Consistency</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white/5 rounded-lg p-2.5">
|
<div className="bg-white/5 rounded-lg p-2.5">
|
||||||
<div className="text-base font-black flex items-baseline gap-1">64% <span className="text-xs text-green-500">↑</span></div>
|
<div className="text-base font-black flex items-baseline gap-1">
|
||||||
|
{isZero ? "0%" : "64%"}
|
||||||
|
{!isZero && <span className="text-xs text-green-500">↑</span>}
|
||||||
|
</div>
|
||||||
<div className="text-xs font-bold text-muted-foreground">Syllabus</div>
|
<div className="text-xs font-bold text-muted-foreground">Syllabus</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,12 +147,12 @@ export function ReadinessGauge() {
|
|||||||
{/* Subject Bars */}
|
{/* Subject Bars */}
|
||||||
<div className="space-y-2 mb-4 relative z-10">
|
<div className="space-y-2 mb-4 relative z-10">
|
||||||
{[
|
{[
|
||||||
{ name: "Physics", val: 68, color: "#00F0FF" },
|
{ name: "Physics", val: isZero ? 0 : 68, color: "#00F0FF" },
|
||||||
{ name: "Chemistry", val: 74, color: "#F59E0B" },
|
{ name: "Chemistry", val: isZero ? 0 : 74, color: "#F59E0B" },
|
||||||
{ name: "Biology", val: 82, color: "#10B981" },
|
{ name: "Biology / Maths", val: isZero ? 0 : 82, color: "#10B981" },
|
||||||
].map(sub => (
|
].map(sub => (
|
||||||
<div key={sub.name} className="flex items-center gap-3">
|
<div key={sub.name} className="flex items-center gap-3">
|
||||||
<span className="text-xs font-bold text-muted-foreground w-20 shrink-0">{sub.name}</span>
|
<span className="text-xs font-bold text-muted-foreground w-24 shrink-0 truncate">{sub.name}</span>
|
||||||
<div className="flex-1 h-2 rounded-full bg-white/10 overflow-hidden">
|
<div className="flex-1 h-2 rounded-full bg-white/10 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full transition-all duration-1000"
|
className="h-full rounded-full transition-all duration-1000"
|
||||||
@@ -147,15 +168,20 @@ export function ReadinessGauge() {
|
|||||||
<div className="flex items-center justify-between px-3 py-2 bg-white/5 rounded-lg mb-4 relative z-10 text-xs font-bold">
|
<div className="flex items-center justify-between px-3 py-2 bg-white/5 rounded-lg mb-4 relative z-10 text-xs font-bold">
|
||||||
<span className="text-muted-foreground">National Avg</span>
|
<span className="text-muted-foreground">National Avg</span>
|
||||||
<span className="text-muted-foreground">58 / 100</span>
|
<span className="text-muted-foreground">58 / 100</span>
|
||||||
<span className="text-green-500">You: +{score - 58} above avg</span>
|
<span className={isZero ? "text-muted-foreground" : "text-green-500"}>
|
||||||
|
{isZero ? "Pending test data" : `You: +${score - 58} above avg`}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-auto px-3.5 py-3 bg-[#00F0FF]/10 border border-[#00F0FF]/20 rounded-lg text-xs md:text-sm text-[#00F0FF] font-bold flex items-center gap-2 relative z-10">
|
<div className="mt-auto px-3.5 py-3 bg-[#00F0FF]/10 border border-[#00F0FF]/20 rounded-lg text-xs md:text-sm text-[#00F0FF] font-bold flex items-center gap-2 relative z-10">
|
||||||
<Flame size={18} className="fill-[#00F0FF]/20 shrink-0" />
|
<Flame size={18} className="fill-[#00F0FF]/20 shrink-0" />
|
||||||
Your speed has dropped slightly. Focus on time-bound practice today.
|
{isZero
|
||||||
|
? "Complete your first diagnostic test to establish your personalized readiness score."
|
||||||
|
: "Your speed has dropped slightly. Focus on time-bound practice today."}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { Flame, Star, Clock, TrendingUp } from "lucide-react"
|
import { Flame, Star, Clock, TrendingUp, Sparkles } from "lucide-react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
const SLOTS = [
|
const DEMO_SLOTS = [
|
||||||
{ label: "6 AM", h: 30, score: 52, peak: false },
|
{ label: "6 AM", h: 30, score: 52, peak: false },
|
||||||
{ label: "9 AM", h: 55, score: 68, peak: false },
|
{ label: "9 AM", h: 55, score: 68, peak: false },
|
||||||
{ label: "12 PM", h: 65, score: 74, peak: false },
|
{ label: "12 PM", h: 65, score: 74, peak: false },
|
||||||
@@ -15,9 +16,25 @@ const SLOTS = [
|
|||||||
{ label: "11 PM", h: 32, score: 48, peak: false },
|
{ label: "11 PM", h: 32, score: 48, peak: false },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const ZERO_SLOTS = [
|
||||||
|
{ label: "6 AM", h: 0, score: 0, peak: false },
|
||||||
|
{ label: "9 AM", h: 0, score: 0, peak: false },
|
||||||
|
{ label: "12 PM", h: 0, score: 0, peak: false },
|
||||||
|
{ label: "2 PM", h: 0, score: 0, peak: false },
|
||||||
|
{ label: "3 PM", h: 0, score: 0, peak: false },
|
||||||
|
{ label: "5 PM", h: 0, score: 0, peak: false },
|
||||||
|
{ label: "7 PM", h: 0, score: 0, peak: false },
|
||||||
|
{ label: "9 PM", h: 0, score: 0, peak: false },
|
||||||
|
{ label: "11 PM", h: 0, score: 0, peak: false },
|
||||||
|
]
|
||||||
|
|
||||||
export function StudyTimeOptimizationCard() {
|
export function StudyTimeOptimizationCard() {
|
||||||
|
const { userId } = useActiveUser()
|
||||||
|
const isDemo = isDemoUser(userId)
|
||||||
const [hovered, setHovered] = useState<number | null>(null)
|
const [hovered, setHovered] = useState<number | null>(null)
|
||||||
const maxH = Math.max(...SLOTS.map(s => s.h))
|
|
||||||
|
const slots = isDemo ? DEMO_SLOTS : ZERO_SLOTS
|
||||||
|
const maxH = Math.max(1, ...slots.map(s => s.h))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -29,11 +46,11 @@ export function StudyTimeOptimizationCard() {
|
|||||||
<Star size={15} className="fill-yellow-500 text-yellow-500" />
|
<Star size={15} className="fill-yellow-500 text-yellow-500" />
|
||||||
<span className="text-sm font-bold text-yellow-500">Peak Window:</span>
|
<span className="text-sm font-bold text-yellow-500">Peak Window:</span>
|
||||||
<span className="text-sm font-bold bg-yellow-500/10 border border-yellow-500/20 px-2.5 py-0.5 rounded-full">
|
<span className="text-sm font-bold bg-yellow-500/10 border border-yellow-500/20 px-2.5 py-0.5 rounded-full">
|
||||||
2 PM – 5 PM
|
{isDemo ? "2 PM – 5 PM" : "Calibrating..."}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto flex items-center gap-1.5 text-xs font-bold text-green-400 bg-green-400/10 border border-green-400/20 px-2.5 py-1 rounded-full">
|
<div className={`ml-auto flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full border ${isDemo ? "text-green-400 bg-green-400/10 border-green-400/20" : "text-muted-foreground bg-white/5 border-white/10"}`}>
|
||||||
<TrendingUp size={11} /> 95% efficiency at peak
|
<TrendingUp size={11} /> {isDemo ? "95% efficiency at peak" : "0% baseline"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -58,7 +75,7 @@ export function StudyTimeOptimizationCard() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<div className="flex items-end gap-1 h-full absolute inset-0">
|
<div className="flex items-end gap-1 h-full absolute inset-0">
|
||||||
{SLOTS.map((slot, i) => (
|
{slots.map((slot, i) => (
|
||||||
<div
|
<div
|
||||||
key={slot.label}
|
key={slot.label}
|
||||||
className="flex-1 flex flex-col items-center justify-end h-full cursor-pointer relative group/bar"
|
className="flex-1 flex flex-col items-center justify-end h-full cursor-pointer relative group/bar"
|
||||||
@@ -69,7 +86,9 @@ export function StudyTimeOptimizationCard() {
|
|||||||
{hovered === i && (
|
{hovered === i && (
|
||||||
<div className="absolute bottom-full mb-1.5 left-1/2 -translate-x-1/2 bg-background border border-border rounded-lg px-2.5 py-1.5 text-xs font-bold whitespace-nowrap shadow-lg z-10 pointer-events-none">
|
<div className="absolute bottom-full mb-1.5 left-1/2 -translate-x-1/2 bg-background border border-border rounded-lg px-2.5 py-1.5 text-xs font-bold whitespace-nowrap shadow-lg z-10 pointer-events-none">
|
||||||
<div className="text-foreground">{slot.label}</div>
|
<div className="text-foreground">{slot.label}</div>
|
||||||
<div className={slot.peak ? "text-yellow-400" : "text-[#00F0FF]"}>Score: {slot.score}%</div>
|
<div className={slot.peak ? "text-yellow-400" : "text-[#00F0FF]"}>
|
||||||
|
Score: {slot.score}%
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
@@ -78,9 +97,11 @@ export function StudyTimeOptimizationCard() {
|
|||||||
? "bg-gradient-to-t from-yellow-600/80 to-yellow-400 shadow-[0_0_14px_rgba(234,179,8,0.5)]"
|
? "bg-gradient-to-t from-yellow-600/80 to-yellow-400 shadow-[0_0_14px_rgba(234,179,8,0.5)]"
|
||||||
: hovered === i
|
: hovered === i
|
||||||
? "bg-gradient-to-t from-[#00F0FF]/70 to-[#00F0FF]"
|
? "bg-gradient-to-t from-[#00F0FF]/70 to-[#00F0FF]"
|
||||||
: "bg-gradient-to-t from-[#00F0FF]/20 to-[#00F0FF]/50"
|
: isDemo
|
||||||
|
? "bg-gradient-to-t from-[#00F0FF]/20 to-[#00F0FF]/50"
|
||||||
|
: "bg-white/5 border border-white/10"
|
||||||
}`}
|
}`}
|
||||||
style={{ height: `${(slot.h / maxH) * 100}%` }}
|
style={{ height: isDemo ? `${(slot.h / maxH) * 100}%` : "4px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -88,7 +109,7 @@ export function StudyTimeOptimizationCard() {
|
|||||||
</div>
|
</div>
|
||||||
{/* X-axis labels */}
|
{/* X-axis labels */}
|
||||||
<div className="flex gap-1 pt-1">
|
<div className="flex gap-1 pt-1">
|
||||||
{SLOTS.map((slot) => (
|
{slots.map((slot) => (
|
||||||
<div key={slot.label} className="flex-1 text-center">
|
<div key={slot.label} className="flex-1 text-center">
|
||||||
<span className="text-[10px] text-muted-foreground font-bold whitespace-nowrap">{slot.label}</span>
|
<span className="text-[10px] text-muted-foreground font-bold whitespace-nowrap">{slot.label}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,9 +121,9 @@ export function StudyTimeOptimizationCard() {
|
|||||||
{/* Efficiency breakdown */}
|
{/* Efficiency breakdown */}
|
||||||
<div className="grid grid-cols-3 gap-2 text-center">
|
<div className="grid grid-cols-3 gap-2 text-center">
|
||||||
{[
|
{[
|
||||||
{ label: "Morning", time: "6–9 AM", eff: 60, color: "text-muted-foreground" },
|
{ label: "Morning", time: "6–9 AM", eff: isDemo ? 60 : 0, color: "text-muted-foreground" },
|
||||||
{ label: "Peak", time: "2–5 PM", eff: 95, color: "text-yellow-400" },
|
{ label: "Peak", time: "2–5 PM", eff: isDemo ? 95 : 0, color: isDemo ? "text-yellow-400" : "text-muted-foreground" },
|
||||||
{ label: "Evening", time: "7–9 PM", eff: 73, color: "text-[#00F0FF]" },
|
{ label: "Evening", time: "7–9 PM", eff: isDemo ? 73 : 0, color: isDemo ? "text-[#00F0FF]" : "text-muted-foreground" },
|
||||||
].map(slot => (
|
].map(slot => (
|
||||||
<div key={slot.label} className="p-2.5 rounded-xl bg-white/5 border border-border/30">
|
<div key={slot.label} className="p-2.5 rounded-xl bg-white/5 border border-border/30">
|
||||||
<div className={`text-base font-black ${slot.color}`}>{slot.eff}%</div>
|
<div className={`text-base font-black ${slot.color}`}>{slot.eff}%</div>
|
||||||
@@ -114,10 +135,13 @@ export function StudyTimeOptimizationCard() {
|
|||||||
|
|
||||||
<div className="px-3.5 py-3 bg-yellow-500/10 border border-yellow-500/20 rounded-xl text-xs md:text-sm text-yellow-500 font-bold flex items-center gap-2">
|
<div className="px-3.5 py-3 bg-yellow-500/10 border border-yellow-500/20 rounded-xl text-xs md:text-sm text-yellow-500 font-bold flex items-center gap-2">
|
||||||
<Flame size={18} className="fill-yellow-500 shrink-0" />
|
<Flame size={18} className="fill-yellow-500 shrink-0" />
|
||||||
You perform best 2–5 PM. Schedule your hardest mock tests and DPPs in this window.
|
{isDemo
|
||||||
|
? "You perform best 2–5 PM. Schedule your hardest mock tests and DPPs in this window."
|
||||||
|
: "Circadian AI calibrating: Complete practice sessions at different times of day to reveal your peak study window."}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,24 +3,33 @@
|
|||||||
import { TrendingUp, TrendingDown, Brain, Zap, Target, Clock } from "lucide-react"
|
import { TrendingUp, TrendingDown, Brain, Zap, Target, Clock } from "lucide-react"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||||
const QUESTIONS = [42, 58, 35, 71, 65, 48, 60]
|
const DEMO_QUESTIONS = [42, 58, 35, 71, 65, 48, 60]
|
||||||
const ACCURACY = [82, 78, 91, 88, 85, 79, 93]
|
const DEMO_ACCURACY = [82, 78, 91, 88, 85, 79, 93]
|
||||||
const HOURS = [2.5, 3.5, 2, 4.5, 4, 3, 3.8]
|
const DEMO_HOURS = [2.5, 3.5, 2, 4.5, 4, 3, 3.8]
|
||||||
|
|
||||||
|
const ZERO_DATA = [0, 0, 0, 0, 0, 0, 0]
|
||||||
|
|
||||||
export function WeeklyProgressReport() {
|
export function WeeklyProgressReport() {
|
||||||
|
const { userId } = useActiveUser()
|
||||||
|
const isDemo = isDemoUser(userId)
|
||||||
const [metric, setMetric] = useState<"questions" | "accuracy" | "hours">("accuracy")
|
const [metric, setMetric] = useState<"questions" | "accuracy" | "hours">("accuracy")
|
||||||
const [hovered, setHovered] = useState<number | null>(null)
|
const [hovered, setHovered] = useState<number | null>(null)
|
||||||
|
|
||||||
|
const QUESTIONS = isDemo ? DEMO_QUESTIONS : ZERO_DATA
|
||||||
|
const ACCURACY = isDemo ? DEMO_ACCURACY : ZERO_DATA
|
||||||
|
const HOURS = isDemo ? DEMO_HOURS : ZERO_DATA
|
||||||
|
|
||||||
const DATA = metric === "questions" ? QUESTIONS : metric === "accuracy" ? ACCURACY : HOURS
|
const DATA = metric === "questions" ? QUESTIONS : metric === "accuracy" ? ACCURACY : HOURS
|
||||||
const maxVal = Math.max(...DATA)
|
const maxVal = Math.max(1, ...DATA)
|
||||||
const avgVal = Math.round((DATA.reduce((a, b) => a + b, 0) / DATA.length) * 10) / 10
|
const avgVal = Math.round((DATA.reduce((a, b) => a + b, 0) / DATA.length) * 10) / 10
|
||||||
const trend = DATA[DATA.length - 1] - DATA[0]
|
const trend = isDemo ? DATA[DATA.length - 1] - DATA[0] : 0
|
||||||
const trendUp = trend >= 0
|
const trendUp = trend >= 0
|
||||||
|
|
||||||
const UNIT = metric === "accuracy" ? "%" : metric === "hours" ? "h" : ""
|
const UNIT = metric === "accuracy" ? "%" : metric === "hours" ? "h" : ""
|
||||||
const avgAccuracy = Math.round(ACCURACY.reduce((a, b) => a + b, 0) / ACCURACY.length)
|
const avgAccuracy = isDemo ? Math.round(ACCURACY.reduce((a, b) => a + b, 0) / ACCURACY.length) : 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
@@ -38,7 +47,7 @@ export function WeeklyProgressReport() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full bg-yellow-500/10 text-yellow-500 border border-yellow-500/20">
|
<div className="flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full bg-yellow-500/10 text-yellow-500 border border-yellow-500/20">
|
||||||
<Zap size={12} />
|
<Zap size={12} />
|
||||||
7/7 days active
|
{isDemo ? "7/7 days active" : "Day 1 Active"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -89,13 +98,15 @@ export function WeeklyProgressReport() {
|
|||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`w-full rounded-t-md transition-all duration-700 ${
|
className={`w-full rounded-t-md transition-all duration-700 ${
|
||||||
i === 6
|
!isDemo
|
||||||
|
? "bg-white/5 border border-white/10"
|
||||||
|
: i === 6
|
||||||
? "bg-gradient-to-t from-[#00F0FF]/60 to-[#00F0FF]"
|
? "bg-gradient-to-t from-[#00F0FF]/60 to-[#00F0FF]"
|
||||||
: hovered === i
|
: hovered === i
|
||||||
? "bg-gradient-to-t from-[#00F0FF]/40 to-[#00F0FF]/80"
|
? "bg-gradient-to-t from-[#00F0FF]/40 to-[#00F0FF]/80"
|
||||||
: "bg-gradient-to-t from-white/10 to-white/25"
|
: "bg-gradient-to-t from-white/10 to-white/25"
|
||||||
}`}
|
}`}
|
||||||
style={{ height: `${(DATA[i] / maxVal) * 100}%` }}
|
style={{ height: isDemo ? `${(DATA[i] / maxVal) * 100}%` : "4px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -116,8 +127,8 @@ export function WeeklyProgressReport() {
|
|||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
{[
|
{[
|
||||||
{ label: "Avg Accuracy", value: `${avgAccuracy}%`, icon: <Target size={12} className="text-green-400" /> },
|
{ label: "Avg Accuracy", value: `${avgAccuracy}%`, icon: <Target size={12} className="text-green-400" /> },
|
||||||
{ label: "Questions / Day", value: `${Math.round(QUESTIONS.reduce((a, b) => a + b, 0) / 7)}`, icon: <Zap size={12} className="text-[#00F0FF]" /> },
|
{ label: "Questions / Day", value: isDemo ? `${Math.round(QUESTIONS.reduce((a, b) => a + b, 0) / 7)}` : "0", icon: <Zap size={12} className="text-[#00F0FF]" /> },
|
||||||
{ label: "Study Hrs / Day", value: `${(HOURS.reduce((a, b) => a + b, 0) / 7).toFixed(1)}h`, icon: <Clock size={12} className="text-amber-400" /> },
|
{ label: "Study Hrs / Day", value: isDemo ? `${(HOURS.reduce((a, b) => a + b, 0) / 7).toFixed(1)}h` : "0.0h", icon: <Clock size={12} className="text-amber-400" /> },
|
||||||
].map(stat => (
|
].map(stat => (
|
||||||
<div key={stat.label} className="p-2.5 rounded-xl bg-white/5 border border-border/30 text-center">
|
<div key={stat.label} className="p-2.5 rounded-xl bg-white/5 border border-border/30 text-center">
|
||||||
<div className="flex items-center justify-center gap-1 mb-0.5">{stat.icon}</div>
|
<div className="flex items-center justify-center gap-1 mb-0.5">{stat.icon}</div>
|
||||||
@@ -131,8 +142,16 @@ export function WeeklyProgressReport() {
|
|||||||
<div className="px-3 py-2.5 bg-[#00F0FF]/10 border border-[#00F0FF]/20 rounded-xl text-xs text-[#00F0FF] font-medium flex items-start gap-2.5">
|
<div className="px-3 py-2.5 bg-[#00F0FF]/10 border border-[#00F0FF]/20 rounded-xl text-xs text-[#00F0FF] font-medium flex items-start gap-2.5">
|
||||||
<Brain size={15} className="shrink-0 mt-0.5" />
|
<Brain size={15} className="shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
|
{isDemo ? (
|
||||||
|
<>
|
||||||
<span className="font-black text-white">Sign errors in Thermodynamics</span> — detected in your last 3 attempts (W = −PΔV misapplied). We've queued 3 targeted practice questions for tomorrow.{" "}
|
<span className="font-black text-white">Sign errors in Thermodynamics</span> — detected in your last 3 attempts (W = −PΔV misapplied). We've queued 3 targeted practice questions for tomorrow.{" "}
|
||||||
<span className="text-[#00F0FF]/80"><strong className="text-white">Tip:</strong> You peak at 7–9 PM. Schedule the hardest topics then.</span>
|
<span className="text-[#00F0FF]/80"><strong className="text-white">Tip:</strong> You peak at 7–9 PM. Schedule the hardest topics then.</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="font-black text-white">AI Progress Engine Calibrating</span> — Complete your first CBT mock test or DPP to begin mapping subject retention, speed trends, and error velocity.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -140,3 +159,4 @@ export function WeeklyProgressReport() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ export const DEMO_USERS = {
|
|||||||
export const DEMO_IDS = new Set(Object.values(DEMO_USERS).map(u => u.id));
|
export const DEMO_IDS = new Set(Object.values(DEMO_USERS).map(u => u.id));
|
||||||
|
|
||||||
/** Returns true if the userId matches a known demo account */
|
/** Returns true if the userId matches a known demo account */
|
||||||
export function isDemoUser(userId: string): boolean {
|
export function isDemoUser(userId?: string | null): boolean {
|
||||||
return DEMO_IDS.has(userId);
|
return !!userId && DEMO_IDS.has(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Compute trial info from user profile and global settings */
|
/** Compute trial info from user profile and global settings */
|
||||||
|
|||||||
Reference in New Issue
Block a user