added access variable to exams soo we can distinguish private, public and confidential exams and also bugfixes and improvements

This commit is contained in:
José Lima
2025-02-09 04:28:34 +00:00
parent f95bce6fa2
commit b175d8797e
32 changed files with 1320 additions and 909 deletions

View File

@@ -38,7 +38,7 @@ const LevelSettings: React.FC = () => {
difficulty,
sections,
minTimer,
isPrivate,
access,
} = useExamEditorStore(state => state.modules[currentModule]);
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<LevelSectionSettings>(
@@ -200,7 +200,7 @@ const LevelSettings: React.FC = () => {
module: "level",
id: title,
difficulty,
private: isPrivate,
access,
};
const result = await axios.post('/api/exam/level', exam);
@@ -243,7 +243,7 @@ const LevelSettings: React.FC = () => {
isDiagnostic: false,
variant: undefined,
difficulty,
private: isPrivate,
access,
} as LevelExam);
setExerciseIndex(0);
setQuestionIndex(0);

View File

@@ -27,7 +27,7 @@ const ListeningSettings: React.FC = () => {
difficulty,
sections,
minTimer,
isPrivate,
access,
instructionsState
} = useExamEditorStore(state => state.modules[currentModule]);
@@ -144,7 +144,7 @@ const ListeningSettings: React.FC = () => {
id: title,
variant: sections.length === 4 ? "full" : "partial",
difficulty,
private: isPrivate,
access,
instructions: instructionsURL
};
@@ -191,7 +191,7 @@ const ListeningSettings: React.FC = () => {
isDiagnostic: false,
variant: sections.length === 4 ? "full" : "partial",
difficulty,
private: isPrivate,
access,
instructions: instructionsState.currentInstructionsURL
} as ListeningExam);
setExerciseIndex(0);

View File

