feat: complete pragativeer v2 frontend and backend, docker setup, and cbt engine

This commit is contained in:
2026-07-06 17:08:13 +05:30
parent ce01795aa9
commit 426b98ddab
66 changed files with 7949 additions and 363 deletions

View File

@@ -4,11 +4,11 @@ import { Search, Download, MoreVertical } from "lucide-react"
export default function StudentsPage() {
const students = [
{ id: "STU-001", name: "Rahul Sharma", email: "rahul@example.com", tier: "AI Pro", target: "NEET", joined: "Oct 12, 2025", tests: 42 },
{ id: "STU-002", name: "Priya Patel", email: "priya@example.com", tier: "Basic", target: "JEE Main", joined: "Nov 05, 2025", tests: 14 },
{ id: "STU-003", name: "Amit Kumar", email: "amit@example.com", tier: "AI Pro", target: "JEE Adv", joined: "Dec 01, 2025", tests: 89 },
{ id: "STU-004", name: "Sneha Reddy", email: "sneha@example.com", tier: "AI Pro", target: "NEET", joined: "Dec 15, 2025", tests: 34 },
{ id: "STU-005", name: "Vikram Singh", email: "vikram@example.com", tier: "Basic", target: "NEET", joined: "Jan 10, 2026", tests: 5 },
{ id: "STU-001", name: "Rahul Sharma", email: "rahul@example.com", tier: "AI Pro", target: "NEET", joined: "Oct 12, 2025", tests: 42, streak: 45, lastLogin: "Today, 08:30 AM" },
{ id: "STU-002", name: "Priya Patel", email: "priya@example.com", tier: "Basic", target: "JEE Main", joined: "Nov 05, 2025", tests: 14, streak: 2, lastLogin: "Yesterday, 14:15 PM" },
{ id: "STU-003", name: "Amit Kumar", email: "amit@example.com", tier: "AI Pro", target: "JEE Adv", joined: "Dec 01, 2025", tests: 89, streak: 0, lastLogin: "3 Days Ago" },
{ id: "STU-004", name: "Sneha Reddy", email: "sneha@example.com", tier: "AI Pro", target: "NEET", joined: "Dec 15, 2025", tests: 34, streak: 12, lastLogin: "Today, 11:45 AM" },
{ id: "STU-005", name: "Vikram Singh", email: "vikram@example.com", tier: "Basic", target: "NEET", joined: "Jan 10, 2026", tests: 5, streak: 0, lastLogin: "1 Week Ago" },
]
return (
@@ -41,8 +41,9 @@ export default function StudentsPage() {
<th className="px-6 py-4 font-semibold">Plan Tier</th>
<th className="px-6 py-4 font-semibold">Target Exam</th>
<th className="px-6 py-4 font-semibold">Tests Taken</th>
<th className="px-6 py-4 font-semibold">Joined Date</th>
<th className="px-6 py-4 font-semibold text-right"></th>
<th className="px-6 py-4 font-semibold text-center">Streak</th>
<th className="px-6 py-4 font-semibold">Last Login</th>
<th className="px-6 py-4 font-semibold text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
@@ -61,8 +62,16 @@ export default function StudentsPage() {
</td>
<td className="px-6 py-4 font-medium">{s.target}</td>
<td className="px-6 py-4 font-mono">{s.tests}</td>
<td className="px-6 py-4 text-muted-foreground">{s.joined}</td>
<td className="px-6 py-4 text-right">
<td className="px-6 py-4 text-center">
<span className={`font-bold ${s.streak > 0 ? 'text-orange-500' : 'text-muted-foreground'}`}>{s.streak}</span>
</td>
<td className="px-6 py-4 text-muted-foreground">{s.lastLogin}</td>
<td className="px-6 py-4 text-right flex items-center justify-end gap-2">
{s.streak === 0 && (
<Button variant="outline" size="sm" className="h-7 text-[10px] border-red-500/50 text-red-500 hover:bg-red-500/10" onClick={() => window.alert(`Alert sent to ${s.name} regarding broken streak.`)}>
Send Alert
</Button>
)}
<button className="p-1.5 hover:text-foreground hover:bg-secondary rounded"><MoreVertical size={16} /></button>
</td>
</tr>

View File

@@ -0,0 +1,67 @@
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
export const runtime = 'edge';
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
// Extract dynamic data from the URL query parameters
const streak = searchParams.get('streak') || '0';
const accuracy = searchParams.get('accuracy') || '0';
const name = searchParams.get('name') || 'Student';
// Renders a high-end, minimalist graphic using pure Tailwind and React
return new ImageResponse(
(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#0a0a0a', // Deep premium dark mode
color: '#ffffff',
fontFamily: 'sans-serif',
padding: '40px',
border: '8px solid #333',
}}
>
<div style={{ display: 'flex', fontSize: 60, fontWeight: 800, marginBottom: 20, letterSpacing: '-0.05em' }}>
PragatiVeer
</div>
<div style={{ display: 'flex', fontSize: 32, color: '#a3a3a3', marginBottom: 60 }}>
{name}'s JEE Preparation Progress
</div>
<div style={{ display: 'flex', width: '100%', justifyContent: 'space-around' }}>
{/* Streak Metric */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<span style={{ fontSize: 80, fontWeight: 900, color: '#f59e0b' }}>{streak}🔥</span>
<span style={{ fontSize: 24, color: '#d4d4d4', marginTop: 10 }}>Day Streak</span>
</div>
{/* Accuracy Metric */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<span style={{ fontSize: 80, fontWeight: 900, color: '#10b981' }}>{accuracy}%</span>
<span style={{ fontSize: 24, color: '#d4d4d4', marginTop: 10 }}>Top Accuracy</span>
</div>
</div>
<div style={{ display: 'flex', position: 'absolute', bottom: 40, fontSize: 20, color: '#525252' }}>
Join me on pragativeer.com/invite
</div>
</div>
),
{
width: 1200,
height: 630,
}
);
} catch (e: any) {
return new Response(`Failed to generate image`, { status: 500 });
}
}

View File

@@ -1,139 +1,571 @@
"use client"
import { useEffect, useState, use, useRef } from "react"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Clock, Flag, ChevronLeft, ChevronRight, Pause, MoreVertical } from "lucide-react"
import { Clock, Flag, ChevronLeft, ChevronRight, Pause as PauseIcon, MoreVertical, Calculator as CalculatorIcon, FlaskConical, Edit3, XCircle, Play, Shield, AlertTriangle } from "lucide-react"
import Image from "next/image"
import { useCBTStore, QuestionStatus } from "@/store/cbtStore"
import { useExamTimer } from "@/hooks/useExamTimer"
import ReactMarkdown from "react-markdown"
import remarkMath from "remark-math"
import rehypeKatex from "rehype-katex"
import "katex/dist/katex.min.css"
import { Calculator } from "@/components/CBT/Calculator"
import { Scratchpad } from "@/components/CBT/Scratchpad"
import { PeriodicTable } from "@/components/CBT/PeriodicTable"
import { ExamResult } from "@/components/CBT/ExamResult"
// Dummy data for testing the CBT client engine
const DUMMY_QUESTIONS = Array.from({ length: 20 }).map((_, i) => ({
id: `q_${i+1}`,
number: i + 1,
subject: "Physics",
text_en: `A solid cylinder of mass M and radius R is rolling without slipping down an inclined plane of angle θ. Question ${i+1}. What is the minimum coefficient of static friction μ_s required to prevent slipping?`,
text_hi: `द्रव्यमान M और त्रिज्या R का एक ठोस बेलन कोण θ के एक आनत तल पर बिना फिसले लुढ़क रहा है। प्रश्न ${i+1}...`,
options: ['(1/3) tan θ', '(2/3) tan θ', '(1/2) tan θ', 'tan θ']
}));
export default function CBTExamPage({ params }: { params: Promise<{ examId: string }> }) {
const resolvedParams = use(params);
const examId = resolvedParams.examId;
const [mounted, setMounted] = useState(false);
const [showCalc, setShowCalc] = useState(false);
const [showScratchpad, setShowScratchpad] = useState(false);
const [showPeriodicTable, setShowPeriodicTable] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [showSubmitConfirm, setShowSubmitConfirm] = useState(false);
// Anti-cheat state
const [testStarted, setTestStarted] = useState(false);
const [violationCount, setViolationCount] = useState(0);
const [showWarning, setShowWarning] = useState(false);
const hasShownWarning = useRef(false);
const hasAutoSubmitted = useRef(false);
const {
initExam,
questions,
answers,
currentQuestionIndex,
selectAnswer,
clearResponse,
markForReview,
goToQuestion,
addTimeSpent,
submitExam,
getAttemptPayload,
submitAttemptToServer,
resetExamState,
status
} = useCBTStore();
const { formattedTime, isTimedOut } = useExamTimer();
const [questionStartTime, setQuestionStartTime] = useState<number>(Date.now());
// Initialize Exam on mount
useEffect(() => {
async function fetchExamData() {
try {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
const testRes = await fetch(`${apiUrl}/api/tests/${examId}?user_id=b0e17378-864d-439e-8806-708dc48a891b`);
if (!testRes.ok) throw new Error("Failed to fetch test structure");
const testData = await testRes.json();
// Extract all question IDs from sections
const questionIds: string[] = [];
Object.values(testData.sections).forEach((section: any) => {
questionIds.push(...section.questions);
});
// Fetch full question objects
const fetchedQuestions = await Promise.all(
questionIds.map(async (id, idx) => {
const qRes = await fetch(`${apiUrl}/api/questions/${id}`);
const qData = await qRes.json();
return {
id: qData.id,
number: idx + 1,
subject: qData.subject,
text_en: qData.question_text,
text_hi: qData.question_text_hi || qData.question_text,
options: Object.values(qData.options || {}),
options_hi: Object.values(qData.options_hi || qData.options || {}),
};
})
);
initExam(examId, fetchedQuestions, testData.total_time * 60);
setMounted(true);
} catch (err) {
console.error("Failed to load exam from network, falling back to dummy data:", err);
initExam(examId, DUMMY_QUESTIONS, 10800);
setMounted(true);
}
}
fetchExamData();
}, [examId, initExam]);
// Track time spent per question
useEffect(() => {
if (testStarted) {
setQuestionStartTime(Date.now());
}
}, [currentQuestionIndex, testStarted]);
// Anti-Cheat: Tab switching listener
useEffect(() => {
if (!testStarted || status === 'SUBMITTED' || status === 'TIMED_OUT') return;
const handleVisibilityChange = () => {
if (document.visibilityState === 'hidden') {
setViolationCount((prev) => prev + 1);
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
}, [testStarted, status]);
// Handle side-effects of violation count changing
useEffect(() => {
if (violationCount === 1 && !hasShownWarning.current) {
hasShownWarning.current = true;
setShowWarning(true);
} else if (violationCount >= 2 && status !== 'SUBMITTED' && status !== 'TIMED_OUT' && !hasAutoSubmitted.current) {
hasAutoSubmitted.current = true;
submitExam('SUBMITTED');
submitAttemptToServer('b0e17378-864d-439e-8806-708dc48a891b')
.then(() => {
window.alert("Test Auto-Submitted due to multiple tab switching violations.");
})
.catch(err => {
console.error("Auto-submit fetch error:", err);
window.alert("Test Auto-Submitted due to multiple tab switching violations.");
});
}
}, [violationCount, status, submitExam, submitAttemptToServer]);
const startTest = async () => {
try {
if (document.documentElement.requestFullscreen) {
await document.documentElement.requestFullscreen();
}
} catch (e) {
console.warn("Fullscreen API failed", e);
}
setTestStarted(true);
};
const handleNext = () => {
if (!currentQuestion) return;
const timeSpent = Math.floor((Date.now() - questionStartTime) / 1000);
addTimeSpent(currentQuestion.id, timeSpent);
if (currentQuestionIndex < questions.length - 1) {
goToQuestion(currentQuestionIndex + 1);
} else {
goToQuestion(0);
}
}
const handlePrev = () => {
if (!currentQuestion) return;
const timeSpent = Math.floor((Date.now() - questionStartTime) / 1000);
addTimeSpent(currentQuestion.id, timeSpent);
if (currentQuestionIndex > 0) {
goToQuestion(currentQuestionIndex - 1);
}
}
const handleGridClick = (index: number) => {
if (!currentQuestion) return;
const timeSpent = Math.floor((Date.now() - questionStartTime) / 1000);
addTimeSpent(currentQuestion.id, timeSpent);
goToQuestion(index);
}
const handleSubmit = () => {
setShowSubmitConfirm(true);
}
const confirmSubmit = async () => {
setShowSubmitConfirm(false);
submitExam('SUBMITTED');
const payload = getAttemptPayload('b0e17378-864d-439e-8806-708dc48a891b');
console.log("EXAM SUBMITTED. PAYLOAD:", JSON.stringify(payload, null, 2));
await submitAttemptToServer('b0e17378-864d-439e-8806-708dc48a891b');
}
if (!mounted || questions.length === 0) return <div className="p-8">Loading Exam Engine...</div>;
if (!testStarted && status !== 'SUBMITTED' && status !== 'TIMED_OUT') {
return (
<div className="h-screen flex flex-col items-center justify-center bg-background p-8 text-center animate-in fade-in duration-500">
<div className="max-w-2xl bg-secondary/10 border border-border p-8 rounded-2xl shadow-2xl">
<h1 className="text-3xl font-bold mb-4 flex items-center justify-center gap-3">
<Shield className="text-red-500" /> Secure Exam Environment
</h1>
<div className="text-left space-y-4 text-muted-foreground mb-8">
<p><strong>Target:</strong> {questions.some(q => q.subject.toLowerCase() === 'biology') ? 'NEET' : 'JEE'} Pattern Test</p>
<ul className="list-disc pl-5 space-y-2">
<li>The test will open in <strong>Full Screen Mode</strong>.</li>
<li><strong>Do not switch tabs or exit full screen.</strong></li>
<li>First violation will result in a strict warning.</li>
<li>Second violation will <strong>automatically submit</strong> your exam to prevent cheating.</li>
<li>Ensure your internet connection is stable before starting.</li>
</ul>
</div>
<Button size="lg" className="w-full text-lg font-bold h-14 shadow-[0_0_20px_-5px_var(--color-primary)]" onClick={startTest}>
I Understand, Start Test
</Button>
</div>
</div>
);
}
if (status === 'SUBMITTED' || status === 'TIMED_OUT') {
const payload = getAttemptPayload('b0e17378-864d-439e-8806-708dc48a891b');
if (!payload) return <div className="p-8 text-center mt-20">Loading results...</div>;
return (
<ExamResult
payload={payload}
onRetake={() => {
resetExamState();
window.location.reload();
}}
/>
)
}
const currentQuestion = questions[currentQuestionIndex];
const currentState = answers[currentQuestion.id];
// Derive palette stats
const paletteStats = Object.values(answers).reduce(
(acc, val) => {
acc[val.status]++;
return acc;
},
{ UNVISITED: 0, NOT_ANSWERED: 0, ANSWERED: 0, MARKED: 0, ANSWERED_MARKED: 0 } as Record<QuestionStatus, number>
);
// Group questions by subject for the side panel
const questionsBySubject = questions.reduce((acc, q, index) => {
if (!acc[q.subject]) {
acc[q.subject] = [];
}
acc[q.subject].push({ ...q, originalIndex: index });
return acc;
}, {} as Record<string, any[]>);
export default function CBTExamPage({ params }: { params: { examId: string } }) {
return (
<>
<header className="border-b border-border bg-background px-6 py-4 flex items-center justify-between sticky top-0 z-10">
<div className="h-screen flex flex-col overflow-hidden">
{/* Warning Modal */}
{showWarning && (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-background border border-red-500 shadow-[0_0_50px_-10px_rgba(239,68,68,0.5)] rounded-2xl w-[400px] overflow-hidden animate-in zoom-in-95 duration-200">
<div className="p-6 text-center space-y-4">
<AlertTriangle size={64} className="text-red-500 mx-auto animate-pulse" />
<h3 className="text-2xl font-bold text-red-500">Warning!</h3>
<p className="text-foreground font-medium">You switched tabs or left the secure window.</p>
<p className="text-sm text-muted-foreground">This is your final warning. Doing this again will automatically submit your exam and your score will be evaluated as is.</p>
<Button
className="w-full mt-4 bg-red-500 hover:bg-red-600 text-white font-bold"
onClick={() => {
setShowWarning(false);
startTest(); // Try to re-enter fullscreen
}}
>
Return to Exam
</Button>
</div>
</div>
</div>
)}
<header className="border-b border-border bg-background px-6 py-4 flex items-center justify-between sticky top-0 z-20 shadow-sm shrink-0">
<div className="flex items-center gap-4">
<div className="relative h-14 w-56 shrink-0">
<div className="relative h-10 w-40 shrink-0">
<Image
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_light_background.svg"
alt="PragatiVeer"
fill
className="object-contain object-left scale-135 origin-left dark:hidden"
priority
alt="PragatiVeer" fill className="object-contain object-left scale-125 origin-left dark:hidden" priority
/>
<Image
src="/PragatiVeer_Logo/SVG_Logos/Rectangular_dark_background.svg"
alt="PragatiVeer"
fill
className="object-contain object-left scale-135 origin-left hidden dark:block"
priority
alt="PragatiVeer" fill className="object-contain object-left scale-125 origin-left hidden dark:block" priority
/>
</div>
<div className="h-6 w-px bg-border hidden md:block"></div>
<div className="hidden md:block">
<h1 className="font-semibold text-sm">AITS Full Syllabus Test 4</h1>
<p className="text-xs text-muted-foreground font-medium mt-0.5">Physics • Section A</p>
<h1 className="font-semibold text-sm">AITS Full Syllabus Test</h1>
<p className="text-xs text-muted-foreground font-medium mt-0.5">{currentQuestion.subject}</p>
</div>
</div>
<div className="flex items-center gap-3 md:gap-6">
<div className="flex items-center gap-2 bg-secondary/50 px-3 py-1.5 md:px-4 md:py-2 rounded-xl font-mono text-base md:text-lg tracking-wider font-medium text-foreground">
<Clock size={18} className="text-muted-foreground" />
02:45:12
<div className="hidden lg:flex items-center gap-2 mr-4">
<Button variant="ghost" size="sm" onClick={() => setShowCalc(true)} className="gap-2 text-xs font-semibold text-muted-foreground hover:text-foreground"><CalculatorIcon size={14}/> Calc</Button>
<Button variant="ghost" size="sm" onClick={() => setShowPeriodicTable(true)} className="gap-2 text-xs font-semibold text-muted-foreground hover:text-foreground"><FlaskConical size={14}/> Periodic Table</Button>
<Button variant="ghost" size="sm" onClick={() => setShowScratchpad(true)} className="gap-2 text-xs font-semibold text-muted-foreground hover:text-foreground"><Edit3 size={14}/> Scratchpad</Button>
</div>
<Button variant="outline" size="sm" className="gap-2 rounded-xl hidden md:flex">
<Pause size={14} /> Pause
<div className={`flex items-center gap-2 px-3 py-1.5 md:px-4 md:py-2 rounded-xl font-mono text-base md:text-lg tracking-wider font-medium border border-border ${isTimedOut ? 'bg-red-500/10 text-red-500' : 'bg-secondary/50 text-foreground'}`}>
<Clock size={18} className="text-muted-foreground" />
<span className={isTimedOut ? "font-bold animate-pulse text-red-500" : "font-bold text-primary"}>{formattedTime}</span>
</div>
<Button variant="outline" size="sm" onClick={() => setIsPaused(!isPaused)} className="gap-2 rounded-xl hidden md:flex border-border">
{isPaused ? <><Play size={14} /> Resume</> : <><PauseIcon size={14} /> Pause</>}
</Button>
<Button size="sm" className="rounded-xl">Submit Exam</Button>
<Button size="sm" onClick={handleSubmit} className="rounded-xl shadow-[0_0_15px_-3px_var(--color-primary)]">Submit Exam</Button>
</div>
</header>
<div className="flex flex-1 overflow-hidden relative">
{/* Main Question Area */}
<main className="flex-1 overflow-y-auto p-6 lg:p-12 pb-24 lg:pb-12">
<div className="max-w-3xl mx-auto space-y-8">
<div className="flex items-center justify-between border-b border-border pb-4">
<Badge variant="secondary" className="px-3 py-1 text-sm bg-secondary text-secondary-foreground font-medium rounded-lg">Question 12</Badge>
<div className="flex items-center gap-3">
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground bg-secondary/30 px-2 py-1 rounded-md">+4 / -1</span>
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-foreground rounded-lg">
<Flag size={16} />
</Button>
{/* Main Question Area - Configured for Bilingual Split Pane */}
<main className="flex-1 overflow-y-auto p-0 flex flex-col xl:flex-row pb-24 lg:pb-0">
<div className="flex-1 p-6 lg:p-10 border-b xl:border-b-0 xl:border-r border-border flex flex-col">
<div className="max-w-2xl mx-auto w-full space-y-8 flex-1">
<div className="flex items-center justify-between border-b border-border pb-4">
<div className="flex items-center gap-2">
<Badge variant="secondary" className="px-3 py-1 text-sm bg-secondary text-secondary-foreground font-medium rounded-lg">Question {currentQuestion.number}</Badge>
<span className="text-[10px] uppercase font-bold text-muted-foreground bg-secondary/30 px-2 py-0.5 rounded-md border border-border">English</span>
</div>
<div className="flex items-center gap-3">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground bg-secondary/30 px-2 py-1 rounded-md">+4 / -1</span>
<Button
variant="ghost"
size="icon"
onClick={() => markForReview(currentQuestion.id)}
className={`h-8 w-8 rounded-lg transition-colors ${currentState?.status.includes('MARKED') ? 'text-amber-500 bg-amber-500/10' : 'text-muted-foreground hover:text-amber-500'}`}
>
<Flag size={16} />
</Button>
</div>
</div>
<div className="prose prose-neutral dark:prose-invert max-w-none text-lg leading-relaxed text-foreground font-medium">
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
{currentQuestion.text_en}
</ReactMarkdown>
</div>
<div className="space-y-3 mt-8">
{currentQuestion.options.map((option: string, idx: number) => {
const letter = String.fromCharCode(65 + idx);
const isSelected = currentState?.selected_answer === letter;
return (
<button
key={idx}
onClick={() => selectAnswer(currentQuestion.id, letter)}
className={`w-full text-left p-4 rounded-xl border transition-all duration-200 flex items-center gap-4 group focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent
${isSelected ? 'border-primary bg-primary/5 shadow-[0_0_15px_-5px_var(--color-primary)]' : 'border-border bg-background hover:border-primary/50 hover:bg-secondary/20'}
`}
>
<div className={`flex-shrink-0 size-8 rounded-lg border flex items-center justify-center font-medium transition-colors
${isSelected ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground group-hover:border-primary/50 group-hover:text-primary group-hover:bg-primary/5'}
`}>
{letter}
</div>
<span className={`text-base font-semibold flex-1 ${isSelected ? 'text-primary' : ''}`}>
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
{option}
</ReactMarkdown>
</span>
</button>
)
})}
</div>
</div>
</div>
{/* Optional Bilingual Hindi Pane */}
<div className="flex-1 p-6 lg:p-10 hidden lg:flex bg-secondary/5 border-b xl:border-b-0">
<div className="max-w-2xl mx-auto space-y-8 w-full">
<div className="flex items-center justify-between border-b border-border pb-4">
<div className="flex items-center gap-2">
<Badge variant="outline" className="px-3 py-1 text-sm text-muted-foreground font-medium rounded-lg">प्रश्न {currentQuestion.number}</Badge>
<span className="text-[10px] uppercase font-bold text-muted-foreground bg-secondary/30 px-2 py-0.5 rounded-md border border-border">हिंदी</span>
</div>
</div>
<div className="prose prose-neutral dark:prose-invert max-w-none text-lg leading-relaxed text-foreground font-medium">
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
{currentQuestion.text_hi}
</ReactMarkdown>
</div>
<div className="prose prose-neutral dark:prose-invert max-w-none text-lg leading-loose text-foreground">
<p>
A solid cylinder of mass <span className="font-mono text-primary bg-secondary/50 px-1.5 py-0.5 rounded-md text-base">M</span> and radius <span className="font-mono text-primary bg-secondary/50 px-1.5 py-0.5 rounded-md text-base">R</span> is rolling without slipping down an inclined plane of angle <span className="font-mono text-primary bg-secondary/50 px-1.5 py-0.5 rounded-md text-base">θ</span>.
What is the minimum coefficient of static friction <span className="font-mono text-primary bg-secondary/50 px-1.5 py-0.5 rounded-md text-base">μ_s</span> required to prevent slipping?
</p>
</div>
<div className="space-y-3 mt-8">
{['(1/3) tan θ', '(2/3) tan θ', '(1/2) tan θ', 'tan θ'].map((option, idx) => (
<button
key={idx}
className="w-full text-left p-4 rounded-xl border border-border bg-background hover:border-primary/50 hover:bg-secondary/20 transition-all duration-300 flex items-center gap-4 group focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
>
<div className="flex-shrink-0 size-8 rounded-lg border border-border flex items-center justify-center font-medium text-muted-foreground group-hover:border-primary/50 group-hover:text-primary group-hover:bg-primary/5 transition-colors">
{String.fromCharCode(65 + idx)}
</div>
<span className="text-base font-medium">{option}</span>
</button>
))}
<div className="space-y-3 mt-8">
{currentQuestion.options_hi?.map((option: string, idx: number) => {
const letter = String.fromCharCode(65 + idx);
const isSelected = currentState?.selected_answer === letter;
return (
<button
key={idx}
onClick={() => selectAnswer(currentQuestion.id, letter)}
className={`w-full text-left p-4 rounded-xl border transition-all duration-200 flex items-center gap-4 group focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent
${isSelected ? 'border-primary bg-primary/5 shadow-[0_0_15px_-5px_var(--color-primary)]' : 'border-border bg-background hover:border-primary/50 hover:bg-secondary/20'}
`}
>
<div className={`flex-shrink-0 size-8 rounded-lg border flex items-center justify-center font-medium transition-colors
${isSelected ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground group-hover:border-primary/50 group-hover:text-primary group-hover:bg-primary/5'}
`}>
{letter}
</div>
<span className={`text-base font-semibold flex-1 ${isSelected ? 'text-primary' : ''}`}>
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
{option}
</ReactMarkdown>
</span>
</button>
)
})}
</div>
</div>
</div>
</main>
{/* Side Panel for Question Grid */}
<aside className="w-80 border-l border-border bg-secondary/10 flex flex-col hidden lg:flex">
<aside className="w-80 border-l border-border bg-secondary/10 flex flex-col hidden lg:flex shrink-0">
<div className="p-5 border-b border-border flex items-center justify-between bg-background">
<h3 className="font-semibold text-sm tracking-tight uppercase text-muted-foreground">Question Palette</h3>
<h3 className="font-bold text-sm tracking-tight uppercase text-muted-foreground">Question Palette</h3>
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-lg"><MoreVertical size={16}/></Button>
</div>
<div className="flex-1 overflow-y-auto p-5">
<div className="mb-8 space-y-3 bg-background border border-border p-4 rounded-xl">
<div className="flex items-center justify-between text-xs font-medium text-muted-foreground">
<div className="flex items-center gap-2"><div className="size-3.5 rounded-full bg-primary shadow-sm"></div> Answered <span className="text-foreground ml-1">(11)</span></div>
<div className="flex items-center gap-2"><div className="size-3.5 rounded-full border-2 border-border bg-background"></div> Not Visited <span className="text-foreground ml-1">(28)</span></div>
<div className="mb-6 grid grid-cols-2 gap-2 bg-background border border-border p-3 rounded-xl">
<div className="flex items-center gap-2 text-[11px] font-bold text-muted-foreground">
<div className="size-4 rounded border border-transparent bg-green-500 shadow-sm text-white flex items-center justify-center">{paletteStats.ANSWERED}</div> <span className="truncate">Answered</span>
</div>
<div className="flex items-center justify-between text-xs font-medium text-muted-foreground">
<div className="flex items-center gap-2"><div className="size-3.5 rounded-full bg-destructive shadow-sm"></div> Not Answered <span className="text-foreground ml-1">(4)</span></div>
<div className="flex items-center gap-2"><div className="size-3.5 rounded-full bg-amber-500 shadow-sm"></div> Marked <span className="text-foreground ml-1">(2)</span></div>
<div className="flex items-center gap-2 text-[11px] font-bold text-muted-foreground">
<div className="size-4 rounded border border-transparent bg-red-500 shadow-sm text-white flex items-center justify-center">{paletteStats.NOT_ANSWERED}</div> <span className="truncate">Not Answered</span>
</div>
<div className="flex items-center gap-2 text-[11px] font-bold text-muted-foreground">
<div className="size-4 rounded border border-border bg-background shadow-sm text-foreground flex items-center justify-center">{paletteStats.UNVISITED}</div> <span className="truncate">Not Visited</span>
</div>
<div className="flex items-center gap-2 text-[11px] font-bold text-muted-foreground">
<div className="size-4 rounded-full border border-transparent bg-amber-500 shadow-sm text-white flex items-center justify-center">{paletteStats.MARKED}</div> <span className="truncate">Marked</span>
</div>
<div className="flex items-center gap-2 text-[11px] font-bold text-muted-foreground col-span-2 mt-1">
<div className="size-4 rounded-full border border-transparent bg-amber-500 shadow-sm text-white flex items-center justify-center relative">
{paletteStats.ANSWERED_MARKED}<div className="absolute -bottom-0.5 -right-0.5 size-1.5 rounded-full bg-green-500 border border-white"></div>
</div>
<span className="truncate">Answered & Marked</span>
</div>
</div>
<div className="space-y-6">
<div>
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-4 flex items-center gap-2">
<span className="h-px bg-border flex-1"></span> Physics <span className="h-px bg-border flex-1"></span>
</h4>
<div className="grid grid-cols-5 gap-2.5">
{Array.from({length: 20}).map((_, i) => (
<button
key={i}
className={`size-11 rounded-lg flex items-center justify-center text-sm font-medium transition-all duration-200 border focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary
${i === 11 ? 'ring-2 ring-primary ring-offset-2 ring-offset-background border-transparent bg-background shadow-sm' : ''}
${i < 5 ? 'bg-primary border-primary text-primary-foreground shadow-sm' : ''}
${i >= 5 && i < 7 ? 'bg-destructive border-destructive text-destructive-foreground shadow-sm' : ''}
${i === 7 ? 'bg-amber-500 border-amber-500 text-amber-50-foreground shadow-sm' : ''}
${i > 11 ? 'bg-background border-border text-muted-foreground hover:border-primary/50 hover:text-foreground' : ''}
`}
>
{i + 1}
</button>
))}
{Object.entries(questionsBySubject).map(([subject, subQuestions]: [string, any]) => (
<div key={subject}>
<h4 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-4 flex items-center gap-2">
<span className="h-px bg-border flex-1"></span> {subject} <span className="h-px bg-border flex-1"></span>
</h4>
<div className="grid grid-cols-5 gap-2.5">
{subQuestions.map((q: any) => {
const i = q.originalIndex;
const qState = answers[q.id];
const isActive = i === currentQuestionIndex;
let btnClass = "bg-background border-border text-muted-foreground hover:border-primary/50 hover:text-foreground";
let shapeClass = "rounded"; // Default shape
if (qState) {
if (qState.status === 'ANSWERED') {
btnClass = "bg-green-500 border-green-500 text-white shadow-sm";
} else if (qState.status === 'NOT_ANSWERED') {
btnClass = "bg-red-500 border-red-500 text-white shadow-sm";
} else if (qState.status === 'MARKED') {
btnClass = "bg-amber-500 border-amber-500 text-white shadow-sm";
shapeClass = "rounded-full";
} else if (qState.status === 'ANSWERED_MARKED') {
btnClass = "bg-amber-500 border-amber-500 text-white shadow-sm relative";
shapeClass = "rounded-full";
}
}
if (isActive) {
btnClass = "ring-2 ring-primary ring-offset-2 ring-offset-background border-transparent bg-background shadow-sm text-foreground";
}
return (
<button
key={i}
onClick={() => handleGridClick(i)}
className={`size-11 flex items-center justify-center text-sm font-bold transition-all duration-200 border focus:outline-none focus:ring-2 focus:ring-primary ${btnClass} ${shapeClass}`}
>
{i + 1}
{qState?.status === 'ANSWERED_MARKED' && <div className="absolute -bottom-1 -right-1 size-3 rounded-full bg-green-500 border-2 border-background"></div>}
</button>
)
})}
</div>
</div>
</div>
))}
</div>
</div>
<div className="p-5 border-t border-border bg-background grid grid-cols-2 gap-3">
<Button variant="outline" className="rounded-xl w-full gap-1.5 h-12"><ChevronLeft size={16}/> Previous</Button>
<Button className="rounded-xl w-full gap-1.5 h-12 shadow-sm">Save & Next <ChevronRight size={16}/></Button>
<div className="p-4 border-t border-border bg-background flex flex-col gap-3">
<div className="flex gap-2">
<Button variant="outline" onClick={() => markForReview(currentQuestion.id)} className="flex-1 text-xs font-bold h-9 bg-secondary/50 hover:bg-amber-500/10 hover:text-amber-500 hover:border-amber-500/50"><Flag size={14} className="mr-1.5"/> Mark</Button>
<Button variant="outline" onClick={() => clearResponse(currentQuestion.id)} className="flex-1 text-xs font-bold h-9 bg-secondary/50 hover:bg-destructive/10 hover:text-destructive hover:border-destructive/50"><XCircle size={14} className="mr-1.5"/> Clear</Button>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handlePrev} className="flex-1 gap-1 h-12 rounded-xl font-bold"><ChevronLeft size={16}/> Back</Button>
<Button onClick={handleNext} className="flex-[2] gap-1 h-12 rounded-xl font-bold shadow-[0_0_15px_-5px_var(--color-primary)]">Save & Next <ChevronRight size={16}/></Button>
</div>
</div>
</aside>
{/* Mobile bottom nav */}
<div className="lg:hidden absolute bottom-0 left-0 right-0 p-4 border-t border-border bg-background/95 backdrop-blur-sm grid grid-cols-2 gap-3">
<Button variant="outline" className="rounded-xl w-full gap-1.5 h-12"><ChevronLeft size={16}/> Previous</Button>
<Button className="rounded-xl w-full gap-1.5 h-12 shadow-sm">Save & Next <ChevronRight size={16}/></Button>
<div className="lg:hidden absolute bottom-0 left-0 right-0 p-4 border-t border-border bg-background/95 backdrop-blur-sm grid grid-cols-2 gap-3 z-30">
<Button variant="outline" onClick={handlePrev} className="rounded-xl w-full gap-1.5 h-12 font-bold"><ChevronLeft size={16}/> Previous</Button>
<Button onClick={handleNext} className="rounded-xl w-full gap-1.5 h-12 font-bold shadow-sm">Save & Next <ChevronRight size={16}/></Button>
</div>
{/* Modals and Overlays */}
{isPaused && (
<div className="absolute inset-0 z-40 bg-background/95 backdrop-blur-md flex flex-col items-center justify-center p-8">
<PauseIcon size={64} className="text-muted-foreground mb-6" />
<h2 className="text-3xl font-bold mb-4">Exam Paused</h2>
<p className="text-muted-foreground max-w-md text-center mb-8">
The exam timer is currently running in the background. Pausing only hides the questions to prevent cheating while taking a break.
</p>
<Button size="lg" onClick={() => setIsPaused(false)} className="rounded-xl h-14 px-10 text-lg font-bold gap-2">
<Play size={20} /> Resume Exam
</Button>
</div>
)}
{showSubmitConfirm && (
<div className="absolute inset-0 z-50 bg-background/80 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-background border border-border shadow-2xl rounded-2xl p-6 max-w-md w-full animate-in fade-in zoom-in duration-200">
<h2 className="text-2xl font-bold mb-2">Submit Exam?</h2>
<p className="text-muted-foreground mb-6">Are you sure you want to submit your exam? You will not be able to change your answers after submission.</p>
<div className="grid grid-cols-2 gap-4 mb-8 p-4 bg-secondary/20 rounded-xl border border-border">
<div className="text-center">
<div className="text-2xl font-bold text-green-500">{paletteStats.ANSWERED + paletteStats.ANSWERED_MARKED}</div>
<div className="text-xs font-semibold text-muted-foreground uppercase">Answered</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-red-500">{paletteStats.NOT_ANSWERED + paletteStats.UNVISITED}</div>
<div className="text-xs font-semibold text-muted-foreground uppercase">Unanswered</div>
</div>
<div className="text-center col-span-2 mt-2 pt-2 border-t border-border/50">
<div className="text-2xl font-bold text-amber-500">{paletteStats.MARKED + paletteStats.ANSWERED_MARKED}</div>
<div className="text-xs font-semibold text-muted-foreground uppercase">Marked for Review</div>
</div>
</div>
<div className="flex gap-3">
<Button variant="outline" className="flex-1 rounded-xl h-12 font-bold" onClick={() => setShowSubmitConfirm(false)}>Cancel</Button>
<Button className="flex-1 rounded-xl h-12 font-bold shadow-[0_0_15px_-3px_var(--color-primary)]" onClick={confirmSubmit}>Yes, Submit</Button>
</div>
</div>
</div>
)}
<Calculator isOpen={showCalc} onClose={() => setShowCalc(false)} />
<Scratchpad isOpen={showScratchpad} onClose={() => setShowScratchpad(false)} />
<PeriodicTable isOpen={showPeriodicTable} onClose={() => setShowPeriodicTable(false)} />
</div>
</>
</div>
)
}

View File

@@ -0,0 +1,170 @@
"use client"
import { useEffect, useState } from "react"
import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
import { StudyPlanCard } from "@/components/AI/StudyPlanCard"
import { ReadinessGauge } from "@/components/AI/ReadinessGauge"
import { CollegePredictorCard } from "@/components/AI/CollegePredictorCard"
import { TutorChat } from "@/components/AI/TutorChat"
import { ReferralCard } from "@/components/AI/ReferralCard"
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"
export default function AICoachDashboard() {
const [mistakes, setMistakes] = useState<MistakePayload[]>([])
const [currentIndex, setCurrentIndex] = useState(0)
const [loading, setLoading] = useState(true)
const fetchMistakes = async () => {
setLoading(true)
try {
const res = await fetch(`http://localhost:8000/api/mistakes/today?user_id=${USER_ID}`)
if (!res.ok) throw new Error("Failed to fetch")
const data = await res.json()
setMistakes(data)
} catch (error) {
console.error(error)
// If backend fails, fallback to empty array
setMistakes([])
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchMistakes()
}, [])
const submitReview = async (mistakeId: string, selectedAnswer: string) => {
const res = await fetch("http://localhost:8000/api/mistakes/review", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mistake_id: mistakeId, selected_answer: selectedAnswer })
})
if (!res.ok) throw new Error("Failed to submit review")
return await res.json()
}
const handleNext = () => {
if (currentIndex < mistakes.length - 1) {
setCurrentIndex(prev => prev + 1)
} else {
// Session finished
setCurrentIndex(mistakes.length) // Push out of bounds to trigger complete state
}
}
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">
<div>
<h1 className="text-3xl font-bold tracking-tight flex items-center gap-3">
<BrainCircuit className="text-primary" size={32}/> AI Coach
</h1>
<p className="text-muted-foreground mt-1 font-medium">Your personalized SM-2 spaced repetition and analytics engine.</p>
</div>
</header>
{/* Summary Metrics */}
<section className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="border-border shadow-sm">
<CardContent className="p-5 flex items-center gap-4">
<div className="p-3 bg-secondary rounded-xl text-primary"><Calculator size={20}/></div>
<div>
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Physics Errors</p>
<h3 className="text-2xl font-bold">{mistakes.filter(m => m.question.subject === 'Physics').length}</h3>
</div>
</CardContent>
</Card>
<Card className="border-border shadow-sm">
<CardContent className="p-5 flex items-center gap-4">
<div className="p-3 bg-secondary rounded-xl text-emerald-500"><BookOpen size={20}/></div>
<div>
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Chemistry Errors</p>
<h3 className="text-2xl font-bold">{mistakes.filter(m => m.question.subject === 'Chemistry').length}</h3>
</div>
</CardContent>
</Card>
<Card className="border-border shadow-sm">
<CardContent className="p-5 flex items-center gap-4">
<div className="p-3 bg-secondary rounded-xl text-blue-500"><Target size={20}/></div>
<div>
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Maths Errors</p>
<h3 className="text-2xl font-bold">{mistakes.filter(m => m.question.subject === 'Maths').length}</h3>
</div>
</CardContent>
</Card>
</section>
{/* Predictive Intelligence */}
<section className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<ReadinessGauge />
<CollegePredictorCard />
</section>
{/* Mistake Review Workspace */}
<section>
<div className="flex items-center justify-between mb-5">
<h2 className="text-xl font-semibold tracking-tight">Daily Mistake Review Queue</h2>
<span className="text-sm font-bold text-muted-foreground bg-secondary px-3 py-1 rounded-full">
{currentIndex < mistakes.length ? `${currentIndex + 1} / ${mistakes.length}` : "Finished"}
</span>
</div>
{loading ? (
<Card className="p-12 flex items-center justify-center border-dashed">
<RefreshCw className="animate-spin text-muted-foreground mr-3" /> Loading queue...
</Card>
) : mistakes.length === 0 ? (
<Card className="p-12 text-center border-dashed bg-secondary/20">
<CheckCircle2 size={48} className="mx-auto text-green-500 mb-4 opacity-50" />
<h3 className="text-xl font-bold mb-2">You're all caught up!</h3>
<p className="text-muted-foreground">No mistakes scheduled for review today. Go take a test!</p>
</Card>
) : currentIndex < mistakes.length ? (
<MistakeReviewCard
data={mistakes[currentIndex]}
onNext={handleNext}
onSubmitReview={submitReview}
/>
) : (
<Card className="p-12 text-center border-dashed bg-primary/5 border-primary/20">
<BrainCircuit size={48} className="mx-auto text-primary mb-4 opacity-80" />
<h3 className="text-xl font-bold mb-2 text-primary">Session Complete</h3>
<p className="text-muted-foreground mb-6">Great job clearing your error queue! The SM-2 engine has scheduled your next reviews.</p>
<Button onClick={() => window.location.reload()} variant="outline" className="rounded-xl">Check for New Errors</Button>
</Card>
)}
</section>
{/* Heatmap & Timeline */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8 pt-8 border-t border-border">
<section>
<StudyPlanCard />
</section>
<section>
<MistakeHeatmap />
</section>
</div>
{/* Tutor Chat Workspace */}
<section className="pt-8 border-t border-border">
<TutorChat />
</section>
{/* Growth & Sharing Hub */}
<section className="grid grid-cols-1 lg:grid-cols-2 gap-8 pt-8 border-t border-border mt-8">
<ReferralCard />
<ProgressShare />
</section>
</div>
)
}

