fix: unify entry animations and eliminate layout flicker on overview, syllabus tracker, and doubt forum
This commit is contained in:
@@ -84,39 +84,46 @@ export default function CommunityPage() {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
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 Doubt Forum...</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasAccessTo("doubt-resolution-community")) {
|
|
||||||
|
if (!hasAccessTo("doubt-resolution-community") && !isDemo) {
|
||||||
return (
|
return (
|
||||||
<div className="p-8 h-full flex flex-col items-center justify-center text-center max-w-2xl mx-auto">
|
<div className="p-8 space-y-8 max-w-7xl mx-auto">
|
||||||
<div className="size-20 rounded-full bg-red-500/10 flex items-center justify-center mx-auto text-red-500 mb-6">
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4 animate-section animate-section-1">
|
||||||
<Lock size={40} />
|
<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)]">
|
||||||
|
<MessageSquare className="text-yellow-600 dark:text-[var(--cyan-ai)]" size={28} />
|
||||||
|
</span>
|
||||||
|
Doubt Forum
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground text-base font-medium max-w-2xl pl-1">
|
||||||
|
Ask questions, share knowledge, and learn together with the community.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-12 text-center max-w-2xl mx-auto bg-secondary/20 border border-border rounded-3xl space-y-6 animate-section animate-section-2">
|
||||||
|
<div className="size-20 rounded-full bg-red-500/10 flex items-center justify-center mx-auto text-red-500">
|
||||||
|
<Lock size={40} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold">Premium Feature</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
The Doubt Forum is a premium feature. Upgrade your plan to ask doubts and interact with tutors and the community.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="px-8 py-3 bg-[var(--cyan-ai)] text-black font-bold rounded-xl shadow-[0_0_20px_-5px_var(--cyan-ai)]"
|
||||||
|
onClick={() => window.location.href = '/upgrade'}
|
||||||
|
>
|
||||||
|
Upgrade Plan
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl font-bold mb-4">Premium Feature</h2>
|
|
||||||
<p className="text-muted-foreground text-lg mb-8">
|
|
||||||
The Doubt Forum is a premium feature. Upgrade your plan to ask doubts and interact with tutors and the community.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
className="px-8 py-3 bg-primary text-primary-foreground font-bold rounded-xl shadow-[0_0_20px_-5px_var(--color-primary)] text-lg"
|
|
||||||
onClick={() => window.location.href = '/upgrade'}
|
|
||||||
>
|
|
||||||
Upgrade Plan
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PremiumGate featureKey="doubt-resolution-community">
|
<div className="p-8 space-y-8 max-w-7xl mx-auto">
|
||||||
<div className="p-8 space-y-6 max-w-7xl mx-auto">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-4 animate-section animate-section-1">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-4 animate-section animate-section-1">
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
<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)]">
|
<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)]">
|
||||||
@@ -159,7 +166,9 @@ export default function CommunityPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 animate-section animate-section-3">
|
<div className="space-y-4 animate-section animate-section-3">
|
||||||
{filteredDoubts.map((doubt) => (
|
{loading ? (
|
||||||
|
<div className="text-center py-12 text-muted-foreground animate-pulse">Loading doubts...</div>
|
||||||
|
) : (<>{filteredDoubts.map((doubt) => (
|
||||||
<Card key={doubt.id} className="border-border shadow-sm hover:border-[var(--cyan-ai-10)] transition-colors cursor-pointer">
|
<Card key={doubt.id} className="border-border shadow-sm hover:border-[var(--cyan-ai-10)] transition-colors cursor-pointer">
|
||||||
<CardContent className="p-0 flex">
|
<CardContent className="p-0 flex">
|
||||||
{/* Upvote Sidebar */}
|
{/* Upvote Sidebar */}
|
||||||
@@ -259,6 +268,7 @@ export default function CommunityPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>)}
|
||||||
</div>
|
</div>
|
||||||
{/* Ask Doubt Modal */}
|
{/* Ask Doubt Modal */}
|
||||||
{isAskModalOpen && (
|
{isAskModalOpen && (
|
||||||
@@ -331,7 +341,7 @@ export default function CommunityPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PremiumGate>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { 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 { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
|
||||||
import { AdaptiveStudyPlan } from "@/components/AI/AdaptiveStudyPlan"
|
import { AdaptiveStudyPlan } from "@/components/AI/AdaptiveStudyPlan"
|
||||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
import { useActiveUser, isDemoUser, DEFAULT_USER_ID } from "@/hooks/useActiveUser"
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const { user, userId, loading: userLoading, hasAccessTo, trialInfo } = useActiveUser()
|
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 [loading, setLoading] = useState(true)
|
||||||
const [dpps, setDpps] = useState<any[]>([])
|
const [dpps, setDpps] = useState<any[]>([])
|
||||||
const [cbts, setCbts] = useState<any[]>([])
|
const [cbts, setCbts] = useState<any[]>([])
|
||||||
@@ -22,8 +44,7 @@ export default function DashboardPage() {
|
|||||||
const progressRef = useRef<HTMLDivElement>(null)
|
const progressRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userLoading) return
|
const currentUserId = user?.id || userId || (typeof window !== "undefined" ? localStorage.getItem("activeUserId") : null) || DEFAULT_USER_ID;
|
||||||
const currentUserId = user?.id || userId
|
|
||||||
if (!currentUserId) {
|
if (!currentUserId) {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
@@ -48,7 +69,7 @@ export default function DashboardPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchStatsAndTests()
|
fetchStatsAndTests()
|
||||||
}, [userLoading, user?.id, userId])
|
}, [user?.id, userId, user?.target_exam])
|
||||||
|
|
||||||
const getShareUrl = () => {
|
const getShareUrl = () => {
|
||||||
const score = isNew ? "0 / 0" : `${userStats?.today_score || 0} / ${userStats?.today_max_score || 60}`
|
const score = isNew ? "0 / 0" : `${userStats?.today_score || 0} / ${userStats?.today_max_score || 60}`
|
||||||
@@ -108,132 +129,127 @@ export default function DashboardPage() {
|
|||||||
setShareMenuOpen(false)
|
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 isNew = userStats ? userStats.is_new_user : true
|
||||||
const isNeet = user?.target_exam === "NEET"
|
const isNeet = user?.target_exam === "NEET"
|
||||||
|
|
||||||
return (
|
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) */}
|
{/* Page Header */}
|
||||||
<section className="animate-section animate-section-1">
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4 animate-section animate-section-1">
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4 mb-8">
|
<div>
|
||||||
<div>
|
<h1 className="text-3xl md:text-4xl font-black tracking-tight flex items-center gap-3 mb-1">
|
||||||
<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)]">
|
||||||
<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} />
|
||||||
<LayoutDashboard className="text-yellow-600 dark:text-[var(--cyan-ai)]" size={28} />
|
</span>
|
||||||
</span>
|
Performance Overview
|
||||||
Performance Overview
|
</h1>
|
||||||
</h1>
|
<p className="text-muted-foreground text-base font-medium max-w-2xl pl-1">
|
||||||
<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.
|
||||||
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>
|
</p>
|
||||||
</div>
|
</CardContent>
|
||||||
<div className="relative">
|
</Card>
|
||||||
<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 */}
|
{/* Overall Accuracy */}
|
||||||
<Card>
|
<Card className="border-border shadow-sm">
|
||||||
<CardContent className="p-5 flex flex-col items-center text-center">
|
<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>
|
<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>
|
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Overall Accuracy</p>
|
||||||
<h3 className="text-2xl font-bold">
|
<h3 className="text-2xl font-bold">
|
||||||
{isNew ? "0%" : `${userStats.overall_accuracy}%`}
|
{isNew ? "0%" : `${userStats.overall_accuracy}%`}
|
||||||
</h3>
|
</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'}`}>
|
<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"}
|
{isNew ? "Start a DPP to calculate" : userStats.overall_accuracy >= 70 ? "On Track" : "Needs Improvement"}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Qs Attempted */}
|
{/* Qs Attempted */}
|
||||||
<Card>
|
<Card className="border-border shadow-sm">
|
||||||
<CardContent className="p-5 flex flex-col items-center text-center">
|
<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>
|
<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>
|
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Qs Attempted</p>
|
||||||
<h3 className="text-2xl font-bold">
|
<h3 className="text-2xl font-bold">
|
||||||
{isNew ? "0" : (userStats.total_questions_attempted || 0).toLocaleString()}
|
{isNew ? "0" : (userStats.total_questions_attempted || 0).toLocaleString()}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-muted-foreground font-medium mt-2">
|
<p className="text-xs text-muted-foreground font-medium mt-2">
|
||||||
{isNew ? "0 questions answered" : "Lifetime total"}
|
{isNew ? "0 questions answered" : "Lifetime total"}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* City Rank */}
|
{/* City Rank */}
|
||||||
<Card className="relative overflow-hidden group">
|
<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>
|
<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">
|
<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="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">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">City Rank</p>
|
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">City Rank</p>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-2xl font-bold">
|
<h3 className="text-2xl font-bold">
|
||||||
{isNew ? (
|
{isNew ? (
|
||||||
<span className="text-muted-foreground text-xl">#--</span>
|
<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></>
|
<>#{userStats.city_rank || 1} <span className="text-base text-muted-foreground font-medium">/ {userStats.city_total || 50}</span></>
|
||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-muted-foreground font-medium mt-2 text-[var(--cyan-ai)]">
|
<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"}`}
|
{isNew ? "Unranked · Take a test" : `Top rank in ${user?.city || "your city"}`}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<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 */}
|
{/* Dynamic DPP Modules */}
|
||||||
<section className="animate-section animate-section-2">
|
<section>
|
||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
<h2 className="text-xl font-semibold tracking-tight flex items-center gap-2">
|
<h2 className="text-xl font-semibold tracking-tight flex items-center gap-2">
|
||||||
Today's DPPs
|
Today's DPPs
|
||||||
</h2>
|
</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>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{dpps.length > 0 ? (
|
{dpps.length > 0 ? (
|
||||||
@@ -257,18 +273,40 @@ export default function DashboardPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Upcoming CBTs */}
|
{/* Upcoming CBTs */}
|
||||||
<section className="animate-section animate-section-3">
|
<section>
|
||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
<h2 className="text-xl font-semibold tracking-tight flex items-center gap-2">
|
<h2 className="text-xl font-semibold tracking-tight flex items-center gap-2">
|
||||||
Upcoming CBTs
|
Upcoming CBTs
|
||||||
</h2>
|
</h2>
|
||||||
|
<Link href="/dashboard/cbt">
|
||||||
|
<Button variant="ghost" size="sm" className="text-muted-foreground font-medium">View All</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{cbts.length > 0 ? (
|
{cbts.length > 0 ? (
|
||||||
@@ -291,6 +329,14 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</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">
|
<Card className="overflow-hidden p-6 border-dashed opacity-70">
|
||||||
<p className="text-muted-foreground text-sm">No upcoming CBTs at the moment.</p>
|
<p className="text-muted-foreground text-sm">No upcoming CBTs at the moment.</p>
|
||||||
@@ -300,13 +346,14 @@ export default function DashboardPage() {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
{/* AI Intelligence Grid */}
|
||||||
<section className="animate-section animate-section-4">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 animate-section animate-section-4">
|
||||||
|
<div>
|
||||||
<AdaptiveStudyPlan />
|
<AdaptiveStudyPlan />
|
||||||
</section>
|
</div>
|
||||||
<section className="animate-section animate-section-5">
|
<div>
|
||||||
<MistakeHeatmap />
|
<MistakeHeatmap />
|
||||||
</section>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } 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, BarChart3 } from "lucide-react"
|
import { ChevronDown, ChevronRight, CheckCircle2, Circle, Clock, Activity, BarChart3, Lock } from "lucide-react"
|
||||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||||
import { PremiumGate } from "@/components/PremiumGate"
|
import { PremiumGate } from "@/components/PremiumGate"
|
||||||
|
|
||||||
@@ -134,17 +134,8 @@ const ZERO_SUBJECTS: Subject[] = [
|
|||||||
// Math/Bio dynamic subjects handled below
|
// Math/Bio dynamic subjects handled below
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function ProgressTrackerPage() {
|
|
||||||
const { userId, user } = useActiveUser()
|
|
||||||
const isDemo = isDemoUser(userId)
|
|
||||||
const [subjects, setSubjects] = useState<Subject[]>([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
const mathDemoSubject: Subject = {
|
||||||
const targetExam = user?.target_exam || "JEE_Main";
|
|
||||||
const isNeet = targetExam === "NEET";
|
|
||||||
const isJeeNeet = targetExam === "JEE_NEET";
|
|
||||||
|
|
||||||
const mathDemoSubject: Subject = {
|
|
||||||
name: "Mathematics",
|
name: "Mathematics",
|
||||||
completionPct: 28,
|
completionPct: 28,
|
||||||
chapters: [
|
chapters: [
|
||||||
@@ -238,21 +229,67 @@ export default function ProgressTrackerPage() {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const base = isDemo ? [...DEMO_SUBJECTS] : [...ZERO_SUBJECTS];
|
function getInitialSubjects(isDemoUser: boolean, targetExam: string): Subject[] {
|
||||||
|
const isNeet = targetExam === "NEET";
|
||||||
if (isJeeNeet) {
|
const isJeeNeet = targetExam === "JEE_NEET";
|
||||||
// 4 Subjects for JEE + NEET: Physics, Chemistry, Mathematics, Biology
|
const base = isDemoUser ? [...DEMO_SUBJECTS] : [...ZERO_SUBJECTS];
|
||||||
base.push(isDemo ? mathDemoSubject : mathZeroSubject);
|
if (isJeeNeet) {
|
||||||
base.push(isDemo ? bioDemoSubject : bioZeroSubject);
|
base.push(isDemoUser ? mathDemoSubject : mathZeroSubject);
|
||||||
} else if (isNeet) {
|
base.push(isDemoUser ? bioDemoSubject : bioZeroSubject);
|
||||||
base.push(isDemo ? bioDemoSubject : bioZeroSubject);
|
} else if (isNeet) {
|
||||||
} else {
|
base.push(isDemoUser ? bioDemoSubject : bioZeroSubject);
|
||||||
base.push(isDemo ? mathDemoSubject : mathZeroSubject);
|
} else {
|
||||||
}
|
base.push(isDemoUser ? mathDemoSubject : mathZeroSubject);
|
||||||
|
}
|
||||||
setSubjects(base);
|
return base;
|
||||||
|
}
|
||||||
|
export default function ProgressTrackerPage() {
|
||||||
|
const { userId, user, hasAccessTo } = useActiveUser();
|
||||||
|
const isDemo = isDemoUser(userId);
|
||||||
|
const [subjects, setSubjects] = useState<Subject[]>(() =>
|
||||||
|
getInitialSubjects(isDemoUser(userId), user?.target_exam || "JEE_Main")
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSubjects(getInitialSubjects(isDemo, user?.target_exam || "JEE_Main"));
|
||||||
}, [isDemo, user?.target_exam]);
|
}, [isDemo, user?.target_exam]);
|
||||||
|
|
||||||
|
if (!hasAccessTo("chapter-wise-progress-tracking") && !isDemo) {
|
||||||
|
return (
|
||||||
|
<div className="p-8 space-y-8 max-w-7xl mx-auto">
|
||||||
|
<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)]">
|
||||||
|
<BarChart3 className="text-yellow-600 dark:text-[var(--cyan-ai)]" size={28} />
|
||||||
|
</span>
|
||||||
|
Syllabus Tracker
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground text-base font-medium max-w-2xl pl-1">
|
||||||
|
Track your syllabus coverage and mastery per chapter to ensure no gaps.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-12 text-center max-w-2xl mx-auto bg-secondary/20 border border-border rounded-3xl space-y-6 animate-section animate-section-2">
|
||||||
|
<div className="size-20 rounded-full bg-red-500/10 flex items-center justify-center mx-auto text-red-500">
|
||||||
|
<Lock size={40} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold">Premium Feature</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Chapter-wise syllabus tracking is available on Pro and AI Coach plans.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="px-8 py-3 bg-[var(--cyan-ai)] text-black font-bold rounded-xl shadow-[0_0_20px_-5px_var(--cyan-ai)]"
|
||||||
|
onClick={() => window.location.href = '/upgrade'}
|
||||||
|
>
|
||||||
|
Upgrade Plan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const toggleChapter = (subjectIndex: number, chapterIndex: number) => {
|
const toggleChapter = (subjectIndex: number, chapterIndex: number) => {
|
||||||
const newSubjects = [...subjects]
|
const newSubjects = [...subjects]
|
||||||
newSubjects[subjectIndex].chapters[chapterIndex].isExpanded = !newSubjects[subjectIndex].chapters[chapterIndex].isExpanded
|
newSubjects[subjectIndex].chapters[chapterIndex].isExpanded = !newSubjects[subjectIndex].chapters[chapterIndex].isExpanded
|
||||||
@@ -260,7 +297,7 @@ export default function ProgressTrackerPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PremiumGate featureKey="chapter-wise-progress-tracking">
|
|
||||||
<div className="p-8 space-y-8 max-w-7xl mx-auto">
|
<div className="p-8 space-y-8 max-w-7xl mx-auto">
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4 animate-section animate-section-1">
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4 animate-section animate-section-1">
|
||||||
<div>
|
<div>
|
||||||
@@ -351,6 +388,6 @@ export default function ProgressTrackerPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PremiumGate>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } 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"
|
||||||
@@ -32,14 +32,10 @@ const NEW_USER_PLAN: PlanItem[] = [
|
|||||||
export function AdaptiveStudyPlan() {
|
export function AdaptiveStudyPlan() {
|
||||||
const { user } = useActiveUser();
|
const { user } = useActiveUser();
|
||||||
const isDemo = isDemoUser(user?.id);
|
const isDemo = isDemoUser(user?.id);
|
||||||
const [items, setItems] = useState<PlanItem[]>(DEMO_PLAN);
|
const [items, setItems] = useState<PlanItem[]>(() => isDemo ? DEMO_PLAN : NEW_USER_PLAN);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDemo) {
|
setItems(isDemo ? DEMO_PLAN : NEW_USER_PLAN);
|
||||||
setItems(DEMO_PLAN);
|
|
||||||
} else {
|
|
||||||
setItems(NEW_USER_PLAN);
|
|
||||||
}
|
|
||||||
}, [isDemo]);
|
}, [isDemo]);
|
||||||
|
|
||||||
const updateStatus = (id: string, newStatus: PlanState) => {
|
const updateStatus = (id: string, newStatus: PlanState) => {
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ const ZERO_HEATMAP_DATA = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function MistakeHeatmap() {
|
export function MistakeHeatmap() {
|
||||||
const { userId } = useActiveUser();
|
const { userId, user } = useActiveUser();
|
||||||
const isDemo = isDemoUser(userId);
|
const isDemo = isDemoUser(user?.id || userId);
|
||||||
|
|
||||||
const getColor = (errorRate: number) => {
|
const getColor = (errorRate: number) => {
|
||||||
if (errorRate === 0) return 'bg-green-600/50 border-green-600/80 text-green-100';
|
if (errorRate === 0) return 'bg-green-600/50 border-green-600/80 text-green-100';
|
||||||
|
|||||||
@@ -13,14 +13,10 @@ interface PremiumGateProps {
|
|||||||
export function PremiumGate({ featureKey, children }: PremiumGateProps) {
|
export function PremiumGate({ featureKey, children }: PremiumGateProps) {
|
||||||
const { hasAccessTo, loading, user } = useActiveUser();
|
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) {
|
if (loading || !user) {
|
||||||
return (
|
return <>{children}</>;
|
||||||
<div className="flex-1 min-h-[400px] flex items-center justify-center">
|
|
||||||
<div className="flex flex-col items-center gap-4">
|
|
||||||
<div className="w-10 h-10 border-4 border-[var(--cyan-ai-20)] border-t-[var(--cyan-ai)] rounded-full animate-spin"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasAccessTo(featureKey)) {
|
if (!hasAccessTo(featureKey)) {
|
||||||
|
|||||||
@@ -147,6 +147,88 @@ async function fetchSettings(): Promise<any> {
|
|||||||
return cachedSettings ?? null;
|
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() {
|
export function useActiveUser() {
|
||||||
const [userId, setUserIdState] = useState<string>(() => {
|
const [userId, setUserIdState] = useState<string>(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
@@ -155,9 +237,32 @@ export function useActiveUser() {
|
|||||||
return DEFAULT_USER_ID;
|
return DEFAULT_USER_ID;
|
||||||
});
|
});
|
||||||
|
|
||||||
const [user, setUser] = useState<UserProfile | null>(null);
|
const [user, setUser] = useState<UserProfile | null>(() => {
|
||||||
const [globalSettings, setGlobalSettings] = useState<any>(null);
|
const currentId = (typeof window !== "undefined" ? localStorage.getItem("activeUserId") : null) || DEFAULT_USER_ID;
|
||||||
const [loading, setLoading] = useState(true);
|
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<any>(() => cachedSettings);
|
||||||
|
const [loading, setLoading] = useState(() => !cachedUserProfile);
|
||||||
const [switching, setSwitching] = useState(false);
|
const [switching, setSwitching] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
// Use a ref for fail count so it doesn't cause re-renders
|
// 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);
|
setUser(data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
failCountRef.current += 1;
|
failCountRef.current += 1;
|
||||||
if (!isPoll) {
|
if (!isPoll) {
|
||||||
console.warn("Backend unreachable, using local demo profile for offline testing:", err.message);
|
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
|
const fallback = getKnownDemoProfile(id) || {
|
||||||
setUser((prev) => prev || {
|
|
||||||
id: id || DEFAULT_USER_ID,
|
id: id || DEFAULT_USER_ID,
|
||||||
name: "Aarav",
|
name: "Aarav",
|
||||||
email: "demo@pragativeer.com",
|
email: "demo@pragativeer.com",
|
||||||
@@ -228,7 +336,9 @@ export function useActiveUser() {
|
|||||||
longest_streak: 21,
|
longest_streak: 21,
|
||||||
created_at: new Date(Date.now() - 5 * 86400000).toISOString(),
|
created_at: new Date(Date.now() - 5 * 86400000).toISOString(),
|
||||||
trial_end: new Date(Date.now() + 10 * 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
|
// Silently swallow polling errors so the UI doesn't crash
|
||||||
} finally {
|
} finally {
|
||||||
if (!isPoll) setLoading(false);
|
if (!isPoll) setLoading(false);
|
||||||
@@ -290,11 +400,13 @@ export function useActiveUser() {
|
|||||||
// Update refs and state before awaiting network
|
// Update refs and state before awaiting network
|
||||||
activeUserIdRef.current = actualUserId;
|
activeUserIdRef.current = actualUserId;
|
||||||
setUserIdState(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
|
// Notify listeners immediately of account switch
|
||||||
if (typeof window !== "undefined") {
|
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
|
// Fetch the new user's profile immediately using the new token
|
||||||
@@ -302,6 +414,10 @@ export function useActiveUser() {
|
|||||||
|
|
||||||
if (newUserRes && newUserRes.ok) {
|
if (newUserRes && newUserRes.ok) {
|
||||||
const data = await newUserRes.json();
|
const data = await newUserRes.json();
|
||||||
|
cachedUserProfile = data;
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
try { localStorage.setItem("pragativeer_cached_user", JSON.stringify(data)); } catch {}
|
||||||
|
}
|
||||||
setUser(data);
|
setUser(data);
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.dispatchEvent(new CustomEvent("pragativeer-user-switched", { detail: { userId: actualUserId, user: data } }));
|
window.dispatchEvent(new CustomEvent("pragativeer-user-switched", { detail: { userId: actualUserId, user: data } }));
|
||||||
|
|||||||
Reference in New Issue
Block a user