171 lines
6.8 KiB
TypeScript
171 lines
6.8 KiB
TypeScript
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
import SortableSection from "../../Shared/SortableSection";
|
|
import getReadingQuestions from '../SectionExercises/reading';
|
|
import { Exercise, LevelPart, ListeningPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam";
|
|
import ExerciseItem, { ReadingExercise } from "./types";
|
|
import Dropdown from "@/components/Dropdown";
|
|
import useExamEditorStore from "@/stores/examEditor";
|
|
import Writing from "../../Exercises/Writing";
|
|
import Speaking from "../../Exercises/Speaking";
|
|
import { ReactElement, ReactNode, useEffect, useState } from "react";
|
|
import {
|
|
DndContext,
|
|
PointerSensor,
|
|
useSensor,
|
|
useSensors,
|
|
DragEndEvent,
|
|
closestCenter,
|
|
UniqueIdentifier,
|
|
} from '@dnd-kit/core';
|
|
import GenLoader from "../../Exercises/Shared/GenLoader";
|
|
import { ExamPart } from "@/stores/examEditor/types";
|
|
import getListeningItems from "./listening";
|
|
import getLevelQuestionItems from "./level";
|
|
import React from "react";
|
|
|
|
|
|
interface QuestionItemsResult {
|
|
ids: string[];
|
|
items: ExerciseItem[];
|
|
}
|
|
|
|
const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
|
const { currentModule, dispatch } = useExamEditorStore();
|
|
const { sections, expandedSections } = useExamEditorStore(
|
|
(state) => state.modules[currentModule]
|
|
);
|
|
|
|
const { genResult, generating, state } = useExamEditorStore(
|
|
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
|
|
);
|
|
|
|
|
|
useEffect(() => {
|
|
if (genResult !== undefined && generating === "exercises") {
|
|
const newExercises = genResult[0].exercises;
|
|
dispatch({
|
|
type: "UPDATE_SECTION_STATE", payload: {
|
|
sectionId, update: {
|
|
exercises: [...(state as ExamPart).exercises, ...newExercises]
|
|
}
|
|
}
|
|
})
|
|
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: undefined } })
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [genResult, dispatch, sectionId, currentModule]);
|
|
|
|
const currentSection = sections.find((s) => s.sectionId === sectionId)!;
|
|
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor),
|
|
);
|
|
|
|
const questionItems = (): QuestionItemsResult => {
|
|
let result: QuestionItemsResult = {
|
|
ids: [],
|
|
items: []
|
|
};
|
|
|
|
switch (currentModule) {
|
|
case "reading": {
|
|
const items = getReadingQuestions(
|
|
(currentSection.state as ReadingPart).exercises as ReadingExercise[],
|
|
sectionId
|
|
);
|
|
result.items = items.filter((item): item is ExerciseItem => item !== undefined);
|
|
result.ids = result.items.map(item => item.id);
|
|
break;
|
|
}
|
|
case "listening": {
|
|
const items = getListeningItems(
|
|
(currentSection.state as ListeningPart).exercises as Exercise[],
|
|
sectionId
|
|
);
|
|
result.items = items.filter((item): item is ExerciseItem => item !== undefined);
|
|
result.ids = result.items.map(item => item.id);
|
|
break;
|
|
}
|
|
case "level": {
|
|
const items = getLevelQuestionItems(
|
|
(currentSection.state as LevelPart).exercises as Exercise[],
|
|
sectionId
|
|
);
|
|
result.items = items.filter((item): item is ExerciseItem => item !== undefined);
|
|
result.ids = result.items.map(item => item.id);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
const background = (component: ReactNode) => {
|
|
return (
|
|
<div className="p-8 shadow-inner border border-gray-200 bg-gray-50 rounded-xl">
|
|
{component}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (currentModule == "writing") return background(<Writing sectionId={sectionId} exercise={currentSection.state as WritingExercise} />);
|
|
if (currentModule == "speaking") return background(<Speaking sectionId={sectionId} exercise={currentSection.state as SpeakingExercise} />);
|
|
|
|
const questions = questionItems();
|
|
|
|
const filteredIds = (questions.ids ?? []).filter(Boolean);
|
|
|
|
function isValidItem(item: ExerciseItem | undefined): item is ExerciseItem {
|
|
return item !== undefined &&
|
|
typeof item.id === 'string' &&
|
|
typeof item.sectionId === 'number' &&
|
|
React.isValidElement(item.label) &&
|
|
React.isValidElement(item.content);
|
|
}
|
|
|
|
const filteredItems = (questions.items ?? []).filter(isValidItem);
|
|
|
|
return (
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={closestCenter}
|
|
onDragEnd={(e) => dispatch({ type: "REORDER_EXERCISES", payload: { event: e, sectionId } })}
|
|
>
|
|
{(currentModule === "level" && questions.ids?.length === 0) ? (
|
|
background(<span className="flex justify-center">Generated exercises will appear here!</span>)
|
|
) : (
|
|
expandedSections.includes(sectionId) &&
|
|
questions.items &&
|
|
questions.items.length > 0 &&
|
|
questions.ids &&
|
|
questions.ids.length > 0 && (
|
|
<div className="mt-4 p-6 rounded-xl shadow-inner border bg-gray-50">
|
|
<SortableContext
|
|
items={filteredIds}
|
|
strategy={verticalListSortingStrategy}
|
|
>
|
|
{filteredItems.map(item => (
|
|
<SortableSection key={item.id} id={item.id}>
|
|
<Dropdown
|
|
className={`w-full text-left p-4 mb-2 bg-gradient-to-r from-ielts-${currentModule}/60 to-ielts-${currentModule} text-white rounded-lg shadow-lg transition-transform transform hover:scale-102`}
|
|
customTitle={item.label}
|
|
contentWrapperClassName="rounded-xl"
|
|
>
|
|
<div className="p-4 shadow-inner border border-gray-200 bg-gray-50 rounded-xl">
|
|
{item.content}
|
|
</div>
|
|
</Dropdown>
|
|
</SortableSection>
|
|
))}
|
|
</SortableContext>
|
|
</div>
|
|
)
|
|
)}
|
|
|
|
{generating === "exercises" && <GenLoader module={currentModule} className="mt-4" />}
|
|
</DndContext >
|
|
);
|
|
}
|
|
|
|
export default SectionExercises;
|