fix(api): fix api logic for exam screen

needs more work for the timercontext
This commit is contained in:
shafin-r
2025-08-31 02:20:55 +06:00
parent 08a560abe5
commit b112a8fdac
7 changed files with 301 additions and 684 deletions

View File

@ -1,4 +1,6 @@
import React, { useState, useEffect } from "react";
"use client";
import React from "react";
import { useRouter } from "next/navigation";
import { ChevronLeft, Layers } from "lucide-react";
import { useTimer } from "@/context/TimerContext";
@ -12,53 +14,25 @@ interface HeaderProps {
displayUser?: boolean;
displaySubject?: string;
displayTabTitle?: string;
examDuration?: string | null;
}
const Header = ({
displayUser,
displaySubject,
displayTabTitle,
examDuration,
}: HeaderProps) => {
const router = useRouter();
const { open } = useModal();
const { clearExam } = useExam();
const [totalSeconds, setTotalSeconds] = useState(
examDuration ? parseInt(examDuration) * 60 : 0
);
const { stopTimer } = useTimer();
const { user, isLoading } = useAuth();
useEffect(() => {
if (!examDuration) return;
const timer = setInterval(() => {
setTotalSeconds((prev) => {
if (prev <= 0) {
clearInterval(timer);
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, [examDuration]);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const { cancelExam } = useExam();
const { stopTimer, timeRemaining } = useTimer();
const { user } = useAuth();
const showExitDialog = () => {
const confirmed = window.confirm("Are you sure you want to quit the exam?");
if (confirmed) {
if (stopTimer) {
stopTimer();
}
clearExam();
router.push("/unit");
stopTimer();
cancelExam();
router.push("/categories");
}
};
@ -66,6 +40,11 @@ const Header = ({
router.back();
};
// format time from context
const hours = Math.floor(timeRemaining / 3600);
const minutes = Math.floor((timeRemaining % 3600) / 60);
const seconds = timeRemaining % 60;
return (
<header className={styles.header}>
{displayUser && (
@ -96,7 +75,8 @@ const Header = ({
</div>
)}
{examDuration && (
{/* Exam timer header */}
{timeRemaining > 0 && (
<div className={styles.examHeader}>
<button onClick={showExitDialog} className={styles.iconButton}>
<ChevronLeft size={30} color="white" />
@ -123,10 +103,7 @@ const Header = ({
</div>
</div>
<button
onClick={open}
className={`${styles.iconButton} ${styles.disabled}`}
>
<button onClick={open} className={`${styles.iconButton}`}>
<Layers size={30} color="white" />
</button>
</div>

View File

@ -1,136 +1,77 @@
import { Question } from "@/types/exam";
import { BookmarkCheck, Bookmark } from "lucide-react";
import React, { useState } from "react";
import { Badge } from "./ui/badge";
"use client";
interface ResultItemProps {
mode: "result";
import React from "react";
import { Question, Answer } from "@/types/exam";
import { Bookmark } from "lucide-react";
interface QuestionItemProps {
question: Question;
selectedAnswer: { answer: string } | undefined;
index: number;
selectedAnswer: Answer;
onSelect: (answer: Answer) => void;
}
interface ExamItemProps {
mode: "exam";
question: Question;
selectedAnswer?: string;
handleSelect: (questionId: number, option: string) => void;
}
type QuestionItemProps = ResultItemProps | ExamItemProps;
const QuestionItem = (props: QuestionItemProps) => {
const [bookmark, setBookmark] = useState(false);
const { question } = props;
const isExam = props.mode === "exam";
// Extract correct type-safe selectedAnswer
const selectedAnswer = isExam
? props.selectedAnswer
: props.selectedAnswer?.answer;
const handleOptionSelect = (key: string) => {
if (isExam && props.handleSelect) {
props.handleSelect(parseInt(question.id), key);
}
};
const letters = ["A", "B", "C", "D"]; // extend if needed
const QuestionItem: React.FC<QuestionItemProps> = ({
question,
index,
selectedAnswer,
onSelect,
}) => {
return (
<div className="border-[0.5px] border-[#8abdff]/60 rounded-2xl p-4 flex flex-col">
<h3 className="text-xl font-semibold ">
{question.id}. {question.question}
</h3>
<div className="border border-blue-100 p-6 bg-slate-100 rounded-3xl mb-6">
<p className="text-lg font-semibold mb-3">
{index + 1}. {question.question}
</p>
{isExam && (
<div className="flex justify-between items-center mb-4">
<div></div>
<button onClick={() => setBookmark(!bookmark)}>
{bookmark ? (
<BookmarkCheck size={25} color="#113768" />
) : (
<Bookmark size={25} color="#113768" />
)}
</button>
</div>
)}
<div className="w-full flex justify-between">
<div></div>
<Bookmark size={24} />
</div>
{isExam ? (
<div className="flex flex-col gap-4 items-start">
{Object.entries(question.options ?? {}).map(([key, value]) => {
const isSelected = selectedAnswer === key;
<div className="flex flex-col gap-3">
{question.options.map((opt, optIdx) => {
const isSelected =
question.type === "Single"
? selectedAnswer === optIdx
: Array.isArray(selectedAnswer) &&
selectedAnswer.includes(optIdx);
return (
return (
<div key={optIdx} className="flex items-center gap-3">
<button
key={key}
className="flex items-center gap-3"
onClick={() => handleOptionSelect(key)}
onClick={() => {
if (question.type === "Single") {
onSelect(optIdx);
} else {
let newAnswers = Array.isArray(selectedAnswer)
? [...selectedAnswer]
: [];
if (newAnswers.includes(optIdx)) {
newAnswers = newAnswers.filter((a) => a !== optIdx);
} else {
newAnswers.push(optIdx);
}
onSelect(newAnswers);
}
}}
className={`w-7 h-7 rounded-full border font-bold
flex items-center justify-center
${
isSelected
? "bg-blue-600 text-white border-blue-600"
: "bg-gray-100 text-gray-900 border-gray-400"
}
hover:bg-blue-500 hover:text-white transition-colors`}
>
<span
className={`flex items-center rounded-full border px-1.5 ${
isSelected ? "text-white bg-[#113768] border-[#113768]" : ""
}`}
>
{key.toUpperCase()}
</span>
<span className="option-description">{value}</span>
{letters[optIdx]}
</button>
);
})}
</div>
) : (
<div className="flex flex-col gap-3">
<div className="flex justify-between items-center">
<div></div>
{!selectedAnswer ? (
<Badge className="bg-yellow-500" variant="destructive">
Skipped
</Badge>
) : selectedAnswer === 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;
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";
} else if (isSelected && !isCorrect) {
optionStyle += " bg-red-600 text-white border-red-600";
} else {
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>
)}
<span className="text-gray-900">{opt}</span>
</div>
);
})}
</div>
</div>
);
};