Files
encoach_frontend/src/components/Exercises/MatchSentences.tsx

178 lines
6.0 KiB
TypeScript

import { errorButtonStyle, infoButtonStyle } from "@/constants/buttonStyles";
import { MatchSentenceExerciseOption, MatchSentenceExerciseSentence, MatchSentencesExercise } from "@/interfaces/exam";
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
import Icon from "@mdi/react";
import clsx from "clsx";
import { Fragment, useEffect, useState } from "react";
import LineTo from "react-lineto";
import { CommonProps } from ".";
import Button from "../Low/Button";
import Xarrow from "react-xarrows";
import useExamStore from "@/stores/examStore";
import { DndContext, DragEndEvent, useDraggable, useDroppable } from "@dnd-kit/core";
function DroppableQuestionArea({ question, answer }: { question: MatchSentenceExerciseSentence; answer?: string }) {
const { isOver, setNodeRef } = useDroppable({ id: `droppable_sentence_${question.id}` });
return (
<div className="grid grid-cols-3 gap-4" ref={setNodeRef}>
<div className="flex items-center gap-3 cursor-pointer col-span-2">
<button
className={clsx(
"bg-mti-purple-ultralight text-mti-purple hover:text-white hover:bg-mti-purple w-8 h-8 rounded-full z-10",
"transition duration-300 ease-in-out",
)}>
{question.id}
</button>
<span>{question.sentence}</span>
</div>
<div
key={`answer_${question.id}_${answer}`}
className={clsx("w-48 h-10 border rounded-xl flex items-center justify-center", isOver && "border-mti-purple-light")}>
{answer && `Paragraph ${answer}`}
</div>
</div>
);
}
function DraggableOptionArea({ option }: { option: MatchSentenceExerciseOption }) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: `draggable_option_${option.id}`,
});
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
zIndex: 99,
}
: undefined;
return (
<div className={clsx("flex items-center justify-start gap-6 cursor-pointer")} ref={setNodeRef} style={style} {...listeners} {...attributes}>
<button
id={`option_${option.id}`}
// onClick={() => selectOption(id)}
className={clsx(
"bg-mti-purple-ultralight text-mti-purple hover:text-white hover:bg-mti-purple px-3 py-2 rounded-full z-10",
"transition duration-300 ease-in-out",
option.id,
)}>
Paragraph {option.id}
</button>
</div>
);
}
export default function MatchSentences({
id,
options,
type,
prompt,
sentences,
userSolutions,
onNext,
onBack,
isPractice = false,
disableProgressButtons = false
}: MatchSentencesExercise & CommonProps) {
const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions);
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
useEffect(() => {
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers, setAnswers]);
const handleDragEnd = (event: DragEndEvent) => {
if (event.over && event.over.id.toString().startsWith("droppable")) {
const optionID = event.active.id.toString().replace("draggable_option_", "");
const sentenceID = event.over.id.toString().replace("droppable_sentence_", "");
setAnswers((prev) => [...prev.filter((x) => x.question.toString() !== sentenceID), { question: sentenceID, option: optionID }]);
}
};
const calculateScore = () => {
const total = sentences.length;
const correct = answers.filter(
(x) => sentences.find((y) => y.id.toString() === x.question.toString())?.solution === x.option || false,
).length;
const missing = total - answers.filter((x) => sentences.find((y) => y.id.toString() === x.question.toString())).length;
return { total, correct, missing };
};
useEffect(() => {
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers, disableProgressButtons])
useEffect(() => {
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasExamEnded]);
const progressButtons = () => (
<div className="flex justify-between w-full gap-8">
<Button
color="purple"
variant="outline"
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice })}
className="max-w-[200px] w-full">
Back
</Button>
<Button
color="purple"
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice })}
className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
)
return (
<div className="flex flex-col gap-4 mt-4">
{!disableProgressButtons && progressButtons()}
<div className={clsx("flex flex-col gap-4 mt-4", !disableProgressButtons && "mb-20")}>
<span className="text-sm w-full leading-6">
{prompt.split("\\n").map((line, index) => (
<Fragment key={index}>
{line}
<br />
</Fragment>
))}
</span>
<DndContext onDragEnd={handleDragEnd}>
<div className="flex flex-col gap-8 w-full items-center justify-between bg-mti-gray-smoke rounded-xl px-24 py-6">
<div className="flex flex-col gap-4">
{sentences.map((question) => (
<DroppableQuestionArea
key={`question_${question.id}`}
question={question}
answer={answers.find((x) => x.question.toString() === question.id.toString())?.option}
/>
))}
</div>
<div className="flex flex-col gap-4">
<span>Drag one of these paragraphs into the slots above:</span>
<div className="flex gap-4 flex-wrap justify-center items-center max-w-lg">
{options.map((option) => (
<DraggableOptionArea key={`answer_${option.id}`} option={option} />
))}
</div>
</div>
</div>
</DndContext>
</div>
{!disableProgressButtons && progressButtons()}
</div>
);
}