Files
encoach_ui_odoo19/src/components/Exercises/Writing.tsx

170 lines
5.4 KiB
TypeScript

/* eslint-disable @next/next/no-img-element */
import { WritingExercise } from "@/interfaces/exam";
import React, { Fragment, useEffect, useState } from "react";
import { Dialog, DialogPanel, Transition, TransitionChild } from "@headlessui/react";
import useExamStore, { usePersistentExamStore } from "@/stores/exam";
import { CommonProps } from "./types";
import { toast } from "react-toastify";
import PracticeBadge from "../Low/PracticeBadge";
const Writing: React.FC<WritingExercise & CommonProps> = ({
id,
prompt,
prefix,
suffix,
type,
wordCounter,
attachment,
userSolutions,
isPractice = false,
registerSolution,
headerButtons,
footerButtons,
preview,
}) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [inputText, setInputText] = useState(userSolutions.length === 1 ? userSolutions[0].solution : "");
const [saveTimer, setSaveTimer] = useState(0);
const examState = useExamStore((state) => state);
const persistentExamState = usePersistentExamStore((state) => state);
const { userSolutions: storeUserSolutions, setUserSolutions, setNavigation } = !preview ? examState : persistentExamState;
useEffect(() => {
if (!preview) setNavigation({ nextDisabled: true });
}, [setNavigation, preview]);
useEffect(() => {
const saveTimerInterval = setInterval(() => {
setSaveTimer((prev) => prev + 1);
}, 1000);
return () => {
clearInterval(saveTimerInterval);
};
}, []);
useEffect(() => {
if (inputText.length > 0 && saveTimer % 10 === 0) {
setUserSolutions([
...storeUserSolutions.filter((x) => x.exercise !== id),
{ exercise: id, solutions: [{ id, solution: inputText.replaceAll(/\s{2,}/g, " ") }], score: { correct: 100, total: 100, missing: 0 }, type, module: "writing" },
]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveTimer]);
useEffect(() => {
if (localStorage.getItem("enable_paste")) return;
const listener = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
e.preventDefault();
}
};
document.addEventListener("keydown", listener);
return () => {
document.removeEventListener("keydown", listener);
};
}, []);
useEffect(() => {
const words = inputText.split(" ").filter((x) => x !== "");
if (wordCounter.type === "min") {
setNavigation({ nextDisabled: !(wordCounter.limit <= words.length) });
} else {
setNavigation({ nextDisabled: false });
if (wordCounter.limit < words.length) {
toast.warning(`You have reached your word limit of ${wordCounter.limit} words!`, { toastId: "word-limit" });
setInputText(words.slice(0, words.length - 1).join(" "));
}
}
}, [inputText, setNavigation, wordCounter]);
useEffect(() => {
registerSolution(() => ({
exercise: id,
solutions: [{ id, solution: inputText.replaceAll(/\s{2,}/g, " ") }],
score: { correct: 100, total: 100, missing: 0 },
type,
isPractice
}));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, type, isPractice, inputText]);
return (
<div className="flex flex-col gap-4 mt-4">
{headerButtons}
{attachment && (
<Transition show={isModalOpen} as={Fragment}>
<Dialog onClose={() => setIsModalOpen(false)} className="relative z-50">
<TransitionChild
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/30" />
</TransitionChild>
<TransitionChild
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">
<div className="fixed inset-0 flex items-center justify-center p-4">
<DialogPanel className="w-fit h-fit rounded-xl bg-white">
<img src={attachment.url} alt={attachment.description} className="max-w-4xl w-full self-center rounded-xl p-4" />
</DialogPanel>
</div>
</TransitionChild>
</Dialog>
</Transition>
)}
<div className="flex flex-col h-full w-full gap-9 mb-20">
<div className="flex flex-col w-full gap-7 bg-mti-gray-smoke rounded-xl py-8 pb-12 px-16">
<span className="whitespace-pre-wrap">{prefix.replaceAll("\\n", "\n")}</span>
<span className="font-semibold whitespace-pre-wrap">{prompt.replaceAll("\\n", "\n")}</span>
{attachment && (
<img
onClick={() => setIsModalOpen(true)}
src={attachment.url}
alt={attachment.description}
className="max-w-md self-center rounded-xl cursor-pointer"
/>
)}
</div>
{isPractice && <PracticeBadge className="w-fit self-end" />}
<div className="w-full h-full flex flex-col gap-4">
<span className="whitespace-pre-wrap">{suffix}</span>
<textarea
onContextMenu={(e) => e.preventDefault()}
className="w-full h-full min-h-[300px] cursor-text px-7 py-8 input border-2 border-mti-gray-platinum bg-white rounded-3xl"
onChange={(e) => setInputText(e.target.value)}
value={inputText}
placeholder="Write your text here..."
spellCheck={false}
/>
<span className="text-base self-end text-mti-gray-cool">Word Count: {inputText.split(" ").filter((x) => x !== "").length}</span>
</div>
</div>
{footerButtons}
</div>
);
}
export default Writing;