generated from muhtadeetaron/nextjs-template
fix(func): fix issues in results page
This commit is contained in:
@ -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" />
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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); // 50–100ms 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}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user