Files
encoach_frontend/src/components/ExamEditor/index.tsx
2024-12-02 17:16:12 +00:00

209 lines
9.2 KiB
TypeScript

import clsx from "clsx";
import SectionRenderer from "./SectionRenderer";
import Checkbox from "../Low/Checkbox";
import Input from "../Low/Input";
import Select from "../Low/Select";
import { capitalize } from "lodash";
import { Difficulty } from "@/interfaces/exam";
import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "react-toastify";
import { ModuleState, SectionState } from "@/stores/examEditor/types";
import { Module } from "@/interfaces";
import useExamEditorStore from "@/stores/examEditor";
import WritingSettings from "./SettingsEditor/writing";
import ReadingSettings from "./SettingsEditor/reading";
import LevelSettings from "./SettingsEditor/level";
import ListeningSettings from "./SettingsEditor/listening";
import SpeakingSettings from "./SettingsEditor/speaking";
import ImportOrStartFromScratch from "./ImportExam/ImportOrFromScratch";
import { defaultSectionSettings } from "@/stores/examEditor/defaults";
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
const { currentModule, dispatch } = useExamEditorStore();
const {
sections,
minTimer,
expandedSections,
examLabel,
isPrivate,
difficulty,
sectionLabels,
importModule
} = useExamEditorStore(state => state.modules[currentModule]);
const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== 0 ? levelParts : 1);
useEffect(() => {
if (levelParts !== 0) {
setNumberOfLevelParts(levelParts);
dispatch({
type: 'UPDATE_MODULE',
payload: {
updates: {
sectionLabels: Array.from({ length: levelParts }).map((_, i) => ({
id: i + 1,
label: `Part ${i + 1}`
}))
},
module: "level"
}
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [levelParts])
useEffect(() => {
const currentSections = sections;
const currentLabels = sectionLabels;
let updatedSections: SectionState[];
let updatedLabels: any;
if (numberOfLevelParts > currentSections.length) {
const newSections = [...currentSections];
const newLabels = [...currentLabels];
for (let i = currentSections.length; i < numberOfLevelParts; i++) {
newSections.push(defaultSectionSettings(currentModule, i + 1));
newLabels.push({
id: i + 1,
label: `Part ${i + 1}`
});
}
updatedSections = newSections;
updatedLabels = newLabels;
} else if (numberOfLevelParts < currentSections.length) {
updatedSections = currentSections.slice(0, numberOfLevelParts);
updatedLabels = currentLabels.slice(0, numberOfLevelParts);
} else {
return;
}
const updatedExpandedSections = expandedSections.filter(
sectionId => updatedSections.some(section => section.sectionId === sectionId)
);
dispatch({
type: 'UPDATE_MODULE',
payload: {
updates: {
sections: updatedSections,
sectionLabels: updatedLabels,
expandedSections: updatedExpandedSections
}
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [numberOfLevelParts]);
const sectionIds = sections.map((section) => section.sectionId)
const updateModule = useCallback((updates: Partial<ModuleState>) => {
dispatch({ type: 'UPDATE_MODULE', payload: { updates } });
}, [dispatch]);
const toggleSection = (sectionId: number) => {
if (expandedSections.length === 1 && sectionIds.includes(sectionId)) {
toast.error("Include at least one section!");
return;
}
dispatch({ type: 'TOGGLE_SECTION', payload: { sectionId } });
};
const ModuleSettings: Record<Module, React.ComponentType> = {
reading: ReadingSettings,
writing: WritingSettings,
speaking: SpeakingSettings,
listening: ListeningSettings,
level: LevelSettings
};
const Settings = ModuleSettings[currentModule];
const showImport = importModule && ["reading", "listening", "level"].includes(currentModule);
return (
<>
{showImport ? <ImportOrStartFromScratch module={currentModule} setNumberOfLevelParts={setNumberOfLevelParts} /> : (
<>
<div className="flex gap-4 w-full items-center">
<div className="flex flex-col gap-3">
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
<Input
type="number"
name="minTimer"
onChange={(e) => updateModule({ minTimer: parseInt(e) < 15 ? 15 : parseInt(e) })}
value={minTimer}
className="max-w-[300px]"
/>
</div>
<div className="flex flex-col gap-3 flex-grow">
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label>
<Select
options={DIFFICULTIES.map((x) => ({ value: x, label: capitalize(x) }))}
onChange={(value) => value && updateModule({ difficulty: value.value as Difficulty })}
value={{ value: difficulty, label: capitalize(difficulty) }}
/>
</div>
{(sectionLabels.length != 0 && currentModule !== "level") ? (
<div className="flex flex-col gap-3">
<label className="font-normal text-base text-mti-gray-dim">{sectionLabels[0].label.split(" ")[0]}</label>
<div className="flex flex-row gap-8">
{sectionLabels.map(({ id, label }) => (
<span
key={id}
className={clsx(
"px-6 py-4 w-48 h-[72px] flex justify-center items-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"transition duration-300 ease-in-out",
sectionIds.includes(id)
? `bg-ielts-${currentModule}/70 border-ielts-${currentModule} text-white`
: "bg-white border-mti-gray-platinum"
)}
onClick={() => toggleSection(id)}
>
{label}
</span>
))}
</div>
</div>
) : (
<div className="flex flex-col gap-3 w-1/3">
<label className="font-normal text-base text-mti-gray-dim">Number of Parts</label>
<Input type="number" name="Number of Parts" min={1} onChange={(v) => setNumberOfLevelParts(parseInt(v))} value={numberOfLevelParts} />
</div>
)}
<div className="flex flex-col gap-3 w-fit h-fit">
<div className="h-6" />
<Checkbox isChecked={isPrivate} onChange={(checked) => updateModule({ isPrivate: checked })}>
Privacy (Only available for Assignments)
</Checkbox>
</div>
</div>
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">Exam Label *</label>
<Input
type="text"
placeholder="Exam Label"
name="label"
onChange={(text) => updateModule({ examLabel: text })}
roundness="xl"
value={examLabel}
required
/>
</div>
<div className="flex flex-row gap-8">
<Settings />
<div className="flex-grow max-w-[66%]">
<SectionRenderer />
</div>
</div>
</>
)}
</>
);
};
export default ExamEditor;