153 lines
5.2 KiB
TypeScript
153 lines
5.2 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}: MatchSentencesExercise & CommonProps) {
|
|
const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions);
|
|
|
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
|
|
|
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(() => {
|
|
console.log(answers);
|
|
}, [answers]);
|
|
|
|
useEffect(() => {
|
|
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [hasExamEnded]);
|
|
|
|
return (
|
|
<>
|
|
<div className="flex flex-col gap-4 mt-4 h-full w-full 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>
|
|
|
|
<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: answers, score: calculateScore(), type})}
|
|
className="max-w-[200px] w-full">
|
|
Back
|
|
</Button>
|
|
|
|
<Button
|
|
color="purple"
|
|
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
|
|
className="max-w-[200px] self-end w-full">
|
|
Next
|
|
</Button>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|