View File

@@ -1,7 +1,35 @@
"use client"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { LineChart, Activity, BrainCircuit } from "lucide-react"
import { LineChart as LineChartIcon, Activity, BrainCircuit } from "lucide-react"
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, Cell } from 'recharts'
export default function AnalyticsPage() {
const trendData = [
{ name: 'Week 1', score: 120 },
{ name: 'Week 2', score: 135 },
{ name: 'Week 3', score: 130 },
{ name: 'Week 4', score: 145 },
{ name: 'Week 5', score: 160 },
{ name: 'Week 6', score: 185 },
]
// For heatmap-like visualization (using a barchart with different colors)
const heatmapData = [
{ topic: 'Kinematics', accuracy: 95 },
{ topic: 'Optics', accuracy: 72 },
{ topic: 'Thermodynamics', accuracy: 45 },
{ topic: 'Calculus', accuracy: 38 },
{ topic: 'Organic Chem', accuracy: 88 },
{ topic: 'Inorganic', accuracy: 65 },
]
const getColor = (accuracy: number) => {
if (accuracy >= 80) return '#10b981' // emerald-500
if (accuracy >= 60) return '#f59e0b' // amber-500
return '#ef4444' // red-500
}
return (
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-6xl mx-auto">
<div>
@@ -12,16 +40,22 @@ export default function AnalyticsPage() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Card className="col-span-1 lg:col-span-2 border-border shadow-sm bg-background">
<CardHeader>
<CardTitle className="flex items-center gap-2"><LineChart className="text-[#00F0FF]" size={20} /> Projected Score Trajectory</CardTitle>
<CardTitle className="flex items-center gap-2"><LineChartIcon className="text-[#00F0FF]" size={20} /> Projected Score Trajectory</CardTitle>
<CardDescription>Your mock scores over the last 6 months projected towards the final exam.</CardDescription>
</CardHeader>
<CardContent className="h-64 flex items-center justify-center border-t border-border mt-4">
{/* Placeholder for an actual chart library like Recharts */}
<div className="text-center text-muted-foreground">
<Activity size={48} className="mx-auto mb-4 opacity-20" />
<p className="font-medium">Data gathering in progress...</p>
<p className="text-sm mt-1">Complete 2 more CBTs to unlock predictive graphing.</p>
</div>
<CardContent className="h-[350px] mt-4">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={trendData} margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#333" vertical={false} />
<XAxis dataKey="name" stroke="#888" tick={{ fill: '#888' }} />
<YAxis stroke="#888" tick={{ fill: '#888' }} domain={[0, 300]} />
<Tooltip
contentStyle={{ backgroundColor: '#1f2937', borderColor: '#374151', borderRadius: '8px' }}
itemStyle={{ color: '#00F0FF' }}
/>
<Line type="monotone" dataKey="score" stroke="#00F0FF" strokeWidth={3} dot={{ r: 4, fill: '#00F0FF' }} activeDot={{ r: 6 }} />
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
@@ -39,21 +73,25 @@ export default function AnalyticsPage() {
<Card className="border-border shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-lg">Subject Mastery</CardTitle>
<CardTitle className="text-lg">Topic Heatmap (Accuracy)</CardTitle>
</CardHeader>
<CardContent className="space-y-4 pt-2">
<div>
<div className="flex justify-between text-sm mb-1 font-medium"><span>Biology</span> <span className="text-emerald-500">89%</span></div>
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden"><div className="h-full bg-emerald-500 w-[89%]"></div></div>
</div>
<div>
<div className="flex justify-between text-sm mb-1 font-medium"><span>Chemistry</span> <span className="text-amber-500">68%</span></div>
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden"><div className="h-full bg-amber-500 w-[68%]"></div></div>
</div>
<div>
<div className="flex justify-between text-sm mb-1 font-medium"><span>Physics</span> <span className="text-destructive">42%</span></div>
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden"><div className="h-full bg-destructive w-[42%]"></div></div>
</div>
<CardContent className="h-[250px] mt-2">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={heatmapData} layout="vertical" margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<XAxis type="number" domain={[0, 100]} hide />
<YAxis dataKey="topic" type="category" stroke="#888" width={100} tick={{ fontSize: 12, fill: '#bbb' }} axisLine={false} tickLine={false} />
<Tooltip
cursor={{fill: 'transparent'}}
contentStyle={{ backgroundColor: '#1f2937', borderColor: '#374151', borderRadius: '8px', color: '#fff' }}
formatter={(value: number) => [`${value}% Accuracy`, 'Accuracy']}
/>
<Bar dataKey="accuracy" radius={[0, 4, 4, 0]} barSize={12}>
{heatmapData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={getColor(entry.accuracy)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>

View File

@@ -0,0 +1,168 @@
"use client";
import { useEffect, useState, use } from "react";
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 { ArrowLeft, CheckCircle2, XCircle, Clock, Award } from "lucide-react";
export default function AttemptHistoryPage({ params }: { params: Promise<{ attemptId: string }> }) {
const { attemptId } = use(params);
const [loading, setLoading] = useState(true);
const [attempt, setAttempt] = useState<any>(null);
const [test, setTest] = useState<any>(null);
const [questions, setQuestions] = useState<Record<string, any>>({});
const userId = "b0e17378-864d-439e-8806-708dc48a891b";
useEffect(() => {
async function fetchHistoryData() {
try {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
// 1. Fetch all attempts to find this specific one
const attemptsRes = await fetch(`${apiUrl}/api/users/${userId}/attempts`);
const attemptsData = await attemptsRes.json();
const foundAttempt = attemptsData.find((a: any) => a.id === attemptId);
if (!foundAttempt) {
throw new Error("Attempt not found");
}
setAttempt(foundAttempt);
// 2. Fetch the test details
const testRes = await fetch(`${apiUrl}/api/tests/${foundAttempt.source_id}?user_id=${userId}`);
const testData = await testRes.json();
setTest(testData);
// 3. Fetch all questions for this attempt to build the answer key
const qs: Record<string, any> = {};
await Promise.all(
foundAttempt.answers.map(async (ans: any) => {
const qRes = await fetch(`${apiUrl}/api/questions/${ans.question_id}`);
if (qRes.ok) {
const qData = await qRes.json();
qs[ans.question_id] = qData;
}
})
);
setQuestions(qs);
} catch (err) {
console.error("Failed to load attempt history:", err);
} finally {
setLoading(false);
}
}
fetchHistoryData();
}, [attemptId, userId]);
if (loading) {
return (
<div className="flex h-screen items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
);
}
if (!attempt) {
return (
<div className="p-8 text-center text-muted-foreground">
Attempt not found. <Link href="/dashboard/cbt" className="text-primary underline">Go back</Link>
</div>
);
}
const scoreCalc = (attempt.score * 4) - ((attempt.total_questions - attempt.score) * 1);
const maxScore = attempt.total_questions * 4;
return (
<div className="max-w-6xl mx-auto p-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<Link href="/dashboard/cbt" className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground mb-6 transition-colors">
<ArrowLeft size={16} className="mr-2" /> Back to Tests
</Link>
<div className="flex flex-col md:flex-row gap-8 mb-10">
<div className="flex-1">
<h1 className="text-3xl font-bold tracking-tight mb-2">{test?.title || 'Practice Test'} Solutions</h1>
<p className="text-muted-foreground">Review your performance, detailed solutions, and identify your weak spots.</p>
</div>
<div className="flex gap-4">
<Card className="bg-secondary/30 border-border min-w-32">
<CardContent className="p-4 flex flex-col items-center justify-center">
<Award className="text-[#00F0FF] mb-2" size={24} />
<div className="text-sm text-muted-foreground font-bold uppercase">Score</div>
<div className="text-2xl font-bold">{scoreCalc} <span className="text-sm text-muted-foreground">/ {maxScore}</span></div>
</CardContent>
</Card>
<Card className="bg-secondary/30 border-border min-w-32">
<CardContent className="p-4 flex flex-col items-center justify-center">
<Clock className="text-primary mb-2" size={24} />
<div className="text-sm text-muted-foreground font-bold uppercase">Time Taken</div>
<div className="text-2xl font-bold">{Math.floor(attempt.time_taken / 60)}m {attempt.time_taken % 60}s</div>
</CardContent>
</Card>
</div>
</div>
<div className="space-y-6">
<h2 className="text-xl font-bold border-b border-border pb-2">Answer Key & Explanations</h2>
{attempt.answers.map((ans: any, idx: number) => {
const q = questions[ans.question_id];
if (!q) return null;
return (
<Card key={ans.question_id} className={`border-l-4 ${ans.is_correct ? 'border-l-green-500' : 'border-l-red-500'}`}>
<CardContent className="p-6">
<div className="flex items-start justify-between gap-4 mb-4">
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-secondary flex items-center justify-center font-bold">
{idx + 1}
</div>
<div>
<h3 className="font-medium text-lg leading-relaxed">{q.text}</h3>
<div className="flex gap-2 mt-2">
<Badge variant="outline" className="text-xs">{q.topic}</Badge>
<Badge variant="outline" className="text-xs">{q.difficulty}</Badge>
</div>
</div>
</div>
{ans.is_correct ? (
<Badge className="bg-green-500/20 text-green-500 border-green-500/30 flex items-center gap-1"><CheckCircle2 size={14}/> Correct</Badge>
) : (
<Badge className="bg-red-500/20 text-red-500 border-red-500/30 flex items-center gap-1"><XCircle size={14}/> Incorrect</Badge>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 ml-12">
<div className="space-y-2">
<p className="text-sm font-bold text-muted-foreground uppercase">Your Answer</p>
<div className={`p-3 rounded-lg border ${ans.is_correct ? 'bg-green-500/10 border-green-500/30 text-green-500' : 'bg-red-500/10 border-red-500/30 text-red-500'}`}>
{q.options[ans.selected_answer] ? `${ans.selected_answer}) ${q.options[ans.selected_answer]}` : 'Not Attempted'}
</div>
</div>
<div className="space-y-2">
<p className="text-sm font-bold text-muted-foreground uppercase">Correct Answer</p>
<div className="p-3 rounded-lg border bg-green-500/10 border-green-500/30 text-green-500">
{q.correct_answer?.correct}) {q.options[q.correct_answer?.correct]}
</div>
</div>
</div>
{q.correct_answer?.explanation && (
<div className="mt-6 ml-12 p-4 bg-secondary/30 rounded-xl border border-border">
<p className="text-sm font-bold mb-2">Explanation:</p>
<p className="text-muted-foreground">{q.correct_answer.explanation}</p>
</div>
)}
</CardContent>
</Card>
);
})}
</div>
</div>
);
}

View File

@@ -1,30 +1,95 @@
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Activity, Clock, PlayCircle, Search } from "lucide-react"
"use client";
import { useEffect, useState } from "react";
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";
interface TestData {
id: string;
title: string;
type: string;
exam_pattern: string;
total_time: number;
difficulty: string;
}
export default function CBTPage() {
const upcomingExams = [
{ title: "AITS Full Syllabus Test 4", date: "Tomorrow, 10:00 AM", duration: "180 Min", marks: 720 },
{ title: "Part Test 3: Mechanics", date: "Oct 24, 2026", duration: "180 Min", marks: 720 },
{ title: "Weekly Revision Test", date: "Oct 28, 2026", duration: "90 Min", marks: 360 },
]
const [exams, setExams] = useState<TestData[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [targetExam, setTargetExam] = useState("");
const [attempts, setAttempts] = useState<any[]>([]);
const pastExams = [
{ title: "AITS Full Syllabus Test 3", date: "Oct 15, 2026", score: 645, total: 720, percentile: "99.4" },
{ title: "Part Test 2: Electromagnetism", date: "Oct 08, 2026", score: 680, total: 720, percentile: "99.8" },
]
const userId = "b0e17378-864d-439e-8806-708dc48a891b";
useEffect(() => {
async function loadCBTExams() {
try {
// 1. Fetch user target exam
const userRes = await fetch(`http://localhost:8000/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);
// 3. Fetch user attempts
const attemptsRes = await fetch(`http://localhost:8000/api/users/${userId}/attempts`);
if (attemptsRes.ok) {
const attemptsData = await attemptsRes.json();
setAttempts(attemptsData);
}
} catch (err) {
console.error("Error loading exams:", err);
} finally {
setLoading(false);
}
}
loadCBTExams();
}, []);
const filteredExams = exams.filter(exam =>
exam.title.toLowerCase().includes(search.toLowerCase())
);
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString("en-US", { year: 'numeric', month: 'short', day: 'numeric' });
}
// Get the highest score for each test if attempted multiple times
const attemptsByTest = attempts.reduce((acc, curr) => {
if (!acc[curr.source_id] || acc[curr.source_id].score < curr.score) {
acc[curr.source_id] = curr;
}
return acc;
}, {} as Record<string, any>);
return (
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-6xl mx-auto">
<div className="flex justify-between items-end">
<div>
<h1 className="text-3xl font-bold tracking-tight">Computer Based Tests (CBT)</h1>
<p className="text-muted-foreground mt-1 font-medium">Real exam environment simulations to conquer NEET/JEE.</p>
<p className="text-muted-foreground mt-1 font-medium">
Real exam environment simulations to conquer {targetExam || "NEET/JEE"}.
</p>
</div>
<div className="relative w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={16} />
<input type="text" placeholder="Search exams..." className="w-full pl-10 pr-4 py-2 bg-background border border-border rounded-xl text-sm focus:ring-2 focus:ring-[#00F0FF] outline-none" />
<input
type="text"
placeholder="Search exams..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-background border border-border rounded-xl text-sm focus:ring-2 focus:ring-[#00F0FF] outline-none"
/>
</div>
</div>
@@ -32,23 +97,49 @@ export default function CBTPage() {
<h2 className="text-xl font-bold border-b border-border pb-2 flex items-center gap-2">
<Clock size={20} className="text-[#00F0FF]" /> Upcoming & Active Exams
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{upcomingExams.map((exam, i) => (
<Card key={i} className="hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group">
<CardContent className="p-6">
<Badge variant="outline" className="mb-4 bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20">Mock Test</Badge>
<h3 className="font-bold text-lg leading-tight mb-2 group-hover:text-[#00F0FF] transition-colors">{exam.title}</h3>
<div className="text-sm text-muted-foreground space-y-1 mb-6">
<div className="font-medium text-foreground">{exam.date}</div>
<div>{exam.duration} • {exam.marks} Marks</div>
</div>
<Button className="w-full gap-2 rounded-xl bg-secondary text-foreground hover:bg-[#00F0FF] hover:text-background transition-colors font-bold">
<PlayCircle size={16} /> Start Exam
</Button>
</CardContent>
</Card>
))}
</div>
{loading ? (
<div className="py-12 text-center text-muted-foreground">Loading test catalog...</div>
) : filteredExams.length === 0 ? (
<div className="py-12 text-center text-muted-foreground bg-secondary/10 border border-dashed border-border rounded-2xl">
No active exams found for {targetExam || "your target exam"}. Try switching it in settings!
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredExams.map((exam) => {
const attempt = attemptsByTest[exam.id];
const isAttempted = !!attempt;
return (
<Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isAttempted ? 'border-primary/20' : ''}`}>
<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 && (
<Badge variant="secondary" className="bg-green-500/10 text-green-500 border-green-500/20">
Attempted
</Badge>
)}
</div>
<h3 className="font-bold text-lg leading-tight mb-2 group-hover:text-[#00F0FF] transition-colors">
{exam.title}
</h3>
<div className="text-sm text-muted-foreground space-y-1 mb-6">
<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'}
</Button>
</Link>
</CardContent>
</Card>
)})}
</div>
)}
</div>
<div className="space-y-6 pt-8">
@@ -56,29 +147,44 @@ export default function CBTPage() {
<Activity size={20} className="text-muted-foreground" /> Past Attempts
</h2>
<div className="space-y-4">
{pastExams.map((exam, i) => (
{attempts.length === 0 ? (
<div className="py-8 text-center text-muted-foreground bg-secondary/10 border border-dashed border-border rounded-2xl">
You haven't attempted any CBT exams yet.
</div>
) : attempts.map((attempt, i) => {
const exam = exams.find(e => e.id === attempt.source_id);
const scoreCalc = (attempt.score * 4) - ((attempt.total_questions - attempt.score) * 1); // rough calc for display
return (
<Card key={i} className="bg-secondary/20">
<CardContent className="p-4 flex items-center justify-between">
<div>
<h3 className="font-bold">{exam.title}</h3>
<p className="text-sm text-muted-foreground font-medium">{exam.date}</p>
<CardContent className="p-4 flex flex-col md:flex-row items-center justify-between gap-4">
<div className="w-full md:w-auto">
<h3 className="font-bold">{exam?.title || 'Unknown Test'}</h3>
<p className="text-sm text-muted-foreground font-medium">{formatDate(attempt.submitted_at)}</p>
</div>
<div className="flex items-center gap-8">
<div className="flex items-center gap-4 md:gap-8 w-full md:w-auto justify-between md:justify-end">
<div className="text-right">
<div className="text-sm text-muted-foreground font-bold uppercase tracking-wider">Score</div>
<div className="font-bold text-lg"><span className="text-foreground">{exam.score}</span><span className="text-muted-foreground">/{exam.total}</span></div>
<div className="text-xs md:text-sm text-muted-foreground font-bold uppercase tracking-wider">Score</div>
<div className="font-bold text-base md:text-lg">
<span className="text-foreground">{scoreCalc}</span>
<span className="text-muted-foreground">/{attempt.total_questions * 4}</span>
</div>
</div>
<div className="text-right">
<div className="text-sm text-muted-foreground font-bold uppercase tracking-wider">Percentile</div>
<div className="font-bold text-lg text-[#00F0FF]">{exam.percentile}</div>
<div className="text-xs md:text-sm text-muted-foreground font-bold uppercase tracking-wider">Accuracy</div>
<div className="font-bold text-base md:text-lg text-[#00F0FF]">{Math.round(attempt.accuracy)}%</div>
</div>
<Button variant="outline" className="rounded-xl border-border hover:bg-secondary">Review Analysis</Button>
<Link href={`/dashboard/cbt/history/${attempt.id}`}>
<Button variant="outline" className="rounded-xl border-border hover:bg-secondary">
View Solutions
</Button>
</Link>
</div>
</CardContent>
</Card>
))}
)})}
</div>
</div>
</div>
)
);
}

View File

@@ -0,0 +1,195 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
import { MessageSquare, ThumbsUp, ChevronUp, Search, Image as ImageIcon, CheckCircle } from "lucide-react"
interface Doubt {
id: string
author: string
avatar: string
timeAgo: string
subject: string
topic: string
title: string
content: string
upvotes: number
replies: number
isSolved: boolean
hasModeratorReply: boolean
}
export default function CommunityPage() {
const [activeFilter, setActiveFilter] = useState<'all' | 'my-doubts' | 'solved' | 'unsolved'>('all')
const [searchQuery, setSearchQuery] = useState('')
const mockDoubts: Doubt[] = [
{
id: "1",
author: "Rahul Verma",
avatar: "RV",
timeAgo: "2 hours ago",
subject: "Physics",
topic: "Rotational Mechanics",
title: "Why does friction act forwards in a rolling wheel?",
content: "I'm confused about the direction of friction when a car accelerates forward. My book says friction pushes the wheel forward, but shouldn't it oppose motion?",
upvotes: 42,
replies: 5,
isSolved: true,
hasModeratorReply: true
},
{
id: "2",
author: "Priya Sharma",
avatar: "PS",
timeAgo: "5 hours ago",
subject: "Chemistry",
topic: "Chemical Kinetics",
title: "How to quickly identify a pseudo first-order reaction?",
content: "Are there any tricks to spot these in MCQs? I always end up wasting 2 minutes trying to derive the rate law.",
upvotes: 18,
replies: 2,
isSolved: false,
hasModeratorReply: false
},
{
id: "3",
author: "Demo Student",
avatar: "DS",
timeAgo: "1 day ago",
subject: "Mathematics",
topic: "Definite Integration",
title: "Applying King's Rule when limits are -a to a",
content: "I keep messing up the signs when applying the property f(x) + f(-x). Can someone show a step-by-step for the modulus function?",
upvotes: 12,
replies: 8,
isSolved: true,
hasModeratorReply: true
}
]
const filteredDoubts = mockDoubts.filter(d => {
if (activeFilter === 'my-doubts' && d.author !== "Demo Student") return false
if (activeFilter === 'solved' && !d.isSolved) return false
if (activeFilter === 'unsolved' && d.isSolved) return false
if (searchQuery && !d.title.toLowerCase().includes(searchQuery.toLowerCase())) return false
return true
})
return (
<div className="p-8 space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-5xl mx-auto">
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight text-foreground flex items-center gap-3">
<MessageSquare className="text-[#00F0FF]" size={32} />
Doubt Forum
</h1>
<p className="text-muted-foreground mt-2 font-medium">Ask questions, share knowledge, and learn together with the community.</p>
</div>
<button className="px-5 py-2.5 bg-[#00F0FF] text-black font-bold rounded-xl shadow-[0_0_15px_-3px_rgba(0,240,255,0.4)] hover:bg-[#00d0dd] transition-colors">
Ask a Doubt
</button>
</div>
<div className="flex flex-col md:flex-row gap-4">
<div className="relative flex-1">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search size={18} className="text-muted-foreground" />
</div>
<input
type="text"
placeholder="Search doubts by keyword..."
className="w-full pl-10 pr-4 py-2.5 bg-card border border-border rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-[#00F0FF]/50"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="flex items-center gap-2 bg-secondary/50 p-1 rounded-xl border border-border shrink-0 overflow-x-auto">
<FilterButton active={activeFilter === 'all'} onClick={() => setActiveFilter('all')}>All Doubts</FilterButton>
<FilterButton active={activeFilter === 'my-doubts'} onClick={() => setActiveFilter('my-doubts')}>My Doubts</FilterButton>
<FilterButton active={activeFilter === 'solved'} onClick={() => setActiveFilter('solved')}>Solved</FilterButton>
<FilterButton active={activeFilter === 'unsolved'} onClick={() => setActiveFilter('unsolved')}>Unsolved</FilterButton>
</div>
</div>
<div className="space-y-4">
{filteredDoubts.map((doubt) => (
<Card key={doubt.id} className="border-border shadow-sm hover:border-[#00F0FF]/50 transition-colors cursor-pointer">
<CardContent className="p-0 flex">
{/* Upvote Sidebar */}
<div className="w-16 bg-secondary/20 border-r border-border flex flex-col items-center py-4 gap-1">
<button className="p-1.5 text-muted-foreground hover:text-[#00F0FF] hover:bg-[#00F0FF]/10 rounded-lg transition-colors">
<ChevronUp size={24} />
</button>
<span className="font-bold text-foreground">{doubt.upvotes}</span>
</div>
{/* Main Content */}
<div className="flex-1 p-5">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-xs font-bold px-2.5 py-1 bg-secondary text-foreground rounded-md">{doubt.subject}</span>
<span className="text-xs font-medium text-muted-foreground">• {doubt.topic}</span>
</div>
<div className="text-xs text-muted-foreground">{doubt.timeAgo}</div>
</div>
<h3 className="text-lg font-bold text-foreground mb-1">{doubt.title}</h3>
<p className="text-sm text-muted-foreground line-clamp-2 mb-4">{doubt.content}</p>
<div className="flex items-center justify-between mt-auto">
<div className="flex items-center gap-2">
<div className="h-6 w-6 rounded-full bg-secondary flex items-center justify-center text-[10px] font-bold">
{doubt.avatar}
</div>
<span className="text-xs font-medium text-foreground">{doubt.author}</span>
{doubt.hasModeratorReply && (
<span className="ml-2 flex items-center gap-1 text-[10px] font-bold text-emerald-500 bg-emerald-500/10 px-2 py-0.5 rounded-sm border border-emerald-500/20">
<CheckCircle size={10} /> Expert Replied
</span>
)}
</div>
<div className="flex items-center gap-4">
{doubt.isSolved && (
<span className="text-xs font-bold text-emerald-500 flex items-center gap-1">
<CheckCircle size={14} /> Solved
</span>
)}
<div className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-colors text-sm font-medium">
<MessageSquare size={16} />
{doubt.replies} Replies
</div>
</div>
</div>
</div>
</CardContent>
</Card>
))}
{filteredDoubts.length === 0 && (
<div className="text-center py-12 text-muted-foreground">
<MessageSquare size={48} className="mx-auto mb-4 opacity-20" />
<p className="font-medium">No doubts found matching your criteria.</p>
</div>
)}
</div>
</div>
)
}
function FilterButton({ children, active, onClick }: { children: React.ReactNode, active: boolean, onClick: () => void }) {
return (
<button
onClick={onClick}
className={`px-4 py-1.5 rounded-lg text-sm font-medium transition-colors whitespace-nowrap ${
active
? 'bg-background shadow-sm border border-border text-foreground'
: 'text-muted-foreground hover:text-foreground'
}`}
>
{children}
</button>
)
}

View File

@@ -1,29 +1,55 @@
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { BrainCircuit, Play, CheckCircle2, ChevronRight } from "lucide-react"
import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock } from "lucide-react"
export default function DPPPage() {
const dpps = [
{ title: "Rotational Dynamics: Top Overturning", subject: "Physics", count: 15, status: "Pending", priority: "High" },
{ title: "Chemical Kinetics: First Order Reactions", subject: "Chemistry", count: 20, status: "Pending", priority: "Medium" },
{ title: "Genetics: Mendelian Inheritance", subject: "Biology", count: 25, status: "Completed", score: "23/25" },
{ title: "Calculus: Definite Integration", subject: "Mathematics", count: 15, status: "Completed", score: "14/15" },
{ title: "Top Overturning", subject: "Physics", topic: "Rotational Dynamics", difficulty: "Hard", count: 15, status: "Pending", priority: "High" },
{ title: "First Order Reactions", subject: "Chemistry", topic: "Chemical Kinetics", difficulty: "Medium", count: 20, status: "Pending", priority: "Medium" },
{ title: "Mendelian Inheritance", subject: "Biology", topic: "Genetics", difficulty: "Easy", count: 25, status: "Completed", score: "23/25" },
{ title: "Definite Integration", subject: "Mathematics", topic: "Calculus", difficulty: "Mixed", count: 15, status: "Completed", score: "14/15" },
]
const getDifficultyColor = (diff: string) => {
switch(diff) {
case 'Easy': return 'text-emerald-500 bg-emerald-500/10 border-emerald-500/20';
case 'Medium': return 'text-amber-500 bg-amber-500/10 border-amber-500/20';
case 'Hard': return 'text-destructive bg-destructive/10 border-destructive/20';
default: return 'text-primary bg-primary/10 border-primary/20';
}
}
return (
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-5xl mx-auto">
<div className="flex justify-between items-center p-8 bg-gradient-to-r from-secondary/50 to-background border border-border rounded-2xl shadow-sm">
<div className="flex flex-col md:flex-row md:items-center justify-between p-8 bg-gradient-to-r from-secondary/50 to-background border border-border rounded-2xl shadow-sm gap-6">
<div>
<h1 className="text-3xl font-bold tracking-tight mb-2">Daily Practice Problems (DPP)</h1>
<p className="text-muted-foreground font-medium max-w-xl">
Hyper-personalized problem sets generated daily by our AI Evaluator to target your specific weak areas based on past exam performance.
</p>
</div>
<Button size="lg" className="h-14 px-8 rounded-xl bg-[#00F0FF] text-background hover:bg-[#00F0FF]/90 font-bold shadow-[0_0_20px_-5px_#00F0FF]">
<Button size="lg" className="h-14 px-8 rounded-xl bg-[#00F0FF] text-background hover:bg-[#00F0FF]/90 font-bold shadow-[0_0_20px_-5px_#00F0FF] shrink-0">
<BrainCircuit className="mr-2" /> Generate Custom DPP
</Button>
</div>
<div className="flex flex-col sm:flex-row justify-between items-center gap-4 border-b border-border pb-6">
<div className="flex items-center gap-2">
<span className="text-sm font-bold text-muted-foreground mr-2"><Filter size={16} className="inline mr-1"/> Difficulty:</span>
{['Mixed', 'Easy', 'Medium', 'Hard'].map((diff) => (
<button key={diff} className={`px-3 py-1.5 rounded-lg text-xs font-bold border transition-colors ${diff === 'Mixed' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background text-muted-foreground border-border hover:border-primary/50'}`}>
{diff}
</button>
))}
<div className="flex items-center gap-1 ml-2 text-xs text-muted-foreground bg-secondary/50 px-2 py-1 rounded-md">
<Lock size={12}/> Pro
</div>
</div>
<Button variant="outline" className="gap-2 rounded-xl text-sm font-bold">
<Download size={16} /> Export as PDF <span className="ml-1 text-[10px] uppercase tracking-wider bg-primary/10 text-primary px-1.5 py-0.5 rounded-md">Pro</span>
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{dpps.map((dpp, i) => (
<Card key={i} className={`group cursor-pointer transition-all hover:shadow-md ${dpp.status === 'Completed' ? 'opacity-70 hover:opacity-100 bg-secondary/10' : 'hover:border-[#00F0FF]/50 bg-background'}`}>
@@ -33,14 +59,23 @@ export default function DPPPage() {
{dpp.status === 'Completed' ? <CheckCircle2 size={24} /> : <div className="size-6 rounded-full border-2 border-muted-foreground group-hover:border-[#00F0FF] transition-colors flex items-center justify-center"></div>}
</div>
<div>
<div className="text-xs font-bold uppercase tracking-wider text-[#00F0FF] mb-1">{dpp.subject}</div>
<h3 className="font-bold text-lg leading-tight group-hover:text-[#00F0FF] transition-colors">{dpp.title}</h3>
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-bold uppercase tracking-wider text-[#00F0FF]">{dpp.subject}</span>
<span className="text-muted-foreground text-xs font-bold">•</span>
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">{dpp.topic}</span>
</div>
<h3 className="font-bold text-lg leading-tight group-hover:text-[#00F0FF] transition-colors flex items-center gap-2">
{dpp.title}
<span className={`text-[10px] uppercase px-2 py-0.5 rounded-md border font-bold tracking-wider ${getDifficultyColor(dpp.difficulty)}`}>
{dpp.difficulty}
</span>
</h3>
<div className="text-sm text-muted-foreground mt-2 font-medium">
{dpp.count} Questions • {dpp.status === 'Completed' ? `Score: ${dpp.score}` : `${dpp.priority} Priority`}
</div>
</div>
</div>
<div className="text-muted-foreground group-hover:text-foreground transition-colors pl-4">
<div className="text-muted-foreground group-hover:text-foreground transition-colors pl-4 shrink-0">
{dpp.status === 'Completed' ? (
<Button variant="ghost" size="sm" className="font-bold">Review</Button>
) : (

View File

@@ -5,6 +5,11 @@ import { BookOpen, LayoutDashboard, Settings, FileText, Activity, LineChart, Tro
import Link from "next/link"
import Image from "next/image"
import { usePathname } from "next/navigation"
import { LanguageToggle } from "@/components/LanguageToggle"
import { ThemeToggle } from "@/components/ThemeToggle"
import { TierBadge } from "@/components/TierBadge"
import { StreakCounter } from "@/components/StreakCounter"
import { AITutorChat } from "@/components/AI/AITutorChat"
export default function DashboardLayout({ children }: { children: ReactNode }) {
const pathname = usePathname()
@@ -50,6 +55,13 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
<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")} />
<div className="pt-4 pb-2">
<p className="px-3 text-xs font-bold uppercase tracking-widest text-muted-foreground">Community & Tracking</p>
</div>
<NavItem href="/dashboard/leaderboard" icon={<Trophy size={18} />} label="Leaderboard" active={pathname.startsWith("/dashboard/leaderboard")} />
<NavItem href="/dashboard/progress" icon={<Activity size={18} />} label="Syllabus Tracker" active={pathname.startsWith("/dashboard/progress")} />
<NavItem href="/dashboard/community" icon={<BookOpen size={18} />} label="Doubt Forum" active={pathname.startsWith("/dashboard/community")} />
<div className="pt-4 pb-2">
<p className="px-3 text-xs font-bold uppercase tracking-widest text-muted-foreground">AI Intelligence</p>
</div>
@@ -57,8 +69,24 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
<NavItem href="/dashboard/rank-predictor" icon={<Trophy size={18} />} label="Rank Predictor" active={pathname.startsWith("/dashboard/rank-predictor")} />
</nav>
<div className="p-4 border-t border-border space-y-2">
<NavItem href="/dashboard/settings" icon={<Settings size={18} />} label="Settings" active={pathname.startsWith("/dashboard/settings")} />
<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 ${
pathname.startsWith("/dashboard/settings")
? "bg-background text-foreground font-medium border-border shadow-sm"
: "text-muted-foreground hover:bg-background/80 hover:text-foreground border-transparent hover:border-border/50"
}`}
>
<Settings size={18} />
<span className="text-sm">Settings</span>
</Link>
{/* Functional User Dropdown */}
<div className="relative mt-2" ref={dropdownRef}>
@@ -81,15 +109,15 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
<button
onClick={() => setDropdownOpen(!dropdownOpen)}
className="w-full flex items-center justify-between p-2 rounded-xl hover:bg-secondary/50 transition-colors border border-transparent hover:border-border"
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>
<div className="text-left">
<p className="text-sm font-bold leading-none">Demo Student</p>
<p className="text-xs text-[#00F0FF] font-medium mt-1">AI Pro Plan</p>
<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>
</div>
<ChevronUp size={16} className={`text-muted-foreground transition-transform ${dropdownOpen ? "rotate-180" : ""}`} />
@@ -99,9 +127,18 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
</aside>
{/* Main Content */}
<main className="flex-1 overflow-y-auto">
{children}
</main>
<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 />
</header>
<main className="flex-1 overflow-y-auto">
{children}
</main>
</div>
{/* Global AI Tutor Chat */}
<AITutorChat />
</div>
)
}

View File

@@ -0,0 +1,142 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Trophy, Medal, Star, Shield, Search } from "lucide-react"
type LeaderboardTab = 'all-india' | 'state' | 'school'
export default function LeaderboardPage() {
const [activeTab, setActiveTab] = useState<LeaderboardTab>('all-india')
const [isAnonymous, setIsAnonymous] = useState(false)
// Mock data representing F-07 requirements (Accuracy x Consistency)
const rankings = [
{ rank: 1, name: "Rahul Verma", score: 9850, accuracy: "92%", streak: 45, isCurrentUser: false },
{ rank: 2, name: "Priya Sharma", score: 9420, accuracy: "89%", streak: 42, isCurrentUser: false },
{ rank: 3, name: "Amit Kumar", score: 9100, accuracy: "87%", streak: 38, isCurrentUser: false },
{ rank: 4, name: "Sneha Patel", score: 8950, accuracy: "85%", streak: 30, isCurrentUser: false },
{ rank: 5, name: "Vikram Singh", score: 8800, accuracy: "84%", streak: 28, isCurrentUser: false },
{ rank: 6, name: "Demo Student", score: 8650, accuracy: "82%", streak: 25, isCurrentUser: true },
{ rank: 7, name: "Ananya Desai", score: 8500, accuracy: "80%", streak: 20, isCurrentUser: false },
{ rank: 8, name: "Karan Johar", score: 8450, accuracy: "79%", streak: 18, isCurrentUser: false },
{ rank: 9, name: "Neha Gupta", score: 8300, accuracy: "78%", streak: 15, isCurrentUser: false },
{ rank: 10, name: "Rohan Das", score: 8100, accuracy: "75%", streak: 12, isCurrentUser: false },
]
return (
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-5xl mx-auto">
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight text-foreground flex items-center gap-3">
<Trophy className="text-yellow-500" size={32} />
Leaderboard
</h1>
<p className="text-muted-foreground mt-2 font-medium">Rankings are based on your Accuracy × Consistency (Streak Bonus).</p>
</div>
<div className="flex items-center gap-3 bg-secondary/50 p-1.5 rounded-xl border border-border">
<button
onClick={() => setActiveTab('all-india')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${activeTab === 'all-india' ? 'bg-background shadow-sm border border-border text-foreground' : 'text-muted-foreground hover:text-foreground'}`}
>
All India
</button>
<button
onClick={() => setActiveTab('state')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${activeTab === 'state' ? 'bg-background shadow-sm border border-border text-foreground' : 'text-muted-foreground hover:text-foreground'}`}
>
State
</button>
<button
onClick={() => setActiveTab('school')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${activeTab === 'school' ? 'bg-background shadow-sm border border-border text-foreground' : 'text-muted-foreground hover:text-foreground'}`}
>
School
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-4">
<div className="flex justify-between items-center bg-card border border-border rounded-xl p-4 shadow-sm sticky top-0 z-10 backdrop-blur-md bg-opacity-90">
<span className="text-sm font-bold text-muted-foreground w-12 text-center">RANK</span>
<span className="text-sm font-bold text-muted-foreground flex-1 px-4">STUDENT</span>
<div className="flex gap-8 w-48 justify-end pr-4">
<span className="text-sm font-bold text-muted-foreground text-center">SCORE</span>
<span className="text-sm font-bold text-muted-foreground text-center">STREAK</span>
</div>
</div>
<div className="space-y-3">
{rankings.map((student) => (
<div
key={student.rank}
className={`flex justify-between items-center p-4 rounded-xl border transition-all ${
student.isCurrentUser
? 'border-[#00F0FF] bg-[#00F0FF]/5 shadow-[0_0_15px_-3px_rgba(0,240,255,0.2)]'
: 'border-border bg-card hover:bg-secondary/20'
}`}
>
<div className="w-12 flex justify-center">
{student.rank === 1 ? <Medal className="text-yellow-500" size={24} /> :
student.rank === 2 ? <Medal className="text-gray-400" size={24} /> :
student.rank === 3 ? <Medal className="text-amber-700" size={24} /> :
<span className={`font-bold text-lg ${student.isCurrentUser ? 'text-[#00F0FF]' : 'text-muted-foreground'}`}>#{student.rank}</span>}
</div>
<div className="flex-1 px-4 flex items-center gap-3">
<div className={`h-10 w-10 rounded-full flex items-center justify-center font-bold text-sm ${
student.isCurrentUser ? 'bg-[#00F0FF]/20 text-[#00F0FF]' : 'bg-secondary text-muted-foreground'
}`}>
{student.name.substring(0, 2).toUpperCase()}
</div>
<div>
<p className={`font-bold ${student.isCurrentUser ? 'text-[#00F0FF]' : 'text-foreground'}`}>
{student.isCurrentUser ? (isAnonymous ? "Anonymous User" : student.name + " (You)") : student.name}
</p>
<p className="text-xs text-muted-foreground flex items-center gap-1">
<Shield size={12} /> Accuracy: {student.accuracy}
</p>
</div>
</div>
<div className="flex gap-8 w-48 justify-end pr-4 items-center">
<div className="text-center font-mono font-bold text-foreground">
{student.score.toLocaleString()}
</div>
<div className="text-center flex items-center gap-1 font-bold text-orange-500">
<Star size={14} className="fill-orange-500" /> {student.streak}
</div>
</div>
</div>
))}
</div>
</div>
<div className="space-y-6">
<Card className="border-[#00F0FF]/30 bg-gradient-to-br from-[#00F0FF]/10 to-transparent">
<CardHeader className="pb-2">
<CardTitle className="text-xl flex items-center gap-2">Your Standing</CardTitle>
</CardHeader>
<CardContent>
<div className="text-4xl font-black text-[#00F0FF] mb-1">#6</div>
<p className="text-sm font-medium text-foreground mb-4">Top 15% of users</p>
<p className="text-xs text-muted-foreground leading-relaxed">
You are 150 points away from breaking into the Top 5. Complete today's DPS to earn your streak bonus!
</p>
<div className="mt-6 flex items-center justify-between border-t border-border pt-4">
<span className="text-sm font-medium text-foreground">Anonymous Mode</span>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" checked={isAnonymous} onChange={() => setIsAnonymous(!isAnonymous)} />
<div className="w-11 h-6 bg-secondary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#00F0FF]"></div>
</label>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
)
}

View File

@@ -6,7 +6,7 @@ export default function MaterialsPage() {
{ name: "Physics Formula Sheets", items: 12, color: "text-blue-500" },
{ name: "Organic Chem Mind Maps", items: 8, color: "text-emerald-500" },
{ name: "Biology NCERT Highlights", items: 22, color: "text-rose-500" },
{ name: "Past Year Papers (2015-2025)", items: 10, color: "text-amber-500" },
{ name: "Math Formulae", items: 15, color: "text-amber-500" },
]
const recentFiles = [

View File

@@ -1,76 +1,117 @@
"use client"
import { useRef } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Activity, BrainCircuit, ArrowRight, Clock } from "lucide-react"
import { Activity, BrainCircuit, ArrowRight, Clock, Target, CheckCircle2, TrendingUp, Lock, Trophy, Share2 } from "lucide-react"
import * as htmlToImage from 'html-to-image'
import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
import { AdaptiveStudyPlan } from "@/components/AI/AdaptiveStudyPlan"
export default function DashboardPage() {
const progressRef = useRef<HTMLDivElement>(null)
const handleExport = async () => {
if (progressRef.current) {
try {
const dataUrl = await htmlToImage.toPng(progressRef.current, { backgroundColor: '#09090b' })
const link = document.createElement('a')
link.download = 'pragativeer-progress.png'
link.href = dataUrl
link.click()
} catch (err) {
console.error("Failed to export image", err)
}
}
}
return (
<div className="p-8 max-w-6xl mx-auto space-y-10 animate-in fade-in duration-500 slide-in-from-bottom-4">
<header className="flex items-center justify-between">
<header className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight">Welcome back, Student</h1>
<p className="text-muted-foreground mt-1 font-medium">Here is your daily AI-curated study plan.</p>
<h1 className="text-3xl font-bold tracking-tight">Welcome back, Demo Student</h1>
<p className="text-muted-foreground mt-1 font-medium">Let's keep up the momentum for JEE Advanced 2027.</p>
</div>
<Button size="lg" className="gap-2 shadow-sm">
<Button size="lg" className="gap-2 shadow-sm bg-[#00F0FF] text-background hover:bg-[#00F0FF]/90 font-bold">
<BrainCircuit size={18} />
Generate AI Plan
</Button>
</header>
{/* AI Predicted Weak Areas */}
{/* Free Tier Upgrade CTA Banner */}
<Card className="bg-primary/5 border-primary/20 relative overflow-hidden">
<div className="absolute top-0 right-0 p-8 opacity-10">
<BrainCircuit size={120} />
</div>
<CardContent className="p-6 md:p-8 flex flex-col md:flex-row md:items-center justify-between gap-6 relative z-10">
<div className="space-y-2">
<Badge className="bg-primary text-primary-foreground font-bold tracking-widest uppercase mb-2">Upgrade to Pro</Badge>
<h2 className="text-2xl font-bold tracking-tight">Unlock Unlimited DPS & Mocks</h2>
<p className="text-muted-foreground max-w-xl font-medium">
You're currently on the Free Tier. Upgrade to Pro for ₹299/mo to access unlimited Daily Practice Sheets, weekly CBT tests, and advanced analytics.
</p>
</div>
<Button size="lg" className="shrink-0 rounded-xl shadow-[0_0_20px_-5px_var(--color-primary)] font-bold">
Upgrade Now
</Button>
</CardContent>
</Card>
{/* Basic Performance Dashboard (F-05) */}
<section>
<div className="flex items-center justify-between mb-5">
<h2 className="text-xl font-semibold tracking-tight">AI Focus Areas</h2>
<h2 className="text-xl font-semibold tracking-tight">Performance Overview</h2>
<Button onClick={handleExport} variant="outline" size="sm" className="gap-2 font-medium">
<Share2 size={16} /> Share Card
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="border-t-4 border-t-destructive">
<CardHeader className="pb-2">
<CardTitle className="text-lg">Rotational Motion</CardTitle>
<CardDescription>Physics</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Mastery</span>
<span className="font-medium text-destructive">42%</span>
</div>
<div className="w-full bg-secondary rounded-full h-1.5 mt-2 overflow-hidden">
<div className="bg-destructive h-1.5 rounded-full w-[42%]"></div>
</div>
<div ref={progressRef} className="grid grid-cols-2 md:grid-cols-4 gap-4 p-4 -mx-4 bg-background rounded-2xl">
<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">42 / 60</h3>
<p className="text-xs text-green-500 font-bold mt-2 flex items-center gap-1"><TrendingUp size={12}/> +5 from yesterday</p>
</CardContent>
</Card>
<Card className="border-t-4 border-t-amber-500">
<CardHeader className="pb-2">
<CardTitle className="text-lg">Chemical Kinetics</CardTitle>
<CardDescription>Chemistry</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Mastery</span>
<span className="font-medium text-amber-500">68%</span>
</div>
<div className="w-full bg-secondary rounded-full h-1.5 mt-2 overflow-hidden">
<div className="bg-amber-500 h-1.5 rounded-full w-[68%]"></div>
</div>
<Card>
<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>
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Overall Accuracy</p>
<h3 className="text-2xl font-bold">68%</h3>
<p className="text-xs text-amber-500 font-bold mt-2">Needs Improvement</p>
</CardContent>
</Card>
<Card className="border-t-4 border-t-emerald-500">
<CardHeader className="pb-2">
<CardTitle className="text-lg">Genetics</CardTitle>
<CardDescription>Biology</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Mastery</span>
<span className="font-medium text-emerald-500">89%</span>
</div>
<div className="w-full bg-secondary rounded-full h-1.5 mt-2 overflow-hidden">
<div className="bg-emerald-500 h-1.5 rounded-full w-[89%]"></div>
</div>
<Card>
<CardContent className="p-5 flex flex-col items-center text-center">
<div className="p-3 bg-secondary rounded-full mb-3 text-[#00F0FF]"><CheckCircle2 size={24} /></div>
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">Qs Attempted</p>
<h3 className="text-2xl font-bold">1,245</h3>
<p className="text-xs text-muted-foreground font-medium mt-2">Lifetime total</p>
</CardContent>
</Card>
<Card className="relative overflow-hidden group">
<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">
<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>
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider mb-1">City Rank</p>
<h3 className="text-2xl font-bold">#42 <span className="text-base text-muted-foreground font-medium">/ 1,200</span></h3>
<p className="text-xs text-muted-foreground font-medium mt-2 text-[#00F0FF]">Top 5% in Jaipur</p>
</CardContent>
</Card>
</div>
</section>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<section>
<AdaptiveStudyPlan />
</section>
<section>
<MistakeHeatmap />
</section>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Dynamic DPP Modules */}
<section>
@@ -96,19 +137,19 @@ export default function DashboardPage() {
</CardContent>
</Card>
<Card className="hover:border-primary/30 hover:shadow-md cursor-pointer transition-all duration-300 group">
<Card className="relative opacity-60 cursor-not-allowed border-dashed">
<CardContent className="p-5 flex items-center justify-between">
<div className="flex gap-4 items-center">
<div className="p-3 bg-secondary rounded-xl text-foreground">
<Activity size={20} />
<div className="p-3 bg-secondary rounded-xl text-muted-foreground">
<Lock size={20} />
</div>
<div>
<h3 className="font-medium text-base group-hover:text-primary transition-colors">Biology: Human Physiology</h3>
<p className="text-sm text-muted-foreground mt-0.5">30 Questions • Revision</p>
<h3 className="font-medium text-base text-muted-foreground">Biology: Human Physiology</h3>
<p className="text-sm text-muted-foreground mt-0.5 flex items-center gap-1"><Lock size={12}/> Pro Feature</p>
</div>
</div>
<Button variant="outline" size="sm" className="gap-2 rounded-xl group-hover:bg-primary group-hover:text-primary-foreground group-hover:border-primary transition-all">
Start <ArrowRight size={14} />
<Button variant="outline" size="sm" disabled className="gap-2 rounded-xl">
Locked
</Button>
</CardContent>
</Card>
@@ -136,10 +177,10 @@ export default function DashboardPage() {
</div>
<div className="p-5 bg-secondary/50 flex items-center justify-between transition-colors hover:bg-secondary/70">
<div>
<h3 className="font-medium text-sm text-foreground">Part Test 3: Mechanics</h3>
<p className="text-xs text-muted-foreground mt-0.5 font-medium">Oct 24, 2026</p>
<h3 className="font-medium text-sm text-foreground flex items-center gap-2"><Lock size={14} className="text-muted-foreground"/> Part Test 3: Mechanics</h3>
<p className="text-xs text-muted-foreground mt-0.5 font-medium ml-6">Oct 24, 2026</p>
</div>
<Button variant="ghost" size="sm" className="h-8 text-xs font-medium">Details</Button>
<Badge variant="secondary" className="text-xs">Pro Only</Badge>
</div>
</Card>
</section>

View File

@@ -0,0 +1,192 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { ChevronDown, ChevronRight, CheckCircle2, Circle, Clock, Activity, Target } from "lucide-react"
type ChapterStatus = 'Mastered' | 'Practiced' | 'In Progress' | 'Not Started'
interface Topic {
name: string
status: ChapterStatus
accuracy: number
questionsAttempted: number
}
interface Chapter {
name: string
topics: Topic[]
isExpanded: boolean
}
interface Subject {
name: string
chapters: Chapter[]
completionPct: number
}
const getStatusIcon = (status: ChapterStatus) => {
switch (status) {
case 'Mastered': return <CheckCircle2 className="text-emerald-500" size={16} />
case 'Practiced': return <CheckCircle2 className="text-amber-500" size={16} />
case 'In Progress': return <Clock className="text-[#00F0FF]" size={16} />
case 'Not Started': return <Circle className="text-muted-foreground" size={16} />
}
}
export default function ProgressTrackerPage() {
const [subjects, setSubjects] = useState<Subject[]>([
{
name: "Physics",
completionPct: 42,
chapters: [
{
name: "Kinematics",
isExpanded: true,
topics: [
{ name: "Motion in One Dimension", status: "Mastered", accuracy: 92, questionsAttempted: 150 },
{ name: "Projectile Motion", status: "Practiced", accuracy: 75, questionsAttempted: 85 },
{ name: "Relative Velocity", status: "In Progress", accuracy: 45, questionsAttempted: 20 },
]
},
{
name: "Laws of Motion",
isExpanded: false,
topics: [
{ name: "Newton's First Law", status: "Practiced", accuracy: 88, questionsAttempted: 40 },
{ name: "Friction", status: "In Progress", accuracy: 50, questionsAttempted: 35 },
]
},
{
name: "Thermodynamics",
isExpanded: false,
topics: [
{ name: "First Law", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
{ name: "Carnot Engine", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
]
}
]
},
{
name: "Chemistry",
completionPct: 68,
chapters: [
{
name: "Atomic Structure",
isExpanded: false,
topics: [
{ name: "Bohr Model", status: "Mastered", accuracy: 95, questionsAttempted: 110 },
{ name: "Quantum Numbers", status: "Mastered", accuracy: 88, questionsAttempted: 95 },
]
}
]
},
{
name: "Mathematics",
completionPct: 22,
chapters: [
{
name: "Calculus",
isExpanded: false,
topics: [
{ name: "Limits", status: "In Progress", accuracy: 60, questionsAttempted: 45 },
{ name: "Derivatives", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
]
}
]
}
])
const toggleChapter = (subjectIndex: number, chapterIndex: number) => {
const newSubjects = [...subjects]
newSubjects[subjectIndex].chapters[chapterIndex].isExpanded = !newSubjects[subjectIndex].chapters[chapterIndex].isExpanded
setSubjects(newSubjects)
}
return (
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-5xl mx-auto">
<div>
<h1 className="text-3xl font-bold tracking-tight text-foreground flex items-center gap-3">
<Target className="text-[#00F0FF]" size={32} />
Syllabus Tracker
</h1>
<p className="text-muted-foreground mt-2 font-medium">Track your syllabus coverage and mastery per chapter to ensure no gaps.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
{subjects.map((sub, i) => (
<Card key={i} className="border-border shadow-sm">
<CardContent className="p-6 text-center">
<h3 className="font-bold text-lg mb-2">{sub.name}</h3>
<div className="text-3xl font-black text-[#00F0FF] mb-3">{sub.completionPct}%</div>
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden">
<div className="h-full bg-[#00F0FF]" style={{ width: `${sub.completionPct}%` }}></div>
</div>
</CardContent>
</Card>
))}
</div>
<div className="space-y-6">
{subjects.map((subject, subIndex) => (
<Card key={subIndex} className="border-border shadow-sm overflow-hidden">
<CardHeader className="bg-secondary/30 pb-4 border-b border-border">
<CardTitle className="text-xl">{subject.name} Syllabus</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="divide-y divide-border">
{subject.chapters.map((chapter, chapIndex) => (
<div key={chapIndex} className="flex flex-col">
<button
onClick={() => toggleChapter(subIndex, chapIndex)}
className="flex items-center justify-between p-4 hover:bg-secondary/20 transition-colors text-left"
>
<div className="flex items-center gap-3">
{chapter.isExpanded ? <ChevronDown size={18} className="text-muted-foreground" /> : <ChevronRight size={18} className="text-muted-foreground" />}
<span className="font-bold">{chapter.name}</span>
</div>
<div className="text-xs font-medium bg-secondary px-3 py-1 rounded-full">
{chapter.topics.length} Topics
</div>
</button>
{chapter.isExpanded && (
<div className="bg-background px-6 pb-4 pt-1 space-y-2">
<div className="flex justify-between items-center text-xs font-bold text-muted-foreground px-2 py-2">
<span className="w-1/2">TOPIC</span>
<span className="w-1/4 text-center">ACCURACY</span>
<span className="w-1/4 text-right">QUESTIONS</span>
</div>
{chapter.topics.map((topic, topIndex) => (
<div key={topIndex} className="flex justify-between items-center bg-secondary/10 p-3 rounded-lg border border-border/50">
<div className="w-1/2 flex items-center gap-3">
{getStatusIcon(topic.status)}
<div>
<p className="font-medium text-sm">{topic.name}</p>
<p className="text-[10px] uppercase tracking-wider text-muted-foreground">{topic.status}</p>
</div>
</div>
<div className="w-1/4 text-center">
{topic.accuracy > 0 ? (
<span className={`font-mono text-sm font-bold ${topic.accuracy > 80 ? 'text-emerald-500' : topic.accuracy > 60 ? 'text-amber-500' : 'text-red-500'}`}>
{topic.accuracy}%
</span>
) : <span className="text-muted-foreground text-sm">-</span>}
</div>
<div className="w-1/4 text-right">
<span className="font-mono text-sm">{topic.questionsAttempted > 0 ? topic.questionsAttempted : '-'}</span>
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
</CardContent>
</Card>
))}
</div>
</div>
)
}

View File

@@ -1,7 +1,69 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
"use client";
import { useEffect, useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
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 [saving, setSaving] = useState(false);
const userId = "b0e17378-864d-439e-8806-708dc48a891b";
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);
}
}
fetchUserData();
}, []);
const handleSave = async () => {
setSaving(true);
try {
const res = await fetch(`http://localhost:8000/api/users/${userId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: `${firstName} ${lastName}`.trim(),
target_exam: targetExam,
}),
});
if (res.ok) {
alert("Settings saved successfully!");
} else {
alert("Failed to save settings: " + (await res.text()));
}
} catch (err) {
console.error("Error saving settings:", err);
alert("Network error. Could not save settings.");
} finally {
setSaving(false);
}
};
if (loading) {
return <div className="p-8">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>
@@ -19,18 +81,30 @@ export default function SettingsPage() {
<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" defaultValue="Demo" 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]" />
<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" defaultValue="Student" 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]" />
<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>
</div>
<div className="space-y-2">
<label className="text-sm font-bold">Email Address</label>
<input type="email" defaultValue="demo@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" />
<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 className="bg-foreground text-background hover:bg-foreground/90 font-bold mt-2">Save Changes</Button>
<Button onClick={handleSave} disabled={saving} className="bg-foreground text-background hover:bg-foreground/90 font-bold mt-2">
{saving ? "Saving..." : "Save Changes"}
</Button>
</CardContent>
</Card>
@@ -43,26 +117,61 @@ export default function SettingsPage() {
<div className="space-y-3">
<label className="text-sm font-bold">Target Examination</label>
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer p-4 border border-[#00F0FF] bg-[#00F0FF]/5 rounded-xl flex-1">
<input type="radio" name="target" defaultChecked className="accent-[#00F0FF] w-4 h-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>
</label>
<label className="flex items-center gap-2 cursor-pointer p-4 border border-border bg-secondary/20 hover:bg-secondary/50 rounded-xl flex-1 transition-colors">
<input type="radio" name="target" className="accent-[#00F0FF] w-4 h-4" />
</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>
</label>
</button>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-bold">Target Year</label>
<select 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">
<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>
</div>
)
);
}

View File

@@ -0,0 +1,99 @@
"use client"
import { Card, CardContent } from "@/components/ui/card"
import { Flame, Gift, Star, Trophy, Target, ArrowRight, CheckCircle2 } from "lucide-react"
export default function StreaksPage() {
const currentStreak = 12;
const bestStreak = 45;
const rewards = [
{ streak: 7, title: "Consistent Learner", reward: "1 Free Premium DPP", unlocked: true },
{ streak: 14, title: "Dedicated Scholar", reward: "AI Performance Analysis Report", unlocked: false },
{ streak: 30, title: "Exam Warrior", reward: "1 Full Length Mock Test", unlocked: false },
{ streak: 50, title: "PragatiVeer Champion", reward: "1-on-1 AI Tutor Session", unlocked: false },
{ streak: 100, title: "Unstoppable", reward: "Merch + 50% Off Next Renewal", unlocked: false },
];
return (
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-5xl mx-auto">
<div className="flex items-center gap-4">
<div className="size-16 rounded-2xl bg-orange-500/10 text-orange-500 flex items-center justify-center">
<Flame size={32} />
</div>
<div>
<h1 className="text-3xl font-bold tracking-tight">Your Learning Streak</h1>
<p className="text-muted-foreground mt-1 font-medium">Build daily habits to unlock premium rewards and boost your rank.</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card className="bg-gradient-to-br from-orange-500/20 to-rose-500/5 border-orange-500/30">
<CardContent className="p-8 flex flex-col items-center justify-center text-center h-full">
<Flame size={64} className="text-orange-500 mb-4 animate-pulse" />
<h2 className="text-6xl font-black text-foreground mb-2">{currentStreak} <span className="text-2xl text-muted-foreground">Days</span></h2>
<p className="text-sm font-bold text-orange-500">CURRENT STREAK</p>
<p className="text-xs text-muted-foreground mt-4 max-w-xs">Complete a Daily Practice Problem (DPP) or CBT Exam every day to keep your streak alive.</p>
</CardContent>
</Card>
<Card className="bg-secondary/20 border-border">
<CardContent className="p-8 h-full flex flex-col justify-between">
<div>
<div className="flex justify-between items-center mb-6">
<p className="text-sm font-bold text-muted-foreground">STREAK STATS</p>
<Target size={20} className="text-primary" />
</div>
<div className="space-y-6">
<div>
<p className="text-sm text-muted-foreground mb-1">Personal Best</p>
<p className="text-3xl font-bold text-foreground">{bestStreak} <span className="text-lg text-muted-foreground">Days</span></p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1">Total Streak Points Earned</p>
<p className="text-3xl font-bold text-[#00F0FF]">1,450</p>
</div>
</div>
</div>
<div className="mt-8 pt-6 border-t border-border">
<p className="text-xs text-muted-foreground leading-relaxed">
<strong className="text-foreground">Pro Tip:</strong> Your streak directly multiplies your leaderboard score. A longer streak gives you a higher Consistency Bonus.
</p>
</div>
</CardContent>
</Card>
</div>
<div className="pt-8">
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2"><Gift className="text-[#00F0FF]" /> Streak Rewards</h2>
<div className="space-y-4">
{rewards.map((reward, i) => (
<div key={i} className={`p-5 rounded-2xl border flex items-center justify-between transition-colors ${reward.unlocked ? 'bg-primary/5 border-primary/30' : 'bg-secondary/10 border-border opacity-70'}`}>
<div className="flex items-center gap-6">
<div className={`size-14 rounded-full flex items-center justify-center font-bold text-xl ${reward.unlocked ? 'bg-primary text-primary-foreground shadow-[0_0_15px_-5px_var(--color-primary)]' : 'bg-secondary text-muted-foreground'}`}>
{reward.streak}
</div>
<div>
<h3 className={`font-bold ${reward.unlocked ? 'text-foreground' : 'text-muted-foreground'}`}>{reward.title}</h3>
<p className={`text-sm ${reward.unlocked ? 'text-primary font-medium' : 'text-muted-foreground'}`}>{reward.reward}</p>
</div>
</div>
<div className="hidden sm:block">
{reward.unlocked ? (
<div className="flex items-center gap-2 text-primary font-bold text-sm bg-primary/10 px-4 py-2 rounded-xl">
<CheckCircle2 size={18} /> Unlocked
</div>
) : (
<div className="text-xs font-bold text-muted-foreground bg-secondary px-4 py-2 rounded-xl">
{reward.streak - currentStreak} days to go
</div>
)}
</div>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -111,3 +111,19 @@ body {
30% { transform: translateX(200%) skewX(-15deg); }
100% { transform: translateX(200%) skewX(-15deg); }
}
/* Custom Scrollbars */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--background);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--muted-foreground);
}

View File

@@ -27,6 +27,7 @@ export default function RootLayout({
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
suppressHydrationWarning
>
<body className="min-h-full flex flex-col">{children}</body>
</html>

View File

@@ -131,7 +131,7 @@ export default function MarketingPage() {
</Button>
</Link>
<Link href="/signup">
<Button size="lg" variant="outline" className="rounded-full h-14 px-8 text-base border-border bg-background/50 backdrop-blur-sm w-full sm:w-auto hover:bg-secondary/50 transition-colors group">
<Button size="lg" variant="outline" className="rounded-full h-14 px-8 text-base border-border bg-background/50 backdrop-blur-sm w-full sm:w-auto hover:bg-secondary/50 hover:text-foreground transition-colors group">
<Play className="mr-2 group-hover:scale-110 transition-transform text-[#00F0FF]" size={18} fill="currentColor" /> Watch Demo
</Button>
</Link>
@@ -413,16 +413,17 @@ export default function MarketingPage() {
transition={{ duration: 0.6, type: "spring" }}
>
<Card className="bg-background/80 backdrop-blur-md border-border p-10 rounded-[2.5rem] flex flex-col hover:border-border/80 transition-colors h-full z-10 relative shadow-xl">
<h3 className="text-3xl font-bold mb-2">Basic</h3>
<p className="text-muted-foreground mb-8 text-lg">Perfect for self-paced revision.</p>
<div className="text-6xl font-bold tracking-tighter mb-10">₹999<span className="text-xl text-muted-foreground font-medium">/mo</span></div>
<h3 className="text-3xl font-bold mb-2">Pro</h3>
<p className="text-muted-foreground mb-8 text-lg">Perfect for self-paced practice.</p>
<div className="text-6xl font-bold tracking-tighter mb-10">₹299<span className="text-xl text-muted-foreground font-medium">/mo</span></div>
<ul className="space-y-5 mb-10 flex-1 font-medium text-lg">
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-primary" /> 10 Full Syllabus CBTs</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-primary" /> Standard Question Bank</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-primary" /> Basic Analytics</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-primary" /> Unlimited Daily Practice Sheets</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-primary" /> Weekly Test Series & Mock Tests</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-primary" /> Detailed Performance Analytics</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-primary" /> Parent Dashboard Access</li>
</ul>
<Link href="/signup">
<Button variant="outline" size="lg" className="w-full rounded-2xl h-16 border-border font-bold text-lg hover:bg-secondary/50">Choose Basic</Button>
<Button variant="outline" size="lg" className="w-full rounded-2xl h-16 border-border font-bold text-lg hover:bg-secondary/50">Choose Pro</Button>
</Link>
</Card>
</motion.div>
@@ -440,17 +441,17 @@ export default function MarketingPage() {
<Badge className="bg-primary text-primary-foreground font-bold px-5 py-2 text-sm rounded-full shadow-[0_0_15px_-2px_var(--color-primary)]">MOST POPULAR</Badge>
</div>
<div className="relative z-10 flex flex-col h-full">
<h3 className="text-3xl font-bold mb-2">AI Pro</h3>
<h3 className="text-3xl font-bold mb-2">AI Coach</h3>
<p className="text-muted-foreground mb-8 text-lg">The complete AI-driven preparation system.</p>
<div className="text-6xl font-bold tracking-tighter mb-10 text-primary drop-shadow-[0_0_10px_var(--color-primary)]">₹2499<span className="text-xl text-foreground font-medium">/mo</span></div>
<div className="text-6xl font-bold tracking-tighter mb-10 text-primary drop-shadow-[0_0_10px_var(--color-primary)]">₹599<span className="text-xl text-foreground font-medium">/mo</span></div>
<ul className="space-y-5 mb-10 flex-1 font-medium text-lg">
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> Unlimited Dynamic CBTs</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> Adaptive AI DPPs</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> Interactive AI Tutor (24/7)</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> Predictive Rank Analytics</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> Everything in Pro</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> AI Spaced Repetition (Mistakes)</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> 24/7 Streaming AI Tutor Chat</li>
<li className="flex items-center gap-4"><CheckCircle2 size={24} className="text-[#00F0FF]" /> Predictive Rank & College Predictor</li>
</ul>
<Link href="/signup">
<Button size="lg" className="w-full rounded-2xl h-16 font-bold text-lg bg-primary text-primary-foreground hover:bg-primary/90 shadow-[0_0_20px_-5px_var(--color-primary)] transition-all">Get AI Pro</Button>
<Button size="lg" className="w-full rounded-2xl h-16 font-bold text-lg bg-primary text-primary-foreground hover:bg-primary/90 shadow-[0_0_20px_-5px_var(--color-primary)] transition-all">Get AI Coach</Button>
</Link>
</div>
</Card>

View File

@@ -0,0 +1,217 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"
import { Activity, Clock, Flame, AlertTriangle, RefreshCw } from "lucide-react"
// Hardcoded mock UUIDs for this sprint
const PARENT_ID = "parent-123"
const STUDENT_ID = "b0e17378-864d-439e-8806-708dc48a891b"
const DICTIONARY = {
en: {
title: "Parent Monitoring Portal",
subtitle: "Real-time analytics for your child's learning journey.",
lang_toggle: "हिंदी",
time_spent: "Weekly Time Spent",
minutes: "Minutes",
streak: "Active Streak",
days: "Days",
tests_attempted: "Tests Attempted",
tests: "This week",
accuracy_trend: "7-Day Accuracy Trend",
areas_of_concern: "Areas of Concern",
attention_needed: "Attention Needed",
pending_errors: "pending errors",
loading: "Loading child data...",
no_concerns: "No current areas of concern. Great job!"
},
hi: {
title: "अभिभावक निगरानी पोर्टल",
subtitle: "आपके बच्चे की सीखने की यात्रा के लिए वास्तविक समय विश्लेषिकी।",
lang_toggle: "English",
time_spent: "साप्ताहिक समय व्यतीत",
minutes: "मिनट",
streak: "सक्रिय लकीर",
days: "दिन",
tests_attempted: "परीक्षण का प्रयास",
tests: "इस सप्ताह",
accuracy_trend: "7-दिवसीय सटीकता प्रवृत्ति",
areas_of_concern: "चिंता के विषय",
attention_needed: "ध्यान देने की आवश्यकता है",
pending_errors: "लंबित त्रुटियां",
loading: "बच्चे का डेटा लोड हो रहा है...",
no_concerns: "चिंता का कोई विषय नहीं है। बहुत बढ़िया!"
}
}
export default function ParentDashboard() {
const [lang, setLang] = useState<'en' | 'hi'>('en')
const [data, setData] = useState<any>(null)
const [loading, setLoading] = useState(true)
const t = DICTIONARY[lang]
useEffect(() => {
async function fetchDashboard() {
try {
const res = await fetch(`http://localhost:8000/api/parent/${PARENT_ID}/dashboard/${STUDENT_ID}`)
if (!res.ok) throw new Error("Failed to fetch")
const payload = await res.json()
setData(payload)
} catch (err) {
console.error(err)
// Fallback mock data if server fails
setData({
student_name: "Rahul",
streak_count: 5,
weekly_minutes_spent: 124.5,
total_tests_attempted_this_week: 3,
accuracy_trend: [
{ date: "Jul 01", accuracy: 40 },
{ date: "Jul 02", accuracy: 55 },
{ date: "Jul 03", accuracy: 50 },
{ date: "Jul 04", accuracy: 70 },
{ date: "Jul 05", accuracy: 65 },
{ date: "Jul 06", accuracy: 80 },
{ date: "Jul 07", accuracy: 85 }
],
areas_of_concern: [
{ topic: "Thermodynamics", pending_errors: 4 },
{ topic: "Integration", pending_errors: 2 }
]
})
} finally {
setLoading(false)
}
}
fetchDashboard()
}, [])
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<RefreshCw className="animate-spin text-muted-foreground mr-3" /> {t.loading}
</div>
)
}
return (
<div className="p-8 max-w-6xl mx-auto space-y-10 animate-in fade-in duration-500">
{/* Header & Language Toggle */}
<header className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-border pb-6">
<div>
<h1 className="text-3xl font-bold tracking-tight text-primary">
{t.title}
</h1>
<p className="text-muted-foreground mt-1 font-medium">{t.subtitle} — {data?.student_name}</p>
</div>
<div className="flex items-center space-x-3">
<Label htmlFor="lang-switch" className="font-bold text-muted-foreground">{lang === 'hi' ? 'English' : 'हिंदी'}</Label>
<Switch
id="lang-switch"
checked={lang === 'hi'}
onCheckedChange={(c) => setLang(c ? 'hi' : 'en')}
/>
</div>
</header>
{/* Metric Blocks */}
<section className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="border-border shadow-sm bg-secondary/10">
<CardContent className="p-6 flex items-center gap-5">
<div className="p-4 bg-primary/10 rounded-2xl text-primary"><Clock size={28}/></div>
<div>
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">{t.time_spent}</p>
<h3 className="text-3xl font-black mt-1">{data?.weekly_minutes_spent} <span className="text-sm font-semibold text-muted-foreground lowercase">{t.minutes}</span></h3>
</div>
</CardContent>
</Card>
<Card className="border-border shadow-sm bg-secondary/10">
<CardContent className="p-6 flex items-center gap-5">
<div className="p-4 bg-amber-500/10 rounded-2xl text-amber-500"><Flame size={28}/></div>
<div>
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">{t.streak}</p>
<h3 className="text-3xl font-black mt-1">{data?.streak_count} <span className="text-sm font-semibold text-muted-foreground lowercase">{t.days}</span></h3>
</div>
</CardContent>
</Card>
<Card className="border-border shadow-sm bg-secondary/10">
<CardContent className="p-6 flex items-center gap-5">
<div className="p-4 bg-emerald-500/10 rounded-2xl text-emerald-500"><Activity size={28}/></div>
<div>
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">{t.tests_attempted}</p>
<h3 className="text-3xl font-black mt-1">{data?.total_tests_attempted_this_week} <span className="text-sm font-semibold text-muted-foreground lowercase">{t.tests}</span></h3>
</div>
</CardContent>
</Card>
</section>
{/* Main Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Chart */}
<Card className="lg:col-span-2 border-border shadow-sm">
<CardHeader>
<CardTitle className="text-lg">{t.accuracy_trend}</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[300px] w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data?.accuracy_trend}>
<CartesianGrid strokeDasharray="3 3" stroke="#333" opacity={0.2} />
<XAxis dataKey="date" stroke="#888" fontSize={12} tickLine={false} axisLine={false} />
<YAxis stroke="#888" fontSize={12} tickLine={false} axisLine={false} domain={[0, 100]} tickFormatter={(val) => `${val}%`} />
<Tooltip
contentStyle={{ backgroundColor: 'hsl(var(--background))', borderColor: 'hsl(var(--border))', borderRadius: '8px' }}
itemStyle={{ color: 'hsl(var(--primary))' }}
/>
<Line
type="monotone"
dataKey="accuracy"
stroke="hsl(var(--primary))"
strokeWidth={4}
dot={{ r: 4, fill: 'hsl(var(--primary))' }}
activeDot={{ r: 8 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
{/* Areas of Concern */}
<Card className="border-border shadow-sm">
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<AlertTriangle className="text-red-500" size={20} />
{t.areas_of_concern}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{!data?.areas_of_concern || data.areas_of_concern.length === 0 ? (
<div className="text-center p-6 bg-secondary/20 rounded-xl">
<p className="text-muted-foreground font-medium">{t.no_concerns}</p>
</div>
) : (
data.areas_of_concern.map((concern: any, idx: number) => (
<div key={idx} className="p-4 rounded-xl border border-red-500/20 bg-red-500/5 relative overflow-hidden group">
<div className="absolute left-0 top-0 bottom-0 w-1 bg-red-500 rounded-l-xl"></div>
<h4 className="font-bold text-foreground mb-1">{concern.topic}</h4>
<p className="text-xs font-semibold text-red-500 uppercase tracking-wider">
{t.attention_needed} • {concern.pending_errors} {t.pending_errors}
</p>
</div>
))
)}
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -2,13 +2,24 @@
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ArrowLeft, Eye, EyeOff } from "lucide-react"
import { ArrowLeft, Eye, EyeOff, CheckCircle2 } from "lucide-react"
import Link from "next/link"
import Image from "next/image"
import { useState } from "react"
import { useRouter } from "next/navigation"
export default function SignupPage() {
const [step, setStep] = useState(1)
const [showPassword, setShowPassword] = useState(false)
const [authMethod, setAuthMethod] = useState<"email" | "phone">("email")
const [showOTP, setShowOTP] = useState(false)
const router = useRouter()
const handleNextStep = () => {
if (step === 1) setStep(2)
else router.push("/dashboard")
}
return (
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-6 selection:bg-primary/20">
<Link href="/" className="absolute top-8 left-8 text-muted-foreground hover:text-foreground flex items-center gap-2 transition-colors font-medium">
@@ -29,85 +40,149 @@ export default function SignupPage() {
</div>
</div>
{/* Progress Bar */}
<div className="flex flex-col gap-2">
<div className="flex justify-between text-xs font-bold text-muted-foreground">
<span className={step >= 1 ? "text-primary" : ""}>Account Details</span>
<span className={step >= 2 ? "text-primary" : ""}>Personalize Profile</span>
</div>
<div className="w-full bg-secondary h-2 rounded-full overflow-hidden">
<div
className="bg-primary h-full transition-all duration-500 ease-in-out"
style={{ width: step === 1 ? '50%' : '100%' }}
></div>
</div>
</div>
<Card className="border-border bg-secondary/10 backdrop-blur-xl shadow-[0_0_50px_-20px_var(--color-primary)] rounded-[2rem] overflow-hidden">
<CardHeader className="text-center pb-2">
<CardTitle className="text-2xl font-bold tracking-tight">Create an account</CardTitle>
<CardDescription className="text-muted-foreground">Start your AI-powered preparation journey today.</CardDescription>
<CardTitle className="text-2xl font-bold tracking-tight">
{step === 1 ? "Create an account" : "Complete your profile"}
</CardTitle>
<CardDescription className="text-muted-foreground">
{step === 1 ? "Start your AI-powered preparation journey today." : "Help us personalize your study plan."}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6 pt-4">
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-bold text-foreground">First Name</label>
<input
type="text"
placeholder="John"
className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
/>
{step === 1 ? (
<div className="space-y-4 animate-in fade-in zoom-in-95">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-bold text-foreground">First Name</label>
<input type="text" placeholder="John" className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-foreground">Last Name</label>
<input type="text" placeholder="Doe" className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"/>
</div>
</div>
<div className="flex bg-secondary p-1 rounded-xl">
<button type="button" onClick={() => setAuthMethod("email")} className={`flex-1 text-sm font-bold py-2 rounded-lg transition-all ${authMethod === "email" ? "bg-background shadow-sm text-foreground" : "text-muted-foreground hover:text-foreground"}`}>Email</button>
<button type="button" onClick={() => setAuthMethod("phone")} className={`flex-1 text-sm font-bold py-2 rounded-lg transition-all ${authMethod === "phone" ? "bg-background shadow-sm text-foreground" : "text-muted-foreground hover:text-foreground"}`}>Phone</button>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-foreground">Last Name</label>
<input
type="text"
placeholder="Doe"
className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
/>
<label className="text-sm font-bold text-foreground">{authMethod === "email" ? "Email Address" : "Phone Number"}</label>
<div className="flex gap-2">
<input type={authMethod === "email" ? "email" : "tel"} placeholder={authMethod === "email" ? "student@example.com" : "+91 9876543210"} className="flex-1 bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"/>
{!showOTP && (
<Button variant="secondary" onClick={() => setShowOTP(true)} className="h-[46px] rounded-xl font-bold">Send OTP</Button>
)}
</div>
</div>
{showOTP && (
<div className="space-y-2 animate-in slide-in-from-top-2">
<label className="text-sm font-bold text-foreground flex justify-between">
<span>Verification Code</span>
<span className="text-primary text-xs">Resend in 30s</span>
</label>
<div className="flex gap-2">
{[1,2,3,4,5,6].map((i) => (
<input key={i} type="text" maxLength={1} className="w-12 h-12 text-center text-lg font-bold bg-background border border-border rounded-xl focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all" />
))}
</div>
</div>
)}
<div className="space-y-2">
<label className="text-sm font-bold text-foreground">Password (Optional if using OTP)</label>
<div className="relative">
<input type={showPassword ? "text" : "password"} placeholder="••••••••" className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all pr-10"/>
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors">
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-foreground">Email</label>
<input
type="email"
placeholder="student@example.com"
className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
/>
) : (
<div className="space-y-4 animate-in fade-in slide-in-from-right-8">
<div className="space-y-2">
<label className="text-sm font-bold text-foreground">Target Exam</label>
<select className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all appearance-none cursor-pointer">
<option value="jee_main">JEE Main</option>
<option value="jee_adv">JEE Advanced (Main + Adv)</option>
<option value="neet">NEET UG</option>
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-foreground">Current Class</label>
<select className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all appearance-none cursor-pointer">
<option value="11">Class 11</option>
<option value="12">Class 12</option>
<option value="dropper">Dropper / 12th Pass</option>
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-foreground">Study Medium</label>
<select className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all appearance-none cursor-pointer">
<option value="en">English</option>
<option value="hi">Hindi (हिंदी)</option>
<option value="bi">Bilingual (English + Hindi)</option>
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-foreground">School / Coaching Name <span className="text-muted-foreground font-normal">(Optional)</span></label>
<input type="text" placeholder="e.g. DPS R.K. Puram" className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-foreground">Password</label>
)}
<Button onClick={handleNextStep} className="w-full rounded-xl h-12 font-bold text-base bg-primary text-primary-foreground hover:bg-primary/90 transition-all shadow-[0_0_20px_-5px_var(--color-primary)]">
{step === 1 ? "Continue" : "Complete Setup"}
</Button>
{step === 1 && (
<>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
placeholder="••••••••"
className="w-full bg-background border border-border rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all pr-10"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-border"></span>
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background/80 px-2 text-muted-foreground font-bold tracking-widest backdrop-blur-md">Or continue with</span>
</div>
</div>
</div>
</div>
<Link href="/dashboard" className="block">
<Button className="w-full rounded-xl h-12 font-bold text-base bg-primary text-primary-foreground hover:bg-primary/90 transition-all shadow-[0_0_20px_-5px_var(--color-primary)]">
Create Account
</Button>
</Link>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-border"></span>
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background/80 px-2 text-muted-foreground font-bold tracking-widest backdrop-blur-md">Or continue with</span>
</div>
</div>
<Link href="/dashboard" className="block">
<Button variant="outline" className="w-full rounded-xl h-12 font-bold bg-background border-border hover:bg-secondary/50 transition-all flex items-center justify-center gap-2">
<svg viewBox="0 0 24 24" className="h-5 w-5"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/><path d="M1 1h22v22H1z" fill="none"/></svg>
Google
</Button>
</Link>
<Button variant="outline" className="w-full rounded-xl h-12 font-bold bg-background border-border hover:bg-secondary/50 transition-all flex items-center justify-center gap-2">
<svg viewBox="0 0 24 24" className="h-5 w-5"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/><path d="M1 1h22v22H1z" fill="none"/></svg>
Google
</Button>
</>
)}
</CardContent>
</Card>
<p className="text-center text-sm text-muted-foreground font-medium">
Already have an account? <Link href="/login" className="font-bold text-foreground hover:text-primary transition-colors">Log in</Link>
</p>
{step === 1 ? (
<p className="text-center text-sm text-muted-foreground font-medium">
Already have an account? <Link href="/login" className="font-bold text-foreground hover:text-primary transition-colors">Log in</Link>
</p>
) : (
<button onClick={() => setStep(1)} className="w-full text-center text-sm font-bold text-muted-foreground hover:text-foreground transition-colors">
Go back
</button>
)}
</div>
</div>
)

226
src/app/upgrade/page.tsx Normal file
View File

@@ -0,0 +1,226 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Check, X, ShieldCheck, Loader2 } from "lucide-react"
const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b"
const WEBHOOK_SECRET = "local_webhook_secret"
export default function UpgradePage() {
const [loadingTier, setLoadingTier] = useState<string | null>(null)
const [showModal, setShowModal] = useState(false)
const [orderData, setOrderData] = useState<any>(null)
const [processing, setProcessing] = useState(false)
const [success, setSuccess] = useState(false)
const handleUpgrade = async (tier: string) => {
setLoadingTier(tier)
try {
const res = await fetch(`http://localhost:8000/api/billing/create-order?user_id=${USER_ID}&plan_tier=${tier}`, {
method: "POST"
})
if (!res.ok) throw new Error("Order creation failed")
const order = await res.json()
setOrderData({ ...order, tier })
setShowModal(true)
} catch (err) {
console.error(err)
alert("Failed to create order")
} finally {
setLoadingTier(null)
}
}
const generateMockSignature = async (payloadStr: string) => {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(WEBHOOK_SECRET),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(payloadStr));
return Array.from(new Uint8Array(signature)).map(b => b.toString(16).padStart(2, '0')).join('');
}
const simulatePayment = async () => {
setProcessing(true)
try {
const payloadObj = {
event: "payment.captured",
payload: {
payment: {
entity: {
id: `pay_mock_${Date.now()}`,
amount: orderData.amount,
notes: {
user_id: USER_ID,
tier_requested: orderData.tier
}
}
}
}
}
const payloadStr = JSON.stringify(payloadObj)
const signature = await generateMockSignature(payloadStr)
const res = await fetch(`http://localhost:8000/api/billing/webhook/razorpay`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Razorpay-Signature": signature
},
body: payloadStr
})
if (!res.ok) throw new Error("Webhook failed")
setSuccess(true)
setTimeout(() => {
window.location.href = "/dashboard"
}, 2000)
} catch (err) {
console.error(err)
alert("Payment simulation failed")
setProcessing(false)
}
}
return (
<div className="p-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
<div className="text-center max-w-2xl mx-auto mb-16">
<h1 className="text-4xl font-extrabold tracking-tight mb-4">Upgrade Your Preparation</h1>
<p className="text-xl text-muted-foreground">Unlock the full power of PragatiVeer to master JEE & NEET.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 items-start">
{/* Free Tier */}
<Card className="border-border">
<CardHeader>
<CardTitle className="text-2xl">Free</CardTitle>
<CardDescription>Basic access for everyone.</CardDescription>
<div className="mt-4 font-black text-4xl">₹0<span className="text-xl text-muted-foreground font-normal">/mo</span></div>
</CardHeader>
<CardContent className="space-y-4 text-sm font-medium">
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Subject-wise diagnostics</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Basic progress tracking</div>
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> Unlimited Tests</div>
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> AI Spaced Repetition</div>
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> Parent Analytics</div>
</CardContent>
<CardFooter>
<Button variant="outline" className="w-full" disabled>Current Plan</Button>
</CardFooter>
</Card>
{/* Pro Tier */}
<Card className="border-border border-primary shadow-lg shadow-primary/10 relative">
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-primary text-primary-foreground px-3 py-1 rounded-full text-xs font-bold tracking-wider uppercase">Most Popular</div>
<CardHeader>
<CardTitle className="text-2xl text-primary">Pro</CardTitle>
<CardDescription>For serious aspirants.</CardDescription>
<div className="mt-4 font-black text-4xl">₹299<span className="text-xl text-muted-foreground font-normal">/mo</span></div>
</CardHeader>
<CardContent className="space-y-4 text-sm font-medium">
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Everything in Free</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Unlimited Sectional Tests</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Performance Analytics</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Parent Dashboard</div>
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> AI Mistake Heatmap</div>
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> Spaced Repetition (SM-2)</div>
</CardContent>
<CardFooter>
<Button
className="w-full"
onClick={() => handleUpgrade("Pro")}
disabled={!!loadingTier}
>
{loadingTier === "Pro" ? <Loader2 className="animate-spin" /> : "Upgrade to Pro"}
</Button>
</CardFooter>
</Card>
{/* AI Coach Tier */}
<Card className="border-border bg-gradient-to-b from-primary/10 to-background border-primary">
<CardHeader>
<CardTitle className="text-2xl flex items-center gap-2">AI Coach <ShieldCheck size={24} className="text-primary"/></CardTitle>
<CardDescription>Personalized mastery loop.</CardDescription>
<div className="mt-4 font-black text-4xl">₹599<span className="text-xl text-muted-foreground font-normal">/mo</span></div>
</CardHeader>
<CardContent className="space-y-4 text-sm font-medium">
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Everything in Pro</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Daily AI Error Review Queue</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> SM-2 Spaced Repetition</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Mistake Heatmap Tracking</div>
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> AI Chat Tutor Support</div>
</CardContent>
<CardFooter>
<Button
className="w-full font-bold"
size="lg"
onClick={() => handleUpgrade("Al_Coach")}
disabled={!!loadingTier}
>
{loadingTier === "Al_Coach" ? <Loader2 className="animate-spin" /> : "Get AI Coach"}
</Button>
</CardFooter>
</Card>
</div>
{/* Mock Razorpay Modal */}
{showModal && (
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<Card className="max-w-md w-full shadow-2xl border-primary">
<CardHeader className="text-center pb-2">
<div className="mx-auto w-16 h-16 bg-blue-600 rounded-lg flex items-center justify-center mb-4">
<span className="text-white font-black text-2xl">RZP</span>
</div>
<CardTitle>Mock Checkout Window</CardTitle>
<CardDescription>Razorpay Test Sandbox Simulation</CardDescription>
</CardHeader>
<CardContent className="space-y-4 mt-4">
<div className="bg-secondary p-4 rounded-xl space-y-2 text-sm font-mono">
<div className="flex justify-between">
<span className="text-muted-foreground">Order ID:</span>
<span className="font-bold">{orderData?.id}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Amount:</span>
<span className="font-bold">₹{(orderData?.amount / 100).toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Tier:</span>
<span className="font-bold">{orderData?.tier}</span>
</div>
</div>
{success ? (
<div className="p-4 bg-green-500/10 text-green-500 rounded-xl text-center font-bold animate-in zoom-in">
Payment Captured! Redirecting...
</div>
) : (
<p className="text-sm text-muted-foreground text-center">
Clicking confirm will securely sign and POST a payment.captured webhook to the backend.
</p>
)}
</CardContent>
<CardFooter className="flex gap-3">
<Button variant="outline" className="w-full" onClick={() => setShowModal(false)} disabled={processing || success}>
Cancel
</Button>
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={simulatePayment} disabled={processing || success}>
{processing ? <Loader2 className="animate-spin" /> : "Confirm Payment"}
</Button>
</CardFooter>
</Card>
</div>
)}
</div>
)
}