fix(func): fix issues in results page

This commit is contained in:
shafin-r
2025-07-16 22:28:26 +06:00
parent 32c9065f6f
commit 5245ab878d
5 changed files with 198 additions and 88 deletions

View File

@ -69,7 +69,9 @@ const ProfilePage = () => {
<ProfileManager userData={userData} edit={editStatus} /> <ProfileManager userData={userData} edit={editStatus} />
<button <button
onClick={() => setEditStatus(!editStatus)} onClick={() => setEditStatus(!editStatus)}
className="p-3 bg-[#113768] w-full flex gap-3 justify-center items-center rounded-full" className={`p-3 ${
editStatus ? "bg-green-500" : "bg-[#113768]"
} w-full flex gap-3 justify-center items-center rounded-full`}
> >
{editStatus ? ( {editStatus ? (
<Save size={20} color="white" /> <Save size={20} color="white" />

View File

@ -58,9 +58,7 @@ const SettingsPage = () => {
<button onClick={() => router.push("/home")}> <button onClick={() => router.push("/home")}>
<MoveLeft size={30} color="#113768" /> <MoveLeft size={30} color="#113768" />
</button> </button>
<h3 className="text-2xl font-semibold text-[#113768]"> <h3 className="text-lg font-semibold text-[#113768]">Settings</h3>
Settings
</h3>
<button onClick={() => router.push("/profile")}> <button onClick={() => router.push("/profile")}>
<UserCircle2 size={30} color="#113768" /> <UserCircle2 size={30} color="#113768" />
</button> </button>

View File

@ -70,11 +70,11 @@ const QuestionItem = React.memo<QuestionItemProps>(
QuestionItem.displayName = "QuestionItem"; QuestionItem.displayName = "QuestionItem";
export default function ExamPage() { export default function ExamPage() {
// All hooks at the top - no conditional calls
const router = useRouter(); const router = useRouter();
const { id } = useParams(); const { id } = useParams();
const time = useSearchParams().get("time"); const time = useSearchParams().get("time");
const { isOpen, close } = useModal(); const { isOpen, close } = useModal();
const { setInitialTime, stopTimer } = useTimer(); const { setInitialTime, stopTimer } = useTimer();
const { const {
currentAttempt, currentAttempt,
@ -89,16 +89,57 @@ export default function ExamPage() {
currentExam, currentExam,
} = useExam(); } = useExam();
// State management
const [questions, setQuestions] = useState<Question[] | null>(null); const [questions, setQuestions] = useState<Question[] | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [submissionLoading, setSubmissionLoading] = useState(false); const [submissionLoading, setSubmissionLoading] = useState(false);
const [componentState, setComponentState] = useState<
"loading" | "redirecting" | "ready"
>("loading");
// Initial checks // Combined initialization effect
useEffect(() => { useEffect(() => {
if (!isHydrated || !isInitialized || isSubmitting) return; let mounted = true;
if (!isExamStarted()) return router.push("/unit");
if (isExamCompleted()) return router.push("/exam/results"); 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, isHydrated,
isInitialized, isInitialized,
@ -108,8 +149,10 @@ export default function ExamPage() {
router, router,
]); ]);
// Fetch questions // Fetch questions effect
useEffect(() => { useEffect(() => {
if (componentState !== "ready") return;
const fetchQuestions = async () => { const fetchQuestions = async () => {
try { try {
const response = await fetch(`${API_URL}/mock/${id}`); const response = await fetch(`${API_URL}/mock/${id}`);
@ -121,9 +164,10 @@ export default function ExamPage() {
setLoading(false); setLoading(false);
} }
}; };
fetchQuestions(); fetchQuestions();
if (time) setInitialTime(Number(time)); if (time) setInitialTime(Number(time));
}, [id, time, setInitialTime]); }, [id, time, setInitialTime, componentState]);
const handleSelect = useCallback( const handleSelect = useCallback(
(questionId: number, option: string) => { (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?")) { if (window.confirm("Are you sure you want to quit the exam?")) {
stopTimer(); stopTimer();
router.push("/unit"); router.push("/unit");
} }
}; }, [stopTimer, router]);
useEffect(() => { useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => { const handleBeforeUnload = (e: BeforeUnloadEvent) => {
@ -197,8 +241,29 @@ export default function ExamPage() {
window.removeEventListener("beforeunload", handleBeforeUnload); window.removeEventListener("beforeunload", handleBeforeUnload);
window.removeEventListener("popstate", handlePopState); window.removeEventListener("popstate", handlePopState);
}; };
}, []); }, [showExitDialog]);
const answeredSet = useMemo(() => {
if (!currentAttempt) return new Set<string>();
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 (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-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">{loadingText}</p>
</div>
</div>
);
}
// Show submission loading
if (submissionLoading) { if (submissionLoading) {
return ( return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <div className="min-h-screen bg-gray-50 flex items-center justify-center">
@ -210,11 +275,7 @@ export default function ExamPage() {
); );
} }
const answeredSet = useMemo(() => { // Render the main exam interface
if (!currentAttempt) return new Set<string>();
return new Set(currentAttempt.answers.map((a) => String(a.questionId)));
}, [currentAttempt]);
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<Header <Header
@ -242,13 +303,11 @@ export default function ExamPage() {
currentAttempt?.answers.length} currentAttempt?.answers.length}
</span> </span>
</p> </p>
{/* more details */}
</div> </div>
<div className="h-[0.5px] border-[0.5px] border-black/10 w-full my-3"></div> <div className="h-[0.5px] border-[0.5px] border-black/10 w-full my-3"></div>
<section className="flex flex-wrap gap-4"> <section className="flex flex-wrap gap-4">
{currentExam?.questions.map((q, idx) => { {currentExam?.questions.map((q, idx) => {
const answered = answeredSet.has(String(q.id)); // ← convert to string const answered = answeredSet.has(String(q.id));
return ( return (
<div <div

View File

@ -2,7 +2,7 @@
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useExam, useExamResults } from "@/context/ExamContext"; import { useExam, useExamResults } from "@/context/ExamContext";
import { useEffect, useState } from "react"; import { useEffect, useState, useRef } from "react";
import React from "react"; import React from "react";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@ -83,48 +83,92 @@ const QuestionItem = ({ question, selectedAnswer }: QuestionItemProps) => (
); );
export default function ResultsPage() { export default function ResultsPage() {
// All hooks at the top - no conditional calls
const router = useRouter(); const router = useRouter();
const { clearExam, isExamCompleted, getApiResponse } = useExam(); 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(() => { useEffect(() => {
if (!isExamCompleted()) { let mounted = true;
router.push("/unit");
return; const initializeComponent = async () => {
} // Allow time for all hooks to initialize
}, [isExamCompleted, router]); 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 ( return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="mt-60 flex flex-col items-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> <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> <p className="text-xl font-medium text-center">{loadingText}</p>
</div> </div>
</div> </div>
</div> </div>
); );
} }
// Get API response data // At this point, we know examResults exists and component is ready
const apiResponse = getApiResponse(); const apiResponse = getApiResponse();
const handleBackToHome = () => { 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(() => { setTimeout(() => {
router.push("/unit"); router.push("/unit");
}, 400); // 50100ms is usually enough }, 400);
}; };
const timeTaken = const timeTaken =
examResults.endTime && examResults.startTime examResults?.endTime && examResults?.startTime
? Math.round( ? Math.round(
(examResults.endTime.getTime() - examResults.startTime.getTime()) / (examResults.endTime.getTime() - examResults.startTime.getTime()) /
1000 / 1000 /
@ -149,10 +193,12 @@ export default function ResultsPage() {
height={60} height={60}
/> />
<h2 className="text-6xl font-bold text-[#113678]"> <h2 className="text-6xl font-bold text-[#113678]">
{( {examResults
(examResults.score / examResults.totalQuestions) * ? (
100 (examResults.score / examResults.totalQuestions) *
).toFixed(1)} 100
).toFixed(1)
: "0"}
% %
</h2> </h2>
</div> </div>
@ -177,11 +223,13 @@ export default function ResultsPage() {
height={60} height={60}
/> />
<h2 className="text-6xl font-bold text-[#113678]"> <h2 className="text-6xl font-bold text-[#113678]">
{( {examResults
((examResults.totalQuestions - examResults.score) / ? (
examResults.totalQuestions) * ((examResults.totalQuestions - examResults.score) /
100 examResults.totalQuestions) *
).toFixed(1)} 100
).toFixed(1)
: "0"}
% %
</h2> </h2>
</div> </div>
@ -206,10 +254,13 @@ export default function ResultsPage() {
height={60} height={60}
/> />
<h2 className="text-6xl font-bold text-[#113678]"> <h2 className="text-6xl font-bold text-[#113678]">
{( {examResults
(examResults.answers.length / examResults.totalQuestions) * ? (
100 (examResults.answers.length /
).toFixed(1)} examResults.totalQuestions) *
100
).toFixed(1)
: "0"}
% %
</h2> </h2>
</div> </div>
@ -227,7 +278,7 @@ export default function ResultsPage() {
</button> </button>
<div className="bg-white rounded-lg shadow-lg px-10 pb-20"> <div className="bg-white rounded-lg shadow-lg px-10 pb-20">
<h1 className="text-2xl font-bold text-[#113768] mb-4 text-center"> <h1 className="text-2xl font-bold text-[#113768] mb-4 text-center">
{examResults?.score < 30 {!examResults?.score || examResults?.score < 30
? "Try harder!" ? "Try harder!"
: examResults?.score < 70 : examResults?.score < 70
? "Getting Better" ? "Getting Better"
@ -247,14 +298,12 @@ export default function ResultsPage() {
<QuestionItem <QuestionItem
key={question.id} key={question.id}
question={question} question={question}
selectedAnswer={examResults.answers?.[question.id]} selectedAnswer={examResults?.answers?.[question.id]}
/> />
))} ))}
</div> </div>
</div> </div>
)} )}
{/* Action Buttons */}
</div> </div>
<button <button
onClick={handleBackToHome} onClick={handleBackToHome}

View File

@ -56,38 +56,40 @@ export default function ProfileManager({
/> />
</div> </div>
<div className="space-y-2"> <div className="flex gap-4">
<Label <div className="space-y-2">
htmlFor="sscRoll" <Label
className="text-sm font-semibold tracking-tighter text-gray-700" htmlFor="sscRoll"
> className="text-sm font-semibold tracking-tighter text-gray-700"
SSC Roll >
</Label> SSC Roll
<Input </Label>
id="sscRoll" <Input
type="text" id="sscRoll"
value={userData?.sscRoll} type="text"
readOnly value={userData?.sscRoll}
className="bg-gray-50 cursor-default py-6" readOnly
disabled={!edit} className="bg-gray-50 cursor-default py-6"
/> disabled={!edit}
</div> />
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label <Label
htmlFor="hscRoll" htmlFor="hscRoll"
className="text-sm font-semibold tracking-tighter text-gray-700" className="text-sm font-semibold tracking-tighter text-gray-700"
> >
HSC Roll HSC Roll
</Label> </Label>
<Input <Input
id="hscRoll" id="hscRoll"
type="text" type="text"
value={userData?.hscRoll} value={userData?.hscRoll}
readOnly readOnly
className="bg-gray-50 cursor-default py-6" className="bg-gray-50 cursor-default py-6"
disabled={!edit} disabled={!edit}
/> />
</div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">