import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam"; import useExamStore from "@/stores/examStore"; import clsx from "clsx"; import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react"; import reactStringReplace from "react-string-replace"; import { CommonProps } from ".."; import Button from "../../Low/Button"; import { v4 } from "uuid"; import MCDropdown from "./MCDropdown"; const FillBlanks: React.FC = ({ id, type, prompt, solutions, text, words, userSolutions, variant, onNext, onBack, }) => { 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 dropdownRef = useRef(null); const excludeWordMCType = (x: any) => { return typeof x === "string" ? x : (x as { letter: string; word: string }); }; useEffect(() => { if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [hasExamEnded]); let correctWords: any; if (exam && (exam.module === "level" || exam.module === "reading") && exam.parts[partIndex].exercises[exerciseIndex].type === "fillBlanks") { 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) => { const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution; if (!solution) return false; const option = correctWords!.find((w: any) => { 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 - answers!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length; return { total, correct, missing }; }; const [openDropdownId, setOpenDropdownId] = useState(null); const renderLines = useCallback( (line: string) => { return (
{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 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" ? ( 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)} /> ) : ( setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id, solution: e.target.value }])} value={userSolution?.solution} /> ); }) }
); }, [variant, words, answers, openDropdownId], ); const memoizedLines = useMemo(() => { return text.split("\\n").map((line, index) => (

{renderLines(line)}

)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [text, variant, renderLines]); const onSelection = (questionID: string, value: string) => { 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 }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [answers]); return (
{variant !== "mc" && ( {prompt.split("\\n").map((line, index) => ( {line}
))}
)} {memoizedLines} {variant !== "mc" && (
Options
{words.map((v) => { v = excludeWordMCType(v); const text = typeof v === "string" ? v : `${v.letter} - ${v.word}`; return ( x.solution.toLowerCase() === (typeof v === "string" ? v : "letter" in v ? v.letter : "").toLowerCase(), ) && "bg-mti-purple-dark text-white", )} key={v4()}> {text} ); })}
)}
); }; export default FillBlanks;