generated from muhtadeetaron/nextjs-template
317 lines
9.5 KiB
TypeScript
317 lines
9.5 KiB
TypeScript
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import { useExam, useExamResults } from "@/context/ExamContext";
|
|
import { useEffect, useState, useRef } 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 ? (
|
|
<Badge className="bg-yellow-500" variant="destructive">
|
|
Skipped
|
|
</Badge>
|
|
) : selectedAnswer.answer === question.correctAnswer ? (
|
|
<Badge className="bg-green-500 text-white" variant="default">
|
|
Correct
|
|
</Badge>
|
|
) : (
|
|
<Badge className="bg-red-500 text-white" variant="default">
|
|
Incorrect
|
|
</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="h-[0.5px] border-[0.5px] border-dashed border-black/20"></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() {
|
|
// All hooks at the top - no conditional calls
|
|
const router = useRouter();
|
|
const { clearExam, isExamCompleted, getApiResponse } = useExam();
|
|
|
|
// Add a ref to track if we're in cleanup mode
|
|
const isCleaningUp = useRef(false);
|
|
|
|
// Conditionally call useExamResults based on cleanup state
|
|
const examResults = !isCleaningUp.current ? useExamResults() : null;
|
|
|
|
// State to control component behavior
|
|
const [componentState, setComponentState] = useState<
|
|
"loading" | "redirecting" | "ready"
|
|
>("loading");
|
|
|
|
// Single useEffect to handle all initialization logic
|
|
useEffect(() => {
|
|
let mounted = true;
|
|
|
|
const initializeComponent = async () => {
|
|
// Allow time for all hooks to initialize
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
|
|
if (!mounted) return;
|
|
|
|
// Check if exam is completed
|
|
if (!isExamCompleted()) {
|
|
setComponentState("redirecting");
|
|
// Small delay before redirect to prevent hook order issues
|
|
setTimeout(() => {
|
|
if (mounted) {
|
|
router.push("/unit");
|
|
}
|
|
}, 100);
|
|
return;
|
|
}
|
|
|
|
// Check if we have exam results
|
|
if (!examResults || !examResults.answers) {
|
|
// Keep loading state
|
|
return;
|
|
}
|
|
|
|
// Everything is ready
|
|
setComponentState("ready");
|
|
};
|
|
|
|
initializeComponent();
|
|
|
|
return () => {
|
|
mounted = false;
|
|
};
|
|
}, [isExamCompleted, router, examResults]);
|
|
|
|
// Always render loading screen for non-ready states
|
|
if (componentState !== "ready") {
|
|
const loadingText =
|
|
componentState === "redirecting" ? "Redirecting..." : "Loading...";
|
|
|
|
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">{loadingText}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// At this point, we know examResults exists and component is ready
|
|
const apiResponse = getApiResponse();
|
|
|
|
const handleBackToHome = () => {
|
|
// Set cleanup flag to prevent useExamResults from running
|
|
isCleaningUp.current = true;
|
|
|
|
clearExam();
|
|
setTimeout(() => {
|
|
router.push("/unit");
|
|
}, 400);
|
|
};
|
|
|
|
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
|
|
? (
|
|
(examResults.score / examResults.totalQuestions) *
|
|
100
|
|
).toFixed(1)
|
|
: "0"}
|
|
%
|
|
</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
|
|
? (
|
|
((examResults.totalQuestions - examResults.score) /
|
|
examResults.totalQuestions) *
|
|
100
|
|
).toFixed(1)
|
|
: "0"}
|
|
%
|
|
</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
|
|
? (
|
|
(examResults.answers.length /
|
|
examResults.totalQuestions) *
|
|
100
|
|
).toFixed(1)
|
|
: "0"}
|
|
%
|
|
</h2>
|
|
</div>
|
|
<div></div>
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="min-h-screen bg-white">
|
|
<button className="p-10" onClick={() => handleBackToHome()}>
|
|
<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-[#113768] mb-4 text-center">
|
|
{!examResults?.score || examResults?.score < 30
|
|
? "Try harder!"
|
|
: examResults?.score < 70
|
|
? "Getting Better"
|
|
: "You did great!"}
|
|
</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>
|
|
)}
|
|
</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>
|
|
);
|
|
}
|