Updated some troubles related to the Level Exam

This commit is contained in:
Tiago Ribeiro
2024-11-07 14:55:44 +00:00
parent 78cf011bf7
commit 7ae91d7bc1
3 changed files with 92 additions and 95 deletions

View File

@@ -3,105 +3,96 @@ import Modal from "@/components/Modal";
import { Exam, LevelExam, MultipleChoiceExercise, ShuffleMap } from "@/interfaces/exam"; import { Exam, LevelExam, MultipleChoiceExercise, ShuffleMap } from "@/interfaces/exam";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import clsx from "clsx"; import clsx from "clsx";
import { useState } from "react"; import { useMemo, useState } from "react";
import { BsFillGrid3X3GapFill } from "react-icons/bs"; import { BsFillGrid3X3GapFill } from "react-icons/bs";
interface Props { interface Props {
showSolutions: boolean; exam: LevelExam
runOnClick: ((index: number) => void) | undefined; showSolutions: boolean;
runOnClick: ((index: number) => void) | undefined;
} }
const MCQuestionGrid: React.FC<Props> = ({showSolutions, runOnClick}) => { const MCQuestionGrid: React.FC<Props> = ({ exam, showSolutions, runOnClick }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { const {
userSolutions, userSolutions,
partIndex: sectionIndex, partIndex: sectionIndex,
exerciseIndex, exerciseIndex,
exam
} = useExamStore((state) => state); } = useExamStore((state) => state);
const isMultipleChoiceLevelExercise = () => { const currentExercise = useMemo(() => (exam as LevelExam).parts[sectionIndex!].exercises[exerciseIndex] as MultipleChoiceExercise, [exam, exerciseIndex, sectionIndex])
if (exam?.module === 'level' && typeof sectionIndex === "number" && sectionIndex > -1) { const userSolution = useMemo(() => userSolutions!.find((x) => x.exercise.toString() == currentExercise.id.toString())!, [currentExercise.id, userSolutions])
const currentExercise = (exam as LevelExam).parts[sectionIndex].exercises[exerciseIndex]; const answeredQuestions = useMemo(() => new Set(userSolution.solutions.map(sol => sol.question.toString())), [userSolution.solutions])
return currentExercise && currentExercise.type === 'multipleChoice'; const exerciseOffset = useMemo(() => Number(currentExercise.questions[0].id), [currentExercise.questions])
} const lastExercise = useMemo(() => exerciseOffset + (currentExercise.questions.length - 1),
return false; [currentExercise.questions.length, exerciseOffset]);
};
if (!isMultipleChoiceLevelExercise() && !userSolutions) return null; const getQuestionColor = (questionId: string, solution: string, userQuestionSolution: string | undefined) => {
const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => {
if (foundMap) return foundMap;
return userSolution.shuffleMaps?.find(map => map.questionID.toString() === questionId.toString()) || null;
}, null as ShuffleMap | null);
const newSolution = questionShuffleMap ? questionShuffleMap?.map[solution] : solution;
const currentExercise = (exam as LevelExam).parts[sectionIndex!].exercises[exerciseIndex] as MultipleChoiceExercise; if (!userSolutions) return "";
const userSolution = userSolutions!.find((x) => x.exercise.toString() == currentExercise.id.toString())!;
const answeredQuestions = new Set(userSolution.solutions.map(sol => sol.question.toString()));
const exerciseOffset = Number(currentExercise.questions[0].id);
const lastExercise = exerciseOffset + (currentExercise.questions.length - 1);
const getQuestionColor = (questionId: string, solution: string, userQuestionSolution: string | undefined) => { if (!userQuestionSolution) {
const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => { return "!bg-mti-gray-davy !border--mti-gray-davy !text-mti-gray-davy !text-white hover:!bg-gray-700";
if (foundMap) return foundMap; }
return userSolution.shuffleMaps?.find(map => map.questionID.toString() === questionId.toString()) || null;
}, null as ShuffleMap | null);
const newSolution = questionShuffleMap ? questionShuffleMap?.map[solution] : solution;
if (!userSolutions) return ""; return userQuestionSolution === newSolution ?
"!bg-mti-purple-light !text-mti-purple-light !text-white hover:!bg-mti-purple-dark" :
"!bg-mti-rose-light !border-mti-rose-light !text-mti-rose-light !text-white hover:!bg-mti-rose-dark";
}
if (!userQuestionSolution) { return (
return "!bg-mti-gray-davy !border--mti-gray-davy !text-mti-gray-davy !text-white hover:!bg-gray-700"; <>
} <Button variant="outline" onClick={() => setIsOpen(true)} padding="p-2" className="rounded-lg">
<BsFillGrid3X3GapFill size={24} />
</Button>
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white shadow-xl transition-all"
>
<>
<h3 className="text-xl font-semibold mb-4 text-center">{`Part ${sectionIndex + 1} (Questions ${exerciseOffset} - ${lastExercise})`}</h3>
<div className="grid grid-cols-5 gap-3 px-4 py-2">
{currentExercise.questions.map((_, index) => {
const questionNumber = exerciseOffset + index;
const isAnswered = answeredQuestions.has(questionNumber.toString());
const solution = currentExercise.questions.find((x) => x.id.toString() == questionNumber.toString())!.solution;
return userQuestionSolution === newSolution ? const userQuestionSolution = currentExercise.userSolutions?.find((x) => x.question.toString() == questionNumber.toString())?.option;
"!bg-mti-purple-light !text-mti-purple-light !text-white hover:!bg-mti-purple-dark" : return (
"!bg-mti-rose-light !border-mti-rose-light !text-mti-rose-light !text-white hover:!bg-mti-rose-dark"; <Button
} variant={showSolutions ? "solid" : (isAnswered ? "solid" : "outline")}
key={index}
return ( className={clsx(
<> "w-12 h-12 flex items-center justify-center rounded-lg text-sm font-bold transition-all duration-200 ease-in-out",
<Button variant="outline" onClick={() => setIsOpen(true)} padding="p-2" className="rounded-lg"> (showSolutions ?
<BsFillGrid3X3GapFill size={24} /> getQuestionColor(questionNumber.toString(), solution, userQuestionSolution) :
</Button> (isAnswered ?
<Modal "bg-mti-purple-light border-mti-purple-light text-white hover:bg-mti-purple-dark hover:border-mti-purple-dark" :
isOpen={isOpen} "bg-white border-gray-400 hover:bg-gray-100 hover:text-gray-700"
onClose={() => setIsOpen(false)} )
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white shadow-xl transition-all" )
> )}
<> onClick={() => { if (typeof runOnClick !== "undefined") { runOnClick(index); } setIsOpen(false); }}
<h3 className="text-xl font-semibold mb-4 text-center">{`Part ${sectionIndex + 1} (Questions ${exerciseOffset} - ${lastExercise})`}</h3> >
<div className="grid grid-cols-5 gap-3 px-4 py-2"> {questionNumber}
{currentExercise.questions.map((_, index) => { </Button>
const questionNumber = exerciseOffset + index; );
const isAnswered = answeredQuestions.has(questionNumber.toString()); })}
const solution = currentExercise.questions.find((x) => x.id.toString() == questionNumber.toString())!.solution; </div>
<p className="mt-4 text-sm text-gray-600 text-center">
const userQuestionSolution = currentExercise.userSolutions?.find((x) => x.question.toString() == questionNumber.toString())?.option; Click a question number to jump to that question
return ( </p>
<Button </>
variant={showSolutions ? "solid" : (isAnswered ? "solid" : "outline")} </Modal>
key={index} </>
className={clsx( );
"w-12 h-12 flex items-center justify-center rounded-lg text-sm font-bold transition-all duration-200 ease-in-out",
(showSolutions ?
getQuestionColor(questionNumber.toString(), solution, userQuestionSolution) :
(isAnswered ?
"bg-mti-purple-light border-mti-purple-light text-white hover:bg-mti-purple-dark hover:border-mti-purple-dark" :
"bg-white border-gray-400 hover:bg-gray-100 hover:text-gray-700"
)
)
)}
onClick={() => { if (typeof runOnClick !== "undefined") { runOnClick(index); } setIsOpen(false); }}
>
{questionNumber}
</Button>
);
})}
</div>
<p className="mt-4 text-sm text-gray-600 text-center">
Click a question number to jump to that question
</p>
</>
</Modal>
</>
);
} }
export default MCQuestionGrid; export default MCQuestionGrid;

