diff --git a/app/exam/[id]/page.tsx b/app/exam/[id]/page.tsx index 3c56fed..7992daf 100644 --- a/app/exam/[id]/page.tsx +++ b/app/exam/[id]/page.tsx @@ -1,22 +1,33 @@ "use client"; import React, { useEffect, useCallback, useState } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; import Header from "@/components/Header"; import QuestionItem from "@/components/QuestionItem"; import BackgroundWrapper from "@/components/BackgroundWrapper"; import { useExamStore } from "@/stores/examStore"; import { useTimerStore } from "@/stores/timerStore"; +import { useExamExitGuard } from "@/hooks/useExamExitGuard"; export default function ExamPage() { const router = useRouter(); + const pathname = usePathname(); const searchParams = useSearchParams(); const test_id = searchParams.get("test_id") || ""; const type = searchParams.get("type") || ""; - const { test, answers, startExam, setAnswer, submitExam, cancelExam } = - useExamStore(); + const { + setStatus, + test, + answers, + startExam, + setAnswer, + submitExam, + cancelExam, + status, + } = useExamStore(); const { resetTimer, stopTimer } = useTimerStore(); + const { showExitDialog } = useExamExitGuard(type); const [isSubmitting, setIsSubmitting] = useState(false); @@ -25,24 +36,52 @@ export default function ExamPage() { if (type && test_id) { startExam(type, test_id).then((fetchedTest) => { if (fetchedTest?.metadata.time_limit_minutes) { + setStatus("in-progress"); // ✅ make sure exam status is set here resetTimer(fetchedTest.metadata.time_limit_minutes * 60, () => { - // Timer ended → auto-submit exam + // Timer ended → auto-submit + setStatus("finished"); stopTimer(); submitExam(type); - router.push(`/exam/results`); + router.replace(`/exam/results`); }); } }); } - }, [type, test_id, startExam, resetTimer, submitExam, router]); + }, [ + type, + test_id, + startExam, + resetTimer, + submitExam, + router, + setStatus, + stopTimer, + ]); - const showExitDialog = useCallback(() => { - if (window.confirm("Are you sure you want to quit the exam?")) { - stopTimer(); - cancelExam(); - router.push(`/categories/${type}s`); - } - }, [stopTimer, cancelExam, router, type]); + // useEffect(() => { + // const handlePopState = (event: PopStateEvent) => { + // if (status === "in-progress") { + // const confirmExit = window.confirm( + // "Are you sure you want to quit the exam?" + // ); + + // if (confirmExit) { + // setStatus("finished"); + // stopTimer(); + // cancelExam(); + // router.replace(`/categories/${type}s`); + // } else { + // // User canceled → push them back to current page + // router.replace(pathname, { scroll: false }); + // } + // } else { + // router.replace(`/categories/${type}s`); + // } + // }; + + // window.addEventListener("popstate", handlePopState); + // return () => window.removeEventListener("popstate", handlePopState); + // }, [status, router, pathname, type, setStatus, stopTimer, cancelExam]); if (!test) { return ( @@ -57,10 +96,11 @@ export default function ExamPage() { const handleSubmitExam = async (type: string) => { try { + setStatus("finished"); // ✅ mark exam finished stopTimer(); setIsSubmitting(true); await submitExam(type); - router.push(`/exam/results`); + router.replace(`/exam/results`); // ✅ replace to prevent back nav } finally { setIsSubmitting(false); } diff --git a/app/exam/pretest/page.tsx b/app/exam/pretest/page.tsx index 7614d6b..329fca0 100644 --- a/app/exam/pretest/page.tsx +++ b/app/exam/pretest/page.tsx @@ -13,7 +13,6 @@ import { import DestructibleAlert from "@/components/DestructibleAlert"; import BackgroundWrapper from "@/components/BackgroundWrapper"; import { API_URL, getToken } from "@/lib/auth"; -import { useExam } from "@/context/ExamContext"; import { Test } from "@/types/exam"; import { Metadata } from "@/types/exam"; import { useExamStore } from "@/stores/examStore"; @@ -21,7 +20,7 @@ import { useExamStore } from "@/stores/examStore"; function PretestPageContent() { const router = useRouter(); const searchParams = useSearchParams(); - const { startExam } = useExamStore(); + const [examData, setExamData] = useState(); // Get params from URL search params @@ -35,6 +34,7 @@ function PretestPageContent() { const [metadata, setMetadata] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(); + const { setStatus } = useExamStore(); useEffect(() => { async function fetchQuestions() { @@ -123,6 +123,7 @@ function PretestPageContent() { function handleStartExam() { if (!examData) return; + setStatus("in-progress"); router.push( `/exam/${id}?type=${type}&test_id=${metadata?.test_id}&attempt_id=${metadata?.attempt_id}` diff --git a/app/exam/results/page.tsx b/app/exam/results/page.tsx index 0c35739..c574f11 100644 --- a/app/exam/results/page.tsx +++ b/app/exam/results/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useRouter } from "next/navigation"; -import React from "react"; +import React, { useEffect } from "react"; import { ArrowLeft } from "lucide-react"; import { useExamStore } from "@/stores/examStore"; import QuestionItem from "@/components/QuestionItem"; @@ -10,7 +10,20 @@ import { getResultViews } from "@/lib/gallery-views"; export default function ResultsPage() { const router = useRouter(); - const { result, clearResult } = useExamStore(); + const { result, clearResult, setStatus, status } = useExamStore(); + + useEffect(() => { + const handlePopState = () => { + if (status !== "finished") { + router.replace(`/categories`); + } + }; + + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, [status, router, setStatus]); if (!result) { return ( @@ -21,8 +34,9 @@ export default function ResultsPage() { } const handleBackToHome = () => { - clearResult(); - router.push("/categories"); + setStatus("not-started"); // ✅ reset exam flow + clearResult(); // ✅ clear stored results + router.replace("/categories"); // ✅ prevent re-entry }; const views = getResultViews(result); diff --git a/hooks/useExamExitGuard.ts b/hooks/useExamExitGuard.ts new file mode 100644 index 0000000..bc40087 --- /dev/null +++ b/hooks/useExamExitGuard.ts @@ -0,0 +1,42 @@ +import { useRouter, usePathname } from "next/navigation"; +import { useEffect } from "react"; +import { useExamStore } from "@/stores/examStore"; +import { useTimerStore } from "@/stores/timerStore"; + +export function useExamExitGuard(type: string) { + const { status, setStatus, cancelExam } = useExamStore(); + const router = useRouter(); + const pathname = usePathname(); + const { stopTimer } = useTimerStore(); + + // Guard page render: always redirect if status invalid + useEffect(() => { + if (status !== "in-progress") { + router.replace(`/categories/${type}s`); + } + }, [status, router, type]); + + // Confirm before leaving page / tab close + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (status === "in-progress") { + e.preventDefault(); + e.returnValue = ""; // shows native browser dialog + } + }; + window.addEventListener("beforeunload", handleBeforeUnload); + return () => window.removeEventListener("beforeunload", handleBeforeUnload); + }, [status]); + + // Call this to quit exam manually + const showExitDialog = () => { + if (window.confirm("Are you sure you want to quit the exam?")) { + setStatus("finished"); + stopTimer(); + cancelExam(); + router.replace(`/categories/${type}s`); + } + }; + + return { showExitDialog }; +} diff --git a/stores/examStore.ts b/stores/examStore.ts index 3930956..bd60321 100644 --- a/stores/examStore.ts +++ b/stores/examStore.ts @@ -7,10 +7,14 @@ import { ExamResult } from "@/types/exam"; // Result type (based on your API response) +type ExamStatus = "not-started" | "in-progress" | "finished"; + interface ExamState { test: Test | null; answers: Answer[]; result: ExamResult | null; + status: ExamStatus; + setStatus: (status: ExamStatus) => void; startExam: (testType: string, testId: string) => Promise; setAnswer: (questionIndex: number, answer: Answer) => void; @@ -23,6 +27,8 @@ export const useExamStore = create((set, get) => ({ test: null, answers: [], result: null, + status: "not-started", + setStatus: (status) => set({ status }), // start exam startExam: async (testType: string, testId: string) => {