@@ -15,135 +15,137 @@ import ReadingComponents from "./components";
import { getExamById } from "@/utils/exams";
const ReadingSettings: React.FC = () => {
const router = useRouter();
const router = useRouter();
const {
setExam,
setExerciseIndex,
setPartIndex,
setQuestionIndex,
setBgColor,
} = usePersistentExamStore();
const {
setExam,
setExerciseIndex,
setPartIndex,
setQuestionIndex,
setBgColor,
} = usePersistentExamStore();
const { currentModule, title } = useExamEditorStore();
const {
focusedSection,
difficulty,
sections,
minTimer,
isPrivate,
type,
} = useExamEditorStore(state => state.modules[currentModule]);
const { currentModule, title } = useExamEditorStore();
const { focusedSection, difficulty, sections, minTimer, access, type } =
useExamEditorStore((state) => state.modules[currentModule]);
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<ReadingSectionSettings>(
currentModule,
focusedSection
);
const { localSettings, updateLocalAndScheduleGlobal } =
useSettingsState<ReadingSectionSettings>(currentModule, focusedSection);
const currentSection = sections.find((section) => section.sectionId == focusedSection)?.state as ReadingPart;
const currentSection = sections.find(
(section) => section.sectionId == focusedSection
)?.state as ReadingPart;
const defaultPresets: Option[] = [
{
label: "Preset: Reading Passage 1",
value:
"Welcome to {part} of the {label}. You will read texts relating to everyday topics and situations. These may include advertisements, brochures, manuals, or official documents. Answer questions that test your ability to locate specific information and understand main ideas.",
},
{
label: "Preset: Reading Passage 2",
value:
"Welcome to {part} of the {label}. You will read texts dealing with general interest topics that may include news articles, company policies, or workplace documents. Answer questions testing your understanding of main ideas, specific details, and the author's views.",
},
{
label: "Preset: Reading Passage 3",
value:
"Welcome to {part} of the {label}. You will read longer academic texts that may include journal articles, academic essays, or research papers. Answer questions testing your ability to understand complex arguments, identify key points, and follow the development of ideas.",
},
];
const defaultPresets: Option[] = [
{
label: "Preset: Reading Passage 1",
value: "Welcome to {part} of the {label}. You will read texts relating to everyday topics and situations. These may include advertisements, brochures, manuals, or official documents. Answer questions that test your ability to locate specific information and understand main ideas."
},
{
label: "Preset: Reading Passage 2",
value: "Welcome to {part} of the {label}. You will read texts dealing with general interest topics that may include news articles, company policies, or workplace documents. Answer questions testing your understanding of main ideas, specific details, and the author's views."
},
{
label: "Preset: Reading Passage 3",
value: "Welcome to {part} of the {label}. You will read longer academic texts that may include journal articles, academic essays, or research papers. Answer questions testing your ability to understand complex arguments, identify key points, and follow the development of ideas."
}
];
const canPreviewOrSubmit = sections.some(
(s) =>
(s.state as ReadingPart).exercises &&
(s.state as ReadingPart).exercises.length > 0
);
const canPreviewOrSubmit = sections.some(
(s) => (s.state as ReadingPart).exercises && (s.state as ReadingPart).exercises.length > 0
);
const submitReading = () => {
if (title === "") {
toast.error("Enter a title for the exam!");
return;
}
const exam: ReadingExam = {
parts: sections.map((s) => {
const exercise = s.state as ReadingPart;
return {
...exercise,
intro: localSettings.currentIntro,
category: localSettings.category
};
}),
isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
minTimer,
module: "reading",
id: title,
variant: sections.length === 3 ? "full" : "partial",
difficulty,
private: isPrivate,
type: type!
const submitReading = () => {
if (title === "") {
toast.error("Enter a title for the exam!");
return;
}
const exam: ReadingExam = {
parts: sections.map((s) => {
const exercise = s.state as ReadingPart;
return {
...exercise,
intro: localSettings.currentIntro,
category: localSettings.category,
};
}),
isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
minTimer,
module: "reading",
id: title,
variant: sections.length === 3 ? "full" : "partial",
difficulty,
access,
type: type!,
};
axios.post(`/api/exam/reading`, exam)
.then((result) => {
playSound("sent");
// Successfully submitted exam
if (result.status === 200) {
toast.success(result.data.message);
} else if (result.status === 207) {
toast.warning(result.data.message);
}
})
.catch((error) => {
console.log(error);
toast.error(error.response.data.error || "Something went wrong while submitting, please try again later.");
})
}
axios
.post(`/api/exam/reading`, exam)
.then((result) => {
playSound("sent");
// Successfully submitted exam
if (result.status === 200) {
toast.success(result.data.message);
} else if (result.status === 207) {
toast.warning(result.data.message);
}
})
.catch((error) => {
console.log(error);
toast.error(
error.response.data.error ||
"Something went wrong while submitting, please try again later."
);
});
};
const preview = () => {
setExam({
parts: sections.map((s) => {
const exercises = s.state as ReadingPart;
return {
...exercises,
intro: s.settings.currentIntro,
category: s.settings.category
};
}),
minTimer,
module: "reading",
id: title,
isDiagnostic: false,
variant: undefined,
difficulty,
private: isPrivate,
type: type!
} as ReadingExam);
setExerciseIndex(0);
setQuestionIndex(0);
setPartIndex(0);
setBgColor("bg-white");
openDetachedTab("popout?type=Exam&module=reading", router)
}
const preview = () => {
setExam({
parts: sections.map((s) => {
const exercises = s.state as ReadingPart;
return {
...exercises,
intro: s.settings.currentIntro,
category: s.settings.category,
};
}),
minTimer,
module: "reading",
id: title,
isDiagnostic: false,
variant: undefined,
difficulty,
access: access,
type: type!,
} as ReadingExam);
setExerciseIndex(0);
setQuestionIndex(0);
setPartIndex(0);
setBgColor("bg-white");
openDetachedTab("popout?type=Exam&module=reading", router);
};
return (
<SettingsEditor
sectionLabel={`Passage ${focusedSection}`}
sectionId={focusedSection}
module="reading"
introPresets={[defaultPresets[focusedSection - 1]]}
preview={preview}
canPreview={canPreviewOrSubmit}
canSubmit={canPreviewOrSubmit}
submitModule={submitReading}
>
<ReadingComponents
{...{ localSettings, updateLocalAndScheduleGlobal, currentSection }}
/>
</SettingsEditor>
);
return (
<SettingsEditor
sectionLabel={`Passage ${focusedSection}`}
sectionId={focusedSection}
module="reading"
introPresets={[defaultPresets[focusedSection - 1]]}
preview={preview}
canPreview={canPreviewOrSubmit}
canSubmit={canPreviewOrSubmit}
submitModule={submitReading}
>
<ReadingComponents
{...{ localSettings, updateLocalAndScheduleGlobal, currentSection }}
/>
</SettingsEditor>
);
};
export default ReadingSettings;

View File

@@ -30,7 +30,7 @@ const SpeakingSettings: React.FC = () => {
} = usePersistentExamStore();
const { title, currentModule } = useExamEditorStore();
const { focusedSection, difficulty, sections, minTimer, isPrivate } = useExamEditorStore((store) => store.modules[currentModule])
const { focusedSection, difficulty, sections, minTimer, access } = useExamEditorStore((store) => store.modules[currentModule])
const section = sections.find((section) => section.sectionId == focusedSection)?.state;
@@ -185,7 +185,7 @@ const SpeakingSettings: React.FC = () => {
variant: undefined,
difficulty,
instructorGender: "varied",
private: isPrivate,
access,
};
const result = await axios.post('/api/exam/speaking', exam);
@@ -238,7 +238,7 @@ const SpeakingSettings: React.FC = () => {
isDiagnostic: false,
variant: undefined,
difficulty,
private: isPrivate,
access,
} as SpeakingExam);
setExerciseIndex(0);
setQuestionIndex(0);

