ENCOA-154: Increase the number of questions per page in all modules (Priority in Level Test)

This commit is contained in:
Tiago Ribeiro
2024-09-04 11:41:48 +01:00
parent becc91d8ea
commit 4654c21d92
2 changed files with 82 additions and 72 deletions

View File

@@ -1,12 +1,12 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import { MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap } from "@/interfaces/exam"; import {MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap} from "@/interfaces/exam";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import clsx from "clsx"; import clsx from "clsx";
import { useEffect, useState } from "react"; import {useEffect, useState} from "react";
import reactStringReplace from "react-string-replace"; import reactStringReplace from "react-string-replace";
import { CommonProps } from "."; import {CommonProps} from ".";
import Button from "../Low/Button"; import Button from "../Low/Button";
import { v4 } from "uuid"; import {v4} from "uuid";
function Question({ function Question({
id, id,
@@ -18,9 +18,8 @@ function Question({
}: MultipleChoiceQuestion & { }: MultipleChoiceQuestion & {
userSolution: string | undefined; userSolution: string | undefined;
onSelectOption?: (option: string) => void; onSelectOption?: (option: string) => void;
showSolution?: boolean, showSolution?: boolean;
}) { }) {
const renderPrompt = (prompt: string) => { const renderPrompt = (prompt: string) => {
return reactStringReplace(prompt, /(<u>.*?<\/u>)/g, (match) => { return reactStringReplace(prompt, /(<u>.*?<\/u>)/g, (match) => {
const word = match.replaceAll("<u>", "").replaceAll("</u>", ""); const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
@@ -49,7 +48,9 @@ function Question({
"flex flex-col items-center border border-mti-gray-platinum p-4 px-8 rounded-xl gap-4 cursor-pointer bg-white relative select-none", "flex flex-col items-center border border-mti-gray-platinum p-4 px-8 rounded-xl gap-4 cursor-pointer bg-white relative select-none",
userSolution === option.id.toString() && "border-mti-purple-light", userSolution === option.id.toString() && "border-mti-purple-light",
)}> )}>
<span key={v4()} className={clsx("text-sm", userSolution !== option.id.toString() && "opacity-50")}>{option.id.toString()}</span> <span key={v4()} className={clsx("text-sm", userSolution !== option.id.toString() && "opacity-50")}>
{option.id.toString()}
</span>
<img src={option.src!} alt={`Option ${option.id.toString()}`} /> <img src={option.src!} alt={`Option ${option.id.toString()}`} />
</div> </div>
))} ))}
@@ -71,38 +72,30 @@ function Question({
); );
} }
export default function MultipleChoice({ id, prompt, type, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) { export default function MultipleChoice({id, prompt, type, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions); const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions);
const { const {questionIndex, exerciseIndex, exam, shuffles, hasExamEnded, partIndex, setQuestionIndex, setCurrentSolution} = useExamStore(
questionIndex, (state) => state,
exerciseIndex, );
exam,
shuffles,
hasExamEnded,
partIndex,
setQuestionIndex,
setCurrentSolution
} = useExamStore((state) => state);
const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles; const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles;
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
useEffect(() => { useEffect(() => {
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type }); if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasExamEnded]); }, [hasExamEnded]);
const onSelectOption = (option: string) => { const onSelectOption = (option: string, question: MultipleChoiceQuestion) => {
const question = questions[questionIndex]; setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), {option, question: question.id}]);
setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), { option, question: question.id }]);
}; };
useEffect(() => { useEffect(() => {
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps }); setCurrentSolution({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps});
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers, setAnswers]) }, [answers, setAnswers]);
const getShuffledSolution = (originalSolution: string, questionShuffleMap: ShuffleMap) => { const getShuffledSolution = (originalSolution: string, questionShuffleMap: ShuffleMap) => {
for (const [newPosition, originalPosition] of Object.entries(questionShuffleMap.map)) { for (const [newPosition, originalPosition] of Object.entries(questionShuffleMap.map)) {
@@ -111,8 +104,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
} }
} }
return originalSolution; return originalSolution;
} };
const calculateScore = () => { const calculateScore = () => {
const total = questions.length; const total = questions.length;
@@ -135,24 +127,24 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
return isSolutionCorrect || false; return isSolutionCorrect || false;
}).length; }).length;
const missing = total - answers!.filter((x) => questions.find((y) => x.question.toString() === y.id.toString())).length; const missing = total - answers!.filter((x) => questions.find((y) => x.question.toString() === y.id.toString())).length;
return { total, correct, missing }; return {total, correct, missing};
}; };
const next = () => { const next = () => {
if (questionIndex === questions.length - 1) { if (questionIndex + 1 >= questions.length - 1) {
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps }); onNext({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps});
} else { } else {
setQuestionIndex(questionIndex + 1); setQuestionIndex(questionIndex + 2);
} }
scrollToTop(); scrollToTop();
}; };
const back = () => { const back = () => {
if (questionIndex === 0) { if (questionIndex === 0) {
onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps }); onBack({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps});
} else { } else {
if (exam?.module === "level" && typeof exam.parts[0].intro !== "undefined" && questionIndex === 0) return; if (exam?.module === "level" && typeof exam.parts[0].intro !== "undefined" && questionIndex === 0) return;
setQuestionIndex(questionIndex - 1); setQuestionIndex(questionIndex - 2);
} }
scrollToTop(); scrollToTop();
@@ -160,35 +152,47 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
return ( return (
<> <>
<div className="flex flex-col gap-2 mt-4 h-fit w-full mb-20 bg-mti-gray-smoke rounded-xl px-16 py-8"> <div className="flex flex-col gap-4 mt-2 mb-20">
{/*<span className="text-xl font-semibold mb-2">{"Select the appropriate option."}</span>*/} <div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
{questionIndex < questions.length && ( {/*<span className="text-xl font-semibold mb-2">{"Select the appropriate option."}</span>*/}
<Question {questionIndex < questions.length && (
{...questions[questionIndex]} <Question
userSolution={answers.find((x) => questions[questionIndex].id === x.question)?.option} {...questions[questionIndex]}
onSelectOption={onSelectOption} userSolution={answers.find((x) => questions[questionIndex].id === x.question)?.option}
/> onSelectOption={(option) => onSelectOption(option, questions[questionIndex])}
/>
)}
</div>
{questionIndex + 1 < questions.length && (
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
<Question
{...questions[questionIndex + 1]}
userSolution={answers.find((x) => questions[questionIndex + 1].id === x.question)?.option}
onSelectOption={(option) => onSelectOption(option, questions[questionIndex + 1])}
/>
</div>
)} )}
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button color="purple" variant="outline" onClick={back} className="max-w-[200px] w-full" <Button
disabled={ color="purple"
exam && exam.module === "level" && variant="outline"
partIndex === 0 && onClick={back}
questionIndex === 0 className="max-w-[200px] w-full"
} disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
>
Back Back
</Button> </Button>
<Button color="purple" onClick={next} className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={next} className="max-w-[200px] self-end w-full">
{ {exam &&
exam && exam.module === "level" && exam.module === "level" &&
partIndex === exam.parts.length - 1 && partIndex === exam.parts.length - 1 &&
exerciseIndex === exam.parts[partIndex].exercises.length - 1 && exerciseIndex === exam.parts[partIndex].exercises.length - 1 &&
questionIndex === questions.length - 1 questionIndex + 1 >= questions.length - 1
? "Submit" : "Next"} ? "Submit"
: "Next"}
</Button> </Button>
</div> </div>
</> </>

View File

@@ -111,10 +111,10 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
}; };
const next = () => { const next = () => {
if (questionIndex === questions.length - 1) { if (questionIndex + 1 >= questions.length - 1) {
onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type}); onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type});
} else { } else {
setQuestionIndex(questionIndex + 1); setQuestionIndex(questionIndex + 2);
} }
}; };
@@ -122,22 +122,34 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
if (questionIndex === 0) { if (questionIndex === 0) {
onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type}); onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type});
} else { } else {
setQuestionIndex(questionIndex - 1); setQuestionIndex(questionIndex - 2);
} }
}; };
return ( return (
<> <>
<div className="flex flex-col gap-4 w-full h-full mb-20"> <div className="flex flex-col gap-4 w-full h-full mb-20">
<div className="flex flex-col gap-2 mt-4 h-full bg-mti-gray-smoke rounded-xl px-16 py-8"> <div className="flex flex-col gap-4 mt-2">
{/*<span className="text-xl font-semibold">{prompt}</span>*/} <div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
{userSolutions && questionIndex < questions.length && ( {/*<span className="text-xl font-semibold">{prompt}</span>*/}
<Question {userSolutions && questionIndex < questions.length && (
{...questions[questionIndex]} <Question
userSolution={userSolutions.find((x) => questions[questionIndex].id === x.question)?.option} {...questions[questionIndex]}
/> userSolution={userSolutions.find((x) => questions[questionIndex].id === x.question)?.option}
/>
)}
</div>
{userSolutions && questionIndex + 1 < questions.length && (
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
<Question
{...questions[questionIndex + 1]}
userSolution={userSolutions.find((x) => questions[questionIndex + 1].id === x.question)?.option}
/>
</div>
)} )}
</div> </div>
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<div className="w-4 h-4 rounded-full bg-mti-purple" /> <div className="w-4 h-4 rounded-full bg-mti-purple" />
@@ -160,13 +172,7 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
variant="outline" variant="outline"
onClick={back} onClick={back}
className="max-w-[200px] w-full" className="max-w-[200px] w-full"
disabled={ disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
exam &&
typeof partIndex !== "undefined" &&
exam.module === "level" &&
questionIndex === 0 &&
partIndex === 0
}>
Back Back
</Button> </Button>