191 lines
8.5 KiB
TypeScript
191 lines
8.5 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, 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 "./Shared/ImportExam/ImportOrFromScratch";
|
|
import { defaultSectionSettings } from "@/stores/examEditor/defaults";
|
|
|
|
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
|
|
|
|
const ExamEditor: React.FC = () => {
|
|
const { currentModule, dispatch } = useExamEditorStore();
|
|
const {
|
|
sections,
|
|
minTimer,
|
|
expandedSections,
|
|
examLabel,
|
|
isPrivate,
|
|
difficulty,
|
|
sectionLabels,
|
|
importModule
|
|
} = useExamEditorStore(state => state.modules[currentModule]);
|
|
|
|
const [numberOfParts, setNumberOfParts] = useState(1);
|
|
|
|
useEffect(() => {
|
|
const currentSections = sections;
|
|
const currentLabels = sectionLabels;
|
|
let updatedSections: SectionState[];
|
|
let updatedLabels: any;
|
|
|
|
if (numberOfParts > currentSections.length) {
|
|
const newSections = [...currentSections];
|
|
const newLabels = [...currentLabels];
|
|
|
|
for (let i = currentSections.length; i < numberOfParts; i++) {
|
|
newSections.push(defaultSectionSettings(currentModule, i + 1));
|
|
newLabels.push({
|
|
id: i + 1,
|
|
label: `Part ${i + 1}`
|
|
});
|
|
}
|
|
|
|
updatedSections = newSections;
|
|
updatedLabels = newLabels;
|
|
} else if (numberOfParts < currentSections.length) {
|
|
updatedSections = currentSections.slice(0, numberOfParts);
|
|
updatedLabels = currentLabels.slice(0, numberOfParts);
|
|
} 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
|
|
}, [numberOfParts]);
|
|
|
|
|
|
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 && (currentModule === "reading" || currentModule === "listening" || currentModule === "level");
|
|
return (
|
|
<>
|
|
{showImport ? <ImportOrStartFromScratch module={currentModule} /> : (
|
|
<>
|
|
<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" onChange={(v) => setNumberOfParts(parseInt(v))} value={numberOfParts} />
|
|
</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"
|
|
defaultValue={examLabel}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="flex flex-row gap-8">
|
|
<Settings />
|
|
<div className="flex-grow max-w-[66%]">
|
|
<SectionRenderer />
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ExamEditor;
|