View File

@@ -23,7 +23,7 @@ const WritingSettings: React.FC = () => {
const {
minTimer,
difficulty,
isPrivate,
access,
sections,
focusedSection,
type,
@@ -81,7 +81,7 @@ const WritingSettings: React.FC = () => {
isDiagnostic: false,
variant: undefined,
difficulty,
private: isPrivate,
access,
type: type!
});
setExerciseIndex(0);
@@ -134,7 +134,7 @@ const WritingSettings: React.FC = () => {
isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
variant: undefined,
difficulty,
private: isPrivate,
access,
type: type!
};

View File

@@ -3,12 +3,12 @@ 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 { capitalize } from "lodash";
import { AccessType, ACCESSTYPE, 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";
@@ -16,243 +16,286 @@ 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 { 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";
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 ExamEditor: React.FC<{
levelParts?: number;
entitiesAllowEditPrivacy: EntityWithRoles[];
}> = ({ levelParts = 0, entitiesAllowEditPrivacy = [] }) => {
const { currentModule, dispatch } = useExamEditorStore();
const {
sections,
minTimer,
expandedSections,
examLabel,
access,
difficulty,
sectionLabels,
importModule,
} = useExamEditorStore((state) => state.modules[currentModule]);
const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== 0 ? levelParts : 1);
const [isResetModuleOpen, setIsResetModuleOpen] = useState(false);
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]);
// 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;
}
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));
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]);
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 sectionIds = sections.map((section) => section.sectionId);
const updateModule = useCallback(
(updates: Partial<ModuleState>) => {
dispatch({type: "UPDATE_MODULE", payload: {updates}});
},
[dispatch],
);
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 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 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 Settings = ModuleSettings[currentModule];
const showImport =
importModule && ["reading", "listening", "level"].includes(currentModule);
const updateLevelParts = (parts: number) => {
setNumberOfLevelParts(parts);
};
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>
</>
)}
</>
);
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
? (Array.isArray(difficulty)
? 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>
<div className="flex flex-row gap-3 w-64">
<Select
label="Access Type"
options={ACCESSTYPE.map((item) => ({
value: item,
label: capitalize(item),
}))}
onChange={(value) => {
if (value?.value) {
updateModule({ access: value.value! as AccessType });
}
}}
value={{ value: access, label: capitalize(access) }}
/>
</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;