342 lines
18 KiB
TypeScript
342 lines
18 KiB
TypeScript
"use client"
|
|
|
|
import { fetchWithAuth } from "@/lib/apiClient";
|
|
import { useRef, useState, useEffect } from "react"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import { BrainCircuit, Play, ChevronRight, Activity, Target, CheckCircle2, AlertTriangle, TrendingUp, Calendar, Zap, LayoutDashboard, Share2, Trophy, Lock, ArrowRight, Clock, Download } from "lucide-react"
|
|
import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
|
|
import { AdaptiveStudyPlan } from "@/components/AI/AdaptiveStudyPlan"
|
|
import { useActiveUser } from "@/hooks/useActiveUser"
|
|
|
|
export default function DashboardPage() {
|
|
const { user, userId, loading: userLoading, hasAccessTo, trialInfo } = useActiveUser()
|
|
const [userStats, setUserStats] = useState<any>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [downloading, setDownloading] = useState(false)
|
|
const [shareMenuOpen, setShareMenuOpen] = useState(false)
|
|
const progressRef = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
if (userLoading) return
|
|
const currentUserId = user?.id || userId
|
|
if (!currentUserId) {
|
|
setLoading(false)
|
|
return
|
|
}
|
|
async function fetchStats() {
|
|
try {
|
|
const res = await fetchWithAuth(`/api/users/${currentUserId}/stats`)
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
setUserStats(data)
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to load user stats", e)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
fetchStats()
|
|
}, [userLoading, user?.id, userId])
|
|
|
|
const getShareUrl = () => {
|
|
const score = isNew ? "0 / 0" : `${userStats?.today_score || 0} / ${userStats?.today_max_score || 60}`
|
|
const accuracy = isNew ? "0%" : `${userStats?.overall_accuracy || 0}%`
|
|
const qs = isNew ? "0" : (userStats?.total_questions_attempted || 0).toLocaleString()
|
|
const rank = isNew ? "#--" : `#${userStats?.city_rank || 1} / ${userStats?.city_total || 50}`
|
|
|
|
const target = user?.target_exam === "NEET" ? "NEET" : "JEE"
|
|
const plan = user?.tier === "AI Coach" ? "AI Coach" : user?.tier || "Free"
|
|
const name = user?.name || "Student"
|
|
const date = new Intl.DateTimeFormat('en-IN', {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short',
|
|
timeZone: 'Asia/Kolkata',
|
|
}).format(new Date());
|
|
|
|
const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
|
|
const themeParam = isDark ? 'dark' : 'light';
|
|
|
|
if (!user) return "";
|
|
return `/api/og/dashboard/${user.id}?theme=${themeParam}&date=${encodeURIComponent(date)}`
|
|
}
|
|
|
|
const handleDownload = async () => {
|
|
try {
|
|
setDownloading(true)
|
|
const url = getShareUrl()
|
|
const res = await fetch(url)
|
|
const blob = await res.blob()
|
|
const blobUrl = window.URL.createObjectURL(blob)
|
|
|
|
const link = document.createElement('a')
|
|
link.download = 'pragativeer-dashboard.png'
|
|
link.href = blobUrl
|
|
document.body.appendChild(link)
|
|
link.click()
|
|
document.body.removeChild(link)
|
|
window.URL.revokeObjectURL(blobUrl)
|
|
} catch (err) {
|
|
console.error("Failed to export image", err)
|
|
} finally {
|
|
setDownloading(false)
|
|
setShareMenuOpen(false)
|
|
}
|
|
}
|
|
|
|
const handleShareFacebook = () => {
|
|
const fullUrl = window.location.origin + getShareUrl()
|
|
window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(fullUrl)}`, '_blank')
|
|
setShareMenuOpen(false)
|
|
}
|
|
|
|
const handleShareWhatsApp = () => {
|
|
const fullUrl = window.location.origin + getShareUrl()
|
|
const text = `Check out my performance on PragatiVeer!\n\n${fullUrl}`
|
|
window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, '_blank')
|
|
setShareMenuOpen(false)
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="h-[70vh] flex flex-col items-center justify-center p-8">
|
|
<div className="h-8 w-8 border-4 border-[var(--cyan-ai)] border-t-transparent rounded-full animate-spin mb-4"></div>
|
|
<p className="text-muted-foreground font-medium animate-pulse">Loading dashboard...</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const isNew = userStats ? userStats.is_new_user : true
|
|
const isNeet = user?.target_exam === "NEET"
|
|
|
|
return (
|
|
<div className="p-8 max-w-7xl mx-auto space-y-10 animate-in fade-in zoom-in-95 slide-in-from-bottom-4 duration-500">
|
|
|
|
{/* Basic Performance Dashboard (F-05) */}
|
|
<section>
|
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4 mb-8">
|
|
<div>
|
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
|
<span className="p-2.5 rounded-xl bg-yellow-50 dark:bg-[var(--cyan-ai-10)] border border-yellow-300/60 dark:border-[var(--cyan-ai-20)]">
|
|
<LayoutDashboard className="text-yellow-600 dark:text-[var(--cyan-ai)]" size={28} />
|
|
</span>
|
|
Performance Overview
|
|
</h1>
|
|
<p className="text-muted-foreground text-base font-medium max-w-2xl pl-1">
|
|
Your overall progress, scores, and active study plan at a glance.
|
|
</p>
|
|
</div>
|
|
<div className="relative">
|
|
<Button onClick={() => setShareMenuOpen(!shareMenuOpen)} variant="outline" size="sm" className="gap-2 font-medium shrink-0" disabled={downloading}>
|
|
{downloading ? <div className="h-4 w-4 border-2 border-current border-t-transparent rounded-full animate-spin"></div> : <Share2 size={16} />}
|
|
{downloading ? 'Generating...' : 'Share Card'}
|
|
</Button>
|
|
{shareMenuOpen && (
|
|
<div className="absolute right-0 top-full mt-2 w-48 bg-white dark:bg-zinc-950 border border-border rounded-xl shadow-xl z-50 overflow-hidden">
|
|
<button onClick={handleShareFacebook} className="flex items-center w-full px-4 py-3 text-sm hover:bg-secondary text-left transition-colors">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-3 text-blue-600"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg>
|
|
Share on Facebook
|
|
</button>
|
|
<button onClick={handleShareWhatsApp} className="flex items-center w-full px-4 py-3 text-sm hover:bg-secondary text-left transition-colors">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-3 text-green-500"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
|
|
Share on WhatsApp
|
|
</button>
|
|
<button onClick={handleDownload} className="flex items-center w-full px-4 py-3 text-sm hover:bg-secondary text-left transition-colors border-t border-border">
|
|
<Download size={16} className="mr-3 text-primary" />
|
|
Download Image
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div ref={progressRef} className="grid grid-cols-2 md:grid-cols-4 gap-4 p-4 -mx-4 bg-background rounded-2xl">
|
|
{/* Today's Score */}
|
|
<Card>
|
|
<CardContent className="p-5 flex flex-col items-center text-center">
|
|
<div className="p-3 bg-secondary rounded-full mb-3 text-primary"><Target size={24} /></div>
|
|
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Today's Score</p>
|
|
<h3 className="text-2xl font-bold">
|
|
{isNew ? "0 / 0" : `${userStats.today_score} / ${userStats.today_max_score || 60}`}
|
|
</h3>
|
|
<p className="text-xs text-muted-foreground font-medium mt-2">
|
|
{isNew ? "No tests taken today" : userStats.today_score > 0 ? "Daily session score" : "Start today's DPP"}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Overall Accuracy */}
|
|
<Card>
|
|
<CardContent className="p-5 flex flex-col items-center text-center">
|
|
<div className="p-3 bg-secondary rounded-full mb-3 text-amber-500"><Activity size={24} /></div>
|
|
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Overall Accuracy</p>
|
|
<h3 className="text-2xl font-bold">
|
|
{isNew ? "0%" : `${userStats.overall_accuracy}%`}
|
|
</h3>
|
|
<p className={`text-xs font-bold mt-2 ${isNew ? 'text-muted-foreground font-normal' : userStats.overall_accuracy >= 70 ? 'text-green-500' : 'text-amber-500'}`}>
|
|
{isNew ? "Start a DPP to calculate" : userStats.overall_accuracy >= 70 ? "On Track" : "Needs Improvement"}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Qs Attempted */}
|
|
<Card>
|
|
<CardContent className="p-5 flex flex-col items-center text-center">
|
|
<div className="p-3 bg-secondary rounded-full mb-3 text-[var(--cyan-ai)]"><CheckCircle2 size={24} /></div>
|
|
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Qs Attempted</p>
|
|
<h3 className="text-2xl font-bold">
|
|
{isNew ? "0" : (userStats.total_questions_attempted || 0).toLocaleString()}
|
|
</h3>
|
|
<p className="text-xs text-muted-foreground font-medium mt-2">
|
|
{isNew ? "0 questions answered" : "Lifetime total"}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* City Rank */}
|
|
<Card className="relative overflow-hidden group">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-background to-secondary/50 z-0"></div>
|
|
<CardContent className="p-5 flex flex-col items-center text-center relative z-10">
|
|
<div className="p-3 bg-background border border-border shadow-sm rounded-full mb-3 text-foreground group-hover:scale-110 transition-transform"><Trophy size={24} /></div>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">City Rank</p>
|
|
</div>
|
|
<h3 className="text-2xl font-bold">
|
|
{isNew ? (
|
|
<span className="text-muted-foreground text-xl">#--</span>
|
|
) : (
|
|
<>#{userStats.city_rank || 1} <span className="text-base text-muted-foreground font-medium">/ {userStats.city_total || 50}</span></>
|
|
)}
|
|
</h3>
|
|
<p className="text-xs text-muted-foreground font-medium mt-2 text-[var(--cyan-ai)]">
|
|
{isNew ? "Unranked · Take a test" : `Top rank in ${user?.city || "your city"}`}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</section>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
|
<section>
|
|
<AdaptiveStudyPlan />
|
|
</section>
|
|
<section>
|
|
<MistakeHeatmap />
|
|
</section>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Dynamic DPP Modules */}
|
|
<section>
|
|
<div className="flex items-center justify-between mb-5">
|
|
<h2 className="text-xl font-semibold tracking-tight flex items-center gap-2">
|
|
Today's DPPs
|
|
</h2>
|
|
<Button variant="ghost" size="sm" className="text-muted-foreground font-medium">View All</Button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
<Card className="hover:border-primary/30 hover:shadow-md cursor-pointer transition-all duration-300 group">
|
|
<CardContent className="p-5 flex items-center justify-between">
|
|
<div className="flex gap-4 items-center">
|
|
<div className="p-3 bg-secondary rounded-xl text-foreground">
|
|
<BrainCircuit size={20} />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-medium text-base group-hover:text-primary transition-colors">Physics: Rotational Dynamics</h3>
|
|
<p className="text-sm text-muted-foreground mt-0.5">20 Questions • High Priority</p>
|
|
</div>
|
|
</div>
|
|
<Button variant="outline" size="sm" className="gap-2 rounded-xl group-hover:bg-primary group-hover:text-primary-foreground group-hover:border-primary transition-all">
|
|
Start <ArrowRight size={14} />
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{hasAccessTo("topic-wise-microtests-unlimited") ? (
|
|
<Card className="hover:border-primary/30 hover:shadow-md cursor-pointer transition-all duration-300 group">
|
|
<CardContent className="p-5 flex items-center justify-between">
|
|
<div className="flex gap-4 items-center">
|
|
<div className="p-3 bg-secondary rounded-xl text-foreground">
|
|
<BrainCircuit size={20} />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-medium text-base group-hover:text-primary transition-colors">
|
|
{isNeet ? "Biology: Human Physiology" : "Maths: Integral Calculus"}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground mt-0.5">30 Questions • Medium Priority</p>
|
|
</div>
|
|
</div>
|
|
<Button variant="outline" size="sm" className="gap-2 rounded-xl group-hover:bg-primary group-hover:text-primary-foreground group-hover:border-primary transition-all">
|
|
Start <ArrowRight size={14} />
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<Card className="relative opacity-60 cursor-not-allowed border-dashed">
|
|
<CardContent className="p-5 flex items-center justify-between">
|
|
<div className="flex gap-4 items-center">
|
|
<div className="p-3 bg-secondary rounded-xl text-muted-foreground">
|
|
<Lock size={20} />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-medium text-base text-muted-foreground">
|
|
{isNeet ? "Biology: Human Physiology" : "Maths: Integral Calculus"}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground mt-0.5 flex items-center gap-1"><Lock size={12}/> Pro Feature</p>
|
|
</div>
|
|
</div>
|
|
<Button variant="outline" size="sm" disabled className="gap-2 rounded-xl">
|
|
Locked
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
{/* Upcoming CBTs */}
|
|
<section>
|
|
<div className="flex items-center justify-between mb-5">
|
|
<h2 className="text-xl font-semibold tracking-tight flex items-center gap-2">
|
|
Upcoming CBTs
|
|
</h2>
|
|
</div>
|
|
<Card className="overflow-hidden">
|
|
<div className="p-6 border-b border-border flex items-center justify-between bg-background">
|
|
<div>
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<Badge variant="outline" className="border-primary/20 bg-primary/5 text-primary">Mock Test</Badge>
|
|
<span className="text-xs text-muted-foreground font-semibold uppercase tracking-wider">Tomorrow, 10:00 AM</span>
|
|
</div>
|
|
<h3 className="font-semibold text-lg tracking-tight">AITS Full Syllabus Test 4</h3>
|
|
<p className="text-sm text-muted-foreground flex items-center gap-1.5 mt-1.5">
|
|
<Clock size={14} /> 180 Minutes • 720 Marks
|
|
</p>
|
|
</div>
|
|
<Button variant="secondary" className="rounded-xl font-medium">Remind Me</Button>
|
|
</div>
|
|
{hasAccessTo("weekly-test-series") ? (
|
|
<div className="p-5 bg-secondary/50 flex items-center justify-between transition-colors hover:bg-secondary/70 cursor-pointer group">
|
|
<div>
|
|
<h3 className="font-medium text-sm text-foreground flex items-center gap-2"><Activity size={14} className="text-primary group-hover:animate-pulse"/> Part Test 3: Mechanics</h3>
|
|
<p className="text-xs text-muted-foreground mt-0.5 font-medium ml-6">Oct 24, 2026</p>
|
|
</div>
|
|
<Button variant="outline" size="sm" className="h-7 text-xs rounded-lg group-hover:bg-primary group-hover:text-primary-foreground group-hover:border-primary">Start <ArrowRight size={12} className="ml-1" /></Button>
|
|
</div>
|
|
) : (
|
|
<div className="p-5 bg-secondary/50 flex items-center justify-between transition-colors hover:bg-secondary/70 opacity-75">
|
|
<div>
|
|
<h3 className="font-medium text-sm text-foreground flex items-center gap-2"><Lock size={14} className="text-muted-foreground"/> Part Test 3: Mechanics</h3>
|
|
<p className="text-xs text-muted-foreground mt-0.5 font-medium ml-6">Oct 24, 2026</p>
|
|
</div>
|
|
<Badge variant="secondary" className="text-xs">Pro Only</Badge>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|