View File

@@ -1,11 +1,11 @@
import { Module } from "@/interfaces"; import { Module } from "@/interfaces";
import { moduleLabels } from "@/utils/moduleUtils"; import { moduleLabels } from "@/utils/moduleUtils";
import clsx from "clsx"; import clsx from "clsx";
import { ReactNode, useState } from "react"; import { ReactNode, useMemo, useState } from "react";
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs"; import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs";
import ProgressBar from "../../Low/ProgressBar"; import ProgressBar from "../../Low/ProgressBar";
import Timer from "../Timer"; import Timer from "../Timer";
import { Exercise } from "@/interfaces/exam"; import { Exercise, LevelExam } from "@/interfaces/exam";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import React from "react"; import React from "react";
import MCQuestionGrid from "./MCQuestionGrid"; import MCQuestionGrid from "./MCQuestionGrid";
@@ -38,9 +38,7 @@ export default function ModuleTitle({
showSolutions = false, showSolutions = false,
runOnClick = undefined runOnClick = undefined
}: Props) { }: Props) {
const { const { exam, partIndex, exerciseIndex: examExerciseIndex, userSolutions } = useExamStore((state) => state);
exam
} = useExamStore((state) => state);
const moduleIcon: { [key in Module]: ReactNode } = { const moduleIcon: { [key in Module]: ReactNode } = {
reading: <BsBook className="text-ielts-reading w-6 h-6" />, reading: <BsBook className="text-ielts-reading w-6 h-6" />,
@@ -50,6 +48,14 @@ export default function ModuleTitle({
level: <BsClipboard className="text-ielts-level w-6 h-6" />, level: <BsClipboard className="text-ielts-level w-6 h-6" />,
}; };
const showGrid = useMemo(() =>
exam?.module === "level"
&& partIndex > -1
&& exam.parts[partIndex].exercises[examExerciseIndex].type === "multipleChoice"
&& !!userSolutions,
[exam, examExerciseIndex, partIndex, userSolutions]
)
return ( return (
<> <>
{showTimer && <Timer minTimer={minTimer} disableTimer={disableTimer} />} {showTimer && <Timer minTimer={minTimer} disableTimer={disableTimer} />}
@@ -67,7 +73,7 @@ export default function ModuleTitle({
return ( return (
<div key={index} className="text-2xl font-semibold flex flex-col gap-2"> <div key={index} className="text-2xl font-semibold flex flex-col gap-2">
{partInstructions.split("\\n").map((line, lineIndex) => ( {partInstructions.split("\\n").map((line, lineIndex) => (
<span key={lineIndex} dangerouslySetInnerHTML={{__html: line.replace('that is not correct', 'that is <span class="font-bold"><u>not correct</u></span>')}}></span> <span key={lineIndex} dangerouslySetInnerHTML={{ __html: line.replace('that is not correct', 'that is <span class="font-bold"><u>not correct</u></span>') }}></span>
))} ))}
</div> </div>
); );
@@ -87,7 +93,7 @@ export default function ModuleTitle({
</div> </div>
<ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" /> <ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" />
</div> </div>
{exam?.module === "level" && <MCQuestionGrid showSolutions={showSolutions} runOnClick={runOnClick}/>} {showGrid && <MCQuestionGrid exam={exam as LevelExam} showSolutions={showSolutions} runOnClick={runOnClick} />}
</div> </div>
</div> </div>
</> </>

View File

@@ -13,7 +13,7 @@ import { Assignment } from "@/interfaces/results";
import { Stat, User } from "@/interfaces/user"; import { Stat, User } from "@/interfaces/user";
import { sessionOptions } from "@/lib/session"; import { sessionOptions } from "@/lib/session";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import { findBy, mapBy, redirect, serialize } from "@/utils"; import { filterBy, findBy, mapBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api"; import { requestUser } from "@/utils/api";
import { activeAssignmentFilter } from "@/utils/assignments"; import { activeAssignmentFilter } from "@/utils/assignments";
import { getAssignmentsByAssignee } from "@/utils/assignments.be"; import { getAssignmentsByAssignee } from "@/utils/assignments.be";
@@ -60,7 +60,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const examIDs = uniqBy( const examIDs = uniqBy(
assignments.flatMap((a) => assignments.flatMap((a) =>
a.exams.filter((e) => e.assignee === user.id).map((e) => ({ module: e.module, id: e.id, key: `${e.module}_${e.id}` })), filterBy(a.exams, 'assignee', user.id).map((e) => ({ module: e.module, id: e.id, key: `${e.module}_${e.id}` })),
), ),
"key", "key",
); );
@@ -93,7 +93,7 @@ export default function OfficialExam({ user, entities, assignments, sessions, ex
state.setSelectedModules(mapBy(assignmentExams.sort(sortByModule), 'module')); state.setSelectedModules(mapBy(assignmentExams.sort(sortByModule), 'module'));
state.setAssignment(assignment); state.setAssignment(assignment);
router.push("/exam"); router.push(`/exam?assignment=${assignment.id}`);
} }
}; };
@@ -112,7 +112,7 @@ export default function OfficialExam({ user, entities, assignments, sessions, ex
state.setShowSolutions(false); state.setShowSolutions(false);
state.setQuestionIndex(session.questionIndex); state.setQuestionIndex(session.questionIndex);
router.push("/exam"); router.push(`/exam?assignment=${session.assignment?.id}`);
}; };
const logout = async () => { const logout = async () => {