Implement active user tiers, class-based Tailwind theme switcher, 3-tiled static dashboard layout, Minio object storage integration, school & state settings database sync, and streak wishes onboarding modal

This commit is contained in:
2026-07-07 13:43:30 +05:30
parent 426b98ddab
commit 04db17ea0e
24 changed files with 913 additions and 236 deletions

View File

@@ -1,7 +1,7 @@
"use client"
import { ReactNode, useState, useEffect, useRef } from "react"
import { BookOpen, LayoutDashboard, Settings, FileText, Activity, LineChart, Trophy, LogOut, User, ChevronUp } from "lucide-react"
import { BookOpen, LayoutDashboard, Settings, FileText, Activity, LineChart, Trophy, LogOut, User, ChevronUp, Lock } from "lucide-react"
import Link from "next/link"
import Image from "next/image"
import { usePathname } from "next/navigation"
@@ -10,11 +10,16 @@ import { ThemeToggle } from "@/components/ThemeToggle"
import { TierBadge } from "@/components/TierBadge"
import { StreakCounter } from "@/components/StreakCounter"
import { AITutorChat } from "@/components/AI/AITutorChat"
import { useActiveUser, DEMO_USERS } from "@/hooks/useActiveUser"
import { StreakOnboardingModal } from "@/components/Dashboard/StreakOnboardingModal"
import { Badge } from "@/components/ui/badge"
export default function DashboardLayout({ children }: { children: ReactNode }) {
const pathname = usePathname()
const [dropdownOpen, setDropdownOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const { userId, user, switchUser, hasAccessTo, loading } = useActiveUser();
// Close dropdown when clicking outside
useEffect(() => {
@@ -27,6 +32,9 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
return () => document.removeEventListener("mousedown", handleClickOutside)
}, [])
const userInitials = user ? user.name.split(" ").map(n => n[0]).join("").toUpperCase() : "DS"
const targetExamLabel = user?.target_exam === "JEE_Main" ? "JEE Main" : user?.target_exam === "JEE_Advanced" ? "JEE Advanced" : user?.target_exam === "NEET" ? "NEET" : "NEET/JEE"
return (
<div className="flex h-screen bg-background overflow-hidden text-foreground selection:bg-primary/20">
{/* Sidebar */}
@@ -51,7 +59,13 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
</div>
<nav className="flex-1 px-4 space-y-1.5 mt-4 overflow-y-auto">
<NavItem href="/dashboard" icon={<LayoutDashboard size={18} />} label="Overview" active={pathname === "/dashboard"} />
<NavItem href="/dashboard/cbt" icon={<Activity size={18} />} label="Exams (CBT)" active={pathname.startsWith("/dashboard/cbt")} />
<NavItem
href="/dashboard/cbt"
icon={<Activity size={18} />}
label="Exams (CBT)"
active={pathname.startsWith("/dashboard/cbt")}
isLocked={!hasAccessTo("weekly-test-series")}
/>
<NavItem href="/dashboard/dpp" icon={<FileText size={18} />} label="Daily Practice" active={pathname.startsWith("/dashboard/dpp")} />
<NavItem href="/dashboard/materials" icon={<BookOpen size={18} />} label="Study Materials" active={pathname.startsWith("/dashboard/materials")} />
@@ -65,17 +79,30 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
<div className="pt-4 pb-2">
<p className="px-3 text-xs font-bold uppercase tracking-widest text-muted-foreground">AI Intelligence</p>
</div>
<NavItem href="/dashboard/analytics" icon={<LineChart size={18} />} label="Performance Analytics" active={pathname.startsWith("/dashboard/analytics")} />
<NavItem href="/dashboard/rank-predictor" icon={<Trophy size={18} />} label="Rank Predictor" active={pathname.startsWith("/dashboard/rank-predictor")} />
<NavItem
href="/dashboard/analytics"
icon={<LineChart size={18} />}
label="Performance Analytics"
active={pathname.startsWith("/dashboard/analytics")}
isLocked={!hasAccessTo("ai-mistake-tracking")}
/>
<NavItem
href="/dashboard/rank-predictor"
icon={<Trophy size={18} />}
label="Rank Predictor"
active={pathname.startsWith("/dashboard/rank-predictor")}
isLocked={!hasAccessTo("ai-mistake-tracking")}
/>
<NavItem
href="/dashboard/ai-coach"
icon={<BrainCircuitIcon />}
label="AI Coach"
active={pathname.startsWith("/dashboard/ai-coach")}
isLocked={!hasAccessTo("ai-mistake-tracking")}
/>
</nav>
<div className="p-4 border-t border-border space-y-2 bg-secondary/30">
<div className="flex justify-end mb-2 px-1">
<Link href="/dashboard/streaks" className="w-full text-right hover:opacity-80 transition-opacity">
<StreakCounter streak={12} />
</Link>
</div>
<Link
href="/dashboard/settings"
className={`flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-300 border ${
@@ -90,19 +117,22 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
{/* Functional User Dropdown */}
<div className="relative mt-2" ref={dropdownRef}>
{dropdownOpen && (
{dropdownOpen && user && (
<div className="absolute bottom-full left-0 w-full mb-2 bg-background border border-border rounded-xl shadow-lg shadow-black/10 overflow-hidden animate-in fade-in slide-in-from-bottom-2 duration-200 z-50">
<div className="px-4 py-3 border-b border-border">
<p className="text-sm font-bold text-foreground">Demo Student</p>
<p className="text-xs text-muted-foreground truncate">demo@pragativeer.com</p>
<p className="text-sm font-bold text-foreground truncate">{user.name}</p>
<p className="text-xs text-muted-foreground truncate">{user.email || user.phone}</p>
</div>
<div className="p-1">
<Link href="/dashboard/settings" className="flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-secondary rounded-lg transition-colors">
<User size={16} /> Edit Profile
</Link>
<Link href="/" className="flex items-center gap-2 px-3 py-2 text-sm text-red-500 hover:bg-red-500/10 rounded-lg transition-colors font-medium mt-1">
<LogOut size={16} /> Log out
</Link>
<button
onClick={() => switchUser(DEMO_USERS.AI_COACH.id)}
className="w-full text-left flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-secondary rounded-lg transition-colors"
>
Reset Demo
</button>
</div>
</div>
)}
@@ -111,50 +141,111 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
onClick={() => setDropdownOpen(!dropdownOpen)}
className="w-full flex items-center justify-between p-2 rounded-xl bg-background/50 hover:bg-background transition-colors border border-border/50 shadow-sm mt-3"
>
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-full bg-[#00F0FF]/20 text-[#00F0FF] flex items-center justify-center font-bold text-sm shadow-[0_0_10px_-2px_#00F0FF]">
DS
<div className="flex items-center gap-3 text-left">
<div className="h-8 w-8 rounded-full bg-[#00F0FF]/25 text-[#00F0FF] flex items-center justify-center font-bold text-sm shadow-[0_0_10px_-2px_#00F0FF] shrink-0">
{userInitials}
</div>
<div className="text-left flex-1 min-w-0">
<p className="text-sm font-bold leading-none truncate">Demo Student</p>
<div className="mt-1.5"><TierBadge tier="AI Coach" /></div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold leading-none truncate">{user ? user.name.split(" ")[0] : "Loading..."}</p>
<div className="mt-1"><TierBadge tier={user ? (user.tier === "Al_Coach" ? "AI Coach" : user.tier) : "Free"} /></div>
</div>
</div>
<ChevronUp size={16} className={`text-muted-foreground transition-transform ${dropdownOpen ? "rotate-180" : ""}`} />
<ChevronUp size={16} className={`text-muted-foreground transition-transform shrink-0 ${dropdownOpen ? "rotate-180" : ""}`} />
</button>
</div>
</div>
</aside>
{/* Main Content */}
<div className="flex-1 flex flex-col h-screen overflow-hidden relative">
<header className="absolute top-6 right-8 z-10 flex items-center gap-3 bg-background/80 backdrop-blur-sm p-1.5 rounded-2xl border border-border/50 shadow-sm">
<LanguageToggle />
<ThemeToggle />
{/* Main Content Area */}
<div className="flex-1 flex flex-col h-screen overflow-hidden">
{/* Top Header Static Bar */}
<header className="h-16 border-b border-border bg-background/95 backdrop-blur-md flex items-center justify-between px-8 shrink-0 z-40">
<div className="flex items-center gap-4">
<h2 className="font-bold text-lg hidden sm:block">
{user ? `Welcome back, ${user.name.split(" ")[0]}! 👋` : "Welcome back!"}
</h2>
{user && (
<Badge variant="outline" className="bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20 font-bold tracking-wide uppercase px-2.5 py-0.5 animate-pulse">
{targetExamLabel} Target
</Badge>
)}
</div>
<div className="flex items-center gap-4">
{/* Streak count moved to top header */}
{user && (
<Link href="/dashboard/streaks" className="hover:opacity-85 transition-opacity flex items-center gap-1.5 bg-secondary/50 border border-border px-3 py-1 rounded-xl">
<StreakCounter streak={user.streak_count} />
</Link>
)}
{/* Profile switcher dropdown for testing */}
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground font-bold hidden lg:inline">Demo Switcher:</span>
<select
value={userId}
onChange={(e) => switchUser(e.target.value)}
className="bg-secondary border border-border text-foreground px-2.5 py-1.5 rounded-xl text-xs font-bold focus:ring-2 focus:ring-[#00F0FF] outline-none cursor-pointer hover:bg-secondary/80 transition-colors"
>
<option value={DEMO_USERS.AI_COACH.id}>Rahul (AI Coach)</option>
<option value={DEMO_USERS.PRO.id}>Priya (Pro Plan)</option>
<option value={DEMO_USERS.FREE.id}>Amit (Free Plan)</option>
</select>
</div>
<div className="flex items-center gap-2 border-l border-border pl-4">
<LanguageToggle />
<ThemeToggle />
</div>
</div>
</header>
<main className="flex-1 overflow-y-auto">
{/* Scrollable page body */}
<main className="flex-1 overflow-y-auto relative bg-background/50">
{children}
</main>
</div>
{/* Global AI Tutor Chat */}
<AITutorChat />
{hasAccessTo("ai-tutor-chat") && <AITutorChat />}
{/* Onboarding Motivation Popup */}
<StreakOnboardingModal />
</div>
)
}
function NavItem({ href, icon, label, active }: { href: string, icon: React.ReactNode, label: string, active?: boolean }) {
function NavItem({ href, icon, label, active, isLocked }: { href: string, icon: React.ReactNode, label: string, active?: boolean, isLocked?: boolean }) {
return (
<Link
href={href}
className={`flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-300 ${
href={isLocked ? "#" : href}
className={`flex items-center justify-between px-3 py-2.5 rounded-xl transition-all duration-300 ${
active
? "bg-secondary text-foreground font-medium border border-border"
: "text-muted-foreground hover:bg-secondary/50 hover:text-foreground border border-transparent"
? "bg-secondary text-foreground font-semibold border border-border"
: isLocked
? "text-muted-foreground/60 cursor-not-allowed border border-transparent"
: "text-muted-foreground hover:bg-secondary/50 hover:text-foreground border border-transparent"
}`}
>
{icon}
<span className="text-sm">{label}</span>
<div className="flex items-center gap-3">
{icon}
<span className="text-sm">{label}</span>
</div>
{isLocked && <Lock size={12} className="text-muted-foreground/50" />}
</Link>
)
}
function BrainCircuitIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-brain-circuit">
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
<path d="M18 8a3 3 0 0 0-3-3h-1"/>
<path d="M9 5H8a3 3 0 0 0-3 3v1"/>
<path d="M5 14v1a3 3 0 0 0 3 3h1"/>
<path d="M14 18h1a3 3 0 0 0 3-3v-1"/>
<path d="M12 18v2"/>
<path d="M12 4V2"/>
</svg>
)
}