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

@@ -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>
);
}