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,127 @@
import React from 'react';
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { CheckCircle2, XCircle, AlertCircle, Clock, Award, ArrowRight, RotateCcw } from "lucide-react";
import Link from 'next/link';
interface ExamAttemptPayload {
score: number;
total_questions: number;
accuracy: number;
time_taken: number;
answers: Array<{
question_id: string;
selected_answer: string | null;
is_correct: boolean;
}>;
}
interface ExamResultProps {
payload: ExamAttemptPayload;
onRetake: () => void;
}
export function ExamResult({ payload, onRetake }: ExamResultProps) {
const answered = payload.answers.filter(a => a.selected_answer !== null).length;
const unanswered = payload.total_questions - answered;
// Calculate correct based on score formula (+4 for correct, -1 for wrong).
// Wait, payload score is just correct count according to seed script right now, let's assume it.
const correctCount = payload.answers.filter(a => a.is_correct).length;
const incorrectCount = answered - correctCount;
// Real score calculation: +4 correct, -1 incorrect
const finalScore = (correctCount * 4) - (incorrectCount * 1);
const maxPossibleScore = payload.total_questions * 4;
const formatTime = (seconds: number) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
if (h > 0) return `${h}h ${m}m ${s}s`;
return `${m}m ${s}s`;
};
return (
<div className="min-h-screen bg-background flex flex-col items-center py-12 px-4 sm:px-6 animate-in fade-in zoom-in duration-500">
<div className="max-w-4xl w-full space-y-8">
<div className="text-center space-y-4">
<div className="inline-flex items-center justify-center size-20 rounded-full bg-green-500/10 mb-4">
<Award className="size-10 text-green-500" />
</div>
<h1 className="text-4xl font-extrabold tracking-tight">Exam Successfully Submitted</h1>
<p className="text-xl text-muted-foreground font-medium">Here is a quick snapshot of your performance.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mt-10">
<Card className="bg-secondary/20 border-border">
<CardContent className="p-6 flex flex-col items-center justify-center text-center space-y-2">
<span className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Final Score</span>
<div className="text-4xl font-black text-primary">
{finalScore} <span className="text-lg text-muted-foreground font-medium">/ {maxPossibleScore}</span>
</div>
</CardContent>
</Card>
<Card className="bg-secondary/20 border-border">
<CardContent className="p-6 flex flex-col items-center justify-center text-center space-y-2">
<span className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Accuracy</span>
<div className="text-4xl font-black text-blue-500">
{Math.round(payload.accuracy)}%
</div>
</CardContent>
</Card>
<Card className="bg-secondary/20 border-border">
<CardContent className="p-6 flex flex-col items-center justify-center text-center space-y-2">
<span className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Time Taken</span>
<div className="flex items-center gap-2 text-2xl font-black text-amber-500 mt-1">
<Clock className="size-6" /> {formatTime(payload.time_taken)}
</div>
</CardContent>
</Card>
<Card className="bg-secondary/20 border-border">
<CardContent className="p-6 flex flex-col items-center justify-center text-center space-y-2">
<span className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Attempted</span>
<div className="text-4xl font-black text-purple-500">
{answered} <span className="text-lg text-muted-foreground font-medium">/ {payload.total_questions}</span>
</div>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-3 gap-4 pt-4">
<div className="bg-green-500/10 border border-green-500/20 rounded-2xl p-6 text-center">
<CheckCircle2 className="size-8 text-green-500 mx-auto mb-2" />
<div className="text-3xl font-black text-green-500">{correctCount}</div>
<div className="text-sm font-bold text-green-600/70 uppercase tracking-wide mt-1">Correct</div>
</div>
<div className="bg-red-500/10 border border-red-500/20 rounded-2xl p-6 text-center">
<XCircle className="size-8 text-red-500 mx-auto mb-2" />
<div className="text-3xl font-black text-red-500">{incorrectCount}</div>
<div className="text-sm font-bold text-red-600/70 uppercase tracking-wide mt-1">Incorrect</div>
</div>
<div className="bg-secondary border border-border rounded-2xl p-6 text-center">
<AlertCircle className="size-8 text-muted-foreground mx-auto mb-2" />
<div className="text-3xl font-black text-foreground">{unanswered}</div>
<div className="text-sm font-bold text-muted-foreground uppercase tracking-wide mt-1">Unanswered</div>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-4 justify-center pt-8">
<Button variant="outline" size="lg" className="rounded-xl h-14 px-8 font-bold text-lg gap-2" onClick={onRetake}>
<RotateCcw className="size-5" /> Retake Mock Test
</Button>
<Link href="/dashboard/cbt">
<Button size="lg" className="rounded-xl h-14 px-8 font-bold text-lg shadow-[0_0_20px_-5px_var(--color-primary)] gap-2 w-full sm:w-auto">
Dashboard <ArrowRight className="size-5" />
</Button>
</Link>
</div>
</div>
</div>
);
}