fix: unify entry animations and eliminate layout flicker on overview, syllabus tracker, and doubt forum

This commit is contained in:
2026-10-23 17:42:34 +05:30
parent 5c9126ed7a
commit f9e0fb752b
7 changed files with 401 additions and 199 deletions

View File

@@ -9,11 +9,33 @@ 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"
import { useActiveUser, isDemoUser, DEFAULT_USER_ID } from "@/hooks/useActiveUser"
export default function DashboardPage() {
const { user, userId, loading: userLoading, hasAccessTo, trialInfo } = useActiveUser()
const [userStats, setUserStats] = useState<any>(null)
const [userStats, setUserStats] = useState<any>(() => {
const currentId = (typeof window !== "undefined" ? localStorage.getItem("activeUserId") : null) || DEFAULT_USER_ID;
if (isDemoUser(currentId)) {
return {
today_score: 42,
today_max_score: 60,
overall_accuracy: 82.0,
total_questions_attempted: 1245,
city_rank: 42,
city_total: 1200,
is_new_user: false,
};
}
return {
today_score: 0,
today_max_score: 0,
overall_accuracy: 0,
total_questions_attempted: 0,
city_rank: null,
city_total: null,
is_new_user: true,
};
})
const [loading, setLoading] = useState(true)
const [dpps, setDpps] = useState<any[]>([])
const [cbts, setCbts] = useState<any[]>([])
@@ -22,8 +44,7 @@ export default function DashboardPage() {
const progressRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (userLoading) return
const currentUserId = user?.id || userId
const currentUserId = user?.id || userId || (typeof window !== "undefined" ? localStorage.getItem("activeUserId") : null) || DEFAULT_USER_ID;
if (!currentUserId) {
setLoading(false)
return
@@ -48,7 +69,7 @@ export default function DashboardPage() {
}
}
fetchStatsAndTests()
}, [userLoading, user?.id, userId])
}, [user?.id, userId, user?.target_exam])
const getShareUrl = () => {
const score = isNew ? "0 / 0" : `${userStats?.today_score || 0} / ${userStats?.today_max_score || 60}`
@@ -108,132 +129,127 @@ export default function DashboardPage() {
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">
<div className="p-8 max-w-7xl mx-auto space-y-8">
{/* Basic Performance Dashboard (F-05) */}
<section className="animate-section animate-section-1">
<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.
{/* Page Header */}
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4 animate-section animate-section-1">
<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>
{/* KPI Stat Cards */}
<div ref={progressRef} className="grid grid-cols-2 md:grid-cols-4 gap-4 animate-section animate-section-2">
{/* Today's Score */}
<Card className="border-border shadow-sm">
<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>
</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>
</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>
{/* Overall Accuracy */}
<Card className="border-border shadow-sm">
<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>
{/* Qs Attempted */}
<Card className="border-border shadow-sm">
<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>
{/* City Rank */}
<Card className="border-border shadow-sm 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>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Tests & DPPs Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 animate-section animate-section-3">
{/* Dynamic DPP Modules */}
<section className="animate-section animate-section-2">
<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>
<Link href="/dashboard/dpp">
<Button variant="ghost" size="sm" className="text-muted-foreground font-medium">View All</Button>
</Link>
</div>
<div className="space-y-4">
{dpps.length > 0 ? (
@@ -257,18 +273,40 @@ export default function DashboardPage() {
</CardContent>
</Card>
))
) : loading ? (
<div className="space-y-4">
{[1, 2].map((i) => (
<Card key={i} className="animate-pulse">
<CardContent className="p-5 flex items-center justify-between">
<div className="flex gap-4 items-center">
<div className="size-11 bg-secondary rounded-xl"></div>
<div className="space-y-2">
<div className="h-4 w-40 bg-secondary rounded"></div>
<div className="h-3 w-28 bg-secondary/60 rounded"></div>
</div>
</div>
<div className="h-8 w-16 bg-secondary rounded-xl"></div>
</CardContent>
</Card>
))}
</div>
) : (
<p className="text-muted-foreground text-sm py-4">No DPPs scheduled for today.</p>
<Card className="overflow-hidden p-6 border-dashed opacity-70">
<p className="text-muted-foreground text-sm">No DPPs scheduled for today.</p>
</Card>
)}
</div>
</section>
{/* Upcoming CBTs */}
<section className="animate-section animate-section-3">
<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>
<Link href="/dashboard/cbt">
<Button variant="ghost" size="sm" className="text-muted-foreground font-medium">View All</Button>
</Link>
</div>
<div className="space-y-4">
{cbts.length > 0 ? (
@@ -291,6 +329,14 @@ export default function DashboardPage() {
</div>
</Card>
))
) : loading ? (
<div className="space-y-4">
<Card className="animate-pulse p-6 space-y-3">
<div className="h-4 w-24 bg-secondary rounded"></div>
<div className="h-5 w-48 bg-secondary rounded"></div>
<div className="h-3 w-32 bg-secondary/60 rounded"></div>
</Card>
</div>
) : (
<Card className="overflow-hidden p-6 border-dashed opacity-70">
<p className="text-muted-foreground text-sm">No upcoming CBTs at the moment.</p>
@@ -300,13 +346,14 @@ export default function DashboardPage() {
</section>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<section className="animate-section animate-section-4">
{/* AI Intelligence Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 animate-section animate-section-4">
<div>
<AdaptiveStudyPlan />
</section>
<section className="animate-section animate-section-5">
</div>
<div>
<MistakeHeatmap />
</section>
</div>
</div>
</div>