Remove unused imports and changed and improved layout design and responsiveness in some components and fixed some bugs.
This commit is contained in:
@@ -95,7 +95,7 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-8 border bg-ielts-${module}/20 rounded-3xl p-8 w-1/3 h-fit`}>
|
||||
<div className={`flex flex-col gap-8 border bg-ielts-${module}/20 rounded-3xl p-8 w-1/3 h-fit -2xl:w-full`}>
|
||||
<div className={`w-full flex justify-center text-ielts-${module} font-bold text-xl`}>{sectionLabel} Settings</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Dropdown
|
||||
|
||||
@@ -233,7 +233,7 @@ const ListeningComponents: React.FC<Props> = ({ currentSection, localSettings, u
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isAudioContextOpen: isOpen }, false)}
|
||||
contentWrapperClassName={level ? `border border-ielts-listening` : ''}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center px-2 pb-4">
|
||||
<div className="flex flex-row flex-wrap gap-2 items-center px-2 pb-4">
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Topic (Optional)</label>
|
||||
<Input
|
||||
|
||||
@@ -5,103 +5,140 @@ import ExercisePicker from "../../ExercisePicker";
|
||||
import { generate } from "../Shared/Generate";
|
||||
import GenerateBtn from "../Shared/GenerateBtn";
|
||||
import { LevelPart, ReadingPart } from "@/interfaces/exam";
|
||||
import { LevelSectionSettings, ReadingSectionSettings } from "@/stores/examEditor/types";
|
||||
import {
|
||||
LevelSectionSettings,
|
||||
ReadingSectionSettings,
|
||||
} from "@/stores/examEditor/types";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
|
||||
interface Props {
|
||||
localSettings: ReadingSectionSettings | LevelSectionSettings;
|
||||
updateLocalAndScheduleGlobal: (updates: Partial<ReadingSectionSettings | LevelSectionSettings>, schedule?: boolean) => void;
|
||||
currentSection: ReadingPart | LevelPart;
|
||||
generatePassageDisabled?: boolean;
|
||||
levelId?: number;
|
||||
level?: boolean;
|
||||
localSettings: ReadingSectionSettings | LevelSectionSettings;
|
||||
updateLocalAndScheduleGlobal: (
|
||||
updates: Partial<ReadingSectionSettings | LevelSectionSettings>,
|
||||
schedule?: boolean
|
||||
) => void;
|
||||
currentSection: ReadingPart | LevelPart;
|
||||
generatePassageDisabled?: boolean;
|
||||
levelId?: number;
|
||||
level?: boolean;
|
||||
}
|
||||
|
||||
const ReadingComponents: React.FC<Props> = ({localSettings, updateLocalAndScheduleGlobal, currentSection, levelId, level = false, generatePassageDisabled = false}) => {
|
||||
const { currentModule } = useExamEditorStore();
|
||||
const {
|
||||
focusedSection,
|
||||
difficulty,
|
||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||
const ReadingComponents: React.FC<Props> = ({
|
||||
localSettings,
|
||||
updateLocalAndScheduleGlobal,
|
||||
currentSection,
|
||||
levelId,
|
||||
level = false,
|
||||
generatePassageDisabled = false,
|
||||
}) => {
|
||||
const { currentModule } = useExamEditorStore();
|
||||
const { focusedSection, difficulty } = useExamEditorStore(
|
||||
(state) => state.modules[currentModule]
|
||||
);
|
||||
|
||||
const generatePassage = useCallback(() => {
|
||||
generate(
|
||||
levelId ? levelId : focusedSection,
|
||||
"reading",
|
||||
"passage",
|
||||
{
|
||||
method: 'GET',
|
||||
queryParams: {
|
||||
difficulty,
|
||||
...(localSettings.readingTopic && { topic: localSettings.readingTopic })
|
||||
}
|
||||
},
|
||||
(data: any) => [{
|
||||
title: data.title,
|
||||
text: data.text
|
||||
}],
|
||||
level ? focusedSection : undefined,
|
||||
level
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSettings.readingTopic, difficulty, focusedSection, levelId]);
|
||||
|
||||
const onTopicChange = useCallback((readingTopic: string) => {
|
||||
updateLocalAndScheduleGlobal({ readingTopic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
title="Generate Passage"
|
||||
module="reading"
|
||||
open={localSettings.isPassageOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isPassageOpen: isOpen }, false)}
|
||||
contentWrapperClassName={level ? `border border-ielts-reading`: ''}
|
||||
disabled={generatePassageDisabled}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center px-2 pb-4">
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Topic (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.readingTopic}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex self-end h-16 mb-1">
|
||||
<GenerateBtn
|
||||
module="reading"
|
||||
genType="passage"
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generatePassage}
|
||||
level={level}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
title="Add Exercises"
|
||||
module="reading"
|
||||
open={localSettings.isReadingTopicOpean}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isReadingTopicOpean: isOpen })}
|
||||
contentWrapperClassName={level ? `border border-ielts-reading`: ''}
|
||||
disabled={currentSection === undefined || currentSection.text === undefined || currentSection.text.content === "" || currentSection.text.title === ""}
|
||||
>
|
||||
<ExercisePicker
|
||||
module="reading"
|
||||
sectionId={levelId !== undefined ? levelId : focusedSection}
|
||||
extraArgs={{ text: currentSection === undefined || currentSection.text === undefined ? "" : currentSection.text.content }}
|
||||
levelSectionId={focusedSection}
|
||||
level={level}
|
||||
/>
|
||||
</Dropdown>
|
||||
</>
|
||||
const generatePassage = useCallback(() => {
|
||||
generate(
|
||||
levelId ? levelId : focusedSection,
|
||||
"reading",
|
||||
"passage",
|
||||
{
|
||||
method: "GET",
|
||||
queryParams: {
|
||||
difficulty,
|
||||
...(localSettings.readingTopic && {
|
||||
topic: localSettings.readingTopic,
|
||||
}),
|
||||
},
|
||||
},
|
||||
(data: any) => [
|
||||
{
|
||||
title: data.title,
|
||||
text: data.text,
|
||||
},
|
||||
],
|
||||
level ? focusedSection : undefined,
|
||||
level
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSettings.readingTopic, difficulty, focusedSection, levelId]);
|
||||
|
||||
const onTopicChange = useCallback(
|
||||
(readingTopic: string) => {
|
||||
updateLocalAndScheduleGlobal({ readingTopic });
|
||||
},
|
||||
[updateLocalAndScheduleGlobal]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
title="Generate Passage"
|
||||
module="reading"
|
||||
open={localSettings.isPassageOpen}
|
||||
setIsOpen={(isOpen: boolean) =>
|
||||
updateLocalAndScheduleGlobal({ isPassageOpen: isOpen }, false)
|
||||
}
|
||||
contentWrapperClassName={level ? `border border-ielts-reading` : ""}
|
||||
disabled={generatePassageDisabled}
|
||||
>
|
||||
<div
|
||||
className="flex flex-row flex-wrap gap-2 items-center px-2 pb-4 "
|
||||
>
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">
|
||||
Topic (Optional)
|
||||
</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.readingTopic}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex self-end h-16 mb-1">
|
||||
<GenerateBtn
|
||||
module="reading"
|
||||
genType="passage"
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generatePassage}
|
||||
level={level}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
title="Add Exercises"
|
||||
module="reading"
|
||||
open={localSettings.isReadingTopicOpean}
|
||||
setIsOpen={(isOpen: boolean) =>
|
||||
updateLocalAndScheduleGlobal({ isReadingTopicOpean: isOpen })
|
||||
}
|
||||
contentWrapperClassName={level ? `border border-ielts-reading` : ""}
|
||||
disabled={
|
||||
currentSection === undefined ||
|
||||
currentSection.text === undefined ||
|
||||
currentSection.text.content === "" ||
|
||||
currentSection.text.title === ""
|
||||
}
|
||||
>
|
||||
<ExercisePicker
|
||||
module="reading"
|
||||
sectionId={levelId !== undefined ? levelId : focusedSection}
|
||||
extraArgs={{
|
||||
text:
|
||||
currentSection === undefined || currentSection.text === undefined
|
||||
? ""
|
||||
: currentSection.text.content,
|
||||
}}
|
||||
levelSectionId={focusedSection}
|
||||
level={level}
|
||||
/>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReadingComponents;
|
||||
|
||||
@@ -19,8 +19,8 @@ const label = (type: string, firstId: string, lastId: string) => {
|
||||
const ExerciseLabel: React.FC<Props> = ({type, firstId, lastId, prompt}) => {
|
||||
return (
|
||||
<div className="flex w-full justify-between items-center mr-4">
|
||||
<span className="font-semibold">{label(type, firstId, lastId)}</span>
|
||||
<div className="text-sm font-light italic">{previewLabel(prompt)}</div>
|
||||
<span className="font-semibold ellipsis-2">{label(type, firstId, lastId)}</span>
|
||||
<div className="text-sm font-light italic ellipsis-2">{previewLabel(prompt)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,216 +24,265 @@ import ListeningInstructions from "./Standalone/ListeningInstructions";
|
||||
const DIFFICULTIES: Difficulty[] = ["A1", "A2", "B1", "B2", "C1", "C2"];
|
||||
|
||||
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 { 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);
|
||||
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;
|
||||
}
|
||||
|
||||
const updatedExpandedSections = expandedSections.filter(
|
||||
sectionId => updatedSections.some(section => section.sectionId === sectionId)
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: 'UPDATE_MODULE',
|
||||
payload: {
|
||||
updates: {
|
||||
sections: updatedSections,
|
||||
sectionLabels: updatedLabels,
|
||||
expandedSections: updatedExpandedSections
|
||||
}
|
||||
}
|
||||
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}`,
|
||||
});
|
||||
// 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);
|
||||
}
|
||||
updatedSections = newSections;
|
||||
updatedLabels = newLabels;
|
||||
} else if (numberOfLevelParts < currentSections.length) {
|
||||
updatedSections = currentSections.slice(0, numberOfLevelParts);
|
||||
updatedLabels = currentLabels.slice(0, numberOfLevelParts);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
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">
|
||||
<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>
|
||||
{(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-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">
|
||||
<Settings />
|
||||
<div className="flex-grow max-w-[66%]">
|
||||
<SectionRenderer />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
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 })}
|
||||
>
|
||||
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;
|
||||
|
||||
@@ -24,7 +24,7 @@ const DroppableQuestionArea: React.FC<DroppableQuestionAreaProps> = ({ question,
|
||||
</div>
|
||||
<div
|
||||
key={`answer_${question.id}_${answer}`}
|
||||
className={clsx("w-48 h-10 border rounded-xl flex items-center justify-center", isOver && "border-mti-purple-light")}>
|
||||
className={clsx("w-48 h-10 border-2 border-mti-purple-light self-center rounded-xl flex items-center justify-center", isOver && "border-mti-purple-dark")}>
|
||||
{answer && `Paragraph ${answer}`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,6 @@ import {useListSearch} from "@/hooks/useListSearch";
|
||||
import usePagination from "@/hooks/usePagination";
|
||||
import { clsx } from "clsx";
|
||||
import {ReactNode} from "react";
|
||||
import Checkbox from "../Low/Checkbox";
|
||||
import Separator from "../Low/Separator";
|
||||
|
||||
interface Props<T> {
|
||||
list: T[];
|
||||
|
||||
@@ -1,109 +1,159 @@
|
||||
import { useListSearch } from "@/hooks/useListSearch"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, PaginationState, useReactTable } from "@tanstack/react-table"
|
||||
import clsx from "clsx"
|
||||
import { useEffect, useState } from "react"
|
||||
import { BsArrowDown, BsArrowUp } from "react-icons/bs"
|
||||
import Button from "../Low/Button"
|
||||
import { useListSearch } from "@/hooks/useListSearch";
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
PaginationState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BsArrowDown, BsArrowUp } from "react-icons/bs";
|
||||
import Button from "../Low/Button";
|
||||
|
||||
interface Props<T> {
|
||||
data: T[]
|
||||
columns: ColumnDef<any, any>[]
|
||||
searchFields: string[][]
|
||||
size?: number
|
||||
onDownload?: (rows: T[]) => void
|
||||
isDownloadLoading?: boolean
|
||||
searchPlaceholder?: string
|
||||
data: T[];
|
||||
columns: ColumnDef<any, any>[];
|
||||
searchFields: string[][];
|
||||
size?: number;
|
||||
onDownload?: (rows: T[]) => void;
|
||||
isDownloadLoading?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export default function Table<T>({ data, columns, searchFields, size = 16, onDownload, isDownloadLoading, searchPlaceholder }: Props<T>) {
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: size,
|
||||
})
|
||||
export default function Table<T>({
|
||||
data,
|
||||
columns,
|
||||
searchFields,
|
||||
size = 16,
|
||||
onDownload,
|
||||
isDownloadLoading,
|
||||
searchPlaceholder,
|
||||
isLoading,
|
||||
}: Props<T>) {
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: size,
|
||||
});
|
||||
|
||||
const { rows, renderSearch } = useListSearch<T>(searchFields, data, searchPlaceholder);
|
||||
const { rows, renderSearch } = useListSearch<T>(
|
||||
searchFields,
|
||||
data,
|
||||
searchPlaceholder
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: rows,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
state: {
|
||||
pagination
|
||||
}
|
||||
});
|
||||
const table = useReactTable({
|
||||
data: rows,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<div className="w-full flex gap-2 items-end">
|
||||
{renderSearch()}
|
||||
{onDownload && (
|
||||
<Button isLoading={isDownloadLoading} className="w-full max-w-[200px] mb-1" variant="outline" onClick={() => onDownload(rows)}>
|
||||
Download
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<div className="w-full flex gap-2 items-end">
|
||||
{renderSearch()}
|
||||
{onDownload && (
|
||||
<Button
|
||||
isLoading={isDownloadLoading}
|
||||
className="w-full max-w-[200px] mb-1"
|
||||
variant="outline"
|
||||
onClick={() => onDownload(rows)}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full flex gap-2 justify-between items-center">
|
||||
<div className="flex items-center gap-4 w-fit">
|
||||
<Button className="w-[200px] h-fit" disabled={!table.getCanPreviousPage()} onClick={() => table.previousPage()}>
|
||||
Previous Page
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-fit">
|
||||
<span className="flex items-center gap-1">
|
||||
<div>Page</div>
|
||||
<strong>
|
||||
{table.getState().pagination.pageIndex + 1} of{' '}
|
||||
{table.getPageCount().toLocaleString()}
|
||||
</strong>
|
||||
<div>| Total: {table.getRowCount().toLocaleString()}</div>
|
||||
</span>
|
||||
<Button className="w-[200px]" disabled={!table.getCanNextPage()} onClick={() => table.nextPage()}>
|
||||
Next Page
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex gap-2 justify-between items-center">
|
||||
<div className="flex items-center gap-4 w-fit">
|
||||
<Button
|
||||
className="w-[200px] h-fit"
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
onClick={() => table.previousPage()}
|
||||
>
|
||||
Previous Page
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-fit">
|
||||
<span className="flex items-center gap-1">
|
||||
<div>Page</div>
|
||||
<strong>
|
||||
{table.getState().pagination.pageIndex + 1} of{" "}
|
||||
{table.getPageCount().toLocaleString()}
|
||||
</strong>
|
||||
<div>| Total: {table.getRowCount().toLocaleString()}</div>
|
||||
</span>
|
||||
<Button
|
||||
className="w-[200px]"
|
||||
disabled={!table.getCanNextPage()}
|
||||
onClick={() => table.nextPage()}
|
||||
>
|
||||
Next Page
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th className="py-4 px-4 text-left" key={header.id} colSpan={header.colSpan}>
|
||||
<div
|
||||
className={clsx(header.column.getCanSort() && 'cursor-pointer select-none', 'flex items-center gap-2')}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{{
|
||||
asc: <BsArrowUp />,
|
||||
desc: <BsArrowDown />,
|
||||
}[header.column.getIsSorted() as string] ?? null}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="px-2 w-full">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th
|
||||
className="py-4 px-4 text-left"
|
||||
key={header.id}
|
||||
colSpan={header.colSpan}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
header.column.getCanSort() &&
|
||||
"cursor-pointer select-none",
|
||||
"flex items-center gap-2"
|
||||
)}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{{
|
||||
asc: <BsArrowUp />,
|
||||
desc: <BsArrowDown />,
|
||||
}[header.column.getIsSorted() as string] ?? null}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="px-2 w-full">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr
|
||||
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
|
||||
key={row.id}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{isLoading && (
|
||||
<div className="min-h-screen flex justify-center items-start">
|
||||
<span className="loading loading-infinity w-32" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ export default function Sidebar({
|
||||
<section
|
||||
className={clsx(
|
||||
"relative flex h-full flex-col justify-between bg-transparent px-4 py-4 pb-8",
|
||||
isMinimized ? "w-fit" : "-xl:w-fit w-1/6",
|
||||
isMinimized ? "w-fit" : "-xl:w-20 w-1/6",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -29,7 +29,7 @@ function QuestionSolutionArea({
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"w-56 h-10 border rounded-xl items-center justify-center flex gap-3 px-2",
|
||||
"w-56 h-10 border self-center rounded-xl items-center justify-center flex gap-3 px-2",
|
||||
!userSolution
|
||||
? "border-mti-gray-davy"
|
||||
: userSolution.option.toString() === question.solution.toString()
|
||||
|
||||
Reference in New Issue
Block a user