import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam"; import clsx from "clsx"; import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react"; import reactStringReplace from "react-string-replace"; import { CommonProps } from "../types"; import { v4 } from "uuid"; import MCDropdown from "./MCDropdown"; import useExamStore, { usePersistentExamStore } from "@/stores/exam"; import PracticeBadge from "@/components/Low/PracticeBadge"; const FillBlanks: React.FC = ({ id, type, isPractice = false, prompt, solutions, text, words, userSolutions, variant, registerSolution, headerButtons, footerButtons, preview, }) => { const examState = useExamStore((state) => state); const persistentExamState = usePersistentExamStore((state) => state); const { exerciseIndex, partIndex, shuffles, exam, } = examState; !preview ? examState : persistentExamState; const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions); 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 }); }; 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 = useCallback(() => { 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 }; }, [answers, correctWords, solutions, text]); useEffect(() => { registerSolution(() => ({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps, isPractice })); // eslint-disable-next-line react-hooks/exhaustive-deps }, [id, answers, type, isPractice, shuffleMaps, calculateScore]); const [openDropdownId, setOpenDropdownId] = useState(null); const renderLines = useCallback( (line: string) => { return (
{reactStringReplace(line, /({{\d+}})/g, (match, i, original) => { 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 inline-block", !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 align-middle" 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 }]); }; return (
{headerButtons}
{variant !== "mc" && ( {prompt.split("\\n").map((line, index) => ( {line}
))}
)} {isPractice && } {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} ); })}
)} {footerButtons}
); }; //FillBlanks.whyDidYouRender = true export default FillBlanks;