diff --git a/app/(tabs)/profile/page.tsx b/app/(tabs)/profile/page.tsx index 77f70a2..86bbd0c 100644 --- a/app/(tabs)/profile/page.tsx +++ b/app/(tabs)/profile/page.tsx @@ -69,7 +69,9 @@ const ProfilePage = () => { -

- Settings -

+

Settings

diff --git a/app/exam/[id]/page.tsx b/app/exam/[id]/page.tsx index c82ce9e..47d60e1 100644 --- a/app/exam/[id]/page.tsx +++ b/app/exam/[id]/page.tsx @@ -70,11 +70,11 @@ const QuestionItem = React.memo( QuestionItem.displayName = "QuestionItem"; export default function ExamPage() { + // All hooks at the top - no conditional calls const router = useRouter(); const { id } = useParams(); const time = useSearchParams().get("time"); const { isOpen, close } = useModal(); - const { setInitialTime, stopTimer } = useTimer(); const { currentAttempt, @@ -89,16 +89,57 @@ export default function ExamPage() { currentExam, } = useExam(); + // State management const [questions, setQuestions] = useState(null); const [loading, setLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); const [submissionLoading, setSubmissionLoading] = useState(false); + const [componentState, setComponentState] = useState< + "loading" | "redirecting" | "ready" + >("loading"); - // Initial checks + // Combined initialization effect useEffect(() => { - if (!isHydrated || !isInitialized || isSubmitting) return; - if (!isExamStarted()) return router.push("/unit"); - if (isExamCompleted()) return router.push("/exam/results"); + let mounted = true; + + const initializeComponent = async () => { + // Wait for hydration and initialization + if (!isHydrated || !isInitialized || isSubmitting) { + return; + } + + // Check exam state and handle redirects + if (!isExamStarted()) { + if (mounted) { + setComponentState("redirecting"); + setTimeout(() => { + if (mounted) router.push("/unit"); + }, 100); + } + return; + } + + if (isExamCompleted()) { + if (mounted) { + setComponentState("redirecting"); + setTimeout(() => { + if (mounted) router.push("/exam/results"); + }, 100); + } + return; + } + + // Component is ready to render + if (mounted) { + setComponentState("ready"); + } + }; + + initializeComponent(); + + return () => { + mounted = false; + }; }, [ isHydrated, isInitialized, @@ -108,8 +149,10 @@ export default function ExamPage() { router, ]); - // Fetch questions + // Fetch questions effect useEffect(() => { + if (componentState !== "ready") return; + const fetchQuestions = async () => { try { const response = await fetch(`${API_URL}/mock/${id}`); @@ -121,9 +164,10 @@ export default function ExamPage() { setLoading(false); } }; + fetchQuestions(); if (time) setInitialTime(Number(time)); - }, [id, time, setInitialTime]); + }, [id, time, setInitialTime, componentState]); const handleSelect = useCallback( (questionId: number, option: string) => { @@ -172,12 +216,12 @@ export default function ExamPage() { } }; - const showExitDialog = () => { + const showExitDialog = useCallback(() => { if (window.confirm("Are you sure you want to quit the exam?")) { stopTimer(); router.push("/unit"); } - }; + }, [stopTimer, router]); useEffect(() => { const handleBeforeUnload = (e: BeforeUnloadEvent) => { @@ -197,8 +241,29 @@ export default function ExamPage() { window.removeEventListener("beforeunload", handleBeforeUnload); window.removeEventListener("popstate", handlePopState); }; - }, []); + }, [showExitDialog]); + const answeredSet = useMemo(() => { + if (!currentAttempt) return new Set(); + return new Set(currentAttempt.answers.map((a) => String(a.questionId))); + }, [currentAttempt]); + + // Show loading/redirecting state + if (componentState === "loading" || componentState === "redirecting") { + const loadingText = + componentState === "redirecting" ? "Redirecting..." : "Loading..."; + + return ( +
+
+
+

{loadingText}

+
+
+ ); + } + + // Show submission loading if (submissionLoading) { return (
@@ -210,11 +275,7 @@ export default function ExamPage() { ); } - const answeredSet = useMemo(() => { - if (!currentAttempt) return new Set(); - return new Set(currentAttempt.answers.map((a) => String(a.questionId))); - }, [currentAttempt]); - + // Render the main exam interface return (

- - {/* more details */}
{currentExam?.questions.map((q, idx) => { - const answered = answeredSet.has(String(q.id)); // ← convert to string + const answered = answeredSet.has(String(q.id)); return (
( ); export default function ResultsPage() { + // All hooks at the top - no conditional calls const router = useRouter(); const { clearExam, isExamCompleted, getApiResponse } = useExam(); - let examResults; + // 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(() => { - if (!isExamCompleted()) { - router.push("/unit"); - return; - } - }, [isExamCompleted, router]); + 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..."; - try { - examResults = useExamResults(); - console.log(examResults); - } catch (error) { - // Handle case where there's no completed exam return (
-

Loading...

+

{loadingText}

); } - // Get API response data + // At this point, we know examResults exists and component is ready const apiResponse = getApiResponse(); const handleBackToHome = () => { - clearExam(); + // Set cleanup flag to prevent useExamResults from running + isCleaningUp.current = true; - // Give time for state to fully reset before pushing new route + clearExam(); setTimeout(() => { router.push("/unit"); - }, 400); // 50–100ms is usually enough + }, 400); }; const timeTaken = - examResults.endTime && examResults.startTime + examResults?.endTime && examResults?.startTime ? Math.round( (examResults.endTime.getTime() - examResults.startTime.getTime()) / 1000 / @@ -149,10 +193,12 @@ export default function ResultsPage() { height={60} />

- {( - (examResults.score / examResults.totalQuestions) * - 100 - ).toFixed(1)} + {examResults + ? ( + (examResults.score / examResults.totalQuestions) * + 100 + ).toFixed(1) + : "0"} %

@@ -177,11 +223,13 @@ export default function ResultsPage() { height={60} />

- {( - ((examResults.totalQuestions - examResults.score) / - examResults.totalQuestions) * - 100 - ).toFixed(1)} + {examResults + ? ( + ((examResults.totalQuestions - examResults.score) / + examResults.totalQuestions) * + 100 + ).toFixed(1) + : "0"} %

@@ -206,10 +254,13 @@ export default function ResultsPage() { height={60} />

- {( - (examResults.answers.length / examResults.totalQuestions) * - 100 - ).toFixed(1)} + {examResults + ? ( + (examResults.answers.length / + examResults.totalQuestions) * + 100 + ).toFixed(1) + : "0"} %

@@ -227,7 +278,7 @@ export default function ResultsPage() {

- {examResults?.score < 30 + {!examResults?.score || examResults?.score < 30 ? "Try harder!" : examResults?.score < 70 ? "Getting Better" @@ -247,14 +298,12 @@ export default function ResultsPage() { ))}

)} - - {/* Action Buttons */}