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:
@@ -1,4 +1,4 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
|
||||
@@ -11,20 +11,22 @@ import { ProgressShare } from "@/components/AI/ProgressShare"
|
||||
import { MistakeReviewCard, MistakePayload } from "@/components/AI/MistakeReviewCard"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { BrainCircuit, BookOpen, Calculator, Target, RefreshCw, CheckCircle2 } from "lucide-react"
|
||||
|
||||
// Hardcode mock UUID for demo fetching
|
||||
const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b"
|
||||
import { BrainCircuit, BookOpen, Calculator, Target, RefreshCw, CheckCircle2, Lock } from "lucide-react"
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function AICoachDashboard() {
|
||||
const [mistakes, setMistakes] = useState<MistakePayload[]>([])
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const { userId, user, hasAccessTo, loading: userLoading } = useActiveUser();
|
||||
|
||||
const fetchMistakes = async () => {
|
||||
if (!userId) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch(`http://localhost:8000/api/mistakes/today?user_id=${USER_ID}`)
|
||||
const res = await fetch(`/api/mistakes/today?user_id=${userId}`)
|
||||
if (!res.ok) throw new Error("Failed to fetch")
|
||||
const data = await res.json()
|
||||
setMistakes(data)
|
||||
@@ -38,11 +40,13 @@ export default function AICoachDashboard() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchMistakes()
|
||||
}, [])
|
||||
if (!userLoading && userId) {
|
||||
fetchMistakes()
|
||||
}
|
||||
}, [userId, userLoading])
|
||||
|
||||
const submitReview = async (mistakeId: string, selectedAnswer: string) => {
|
||||
const res = await fetch("http://localhost:8000/api/mistakes/review", {
|
||||
const res = await fetch("/api/mistakes/review", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ mistake_id: mistakeId, selected_answer: selectedAnswer })
|
||||
@@ -61,6 +65,33 @@ export default function AICoachDashboard() {
|
||||
}
|
||||
}
|
||||
|
||||
if (userLoading) {
|
||||
return <div className="p-8 text-center text-muted-foreground animate-pulse">Loading AI Coach...</div>
|
||||
}
|
||||
|
||||
if (!hasAccessTo("ai-mistake-tracking")) {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto text-center py-20 px-8 space-y-8 animate-in fade-in duration-500">
|
||||
<div className="size-20 rounded-full bg-[#00F0FF]/10 flex items-center justify-center mx-auto text-[#00F0FF] border border-[#00F0FF]/20 shadow-[0_0_20px_rgba(0,240,255,0.1)]">
|
||||
<BrainCircuit size={40} />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-4xl font-extrabold tracking-tight">AI Coach Workspace</h1>
|
||||
<p className="text-muted-foreground text-lg max-w-xl mx-auto leading-relaxed">
|
||||
Mistake tracking, Spaced Repetition (SM-2), AI Tutor Chats, and Readiness scores are premium features reserved for **AI Coach** subscribers.
|
||||
</p>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<Link href="/upgrade">
|
||||
<Button size="lg" className="rounded-xl px-8 font-bold bg-[#00F0FF] text-background hover:bg-[#00F0FF]/90 shadow-[0_0_25px_-5px_#00F0FF]">
|
||||
Upgrade to AI Coach Plan
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-6xl mx-auto space-y-10 animate-in fade-in duration-500">
|
||||
<header className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
|
||||
@@ -5,7 +5,8 @@ import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Activity, Clock, PlayCircle, Search } from "lucide-react";
|
||||
import { Activity, Clock, PlayCircle, Search, Lock } from "lucide-react";
|
||||
import { useActiveUser } from "@/hooks/useActiveUser";
|
||||
|
||||
interface TestData {
|
||||
id: string;
|
||||
@@ -22,27 +23,31 @@ export default function CBTPage() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [targetExam, setTargetExam] = useState("");
|
||||
const [attempts, setAttempts] = useState<any[]>([]);
|
||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
||||
|
||||
const userId = "b0e17378-864d-439e-8806-708dc48a891b";
|
||||
const { userId, user, hasAccessTo, loading: userLoading } = useActiveUser();
|
||||
|
||||
useEffect(() => {
|
||||
if (userLoading || !userId) return;
|
||||
|
||||
async function loadCBTExams() {
|
||||
try {
|
||||
// 1. Fetch user target exam
|
||||
const userRes = await fetch(`http://localhost:8000/api/users/${userId}`);
|
||||
const userRes = await fetch(`/api/users/${userId}`);
|
||||
if (!userRes.ok) throw new Error("Failed to load user target exam");
|
||||
const userData = await userRes.json();
|
||||
const pattern = userData.target_exam;
|
||||
setTargetExam(pattern === "NEET" ? "NEET" : "JEE Main");
|
||||
|
||||
// 2. Fetch tests filtered by exam
|
||||
const testRes = await fetch(`http://localhost:8000/api/tests?exam_pattern=${pattern}`);
|
||||
if (!testRes.ok) throw new Error("Failed to fetch tests");
|
||||
const testsData = await testRes.json();
|
||||
setExams(testsData);
|
||||
const testRes = await fetch(`/api/tests?exam_pattern=${pattern}&user_id=${userId}`);
|
||||
if (testRes.ok) {
|
||||
const testsData = await testRes.json();
|
||||
setExams(testsData);
|
||||
}
|
||||
|
||||
// 3. Fetch user attempts
|
||||
const attemptsRes = await fetch(`http://localhost:8000/api/users/${userId}/attempts`);
|
||||
const attemptsRes = await fetch(`/api/users/${userId}/attempts`);
|
||||
if (attemptsRes.ok) {
|
||||
const attemptsData = await attemptsRes.json();
|
||||
setAttempts(attemptsData);
|
||||
@@ -54,7 +59,7 @@ export default function CBTPage() {
|
||||
}
|
||||
}
|
||||
loadCBTExams();
|
||||
}, []);
|
||||
}, [userId, userLoading]);
|
||||
|
||||
const filteredExams = exams.filter(exam =>
|
||||
exam.title.toLowerCase().includes(search.toLowerCase())
|
||||
@@ -109,19 +114,24 @@ export default function CBTPage() {
|
||||
{filteredExams.map((exam) => {
|
||||
const attempt = attemptsByTest[exam.id];
|
||||
const isAttempted = !!attempt;
|
||||
const isLocked = !hasAccessTo("weekly-test-series");
|
||||
|
||||
return (
|
||||
<Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isAttempted ? 'border-primary/20' : ''}`}>
|
||||
<Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isAttempted ? 'border-primary/20' : ''} ${isLocked ? 'opacity-80' : ''}`}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<Badge variant="outline" className="bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20">
|
||||
{exam.type} Test
|
||||
</Badge>
|
||||
{isAttempted && (
|
||||
{isLocked ? (
|
||||
<Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20 flex items-center gap-1">
|
||||
<Lock size={12} /> Premium
|
||||
</Badge>
|
||||
) : isAttempted ? (
|
||||
<Badge variant="secondary" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
Attempted
|
||||
</Badge>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
<h3 className="font-bold text-lg leading-tight mb-2 group-hover:text-[#00F0FF] transition-colors">
|
||||
{exam.title}
|
||||
@@ -130,11 +140,20 @@ export default function CBTPage() {
|
||||
<div className="font-medium text-foreground">Available Now</div>
|
||||
<div>{exam.total_time} Min • {exam.difficulty} Difficulty</div>
|
||||
</div>
|
||||
<Link href={`/cbt/${exam.id}`}>
|
||||
<Button className={`w-full gap-2 rounded-xl transition-colors font-bold ${isAttempted ? 'bg-secondary border border-border text-foreground' : 'bg-secondary text-foreground hover:bg-[#00F0FF] hover:text-background'}`}>
|
||||
<PlayCircle size={16} /> {isAttempted ? 'Retake Exam' : 'Start Exam'}
|
||||
{isLocked ? (
|
||||
<Button
|
||||
onClick={() => setShowUpgradeModal(true)}
|
||||
className="w-full gap-2 rounded-xl bg-secondary text-muted-foreground border border-dashed border-border hover:border-[#00F0FF]/50 hover:text-foreground font-bold"
|
||||
>
|
||||
<Lock size={16} /> Unlock Test
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={`/cbt/${exam.id}`}>
|
||||
<Button className={`w-full gap-2 rounded-xl transition-colors font-bold ${isAttempted ? 'bg-secondary border border-border text-foreground' : 'bg-secondary text-foreground hover:bg-[#00F0FF] hover:text-background'}`}>
|
||||
<PlayCircle size={16} /> {isAttempted ? 'Retake Exam' : 'Start Exam'}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)})}
|
||||
@@ -185,6 +204,30 @@ export default function CBTPage() {
|
||||
)})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showUpgradeModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
|
||||
<div className="bg-background border border-border shadow-2xl rounded-2xl w-[450px] p-8 text-center space-y-6 animate-in zoom-in-95 duration-200">
|
||||
<div className="size-16 rounded-full bg-red-500/10 flex items-center justify-center mx-auto text-red-500">
|
||||
<Lock size={32} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold">Upgrade to Premium Plan</h3>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
This CBT Mock Test is a premium feature. Free users get access to 1 Daily Practice Sheet and 5 Topic-wise Micro-tests. Upgrade to Pro or AI Coach to unlock unlimited practice and full-length weekly test series!
|
||||
</p>
|
||||
<div className="flex gap-4 pt-2">
|
||||
<Button variant="outline" className="flex-1 rounded-xl" onClick={() => setShowUpgradeModal(false)}>
|
||||
Maybe Later
|
||||
</Button>
|
||||
<Link href="/upgrade" className="flex-1">
|
||||
<Button className="w-full rounded-xl bg-primary text-primary-foreground font-bold shadow-[0_0_15px_-3px_var(--color-primary)]">
|
||||
View Plans
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,53 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useActiveUser } from "@/hooks/useActiveUser";
|
||||
import { User as UserIcon, Upload, CheckCircle2 } from "lucide-react";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [targetExam, setTargetExam] = useState<"JEE_Main" | "NEET">("JEE_Main");
|
||||
const [targetYear, setTargetYear] = useState("2026");
|
||||
const [firstName, setFirstName] = useState("Rahul");
|
||||
const [lastName, setLastName] = useState("Sharma");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { userId, user, refreshUser, loading: userLoading } = useActiveUser();
|
||||
const [targetExam, setTargetExam] = useState<"JEE_Main" | "JEE_Advanced" | "NEET">("JEE_Main");
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [school, setSchool] = useState("");
|
||||
const [state, setState] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
|
||||
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const userId = "b0e17378-864d-439e-8806-708dc48a891b";
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchUserData() {
|
||||
try {
|
||||
const res = await fetch(`http://localhost:8000/api/users/${userId}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setTargetExam(data.target_exam === "NEET" ? "NEET" : "JEE_Main");
|
||||
if (data.name) {
|
||||
const parts = data.name.split(" ");
|
||||
setFirstName(parts[0] || "");
|
||||
setLastName(parts.slice(1).join(" ") || "");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load user profile:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
if (userLoading || !user) return;
|
||||
|
||||
setTargetExam(user.target_exam);
|
||||
setSchool(user.school || "");
|
||||
setState(user.state || "");
|
||||
setEmail(user.email || "");
|
||||
setPhone(user.phone || "");
|
||||
setAvatarUrl(localStorage.getItem(`user_avatar_${user.id}`) || null);
|
||||
|
||||
if (user.name) {
|
||||
const parts = user.name.split(" ");
|
||||
setFirstName(parts[0] || "");
|
||||
setLastName(parts.slice(1).join(" ") || "");
|
||||
}
|
||||
fetchUserData();
|
||||
}, []);
|
||||
setLoading(false);
|
||||
}, [user, userLoading]);
|
||||
|
||||
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file || !userId) return;
|
||||
|
||||
setUploading(true);
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/users/${userId}/upload-avatar`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Upload failed");
|
||||
const data = await res.json();
|
||||
setAvatarUrl(data.url);
|
||||
localStorage.setItem(`user_avatar_${userId}`, data.url);
|
||||
alert("Profile picture uploaded successfully to Minio!");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to upload profile picture.");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!userId) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch(`http://localhost:8000/api/users/${userId}`, {
|
||||
const res = await fetch(`/api/users/${userId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: `${firstName} ${lastName}`.trim(),
|
||||
target_exam: targetExam,
|
||||
school: school.trim(),
|
||||
state: state.trim(),
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
await refreshUser();
|
||||
alert("Settings saved successfully!");
|
||||
} else {
|
||||
alert("Failed to save settings: " + (await res.text()));
|
||||
@@ -60,117 +92,166 @@ export default function SettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-8">Loading settings...</div>;
|
||||
if (loading || userLoading) {
|
||||
return <div className="p-8 text-center text-muted-foreground animate-pulse">Loading settings...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-4xl mx-auto">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Account Settings</h1>
|
||||
<p className="text-muted-foreground mt-1 font-medium">Manage your personal details and platform preferences.</p>
|
||||
<p className="text-muted-foreground mt-1 font-medium">Manage your personal details, school details, and platform preferences.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card className="border-border bg-background shadow-sm">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{/* Profile photo upload tile */}
|
||||
<Card className="border-border bg-background shadow-sm md:col-span-1 h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle>Profile Details</CardTitle>
|
||||
<CardDescription>Update your personal information.</CardDescription>
|
||||
<CardTitle>Profile Photo</CardTitle>
|
||||
<CardDescription>Upload photo to Minio storage.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#00F0FF]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#00F0FF]"
|
||||
/>
|
||||
</div>
|
||||
<CardContent className="flex flex-col items-center space-y-4">
|
||||
<div className="relative size-32 rounded-full border border-border bg-secondary/50 flex items-center justify-center overflow-hidden">
|
||||
{avatarUrl ? (
|
||||
<img src={avatarUrl} alt="Avatar" className="size-full object-cover" />
|
||||
) : (
|
||||
<UserIcon className="size-16 text-muted-foreground" />
|
||||
)}
|
||||
{uploading && (
|
||||
<div className="absolute inset-0 bg-black/50 flex items-center justify-center text-white text-xs font-bold">
|
||||
Uploading...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold">Email Address</label>
|
||||
<input type="email" defaultValue="student@pragativeer.com" disabled className="w-full bg-secondary border border-border rounded-lg px-4 py-2 text-sm text-muted-foreground cursor-not-allowed" />
|
||||
</div>
|
||||
<Button onClick={handleSave} disabled={saving} className="bg-foreground text-background hover:bg-foreground/90 font-bold mt-2">
|
||||
{saving ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
|
||||
<label className="w-full">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleAvatarUpload}
|
||||
className="hidden"
|
||||
disabled={uploading}
|
||||
/>
|
||||
<Button type="button" variant="outline" className="w-full gap-2 rounded-xl border-dashed" asChild>
|
||||
<span>
|
||||
<Upload size={16} /> {avatarUrl ? "Change Photo" : "Upload Photo"}
|
||||
</span>
|
||||
</Button>
|
||||
</label>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-border bg-background shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Academic Preferences</CardTitle>
|
||||
<CardDescription>Tailor the AI to your specific exam targets.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-bold">Target Examination</label>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTargetExam("NEET")}
|
||||
className={`flex items-center gap-2 cursor-pointer p-4 border rounded-xl flex-1 text-left transition-all ${
|
||||
targetExam === "NEET"
|
||||
? "border-[#00F0FF] bg-[#00F0FF]/5"
|
||||
: "border-border bg-secondary/20 hover:bg-secondary/50"
|
||||
}`}
|
||||
>
|
||||
{/* Profile settings fields tile */}
|
||||
<div className="md:col-span-2 space-y-6">
|
||||
<Card className="border-border bg-background shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Profile Details</CardTitle>
|
||||
<CardDescription>Update your personal and school information.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold">First Name</label>
|
||||
<input
|
||||
type="radio"
|
||||
name="target"
|
||||
checked={targetExam === "NEET"}
|
||||
onChange={() => {}}
|
||||
className="accent-[#00F0FF] w-4 h-4"
|
||||
type="text"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#00F0FF]"
|
||||
/>
|
||||
<span className="font-bold">NEET (UG)</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTargetExam("JEE_Main")}
|
||||
className={`flex items-center gap-2 cursor-pointer p-4 border rounded-xl flex-1 text-left transition-all ${
|
||||
targetExam === "JEE_Main"
|
||||
? "border-[#00F0FF] bg-[#00F0FF]/5"
|
||||
: "border-border bg-secondary/20 hover:bg-secondary/50"
|
||||
}`}
|
||||
>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold">Last Name</label>
|
||||
<input
|
||||
type="radio"
|
||||
name="target"
|
||||
checked={targetExam === "JEE_Main"}
|
||||
onChange={() => {}}
|
||||
className="accent-[#00F0FF] w-4 h-4"
|
||||
type="text"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#00F0FF]"
|
||||
/>
|
||||
<span className="font-bold">JEE Main & Adv</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold">Target Year</label>
|
||||
<select
|
||||
value={targetYear}
|
||||
onChange={(e) => setTargetYear(e.target.value)}
|
||||
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#00F0FF] appearance-none font-medium"
|
||||
>
|
||||
<option>2026</option>
|
||||
<option>2027</option>
|
||||
</select>
|
||||
</div>
|
||||
<Button onClick={handleSave} disabled={saving} className="bg-foreground text-background hover:bg-foreground/90 font-bold mt-2">
|
||||
{saving ? "Saving..." : "Save Preferences"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold">School Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={school}
|
||||
placeholder="Enter your school name"
|
||||
onChange={(e) => setSchool(e.target.value)}
|
||||
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#00F0FF]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold">State</label>
|
||||
<input
|
||||
type="text"
|
||||
value={state}
|
||||
placeholder="Enter your state (e.g. Rajasthan, Delhi)"
|
||||
onChange={(e) => setState(e.target.value)}
|
||||
className="w-full bg-secondary/50 border border-border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#00F0FF]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold">Email Address</label>
|
||||
<input type="email" value={email || "Not Provided"} disabled className="w-full bg-secondary border border-border rounded-lg px-4 py-2 text-sm text-muted-foreground cursor-not-allowed" />
|
||||
</div>
|
||||
<Button onClick={handleSave} disabled={saving} className="bg-foreground text-background hover:bg-foreground/90 font-bold mt-2 rounded-xl px-6">
|
||||
{saving ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-border bg-background shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Academic Preferences</CardTitle>
|
||||
<CardDescription>Tailor the AI to your specific exam targets.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-bold">Target Examination</label>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTargetExam("NEET")}
|
||||
className={`flex items-center gap-2 cursor-pointer p-4 border rounded-xl flex-1 text-left transition-all ${
|
||||
targetExam === "NEET"
|
||||
? "border-[#00F0FF] bg-[#00F0FF]/5"
|
||||
: "border-border bg-secondary/20 hover:bg-secondary/50"
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="target"
|
||||
checked={targetExam === "NEET"}
|
||||
onChange={() => {}}
|
||||
className="accent-[#00F0FF] w-4 h-4"
|
||||
/>
|
||||
<span className="font-bold">NEET (UG)</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTargetExam("JEE_Main")}
|
||||
className={`flex items-center gap-2 cursor-pointer p-4 border rounded-xl flex-1 text-left transition-all ${
|
||||
targetExam === "JEE_Main"
|
||||
? "border-[#00F0FF] bg-[#00F0FF]/5"
|
||||
: "border-border bg-secondary/20 hover:bg-secondary/50"
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="target"
|
||||
checked={targetExam === "JEE_Main"}
|
||||
onChange={() => {}}
|
||||
className="accent-[#00F0FF] w-4 h-4"
|
||||
/>
|
||||
<span className="font-bold">JEE Main & Adv</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleSave} disabled={saving} className="bg-foreground text-background hover:bg-foreground/90 font-bold mt-2 rounded-xl px-6">
|
||||
{saving ? "Saving..." : "Save Preferences"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user