generated from muhtadeetaron/nextjs-template
fix(func): fix issues in results page
This commit is contained in:
@ -70,11 +70,11 @@ const QuestionItem = React.memo<QuestionItemProps>(
|
||||
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<Question[] | null>(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<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) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
@ -210,11 +275,7 @@ export default function ExamPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const answeredSet = useMemo(() => {
|
||||
if (!currentAttempt) return new Set<string>();
|
||||
return new Set(currentAttempt.answers.map((a) => String(a.questionId)));
|
||||
}, [currentAttempt]);
|
||||
|
||||
// Render the main exam interface
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header
|
||||
@ -242,13 +303,11 @@ export default function ExamPage() {
|
||||
currentAttempt?.answers.length}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{/* more details */}
|
||||
</div>
|
||||
<div className="h-[0.5px] border-[0.5px] border-black/10 w-full my-3"></div>
|
||||
<section className="flex flex-wrap gap-4">
|
||||
{currentExam?.questions.map((q, idx) => {
|
||||
const answered = answeredSet.has(String(q.id)); // ← convert to string
|
||||
const answered = answeredSet.has(String(q.id));
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user