221 lines
7.8 KiB
TypeScript
221 lines
7.8 KiB
TypeScript
import {FillBlanksExercise, FillBlanksMCOption, ShuffleMap} from "@/interfaces/exam";
|
|
import clsx from "clsx";
|
|
import reactStringReplace from "react-string-replace";
|
|
import {CommonProps} from ".";
|
|
import {Fragment} from "react";
|
|
import Button from "../Low/Button";
|
|
import useExamStore from "@/stores/examStore";
|
|
|
|
export default function FillBlanksSolutions({id, type, prompt, solutions, words, text, onNext, onBack}: FillBlanksExercise & CommonProps) {
|
|
const storeUserSolutions = useExamStore((state) => state.userSolutions);
|
|
const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state);
|
|
|
|
const correctUserSolutions = storeUserSolutions.find((solution) => solution.exercise === id)?.solutions;
|
|
|
|
const shuffles = useExamStore((state) => state.shuffles);
|
|
|
|
const calculateScore = () => {
|
|
const total = text.match(/({{\d+}})/g)?.length || 0;
|
|
const correct = correctUserSolutions!.filter((x) => {
|
|
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
|
|
if (!solution) return false;
|
|
|
|
const option = words.find((w) => {
|
|
if (typeof w === "string") {
|
|
return w.toLowerCase() === x.solution.toLowerCase();
|
|
} else if ("letter" in w) {
|
|
return w.letter.toLowerCase() === x.solution.toLowerCase();
|
|
} else {
|
|
return w.id.toString() === x.id.toString();
|
|
}
|
|
});
|
|
if (!option) return false;
|
|
|
|
if (typeof option === "string") {
|
|
return solution.toLowerCase() === option.toLowerCase();
|
|
} else if ("letter" in option) {
|
|
return solution.toLowerCase() === option.word.toLowerCase();
|
|
} else if ("options" in option) {
|
|
return option.options[solution as keyof typeof option.options] == x.solution;
|
|
}
|
|
return false;
|
|
}).length;
|
|
const missing = total - correctUserSolutions!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
|
return {total, correct, missing};
|
|
};
|
|
|
|
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
|
|
return Array.isArray(words) && words.every((word) => word && typeof word === "object" && "id" in word && "options" in word);
|
|
};
|
|
|
|
const renderLines = (line: string) => {
|
|
return (
|
|
<span>
|
|
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
|
const questionId = match.replaceAll(/[\{\}]/g, "");
|
|
const userSolution = correctUserSolutions!.find((x) => x.id.toString() === questionId.toString());
|
|
const answerSolution = solutions.find((sol) => sol.id.toString() === questionId.toString())!.solution;
|
|
const questionShuffleMap = shuffles.find((x) => x.exerciseID == id)?.shuffles.find((y) => y.questionID == questionId);
|
|
const newAnswerSolution = questionShuffleMap
|
|
? questionShuffleMap.map[answerSolution].toLowerCase()
|
|
: answerSolution.toLowerCase();
|
|
|
|
if (!userSolution) {
|
|
let answerText;
|
|
if (typeCheckWordsMC(words)) {
|
|
const options = words.find((x) => x.id.toString() === questionId.toString());
|
|
const correctKey = Object.keys(options!.options).find((key) => key.toLowerCase() === newAnswerSolution);
|
|
answerText = options!.options[correctKey as keyof typeof options];
|
|
} else {
|
|
answerText = answerSolution;
|
|
}
|
|
|
|
return (
|
|
<button
|
|
className={clsx(
|
|
"rounded-full hover:text-white hover:bg-mti-gray-davy transition duration-300 ease-in-out my-1 px-5 py-2 text-center text-white bg-mti-gray-davy",
|
|
)}>
|
|
{answerText}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
const userSolutionWord = words.find((w) =>
|
|
typeof w === "string"
|
|
? w.toLowerCase() === userSolution.solution.toLowerCase()
|
|
: "letter" in w
|
|
? w.letter.toLowerCase() === userSolution.solution.toLowerCase()
|
|
: "options" in w
|
|
? w.id === userSolution.questionId
|
|
: false,
|
|
);
|
|
|
|
const userSolutionText =
|
|
typeof userSolutionWord === "string"
|
|
? userSolutionWord
|
|
: userSolutionWord && "letter" in userSolutionWord
|
|
? userSolutionWord.word
|
|
: userSolutionWord && "options" in userSolutionWord
|
|
? userSolution.solution
|
|
: userSolution.solution;
|
|
|
|
let correct;
|
|
let solutionText;
|
|
if (typeCheckWordsMC(words)) {
|
|
const options = words.find((x) => x.id.toString() === questionId.toString());
|
|
if (options) {
|
|
const correctKey = Object.keys(options.options).find((key) => key.toLowerCase() === newAnswerSolution);
|
|
correct = userSolution.solution == options.options[correctKey as keyof typeof options.options];
|
|
solutionText = options.options[correctKey as keyof typeof options.options] || answerSolution;
|
|
} else {
|
|
correct = false;
|
|
solutionText = answerSolution;
|
|
}
|
|
} else {
|
|
correct = userSolutionText === answerSolution;
|
|
solutionText = answerSolution;
|
|
}
|
|
|
|
if (correct) {
|
|
return (
|
|
<button
|
|
className={clsx(
|
|
"rounded-full hover:text-white hover:bg-mti-purple transition duration-300 ease-in-out my-1",
|
|
userSolution && "px-5 py-2 text-center text-white bg-mti-purple-light",
|
|
)}>
|
|
{solutionText}
|
|
</button>
|
|
);
|
|
} else {
|
|
return (
|
|
<>
|
|
<button
|
|
className={clsx(
|
|
"rounded-full hover:text-white hover:bg-mti-rose transition duration-300 ease-in-out my-1 mr-1",
|
|
userSolution && "px-5 py-2 text-center text-white bg-mti-rose-light",
|
|
)}>
|
|
{userSolutionText}
|
|
</button>
|
|
|
|
<button
|
|
className={clsx(
|
|
"rounded-full hover:text-white hover:bg-mti-purple transition duration-300 ease-in-out my-1",
|
|
userSolution && "px-5 py-2 text-center text-white bg-mti-purple-light",
|
|
)}>
|
|
{solutionText}
|
|
</button>
|
|
</>
|
|
);
|
|
}
|
|
})}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<div className="flex justify-between w-full gap-8">
|
|
<Button
|
|
color="purple"
|
|
variant="outline"
|
|
onClick={() => onBack({exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type})}
|
|
className="max-w-[200px] w-full"
|
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
|
Back
|
|
</Button>
|
|
|
|
<Button
|
|
color="purple"
|
|
onClick={() => onNext({exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type})}
|
|
className="max-w-[200px] self-end w-full">
|
|
Next
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
|
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
|
|
{correctUserSolutions &&
|
|
text.split("\\n").map((line, index) => (
|
|
<p key={index}>
|
|
{renderLines(line)}
|
|
<br />
|
|
</p>
|
|
))}
|
|
</span>
|
|
<div className="flex gap-4 items-center">
|
|
<div className="flex gap-2 items-center">
|
|
<div className="w-4 h-4 rounded-full bg-mti-purple" />
|
|
Correct
|
|
</div>
|
|
<div className="flex gap-2 items-center">
|
|
<div className="w-4 h-4 rounded-full bg-mti-gray-davy" />
|
|
Unanswered
|
|
</div>
|
|
<div className="flex gap-2 items-center">
|
|
<div className="w-4 h-4 rounded-full bg-mti-rose" />
|
|
Wrong
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
|
<Button
|
|
color="purple"
|
|
variant="outline"
|
|
onClick={() => onBack({exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type})}
|
|
className="max-w-[200px] w-full"
|
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
|
Back
|
|
</Button>
|
|
|
|
<Button
|
|
color="purple"
|
|
onClick={() => onNext({exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type})}
|
|
className="max-w-[200px] self-end w-full">
|
|
Next
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|