Files
pragativeer-v2/src/app/dashboard/cbt/page.tsx

191 lines
8.4 KiB
TypeScript

"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 [exams, setExams] = useState<TestData[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [targetExam, setTargetExam] = useState("");
const [attempts, setAttempts] = useState<any[]>([]);
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 {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..."
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>
<div className="space-y-6">
<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>
{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">
<h2 className="text-xl font-bold border-b border-border pb-2 flex items-center gap-2">
<Activity size={20} className="text-muted-foreground" /> Past Attempts
</h2>
<div className="space-y-4">
{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 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-4 md:gap-8 w-full md:w-auto justify-between md:justify-end">
<div className="text-right">
<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-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>
<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>
);
}