205 lines
6.5 KiB
TypeScript
205 lines
6.5 KiB
TypeScript
/* eslint-disable @next/next/no-img-element */
|
|
import { WritingExercise } from "@/interfaces/exam";
|
|
import { CommonProps } from ".";
|
|
import React, { Fragment, useEffect, useRef, useState } from "react";
|
|
import { toast } from "react-toastify";
|
|
import Button from "../Low/Button";
|
|
import { Dialog, Transition } from "@headlessui/react";
|
|
import useExamStore from "@/stores/examStore";
|
|
|
|
export default function Writing({
|
|
id,
|
|
prompt,
|
|
prefix,
|
|
suffix,
|
|
type,
|
|
wordCounter,
|
|
attachment,
|
|
userSolutions,
|
|
isPractice = false,
|
|
onNext,
|
|
onBack,
|
|
enableNavigation = false
|
|
}: WritingExercise & CommonProps) {
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [inputText, setInputText] = useState(userSolutions.length === 1 ? userSolutions[0].solution : "");
|
|
const [isSubmitEnabled, setIsSubmitEnabled] = useState(false);
|
|
const [saveTimer, setSaveTimer] = useState(0);
|
|
|
|
const { userSolutions: storeUserSolutions, setUserSolutions } = useExamStore((state) => state);
|
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
|
|
|
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 }], 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(() => {
|
|
if (hasExamEnded)
|
|
onNext({ exercise: id, solutions: [{ id, solution: inputText }], score: { correct: 100, total: 100, missing: 0 }, type, module: "writing", isPractice });
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [hasExamEnded]);
|
|
|
|
useEffect(() => {
|
|
const words = inputText.split(" ").filter((x) => x !== "");
|
|
|
|
if (wordCounter.type === "min") {
|
|
setIsSubmitEnabled(wordCounter.limit <= words.length || enableNavigation);
|
|
} else {
|
|
setIsSubmitEnabled(true);
|
|
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(" "));
|
|
}
|
|
}
|
|
}, [enableNavigation, inputText, wordCounter]);
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4 mt-4">
|
|
<div className="flex justify-between w-full gap-8">
|
|
<Button
|
|
color="purple"
|
|
variant="outline"
|
|
onClick={() =>
|
|
onBack({ exercise: id, solutions: [{ id, solution: inputText }], score: { correct: 100, total: 100, missing: 0 }, type, isPractice })
|
|
}
|
|
className="max-w-[200px] self-end w-full">
|
|
Back
|
|
</Button>
|
|
<Button
|
|
color="purple"
|
|
disabled={!isSubmitEnabled}
|
|
onClick={() =>
|
|
onNext({
|
|
exercise: id,
|
|
solutions: [{ id, solution: inputText.replaceAll(/\s{2,}/g, " ") }],
|
|
score: { correct: 100, total: 100, missing: 0 },
|
|
type,
|
|
module: "writing", isPractice
|
|
})
|
|
}
|
|
className="max-w-[200px] self-end w-full">
|
|
Next
|
|
</Button>
|
|
</div>
|
|
|
|
{attachment && (
|
|
<Transition show={isModalOpen} as={Fragment}>
|
|
<Dialog onClose={() => setIsModalOpen(false)} className="relative z-50">
|
|
<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/30" />
|
|
</Transition.Child>
|
|
|
|
<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">
|
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
|
<Dialog.Panel 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" />
|
|
</Dialog.Panel>
|
|
</div>
|
|
</Transition.Child>
|
|
</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>
|
|
|
|
<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>
|
|
|
|
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
|
<Button
|
|
color="purple"
|
|
variant="outline"
|
|
onClick={() =>
|
|
onBack({ exercise: id, solutions: [{ id, solution: inputText }], score: { correct: 100, total: 100, missing: 0 }, type, isPractice })
|
|
}
|
|
className="max-w-[200px] self-end w-full">
|
|
Back
|
|
</Button>
|
|
<Button
|
|
color="purple"
|
|
disabled={!isSubmitEnabled}
|
|
onClick={() =>
|
|
onNext({
|
|
exercise: id,
|
|
solutions: [{ id, solution: inputText.replaceAll(/\s{2,}/g, " ") }],
|
|
score: { correct: 100, total: 100, missing: 0 },
|
|
type,
|
|
module: "writing", isPractice
|
|
})
|
|
}
|
|
className="max-w-[200px] self-end w-full">
|
|
Next
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|