generated from muhtadeetaron/nextjs-template
fix(api): fix api logic for exam screen
needs more work for the timercontext
This commit is contained in:
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user