259 lines
8.5 KiB
TypeScript
259 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, 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";
|
|
import Button from "../Low/Button";
|
|
import ResetModule from "./Standalone/ResetModule";
|
|
import ListeningInstructions from "./Standalone/ListeningInstructions";
|
|
import {EntityWithRoles} from "@/interfaces/entity";
|
|
|
|
const DIFFICULTIES: Difficulty[] = ["A1", "A2", "B1", "B2", "C1", "C2"];
|
|
|
|
const ExamEditor: React.FC<{levelParts?: number; entitiesAllowEditPrivacy: EntityWithRoles[]}> = ({
|
|
levelParts = 0,
|
|
entitiesAllowEditPrivacy = [],
|
|
}) => {
|
|
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);
|
|
const [isResetModuleOpen, setIsResetModuleOpen] = useState(false);
|
|
|
|
// For exam edits
|
|
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 ((currentModule === "level" && currentSections.length !== currentLabels.length) || numberOfLevelParts !== currentSections.length) {
|
|
const newSections = [...currentSections];
|
|
const newLabels = [...currentLabels];
|
|
for (let i = currentLabels.length; i < numberOfLevelParts; i++) {
|
|
if (currentSections.length !== numberOfLevelParts) 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);
|
|
|
|
const updateLevelParts = (parts: number) => {
|
|
setNumberOfLevelParts(parts);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{showImport ? (
|
|
<ImportOrStartFromScratch module={currentModule} setNumberOfLevelParts={updateLevelParts} />
|
|
) : (
|
|
<>
|
|
{isResetModuleOpen && (
|
|
<ResetModule
|
|
module={currentModule}
|
|
isOpen={isResetModuleOpen}
|
|
setIsOpen={setIsResetModuleOpen}
|
|
setNumberOfLevelParts={setNumberOfLevelParts}
|
|
/>
|
|
)}
|
|
<div className="flex gap-4 w-full items-center -xl:flex-col">
|
|
<div className="flex flex-row gap-3 w-full">
|
|
<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
|
|
isMulti={true}
|
|
options={DIFFICULTIES.map((x) => ({
|
|
value: x,
|
|
label: capitalize(x),
|
|
}))}
|
|
onChange={(values) => {
|
|
const selectedDifficulties = values ? values.map((v) => v.value as Difficulty) : [];
|
|
updateModule({difficulty: selectedDifficulties});
|
|
}}
|
|
value={
|
|
difficulty
|
|
? difficulty.map((d) => ({
|
|
value: d,
|
|
label: capitalize(d),
|
|
}))
|
|
: null
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{sectionLabels.length != 0 && currentModule !== "level" ? (
|
|
<div className="flex flex-col gap-3 -xl:w-full">
|
|
<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})}
|
|
disabled={entitiesAllowEditPrivacy.length === 0}>
|
|
Privacy (Only available for Assignments)
|
|
</Checkbox>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-row gap-3 w-full">
|
|
<div className="flex flex-col gap-3 flex-grow">
|
|
<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>
|
|
{currentModule === "listening" && <ListeningInstructions />}
|
|
<Button
|
|
onClick={() => setIsResetModuleOpen(true)}
|
|
customColor={`bg-ielts-${currentModule}/70 hover:bg-ielts-${currentModule} border-ielts-${currentModule}`}
|
|
className={`text-white self-end`}>
|
|
Reset Module
|
|
</Button>
|
|
</div>
|
|
<div className="flex flex-row gap-8 -2xl:flex-col">
|
|
<Settings />
|
|
<div className="flex-grow max-w-[66%] -2xl:max-w-full">
|
|
<SectionRenderer />
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ExamEditor;
|