generated from muhtadeetaron/nextjs-template
273 lines
8.2 KiB
TypeScript
273 lines
8.2 KiB
TypeScript
"use client";
|
||
|
||
import { useRouter } from "next/navigation";
|
||
import { useExam, useExamResults } from "@/context/ExamContext";
|
||
import { useEffect, useState } from "react";
|
||
import React from "react";
|
||
import { ArrowLeft } from "lucide-react";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import Image from "next/image";
|
||
import SlidingGallery from "@/components/SlidingGallery";
|
||
|
||
interface Question {
|
||
correctAnswer: string;
|
||
id: number;
|
||
question: string;
|
||
options: Record<string, string>;
|
||
solution?: string;
|
||
}
|
||
|
||
interface QuestionItemProps {
|
||
question: Question;
|
||
selectedAnswer: string | undefined;
|
||
}
|
||
|
||
const QuestionItem = ({ question, selectedAnswer }: QuestionItemProps) => (
|
||
<div className="border border-[#8abdff]/50 rounded-2xl p-4 flex flex-col gap-7">
|
||
<h3 className="text-xl font-medium">
|
||
{question.id}. {question.question}
|
||
</h3>
|
||
|
||
<div className="flex justify-between items-center">
|
||
<div></div>
|
||
|
||
{selectedAnswer?.answer === question.correctAnswer ? (
|
||
<Badge className="bg-green-500 text-white" variant="default">
|
||
Correct
|
||
</Badge>
|
||
) : selectedAnswer?.answer !== question.correctAnswer ? (
|
||
<Badge className="bg-red-500 text-white" variant="default">
|
||
Incorrect
|
||
</Badge>
|
||
) : (
|
||
<Badge className="bg-yellow-500" variant="destructive">
|
||
Skipped
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-4 items-start">
|
||
{Object.entries(question.options).map(([key, value]) => {
|
||
const isCorrect = key === question.correctAnswer;
|
||
const isSelected = key === selectedAnswer?.answer;
|
||
|
||
let optionStyle =
|
||
"px-2 py-1 flex items-center rounded-full border font-medium text-sm";
|
||
|
||
if (isCorrect) {
|
||
optionStyle += " bg-green-600 text-white border-green-600";
|
||
}
|
||
|
||
if (isSelected && !isCorrect) {
|
||
optionStyle += " bg-red-600 text-white border-red-600";
|
||
}
|
||
|
||
if (!isCorrect && !isSelected) {
|
||
optionStyle += " border-gray-300 text-gray-700";
|
||
}
|
||
|
||
return (
|
||
<div key={key} className="flex items-center gap-3">
|
||
<span className={optionStyle}>{key.toUpperCase()}</span>
|
||
<span className="option-description">{value}</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-2">
|
||
<h3 className="text-lg font-bold text-black/40">Solution:</h3>
|
||
<p className="text-lg">{question.solution}</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
export default function ResultsPage() {
|
||
const router = useRouter();
|
||
const { clearExam, isExamCompleted, getApiResponse } = useExam();
|
||
let examResults;
|
||
|
||
useEffect(() => {
|
||
if (!isExamCompleted()) {
|
||
router.push("/unit");
|
||
return;
|
||
}
|
||
}, [isExamCompleted, router]);
|
||
|
||
try {
|
||
examResults = useExamResults();
|
||
} catch (error) {
|
||
// Handle case where there's no completed exam
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||
<div className="text-center">
|
||
<div className="mt-60 flex flex-col items-center">
|
||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4"></div>
|
||
<p className="text-xl font-medium text-center">Loading...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Get API response data
|
||
const apiResponse = getApiResponse();
|
||
|
||
const handleBackToHome = () => {
|
||
clearExam();
|
||
|
||
// Give time for state to fully reset before pushing new route
|
||
setTimeout(() => {
|
||
router.push("/unit");
|
||
}, 400); // 50–100ms is usually enough
|
||
};
|
||
|
||
const timeTaken =
|
||
examResults.endTime && examResults.startTime
|
||
? Math.round(
|
||
(examResults.endTime.getTime() - examResults.startTime.getTime()) /
|
||
1000 /
|
||
60
|
||
)
|
||
: 0;
|
||
|
||
const resultViews = [
|
||
{
|
||
id: 1,
|
||
content: (
|
||
<div className="w-full">
|
||
<div className="bg-blue-50/60 border border-[#113678]/50 rounded-4xl h-[170px] flex flex-col items-center justify-center gap-4">
|
||
<div className="text-xl text-black ">
|
||
<span className="font-bold">Accuracy</span> Rate:
|
||
</div>
|
||
<div className="flex gap-4">
|
||
<Image
|
||
src="/images/icons/accuracy.png"
|
||
alt="accuracy"
|
||
width={60}
|
||
height={60}
|
||
/>
|
||
<h2 className="text-6xl font-bold text-[#113678]">
|
||
{(
|
||
(examResults.score / examResults.totalQuestions) *
|
||
100
|
||
).toFixed(1)}
|
||
%
|
||
</h2>
|
||
</div>
|
||
<div></div>
|
||
</div>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
id: 2,
|
||
content: (
|
||
<div className=" w-full">
|
||
<div className="bg-blue-50/60 border border-[#113678]/50 rounded-4xl h-[170px] flex flex-col items-center justify-center gap-3">
|
||
<div className="text-xl text-black ">
|
||
<span className="font-bold">Error</span> Rate:
|
||
</div>
|
||
<div className="flex gap-4">
|
||
<Image
|
||
src="/images/icons/error.png"
|
||
alt="accuracy"
|
||
width={60}
|
||
height={60}
|
||
/>
|
||
<h2 className="text-6xl font-bold text-[#113678]">
|
||
{(
|
||
((examResults.totalQuestions - examResults.score) /
|
||
examResults.totalQuestions) *
|
||
100
|
||
).toFixed(1)}
|
||
%
|
||
</h2>
|
||
</div>
|
||
<div></div>
|
||
</div>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
id: 3,
|
||
content: (
|
||
<div className="my-8 w-full">
|
||
<div className="bg-blue-50/60 border border-[#113678]/50 rounded-4xl h-[170px] flex flex-col items-center justify-center gap-4">
|
||
<div className="text-xl text-black">
|
||
<span className="font-bold">Attempt</span> Rate:
|
||
</div>
|
||
<div className="flex gap-4">
|
||
<Image
|
||
src="/images/icons/attempt.png"
|
||
alt="accuracy"
|
||
width={60}
|
||
height={60}
|
||
/>
|
||
<h2 className="text-6xl font-bold text-[#113678]">
|
||
{(
|
||
(examResults.answers.length / examResults.totalQuestions) *
|
||
100
|
||
).toFixed(1)}
|
||
%
|
||
</h2>
|
||
</div>
|
||
<div></div>
|
||
</div>
|
||
</div>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div className="min-h-screen bg-white">
|
||
<button className="p-10" onClick={() => router.push("/unit")}>
|
||
<ArrowLeft size={30} color="black" />
|
||
</button>
|
||
<div className="bg-white rounded-lg shadow-lg px-10 pb-20">
|
||
<h1 className="text-2xl font-bold text-gray-900 mb-4 text-center">
|
||
Keep up the good work!
|
||
</h1>
|
||
|
||
{/* Score Display */}
|
||
<SlidingGallery className="my-8" views={resultViews} height="170px" />
|
||
|
||
{apiResponse && (
|
||
<div className="mb-8">
|
||
<h3 className="text-2xl font-bold text-[#113768] mb-4">
|
||
Solutions
|
||
</h3>
|
||
<div className="flex flex-col gap-7">
|
||
{apiResponse.questions?.map((question) => (
|
||
<QuestionItem
|
||
key={question.id}
|
||
question={question}
|
||
selectedAnswer={examResults.answers?.[question.id]}
|
||
/>
|
||
))}
|
||
</div>
|
||
<div className="flex gap-4 items-center mb-6 text-sm text-gray-600">
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-4 h-4 bg-green-600 rounded-full"></div>{" "}
|
||
Correct
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-4 h-4 bg-red-600 rounded-full"></div> Your
|
||
Answer (Incorrect)
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Action Buttons */}
|
||
</div>
|
||
<button
|
||
onClick={handleBackToHome}
|
||
className="fixed bottom-0 w-full bg-blue-900 text-white h-[74px] font-bold text-lg disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
Finish
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|