From f9e0fb752ba04fa54bc92aff6262c0a1ee07a5dd Mon Sep 17 00:00:00 2001 From: ectasy663 Date: Fri, 23 Oct 2026 17:42:34 +0530 Subject: [PATCH] fix: unify entry animations and eliminate layout flicker on overview, syllabus tracker, and doubt forum --- src/app/dashboard/community/page.tsx | 64 +++--- src/app/dashboard/page.tsx | 291 ++++++++++++++---------- src/app/dashboard/progress/page.tsx | 89 +++++--- src/components/AI/AdaptiveStudyPlan.tsx | 10 +- src/components/AI/MistakeHeatmap.tsx | 4 +- src/components/PremiumGate.tsx | 10 +- src/hooks/useActiveUser.ts | 132 ++++++++++- 7 files changed, 401 insertions(+), 199 deletions(-) diff --git a/src/app/dashboard/community/page.tsx b/src/app/dashboard/community/page.tsx index 9cdd9be..6174324 100644 --- a/src/app/dashboard/community/page.tsx +++ b/src/app/dashboard/community/page.tsx @@ -84,39 +84,46 @@ export default function CommunityPage() { return true }) - if (loading) { - return ( -
-
-

Loading Doubt Forum...

-
- ) - } - if (!hasAccessTo("doubt-resolution-community")) { + + if (!hasAccessTo("doubt-resolution-community") && !isDemo) { return ( -
-
- +
+
+
+

+ + + + Doubt Forum +

+

+ Ask questions, share knowledge, and learn together with the community. +

+
+
+
+
+ +
+

Premium Feature

+

+ The Doubt Forum is a premium feature. Upgrade your plan to ask doubts and interact with tutors and the community. +

+
-

Premium Feature

-

- The Doubt Forum is a premium feature. Upgrade your plan to ask doubts and interact with tutors and the community. -

-
) } return ( - -
-
+
+

@@ -159,7 +166,9 @@ export default function CommunityPage() {

- {filteredDoubts.map((doubt) => ( + {loading ? ( +
Loading doubts...
+ ) : (<>{filteredDoubts.map((doubt) => ( {/* Upvote Sidebar */} @@ -259,6 +268,7 @@ export default function CommunityPage() {

)} + )}
{/* Ask Doubt Modal */} {isAskModalOpen && ( @@ -331,7 +341,7 @@ export default function CommunityPage() {
)}
- + ) } diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 4a8c069..fdf58ee 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -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(null) + const [userStats, setUserStats] = useState(() => { + 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([]) const [cbts, setCbts] = useState([]) @@ -22,8 +44,7 @@ export default function DashboardPage() { const progressRef = useRef(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 ( -
-
-

Loading dashboard...

-
- ) - } const isNew = userStats ? userStats.is_new_user : true const isNeet = user?.target_exam === "NEET" return ( -
+
- {/* Basic Performance Dashboard (F-05) */} -
-
-
-

- - - - Performance Overview -

-

- Your overall progress, scores, and active study plan at a glance. + {/* Page Header */} +

+
+

+ + + + Performance Overview +

+

+ Your overall progress, scores, and active study plan at a glance. +

+
+
+ + {shareMenuOpen && ( +
+ + + +
+ )} +
+
+ + {/* KPI Stat Cards */} +
+ {/* Today's Score */} + + +
+

Today's Score

+

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

+

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

-
-
- - {shareMenuOpen && ( -
- - - -
- )} -
-
-
- {/* Today's Score */} - - -
-

Today's Score

-

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

-

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

-
-
+ + - {/* Overall Accuracy */} - - -
-

Overall Accuracy

-

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

-

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

-
-
+ {/* Overall Accuracy */} + + +
+

Overall Accuracy

+

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

+

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

+
+
- {/* Qs Attempted */} - - -
-

Qs Attempted

-

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

-

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

-
-
+ {/* Qs Attempted */} + + +
+

Qs Attempted

+

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

+

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

+
+
- {/* City Rank */} - -
- -
-
-

City Rank

-
-

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

-

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

-
-
-
-
+ {/* City Rank */} + +
+ +
+
+

City Rank

+
+

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

+

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

+
+
+
-
+ {/* Tests & DPPs Grid */} +
{/* Dynamic DPP Modules */} -
+

Today's DPPs

- + + +
{dpps.length > 0 ? ( @@ -257,18 +273,40 @@ export default function DashboardPage() { )) + ) : loading ? ( +
+ {[1, 2].map((i) => ( + + +
+
+
+
+
+
+
+
+
+
+ ))} +
) : ( -

No DPPs scheduled for today.

+ +

No DPPs scheduled for today.

+
)}
{/* Upcoming CBTs */} -
+

Upcoming CBTs

+ + +
{cbts.length > 0 ? ( @@ -291,6 +329,14 @@ export default function DashboardPage() {
)) + ) : loading ? ( +
+ +
+
+
+
+
) : (

No upcoming CBTs at the moment.

@@ -300,13 +346,14 @@ export default function DashboardPage() {
-
-
+ {/* AI Intelligence Grid */} +
+
-
-
+
+
- +
diff --git a/src/app/dashboard/progress/page.tsx b/src/app/dashboard/progress/page.tsx index 47223c5..9dd3571 100644 --- a/src/app/dashboard/progress/page.tsx +++ b/src/app/dashboard/progress/page.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react" import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" -import { ChevronDown, ChevronRight, CheckCircle2, Circle, Clock, Activity, BarChart3 } from "lucide-react" +import { ChevronDown, ChevronRight, CheckCircle2, Circle, Clock, Activity, BarChart3, Lock } from "lucide-react" import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser" import { PremiumGate } from "@/components/PremiumGate" @@ -134,17 +134,8 @@ const ZERO_SUBJECTS: Subject[] = [ // Math/Bio dynamic subjects handled below ] -export default function ProgressTrackerPage() { - const { userId, user } = useActiveUser() - const isDemo = isDemoUser(userId) - const [subjects, setSubjects] = useState([]) - useEffect(() => { - const targetExam = user?.target_exam || "JEE_Main"; - const isNeet = targetExam === "NEET"; - const isJeeNeet = targetExam === "JEE_NEET"; - - const mathDemoSubject: Subject = { +const mathDemoSubject: Subject = { name: "Mathematics", completionPct: 28, chapters: [ @@ -238,21 +229,67 @@ export default function ProgressTrackerPage() { ] }; - const base = isDemo ? [...DEMO_SUBJECTS] : [...ZERO_SUBJECTS]; - - if (isJeeNeet) { - // 4 Subjects for JEE + NEET: Physics, Chemistry, Mathematics, Biology - base.push(isDemo ? mathDemoSubject : mathZeroSubject); - base.push(isDemo ? bioDemoSubject : bioZeroSubject); - } else if (isNeet) { - base.push(isDemo ? bioDemoSubject : bioZeroSubject); - } else { - base.push(isDemo ? mathDemoSubject : mathZeroSubject); - } - - setSubjects(base); +function getInitialSubjects(isDemoUser: boolean, targetExam: string): Subject[] { + const isNeet = targetExam === "NEET"; + const isJeeNeet = targetExam === "JEE_NEET"; + const base = isDemoUser ? [...DEMO_SUBJECTS] : [...ZERO_SUBJECTS]; + if (isJeeNeet) { + base.push(isDemoUser ? mathDemoSubject : mathZeroSubject); + base.push(isDemoUser ? bioDemoSubject : bioZeroSubject); + } else if (isNeet) { + base.push(isDemoUser ? bioDemoSubject : bioZeroSubject); + } else { + base.push(isDemoUser ? mathDemoSubject : mathZeroSubject); + } + return base; +} +export default function ProgressTrackerPage() { + const { userId, user, hasAccessTo } = useActiveUser(); + const isDemo = isDemoUser(userId); + const [subjects, setSubjects] = useState(() => + getInitialSubjects(isDemoUser(userId), user?.target_exam || "JEE_Main") + ); + + useEffect(() => { + setSubjects(getInitialSubjects(isDemo, user?.target_exam || "JEE_Main")); }, [isDemo, user?.target_exam]); + if (!hasAccessTo("chapter-wise-progress-tracking") && !isDemo) { + return ( +
+
+
+

+ + + + Syllabus Tracker +

+

+ Track your syllabus coverage and mastery per chapter to ensure no gaps. +

+
+
+
+
+ +
+

Premium Feature

+

+ Chapter-wise syllabus tracking is available on Pro and AI Coach plans. +

+ +
+
+ ); + } + + const toggleChapter = (subjectIndex: number, chapterIndex: number) => { const newSubjects = [...subjects] newSubjects[subjectIndex].chapters[chapterIndex].isExpanded = !newSubjects[subjectIndex].chapters[chapterIndex].isExpanded @@ -260,7 +297,7 @@ export default function ProgressTrackerPage() { } return ( - +
@@ -351,6 +388,6 @@ export default function ProgressTrackerPage() { ))}
- + ) } diff --git a/src/components/AI/AdaptiveStudyPlan.tsx b/src/components/AI/AdaptiveStudyPlan.tsx index 5c02ae8..344aad0 100644 --- a/src/components/AI/AdaptiveStudyPlan.tsx +++ b/src/components/AI/AdaptiveStudyPlan.tsx @@ -1,4 +1,4 @@ -"use client" +"use client" import { useState, useEffect } from "react" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" @@ -32,14 +32,10 @@ const NEW_USER_PLAN: PlanItem[] = [ export function AdaptiveStudyPlan() { const { user } = useActiveUser(); const isDemo = isDemoUser(user?.id); - const [items, setItems] = useState(DEMO_PLAN); + const [items, setItems] = useState(() => isDemo ? DEMO_PLAN : NEW_USER_PLAN); useEffect(() => { - if (isDemo) { - setItems(DEMO_PLAN); - } else { - setItems(NEW_USER_PLAN); - } + setItems(isDemo ? DEMO_PLAN : NEW_USER_PLAN); }, [isDemo]); const updateStatus = (id: string, newStatus: PlanState) => { diff --git a/src/components/AI/MistakeHeatmap.tsx b/src/components/AI/MistakeHeatmap.tsx index 45373be..7fba716 100644 --- a/src/components/AI/MistakeHeatmap.tsx +++ b/src/components/AI/MistakeHeatmap.tsx @@ -20,8 +20,8 @@ const ZERO_HEATMAP_DATA = [ ]; export function MistakeHeatmap() { - const { userId } = useActiveUser(); - const isDemo = isDemoUser(userId); + const { userId, user } = useActiveUser(); + const isDemo = isDemoUser(user?.id || userId); const getColor = (errorRate: number) => { if (errorRate === 0) return 'bg-green-600/50 border-green-600/80 text-green-100'; diff --git a/src/components/PremiumGate.tsx b/src/components/PremiumGate.tsx index 7cca1c7..7e98faa 100644 --- a/src/components/PremiumGate.tsx +++ b/src/components/PremiumGate.tsx @@ -13,14 +13,10 @@ interface PremiumGateProps { export function PremiumGate({ featureKey, children }: PremiumGateProps) { const { hasAccessTo, loading, user } = useActiveUser(); + // Allow children to render immediately without a blocking center spinner + // so that page entry animations (animate-section) execute smoothly on route transition. if (loading || !user) { - return ( -
-
-
-
-
- ); + return <>{children}; } if (!hasAccessTo(featureKey)) { diff --git a/src/hooks/useActiveUser.ts b/src/hooks/useActiveUser.ts index 11ff27b..da4fd0a 100644 --- a/src/hooks/useActiveUser.ts +++ b/src/hooks/useActiveUser.ts @@ -147,6 +147,88 @@ async function fetchSettings(): Promise { return cachedSettings ?? null; } +let cachedUserProfile: UserProfile | null = null; + +export function getKnownDemoProfile(id: string): UserProfile | null { + if (id === DEMO_USERS.AICOACH_JEE.id) { + return { + id: DEMO_USERS.AICOACH_JEE.id, + name: "Aarav", + email: "aarav.jee@pragativeer.com", + phone: "9999999991", + class: "12th", + target_exam: "JEE_Main", + medium: "English", + school: "Delhi Public School", + city: "New Delhi", + state: "Delhi", + tier: "AI Coach", + streak_count: 14, + longest_streak: 21, + created_at: new Date(Date.now() - 30 * 86400000).toISOString(), + trial_end: new Date(Date.now() + 60 * 86400000).toISOString(), + }; + } + if (id === DEMO_USERS.AICOACH_NEET.id) { + return { + id: DEMO_USERS.AICOACH_NEET.id, + name: "Neha Verma", + email: "neha.neet@pragativeer.com", + phone: "9999999992", + class: "12th", + target_exam: "NEET", + medium: "English", + school: "Kendriya Vidyalaya", + city: "Kota", + state: "Rajasthan", + tier: "AI Coach", + streak_count: 18, + longest_streak: 25, + created_at: new Date(Date.now() - 30 * 86400000).toISOString(), + trial_end: new Date(Date.now() + 60 * 86400000).toISOString(), + }; + } + if (id === DEMO_USERS.AICOACH_JEE_NEET.id) { + return { + id: DEMO_USERS.AICOACH_JEE_NEET.id, + name: "Rahul Verma", + email: "rahul.jeeneet@pragativeer.com", + phone: "9999999993", + class: "12th", + target_exam: "JEE_NEET", + medium: "English", + school: "Modern School", + city: "Mumbai", + state: "Maharashtra", + tier: "AI Coach", + streak_count: 12, + longest_streak: 15, + created_at: new Date(Date.now() - 30 * 86400000).toISOString(), + trial_end: new Date(Date.now() + 60 * 86400000).toISOString(), + }; + } + if (id === DEMO_USERS.FREE_JEE.id) { + return { + id: DEMO_USERS.FREE_JEE.id, + name: "New Student", + email: "new.student@pragativeer.com", + phone: "9999999994", + class: "11th", + target_exam: "JEE_Main", + medium: "English", + school: "Army Public School", + city: "Jaipur", + state: "Rajasthan", + tier: "Free", + streak_count: 0, + longest_streak: 0, + created_at: new Date().toISOString(), + trial_end: undefined, + }; + } + return null; +} + export function useActiveUser() { const [userId, setUserIdState] = useState(() => { if (typeof window !== "undefined") { @@ -155,9 +237,32 @@ export function useActiveUser() { return DEFAULT_USER_ID; }); - const [user, setUser] = useState(null); - const [globalSettings, setGlobalSettings] = useState(null); - const [loading, setLoading] = useState(true); + const [user, setUser] = useState(() => { + const currentId = (typeof window !== "undefined" ? localStorage.getItem("activeUserId") : null) || DEFAULT_USER_ID; + if (cachedUserProfile && cachedUserProfile.id === currentId) { + return cachedUserProfile; + } + const demo = getKnownDemoProfile(currentId); + if (demo) { + cachedUserProfile = demo; + return demo; + } + if (typeof window !== "undefined") { + try { + const stored = localStorage.getItem("pragativeer_cached_user"); + if (stored) { + const parsed = JSON.parse(stored); + if (parsed && parsed.id === currentId) { + cachedUserProfile = parsed; + return parsed; + } + } + } catch {} + } + return null; + }); + const [globalSettings, setGlobalSettings] = useState(() => cachedSettings); + const [loading, setLoading] = useState(() => !cachedUserProfile); const [switching, setSwitching] = useState(false); const [error, setError] = useState(null); // Use a ref for fail count so it doesn't cause re-renders @@ -205,14 +310,17 @@ export function useActiveUser() { } } + cachedUserProfile = data; + if (typeof window !== "undefined") { + try { localStorage.setItem("pragativeer_cached_user", JSON.stringify(data)); } catch {} + } setUser(data); } catch (err: any) { failCountRef.current += 1; if (!isPoll) { console.warn("Backend unreachable, using local demo profile for offline testing:", err.message); } - // If backend is unreachable (e.g. running only npm run dev), provide demo user so testing works - setUser((prev) => prev || { + const fallback = getKnownDemoProfile(id) || { id: id || DEFAULT_USER_ID, name: "Aarav", email: "demo@pragativeer.com", @@ -228,7 +336,9 @@ export function useActiveUser() { longest_streak: 21, created_at: new Date(Date.now() - 5 * 86400000).toISOString(), trial_end: new Date(Date.now() + 10 * 86400000).toISOString(), - }); + }; + if (!cachedUserProfile) cachedUserProfile = fallback; + setUser((prev) => prev || fallback); // Silently swallow polling errors so the UI doesn't crash } finally { if (!isPoll) setLoading(false); @@ -290,11 +400,13 @@ export function useActiveUser() { // Update refs and state before awaiting network activeUserIdRef.current = actualUserId; setUserIdState(actualUserId); - setUser(null); // Clear old user immediately so stale data isn't shown + const demo = getKnownDemoProfile(actualUserId); + cachedUserProfile = demo; + setUser(demo); // Notify listeners immediately of account switch if (typeof window !== "undefined") { - window.dispatchEvent(new CustomEvent("pragativeer-user-switched", { detail: { userId: actualUserId } })); + window.dispatchEvent(new CustomEvent("pragativeer-user-switched", { detail: { userId: actualUserId, user: demo } })); } // Fetch the new user's profile immediately using the new token @@ -302,6 +414,10 @@ export function useActiveUser() { if (newUserRes && newUserRes.ok) { const data = await newUserRes.json(); + cachedUserProfile = data; + if (typeof window !== "undefined") { + try { localStorage.setItem("pragativeer_cached_user", JSON.stringify(data)); } catch {} + } setUser(data); if (typeof window !== "undefined") { window.dispatchEvent(new CustomEvent("pragativeer-user-switched", { detail: { userId: actualUserId, user: data } }));