-
-
-
-
-
- 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 */}
-
-
-
-
-
-
- {isNew ? (
- #--
- ) : (
- <>#{userStats.city_rank || 1} / {userStats.city_total || 50}>
- )}
-
-
- {isNew ? "Unranked · Take a test" : `Top rank in ${user?.city || "your city"}`}
-
-
-
-
-
+ {/* 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 } }));