generated from muhtadeetaron/nextjs-template
157 lines
5.1 KiB
TypeScript
157 lines
5.1 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useCallback, useMemo } from "react";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import { useTimer } from "@/context/TimerContext";
|
|
import { useExam } from "@/context/ExamContext";
|
|
import Header from "@/components/Header";
|
|
import { useModal } from "@/context/ModalContext";
|
|
import Modal from "@/components/ExamModal";
|
|
import QuestionItem from "@/components/QuestionItem";
|
|
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
|
|
|
export default function ExamPage() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const test_id = searchParams.get("test_id") || "";
|
|
const type = searchParams.get("type") || "";
|
|
|
|
const { isOpen, close, open } = useModal();
|
|
const { timeRemaining, setInitialTime, stopTimer } = useTimer();
|
|
const { test, answers, startExam, setAnswer, submitExam, cancelExam } =
|
|
useExam();
|
|
|
|
// Start exam + timer
|
|
useEffect(() => {
|
|
if (type && test_id) {
|
|
startExam(type, test_id).then(() => {
|
|
if (test?.metadata.time_limit_minutes) {
|
|
setInitialTime(test.metadata.time_limit_minutes * 60); // convert to seconds
|
|
}
|
|
});
|
|
}
|
|
}, [type, test_id, startExam, setInitialTime]);
|
|
|
|
const showExitDialog = useCallback(() => {
|
|
if (window.confirm("Are you sure you want to quit the exam?")) {
|
|
stopTimer();
|
|
cancelExam();
|
|
router.push("/unit");
|
|
}
|
|
}, [stopTimer, cancelExam, router]);
|
|
|
|
if (!test) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
<div className="flex flex-col items-center justify-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-900 mb-4"></div>
|
|
<p className="text-lg font-medium text-gray-900">Loading exam...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// answered set for modal overview
|
|
// const answeredSet = useMemo(
|
|
// () =>
|
|
// new Set(
|
|
// answers
|
|
// .map((a, idx) =>
|
|
// a !== null && a !== undefined ? idx.toString() : null
|
|
// )
|
|
// .filter(Boolean) as string[]
|
|
// ),
|
|
// [answers]
|
|
// );
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
{/* Header with live timer */}
|
|
<Header />
|
|
|
|
{/* Modal: Question overview */}
|
|
{/* <Modal open={isOpen} onClose={close} title="Exam Overview">
|
|
<div>
|
|
<div className="flex gap-6 mb-4">
|
|
<p className="font-medium">Questions: {test.questions.length}</p>
|
|
<p className="font-medium">
|
|
Answered:{" "}
|
|
<span className="text-blue-900 font-bold">
|
|
{answeredSet.size}
|
|
</span>
|
|
</p>
|
|
<p className="font-medium">
|
|
Skipped:{" "}
|
|
<span className="text-yellow-600 font-bold">
|
|
{test.questions.length - answeredSet.size}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
|
|
<div className="h-[0.5px] border-[0.5px] border-black/10 w-full my-3"></div>
|
|
|
|
<section className="flex flex-wrap gap-4">
|
|
{test.questions.map((q, idx) => {
|
|
const answered = answeredSet.has(String(idx));
|
|
return (
|
|
<div
|
|
key={q.question_id}
|
|
className={`h-12 w-12 rounded-full flex items-center justify-center cursor-pointer
|
|
${
|
|
answered
|
|
? "bg-blue-900 text-white"
|
|
: "bg-gray-200 text-gray-900"
|
|
}
|
|
hover:opacity-80 transition`}
|
|
onClick={() => {
|
|
// optional: scroll to question
|
|
const el = document.getElementById(`question-${idx}`);
|
|
if (el) {
|
|
el.scrollIntoView({ behavior: "smooth" });
|
|
close();
|
|
}
|
|
}}
|
|
>
|
|
{idx + 1}
|
|
</div>
|
|
);
|
|
})}
|
|
</section>
|
|
</div>
|
|
</Modal> */}
|
|
|
|
{/* Questions */}
|
|
<BackgroundWrapper>
|
|
<div className="container mx-auto px-6 py-8 mb-20">
|
|
{test.questions.map((q, idx) => (
|
|
<div id={`question-${idx}`} key={q.question_id}>
|
|
<QuestionItem
|
|
question={q}
|
|
index={idx}
|
|
selectedAnswer={answers[idx]}
|
|
onSelect={(answer) => setAnswer(idx, answer)}
|
|
/>
|
|
</div>
|
|
))}
|
|
|
|
{/* Bottom submit bar */}
|
|
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 flex">
|
|
<button
|
|
onClick={submitExam}
|
|
className="flex-1 bg-blue-900 text-white p-6 font-bold text-lg hover:bg-blue-800 transition-colors"
|
|
>
|
|
Submit
|
|
</button>
|
|
<button
|
|
onClick={showExitDialog}
|
|
className="flex-1 bg-gray-200 text-gray-900 p-6 font-bold text-lg hover:bg-gray-300 transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</BackgroundWrapper>
|
|
</div>
|
|
);
|
|
}
|