ENCOA-182, ENCOA-185, ENCOA-177, ENCOA-168, ENCOA-186, ENCOA-176, ENCOA-189, ENCOA-167
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import {FillBlanksExercise, FillBlanksMCOption} from "@/interfaces/exam";
|
||||
import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import clsx from "clsx";
|
||||
import {Fragment, useCallback, useEffect, useMemo, useState} from "react";
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import {CommonProps} from "..";
|
||||
import { CommonProps } from "..";
|
||||
import Button from "../../Low/Button";
|
||||
import {v4} from "uuid";
|
||||
import { v4 } from "uuid";
|
||||
import MCDropdown from "./MCDropdown";
|
||||
|
||||
const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
id,
|
||||
@@ -19,24 +20,19 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
onNext,
|
||||
onBack,
|
||||
}) => {
|
||||
const {shuffles, exam, partIndex, questionIndex, exerciseIndex} = useExamStore((state) => state);
|
||||
const [answers, setAnswers] = useState<{id: string; solution: string}[]>(userSolutions);
|
||||
const { shuffles, exam, partIndex, questionIndex, exerciseIndex } = useExamStore((state) => state);
|
||||
const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions);
|
||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
||||
const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles;
|
||||
|
||||
const [currentMCSelection, setCurrentMCSelection] = useState<{id: string; selection: FillBlanksMCOption}>();
|
||||
|
||||
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 dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const excludeWordMCType = (x: any) => {
|
||||
return typeof x === "string" ? x : (x as {letter: string; word: string});
|
||||
return typeof x === "string" ? x : (x as { letter: string; word: string });
|
||||
};
|
||||
|
||||
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
|
||||
}, [hasExamEnded]);
|
||||
|
||||
@@ -45,6 +41,19 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
correctWords = (exam.parts[partIndex].exercises[exerciseIndex] as FillBlanksExercise).words;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setOpenDropdownId(null);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const calculateScore = () => {
|
||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||
const correct = answers!.filter((x) => {
|
||||
@@ -71,56 +80,55 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
return false;
|
||||
}).length;
|
||||
const missing = total - answers!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||
return {total, correct, missing};
|
||||
return { total, correct, missing };
|
||||
};
|
||||
|
||||
const [openDropdownId, setOpenDropdownId] = useState<string | null>(null);
|
||||
|
||||
const renderLines = useCallback(
|
||||
(line: string) => {
|
||||
return (
|
||||
<div className="text-base leading-5" key={v4()}>
|
||||
<div className="text-xl leading-5" key={v4()} ref={dropdownRef}>
|
||||
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
||||
const id = match.replaceAll(/[\{\}]/g, "");
|
||||
const userSolution = answers.find((x) => x.id === id);
|
||||
const styles = clsx(
|
||||
"rounded-full hover:text-white transition duration-300 ease-in-out my-1 px-5 py-2 text-center",
|
||||
currentMCSelection?.id == id && "!bg-mti-purple !text-white !outline-none !ring-0",
|
||||
"rounded-full hover:text-white transition duration-300 ease-in-out my-1 px-5 py-2 text-center w-fit",
|
||||
!userSolution && "text-center text-mti-purple-light bg-mti-purple-ultralight",
|
||||
userSolution && "text-center text-mti-purple-dark bg-mti-purple-ultralight",
|
||||
);
|
||||
|
||||
const currentSelection = words.find((x) => {
|
||||
if (typeof x !== "string" && "id" in x) {
|
||||
return (x as FillBlanksMCOption).id.toString() == id.toString();
|
||||
}
|
||||
return false;
|
||||
}) as FillBlanksMCOption;
|
||||
|
||||
return variant === "mc" ? (
|
||||
<>
|
||||
{/*<span className="mr-2">{`(${id})`}</span>*/}
|
||||
<button
|
||||
className={styles}
|
||||
onClick={() => {
|
||||
setCurrentMCSelection({
|
||||
id: id,
|
||||
selection: words.find((x) => {
|
||||
if (typeof x !== "string" && "id" in x) {
|
||||
return (x as FillBlanksMCOption).id.toString() == id.toString();
|
||||
}
|
||||
return false;
|
||||
}) as FillBlanksMCOption,
|
||||
});
|
||||
}}>
|
||||
{userSolution?.solution === undefined ? (
|
||||
<span className="text-transparent select-none">placeholder</span>
|
||||
) : (
|
||||
<span> {userSolution.solution} </span>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
<MCDropdown
|
||||
id={id}
|
||||
options={currentSelection.options}
|
||||
onSelect={(value) => onSelection(id, value)}
|
||||
selectedValue={userSolution?.solution}
|
||||
className="inline-block py-2 px-1"
|
||||
width={220}
|
||||
isOpen={openDropdownId === id}
|
||||
onToggle={()=> setOpenDropdownId(prevId => prevId === id ? null : id)}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
className={styles}
|
||||
onChange={(e) => setAnswers((prev) => [...prev.filter((x) => x.id !== id), {id, solution: e.target.value}])}
|
||||
onChange={(e) => setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id, solution: e.target.value }])}
|
||||
value={userSolution?.solution}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
})
|
||||
}
|
||||
</div >
|
||||
);
|
||||
},
|
||||
[variant, words, setCurrentMCSelection, answers, currentMCSelection],
|
||||
[variant, words, answers, openDropdownId],
|
||||
);
|
||||
|
||||
const memoizedLines = useMemo(() => {
|
||||
@@ -131,15 +139,15 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
</p>
|
||||
));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [text, variant, renderLines, currentMCSelection]);
|
||||
}, [text, variant, renderLines]);
|
||||
|
||||
const onSelection = (questionID: string, value: string) => {
|
||||
setAnswers((prev) => [...prev.filter((x) => x.id !== questionID), {id: questionID, solution: value}]);
|
||||
setAnswers((prev) => [...prev.filter((x) => x.id !== questionID), { id: questionID, solution: value }]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (variant === "mc") {
|
||||
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
|
||||
}, [answers]);
|
||||
@@ -150,19 +158,19 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
<Button
|
||||
color="purple"
|
||||
variant="outline"
|
||||
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps})}
|
||||
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })}
|
||||
className="max-w-[200px] w-full"
|
||||
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
||||
Back
|
||||
Previous Page
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="purple"
|
||||
onClick={() => {
|
||||
onNext({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps});
|
||||
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
||||
}}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
Next Page
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -178,38 +186,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
</span>
|
||||
)}
|
||||
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">{memoizedLines}</span>
|
||||
{variant === "mc" && typeCheckWordsMC(words) ? (
|
||||
<>
|
||||
{currentMCSelection && (
|
||||
<div className="bg-mti-gray-smoke rounded-xl flex flex-col gap-4 px-16 py-8">
|
||||
<span className="font-medium text-lg text-mti-purple-dark mb-4 px-2">{`${currentMCSelection.id} - Select the appropriate word.`}</span>
|
||||
<div className="flex gap-4 flex-wrap justify-between">
|
||||
{currentMCSelection.selection?.options &&
|
||||
Object.entries(currentMCSelection.selection.options)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([key, value]) => {
|
||||
return (
|
||||
<div
|
||||
key={v4()}
|
||||
onClick={() => onSelection(currentMCSelection.id, value)}
|
||||
className={clsx(
|
||||
"flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base",
|
||||
!!answers.find(
|
||||
(x) =>
|
||||
x.solution.toLocaleLowerCase() === value.toLocaleLowerCase() &&
|
||||
x.id === currentMCSelection.id,
|
||||
) && "!bg-mti-purple-light !text-white",
|
||||
)}>
|
||||
<span className="font-semibold">{key}.</span>
|
||||
<span>{value}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
{variant !== "mc" && (
|
||||
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-4">
|
||||
<span className="font-medium text-mti-purple-dark">Options</span>
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
@@ -240,19 +217,19 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
<Button
|
||||
color="purple"
|
||||
variant="outline"
|
||||
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps})}
|
||||
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })}
|
||||
className="max-w-[200px] w-full"
|
||||
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
||||
Back
|
||||
Previous Page
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="purple"
|
||||
onClick={() => {
|
||||
onNext({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps});
|
||||
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
||||
}}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
Next Page
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user