fix(ui): fix results screen

This commit is contained in:
shafin-r
2025-07-22 17:59:15 +06:00
parent 5245ab878d
commit 341a46e788
8 changed files with 450 additions and 323 deletions

View File

@ -2,312 +2,118 @@
import { useRouter } from "next/navigation";
import { useExam, useExamResults } from "@/context/ExamContext";
import { useEffect, useState, useRef } from "react";
import { useEffect, useState } from "react";
import React from "react";
import { ArrowLeft } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import Image from "next/image";
import SlidingGallery from "@/components/SlidingGallery";
interface Question {
correctAnswer: string;
id: number;
question: string;
options: Record<string, string>;
solution?: string;
}
interface QuestionItemProps {
question: Question;
selectedAnswer: string | undefined;
}
const QuestionItem = ({ question, selectedAnswer }: QuestionItemProps) => (
<div className="border border-[#8abdff]/50 rounded-2xl p-4 flex flex-col gap-7">
<h3 className="text-xl font-medium">
{question.id}. {question.question}
</h3>
<div className="flex justify-between items-center">
<div></div>
{!selectedAnswer ? (
<Badge className="bg-yellow-500" variant="destructive">
Skipped
</Badge>
) : selectedAnswer.answer === question.correctAnswer ? (
<Badge className="bg-green-500 text-white" variant="default">
Correct
</Badge>
) : (
<Badge className="bg-red-500 text-white" variant="default">
Incorrect
</Badge>
)}
</div>
<div className="flex flex-col gap-4 items-start">
{Object.entries(question.options).map(([key, value]) => {
const isCorrect = key === question.correctAnswer;
const isSelected = key === selectedAnswer?.answer;
let optionStyle =
"px-2 py-1 flex items-center rounded-full border font-medium text-sm";
if (isCorrect) {
optionStyle += " bg-green-600 text-white border-green-600";
}
if (isSelected && !isCorrect) {
optionStyle += " bg-red-600 text-white border-red-600";
}
if (!isCorrect && !isSelected) {
optionStyle += " border-gray-300 text-gray-700";
}
return (
<div key={key} className="flex items-center gap-3">
<span className={optionStyle}>{key.toUpperCase()}</span>
<span className="option-description">{value}</span>
</div>
);
})}
</div>
<div className="h-[0.5px] border-[0.5px] border-dashed border-black/20"></div>
<div className="flex flex-col gap-2">
<h3 className="text-lg font-bold text-black/40">Solution:</h3>
<p className="text-lg">{question.solution}</p>
</div>
</div>
);
import QuestionItem from "@/components/QuestionItem";
import { getResultViews } from "@/lib/resultViews";
export default function ResultsPage() {
// All hooks at the top - no conditional calls
const router = useRouter();
const { clearExam, isExamCompleted, getApiResponse } = useExam();
const {
clearExam,
isExamCompleted,
getApiResponse,
currentAttempt,
isHydrated,
} = useExam();
// Add a ref to track if we're in cleanup mode
const isCleaningUp = useRef(false);
const [isLoading, setIsLoading] = useState(true);
// 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(() => {
let mounted = true;
// Wait for hydration first
if (!isHydrated) return;
const initializeComponent = async () => {
// Allow time for all hooks to initialize
await new Promise((resolve) => setTimeout(resolve, 50));
// Check if exam is completed, redirect if not
if (!isExamCompleted() || !currentAttempt) {
router.push("/unit");
return;
}
if (!mounted) return;
// If we have exam results, we're ready to render
if (currentAttempt?.answers) {
setIsLoading(false);
}
}, [isExamCompleted, currentAttempt, isHydrated, router]);
// 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...";
const handleBackToHome = () => {
clearExam();
router.push("/unit");
};
// Show loading screen while initializing or if no exam results
if (isLoading || !currentAttempt) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-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>
<p className="text-xl font-medium text-center">{loadingText}</p>
<p className="text-xl font-medium text-center">Loading...</p>
</div>
</div>
</div>
);
}
// At this point, we know examResults exists and component is ready
const apiResponse = getApiResponse();
const handleBackToHome = () => {
// Set cleanup flag to prevent useExamResults from running
isCleaningUp.current = true;
clearExam();
setTimeout(() => {
router.push("/unit");
}, 400);
};
const timeTaken =
examResults?.endTime && examResults?.startTime
currentAttempt.endTime && currentAttempt.startTime
? Math.round(
(examResults.endTime.getTime() - examResults.startTime.getTime()) /
(currentAttempt.endTime.getTime() -
currentAttempt.startTime.getTime()) /
1000 /
60
)
: 0;
const resultViews = [
{
id: 1,
content: (
<div className="w-full">
<div className="bg-blue-50/60 border border-[#113678]/50 rounded-4xl h-[170px] flex flex-col items-center justify-center gap-4">
<div className="text-xl text-black ">
<span className="font-bold">Accuracy</span> Rate:
</div>
<div className="flex gap-4">
<Image
src="/images/icons/accuracy.png"
alt="accuracy"
width={60}
height={60}
/>
<h2 className="text-6xl font-bold text-[#113678]">
{examResults
? (
(examResults.score / examResults.totalQuestions) *
100
).toFixed(1)
: "0"}
%
</h2>
</div>
<div></div>
</div>
</div>
),
},
{
id: 2,
content: (
<div className=" w-full">
<div className="bg-blue-50/60 border border-[#113678]/50 rounded-4xl h-[170px] flex flex-col items-center justify-center gap-3">
<div className="text-xl text-black ">
<span className="font-bold">Error</span> Rate:
</div>
<div className="flex gap-4">
<Image
src="/images/icons/error.png"
alt="accuracy"
width={60}
height={60}
/>
<h2 className="text-6xl font-bold text-[#113678]">
{examResults
? (
((examResults.totalQuestions - examResults.score) /
examResults.totalQuestions) *
100
).toFixed(1)
: "0"}
%
</h2>
</div>
<div></div>
</div>
</div>
),
},
{
id: 3,
content: (
<div className="my-8 w-full">
<div className="bg-blue-50/60 border border-[#113678]/50 rounded-4xl h-[170px] flex flex-col items-center justify-center gap-4">
<div className="text-xl text-black">
<span className="font-bold">Attempt</span> Rate:
</div>
<div className="flex gap-4">
<Image
src="/images/icons/attempt.png"
alt="accuracy"
width={60}
height={60}
/>
<h2 className="text-6xl font-bold text-[#113678]">
{examResults
? (
(examResults.answers.length /
examResults.totalQuestions) *
100
).toFixed(1)
: "0"}
%
</h2>
</div>
<div></div>
</div>
</div>
),
},
];
const views = getResultViews(currentAttempt);
// Get score-based message
const getScoreMessage = () => {
if (!currentAttempt.score || currentAttempt.score < 30)
return "Try harder!";
if (currentAttempt.score < 70) return "Getting Better";
return "You did great!";
};
return (
<div className="min-h-screen bg-white">
<button className="p-10" onClick={() => handleBackToHome()}>
<button className="p-10" onClick={handleBackToHome}>
<ArrowLeft size={30} color="black" />
</button>
<div className="bg-white rounded-lg shadow-lg px-10 pb-20">
<h1 className="text-2xl font-bold text-[#113768] mb-4 text-center">
{!examResults?.score || examResults?.score < 30
? "Try harder!"
: examResults?.score < 70
? "Getting Better"
: "You did great!"}
{getScoreMessage()}
</h1>
{/* Score Display */}
<SlidingGallery className="my-8" views={resultViews} height="170px" />
<SlidingGallery className="my-8" views={views} height="170px" />
{apiResponse && (
{apiResponse?.questions && (
<div className="mb-8">
<h3 className="text-2xl font-bold text-[#113768] mb-4">
<h3 className="text-2xl font-bold text-[#113768] mb-4">
Solutions
</h3>
<div className="flex flex-col gap-7">
{apiResponse.questions?.map((question) => (
{apiResponse.questions.map((question) => (
<QuestionItem
key={question.id}
question={question}
selectedAnswer={examResults?.answers?.[question.id]}
selectedAnswer={currentAttempt.answers[question.id]}
mode="result"
/>
))}
</div>
</div>
)}
</div>
<button
onClick={handleBackToHome}
className="fixed bottom-0 w-full bg-blue-900 text-white h-[74px] font-bold text-lg disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="fixed bottom-0 w-full bg-blue-900 text-white h-[74px] font-bold text-lg disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Finish
</button>