114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
|
import {FillBlanksExercise} from "@/interfaces/exam";
|
|
import {Dialog, Transition} from "@headlessui/react";
|
|
import clsx from "clsx";
|
|
import {Fragment, useState} from "react";
|
|
import reactStringReplace from "react-string-replace";
|
|
|
|
interface WordsPopoutProps {
|
|
words: {word: string; isDisabled: boolean}[];
|
|
isOpen: boolean;
|
|
onCancel: () => void;
|
|
onAnswer: (answer: string) => void;
|
|
}
|
|
|
|
function WordsPopout({words, isOpen, onCancel, onAnswer}: WordsPopoutProps) {
|
|
return (
|
|
<Transition appear show={isOpen} as={Fragment}>
|
|
<Dialog as="div" className="relative z-10" onClose={onCancel}>
|
|
<Transition.Child
|
|
as={Fragment}
|
|
enter="ease-out duration-300"
|
|
enterFrom="opacity-0"
|
|
enterTo="opacity-100"
|
|
leave="ease-in duration-200"
|
|
leaveFrom="opacity-100"
|
|
leaveTo="opacity-0">
|
|
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
|
</Transition.Child>
|
|
|
|
<div className="fixed inset-0 overflow-y-auto">
|
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
|
<Transition.Child
|
|
as={Fragment}
|
|
enter="ease-out duration-300"
|
|
enterFrom="opacity-0 scale-95"
|
|
enterTo="opacity-100 scale-100"
|
|
leave="ease-in duration-200"
|
|
leaveFrom="opacity-100 scale-100"
|
|
leaveTo="opacity-0 scale-95">
|
|
<Dialog.Panel className="w-fit transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all flex flex-col">
|
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
|
List of words
|
|
</Dialog.Title>
|
|
<div className="mt-4 grid grid-cols-3 gap-4">
|
|
{words.map((word) => (
|
|
<button
|
|
key={word.word}
|
|
onClick={() => onAnswer(word.word)}
|
|
disabled={word.isDisabled}
|
|
className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)}>
|
|
{word.word}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-4 self-end">
|
|
<button onClick={onCancel} className={clsx("btn btn-wide gap-4 relative text-white", errorButtonStyle)}>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</Dialog.Panel>
|
|
</Transition.Child>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
</Transition>
|
|
);
|
|
}
|
|
|
|
export default function FillBlanks({allowRepetition, prompt, solutions, text, words}: FillBlanksExercise) {
|
|
const [userSolutions, setUserSolutions] = useState<{id: string; solution: string}[]>([]);
|
|
const [currentBlankId, setCurrentBlankId] = useState<string>();
|
|
|
|
const renderLines = (line: string) => {
|
|
return (
|
|
<span>
|
|
{reactStringReplace(line, /({{\d}})/g, (match) => {
|
|
const id = match.replaceAll(/[\{\}]/g, "");
|
|
const userSolution = userSolutions.find((x) => x.id === id);
|
|
|
|
return (
|
|
<button className="border-2 rounded-xl px-4 text-blue-400 border-blue-400" onClick={() => setCurrentBlankId(id)}>
|
|
{userSolution ? userSolution.solution : id}
|
|
</button>
|
|
);
|
|
})}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col">
|
|
<WordsPopout
|
|
words={words.map((word) => ({word, isDisabled: allowRepetition ? false : userSolutions.map((x) => x.solution).includes(word)}))}
|
|
isOpen={!!currentBlankId}
|
|
onCancel={() => setCurrentBlankId(undefined)}
|
|
onAnswer={(solution: string) => {
|
|
setUserSolutions((prev) => [...prev.filter((x) => x.id !== currentBlankId), {id: currentBlankId!, solution}]);
|
|
setCurrentBlankId(undefined);
|
|
}}
|
|
/>
|
|
<span className="text-lg font-medium text-center px-48">{prompt}</span>
|
|
<span>
|
|
{text.split("\n").map((line) => (
|
|
<>
|
|
{renderLines(line)}
|
|
<br />
|
|
</>
|
|
))}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|