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

@@ -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;