198 lines
9.5 KiB
TypeScript
198 lines
9.5 KiB
TypeScript
import React from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { CheckCircle2, XCircle, AlertCircle, Clock, Award, ArrowRight, RotateCcw, Share2, Mail } from "lucide-react";
|
|
|
|
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;
|
|
attemptId: string | null;
|
|
examType?: string; // 'CBT' or 'DPS'
|
|
onRetake: () => void;
|
|
}
|
|
|
|
export function ExamResult({ payload, attemptId, examType, onRetake }: ExamResultProps) {
|
|
const router = useRouter();
|
|
const isDPP = examType === 'DPS';
|
|
const backRoute = isDPP ? '/dashboard/ai-coach/cbt-dpp' : '/dashboard/cbt';
|
|
const backLabel = isDPP ? 'Back to PrepPilot AI' : 'Back to Mock Tests';
|
|
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 flex items-center justify-center gap-1.5 ${
|
|
finalScore <= 0
|
|
? "text-red-500"
|
|
: (finalScore / maxPossibleScore) >= 0.6
|
|
? "text-green-500"
|
|
: (finalScore / maxPossibleScore) >= 0.35
|
|
? "text-amber-500"
|
|
: "text-red-500"
|
|
}`}>
|
|
<span>{finalScore}</span>
|
|
<span className="text-lg text-muted-foreground font-medium">/ {maxPossibleScore}</span>
|
|
{finalScore <= 0 && (
|
|
<span className="text-[11px] font-extrabold uppercase px-2 py-0.5 rounded-md bg-red-500/10 text-red-500 border border-red-500/20">
|
|
Failing
|
|
</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" /> {isDPP ? 'Retake DPP' : 'Retake Mock Test'}
|
|
</Button>
|
|
{attemptId && (
|
|
<Button
|
|
variant="default"
|
|
size="lg"
|
|
className="rounded-xl h-14 px-8 font-bold text-lg bg-indigo-600 hover:bg-indigo-700 text-white gap-2 w-full sm:w-auto"
|
|
onClick={async () => {
|
|
try {
|
|
if (document.fullscreenElement) {
|
|
await document.exitFullscreen();
|
|
}
|
|
} catch (err) {
|
|
console.error("Error exiting fullscreen", err);
|
|
}
|
|
router.push(isDPP ? `/dashboard/ai-coach/cbt-dpp/history/${attemptId}` : `/dashboard/cbt/history/${attemptId}`);
|
|
}}
|
|
>
|
|
View Solutions
|
|
</Button>
|
|
)}
|
|
<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"
|
|
onClick={async () => {
|
|
try {
|
|
if (document.fullscreenElement) {
|
|
await document.exitFullscreen();
|
|
}
|
|
} catch (err) {
|
|
console.error("Error exiting fullscreen", err);
|
|
}
|
|
router.push(backRoute);
|
|
}}
|
|
>
|
|
{backLabel} <ArrowRight className="size-5" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-center pt-8 border-t border-border mt-8 space-y-4">
|
|
<h3 className="text-xl font-bold tracking-tight">Challenge a Friend</h3>
|
|
<p className="text-muted-foreground text-center max-w-md">Share this test with your friends and see if they can beat your score of {finalScore}!</p>
|
|
<div className="flex gap-4">
|
|
<Button variant="outline" className="gap-2 font-bold rounded-xl" onClick={() => {
|
|
const subject = encodeURIComponent("Can you beat my PragatiVeer Mock Test Score?");
|
|
const body = encodeURIComponent(`I just scored ${finalScore}/${maxPossibleScore} with ${Math.round(payload.accuracy)}% accuracy in a PragatiVeer Mock Test!\n\nTake the challenge here: https://pragativeer.com/challenge`);
|
|
window.open(`mailto:?subject=${subject}&body=${body}`);
|
|
}}>
|
|
<Mail className="size-4" /> Share via Email
|
|
</Button>
|
|
<Button className="gap-2 font-bold rounded-xl bg-green-500 hover:bg-green-600 text-white shadow-lg" onClick={() => {
|
|
const text = encodeURIComponent(`I just scored ${finalScore}/${maxPossibleScore} in a PragatiVeer Mock Test! Can you beat me? \nTake the challenge here: https://pragativeer.com/challenge`);
|
|
window.open(`https://wa.me/?text=${text}`, '_blank');
|
|
}}>
|
|
<Share2 className="size-4" /> Share on WhatsApp
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|