Exam Edit on ExamList
This commit is contained in:
@@ -22,18 +22,13 @@ const PromptEdit: React.FC<Props> = ({ value, onChange, wrapperCard = true }) =>
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTextChange = (text: string) => {
|
|
||||||
const escapedText = text.replace(/\n/g, '\\n');
|
|
||||||
onChange(escapedText);
|
|
||||||
};
|
|
||||||
|
|
||||||
const promptEditTsx = (
|
const promptEditTsx = (
|
||||||
<div className="flex justify-between items-start gap-4">
|
<div className="flex justify-between items-start gap-4">
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<AutoExpandingTextArea
|
<AutoExpandingTextArea
|
||||||
className="flex-1 p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none min-h-[100px]"
|
className="flex-1 p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none min-h-[100px]"
|
||||||
value={value.replace(/\\n/g, '\n')}
|
value={value}
|
||||||
onChange={handleTextChange}
|
onChange={onChange}
|
||||||
onBlur={() => setEditing(false)}
|
onBlur={() => setEditing(false)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
|||||||
});
|
});
|
||||||
}, [updateLocalAndScheduleGlobal]);
|
}, [updateLocalAndScheduleGlobal]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
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`}>
|
||||||
<div className={`w-full flex justify-center text-ielts-${module} font-bold text-xl`}>{sectionLabel} Settings</div>
|
<div className={`w-full flex justify-center text-ielts-${module} font-bold text-xl`}>{sectionLabel} Settings</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { defaultSectionSettings } from "@/stores/examEditor/defaults";
|
|||||||
|
|
||||||
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
|
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
|
||||||
|
|
||||||
const ExamEditor: React.FC = () => {
|
const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
|
||||||
const { currentModule, dispatch } = useExamEditorStore();
|
const { currentModule, dispatch } = useExamEditorStore();
|
||||||
const {
|
const {
|
||||||
sections,
|
sections,
|
||||||
@@ -33,7 +33,23 @@ const ExamEditor: React.FC = () => {
|
|||||||
importModule
|
importModule
|
||||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||||
|
|
||||||
const [numberOfLevelParts, setNumberOfLevelParts] = useState(1);
|
const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== undefined ? levelParts : 1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNumberOfLevelParts(levelParts);
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_MODULE',
|
||||||
|
payload: {
|
||||||
|
updates: {
|
||||||
|
sectionLabels: Array.from({ length: levelParts }).map((_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
label: `Part ${i + 1}`
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [levelParts])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentSections = sections;
|
const currentSections = sections;
|
||||||
@@ -75,7 +91,7 @@ const ExamEditor: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [numberOfLevelParts]);
|
}, [numberOfLevelParts]);
|
||||||
|
|
||||||
|
|
||||||
@@ -106,7 +122,7 @@ const ExamEditor: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showImport ? <ImportOrStartFromScratch module={currentModule} setNumberOfLevelParts={setNumberOfLevelParts}/> : (
|
{showImport ? <ImportOrStartFromScratch module={currentModule} setNumberOfLevelParts={setNumberOfLevelParts} /> : (
|
||||||
<>
|
<>
|
||||||
<div className="flex gap-4 w-full items-center">
|
<div className="flex gap-4 w-full items-center">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, { useEffect, useRef, ChangeEvent } from 'react';
|
import React, { useRef, useEffect, ChangeEvent } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
onChange: (value: string) => void;
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ const AutoExpandingTextArea: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={value}
|
value={value.replace(/\\n/g, '\n')}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className={className}
|
className={className}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import useExamTimer from "@/hooks/useExamTimer";
|
|||||||
import ProgressButtons from "../components/ProgressButtons";
|
import ProgressButtons from "../components/ProgressButtons";
|
||||||
import useExamNavigation from "../Navigation/useExamNavigation";
|
import useExamNavigation from "../Navigation/useExamNavigation";
|
||||||
import { calculateExerciseIndex } from "../utils/calculateExerciseIndex";
|
import { calculateExerciseIndex } from "../utils/calculateExerciseIndex";
|
||||||
|
import { defaultExamUserSolutions } from "@/utils/exams";
|
||||||
|
|
||||||
|
|
||||||
const Level: React.FC<ExamProps<LevelExam>> = ({ exam, showSolutions = false, preview = false }) => {
|
const Level: React.FC<ExamProps<LevelExam>> = ({ exam, showSolutions = false, preview = false }) => {
|
||||||
@@ -55,7 +56,6 @@ const Level: React.FC<ExamProps<LevelExam>> = ({ exam, showSolutions = false, pr
|
|||||||
const { finalizeModule, timeIsUp } = flags;
|
const { finalizeModule, timeIsUp } = flags;
|
||||||
|
|
||||||
const timer = useRef(exam.minTimer - timeSpentCurrentModule / 60);
|
const timer = useRef(exam.minTimer - timeSpentCurrentModule / 60);
|
||||||
const [isFirstTimeRender, setIsFirstTimeRender] = useState(partIndex === 0 && exerciseIndex == 0 && !showSolutions);
|
|
||||||
|
|
||||||
// In case client want to switch back
|
// In case client want to switch back
|
||||||
const textRenderDisabled = true;
|
const textRenderDisabled = true;
|
||||||
@@ -92,12 +92,12 @@ const Level: React.FC<ExamProps<LevelExam>> = ({ exam, showSolutions = false, pr
|
|||||||
const {
|
const {
|
||||||
nextExercise, previousExercise,
|
nextExercise, previousExercise,
|
||||||
showPartDivider, setShowPartDivider,
|
showPartDivider, setShowPartDivider,
|
||||||
seenParts, setSeenParts,
|
seenParts, setSeenParts, startNow
|
||||||
} = useExamNavigation(
|
} = useExamNavigation(
|
||||||
{
|
{
|
||||||
exam, module: "level", showBlankModal: showQuestionsModal,
|
exam, module: "level", showBlankModal: showQuestionsModal,
|
||||||
setShowBlankModal: setShowQuestionsModal, showSolutions,
|
setShowBlankModal: setShowQuestionsModal, showSolutions,
|
||||||
preview, disableBetweenParts: true, modalBetweenParts: true ,modalKwargs
|
preview, disableBetweenParts: true, modalBetweenParts: true, modalKwargs
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -106,6 +106,13 @@ const Level: React.FC<ExamProps<LevelExam>> = ({ exam, showSolutions = false, pr
|
|||||||
setSolutionWasUpdated(true);
|
setSolutionWasUpdated(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (preview) {
|
||||||
|
setUserSolutions(defaultExamUserSolutions(exam));
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
const [contextWords, setContextWords] = useState<{ match: string, originalLine: string }[] | undefined>(undefined);
|
const [contextWords, setContextWords] = useState<{ match: string, originalLine: string }[] | undefined>(undefined);
|
||||||
const [contextWordLines, setContextWordLines] = useState<number[] | undefined>(undefined);
|
const [contextWordLines, setContextWordLines] = useState<number[] | undefined>(undefined);
|
||||||
const [totalLines, setTotalLines] = useState<number>(0);
|
const [totalLines, setTotalLines] = useState<number>(0);
|
||||||
@@ -351,17 +358,17 @@ const Level: React.FC<ExamProps<LevelExam>> = ({ exam, showSolutions = false, pr
|
|||||||
</Modal>
|
</Modal>
|
||||||
<QuestionsModal isOpen={showQuestionsModal} {...questionModalKwargs} />
|
<QuestionsModal isOpen={showQuestionsModal} {...questionModalKwargs} />
|
||||||
{
|
{
|
||||||
!(partIndex === 0 && questionIndex === 0 && (showPartDivider || isFirstTimeRender)) &&
|
(!showPartDivider && !startNow) &&
|
||||||
<Timer minTimer={exam.minTimer} disableTimer={showSolutions || preview} standalone={true} />
|
<Timer minTimer={exam.minTimer} disableTimer={showSolutions || preview} standalone={true} />
|
||||||
}
|
}
|
||||||
{(showPartDivider || isFirstTimeRender) ?
|
{(showPartDivider || startNow) ?
|
||||||
<PartDivider
|
<PartDivider
|
||||||
module="level"
|
module="level"
|
||||||
sectionLabel="Part"
|
sectionLabel="Part"
|
||||||
defaultTitle="Placement Test"
|
defaultTitle="Placement Test"
|
||||||
section={exam.parts[partIndex]}
|
section={exam.parts[partIndex]}
|
||||||
sectionIndex={partIndex}
|
sectionIndex={partIndex}
|
||||||
onNext={() => { setShowPartDivider(false); setIsFirstTimeRender(false); setBgColor("bg-white"); setSeenParts(prev => new Set(prev).add(partIndex)); }}
|
onNext={() => { setShowPartDivider(false); setBgColor("bg-white"); setSeenParts(prev => new Set(prev).add(partIndex)); }}
|
||||||
/> : (
|
/> : (
|
||||||
<>
|
<>
|
||||||
{exam.parts.length > 1 && <SectionNavbar
|
{exam.parts.length > 1 && <SectionNavbar
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ const Listening: React.FC<ExamProps<ListeningExam>> = ({ exam, showSolutions = f
|
|||||||
: (!startNow && !showPartDivider && !isBetweenParts && !showSolutions) && renderExercise(e, exam.id, registerSolution, preview))}
|
: (!startNow && !showPartDivider && !isBetweenParts && !showSolutions) && renderExercise(e, exam.id, registerSolution, preview))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [partIndex, startNow, showPartDivider, isBetweenParts, showSolutions]);
|
}, [partIndex, startNow, showPartDivider, isBetweenParts, showSolutions]);
|
||||||
|
|
||||||
|
|
||||||
@@ -128,6 +129,7 @@ const Listening: React.FC<ExamProps<ListeningExam>> = ({ exam, showSolutions = f
|
|||||||
timesListened={timesListened}
|
timesListened={timesListened}
|
||||||
setShowTextModal={setShowTextModal}
|
setShowTextModal={setShowTextModal}
|
||||||
setTimesListened={setTimesListened}
|
setTimesListened={setTimesListened}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
/>, [partIndex, assignment, timesListened, setShowTextModal, setTimesListened])
|
/>, [partIndex, assignment, timesListened, setShowTextModal, setTimesListened])
|
||||||
|
|
||||||
const memoizedInstructions = useMemo(()=>
|
const memoizedInstructions = useMemo(()=>
|
||||||
|
|||||||
@@ -30,7 +30,22 @@ const PartDivider: React.FC<Props> = ({ sectionIndex, sectionLabel, section, mod
|
|||||||
<div className={`w-12 h-12 bg-ielts-${module} flex items-center justify-center rounded-lg`}>{moduleIcon[module]}</div>
|
<div className={`w-12 h-12 bg-ielts-${module} flex items-center justify-center rounded-lg`}>{moduleIcon[module]}</div>
|
||||||
<p className="text-3xl">{section.intro ? `${sectionLabel} ${sectionIndex + 1}` : defaultTitle}</p>
|
<p className="text-3xl">{section.intro ? `${sectionLabel} ${sectionIndex + 1}` : defaultTitle}</p>
|
||||||
</div>
|
</div>
|
||||||
{section.intro && section.intro.split('\\n\\n').map((x, index) => <p key={`line-${index}`} className="text-2xl text-clip" dangerouslySetInnerHTML={{ __html: x.replace('that is not correct', 'that is <span class="font-bold"><u>not correct</u></span>') }}></p>)}
|
{section.intro && section.intro
|
||||||
|
.replace(/\\n/g, '\n')
|
||||||
|
.split('\n')
|
||||||
|
.map((x, index) => (
|
||||||
|
<p
|
||||||
|
key={`line-${index}`}
|
||||||
|
className="text-2xl text-clip"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: x.replace(
|
||||||
|
'that is not correct',
|
||||||
|
'that is <span class="font-bold"><u>not correct</u></span>'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
></p>
|
||||||
|
))
|
||||||
|
}
|
||||||
<div className="flex items-center justify-center mt-4">
|
<div className="flex items-center justify-center mt-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => onNext()}
|
onClick={() => onNext()}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ type PartExam = {
|
|||||||
const answeredEveryQuestionInPart = (exam: PartExam, partIndex: number, userSolutions: UserSolution[]) => {
|
const answeredEveryQuestionInPart = (exam: PartExam, partIndex: number, userSolutions: UserSolution[]) => {
|
||||||
return exam.parts[partIndex].exercises.every((exercise) => {
|
return exam.parts[partIndex].exercises.every((exercise) => {
|
||||||
const userSolution = userSolutions.find(x => x.exercise === exercise.id);
|
const userSolution = userSolutions.find(x => x.exercise === exercise.id);
|
||||||
|
|
||||||
switch (exercise.type) {
|
switch (exercise.type) {
|
||||||
case 'multipleChoice':
|
case 'multipleChoice':
|
||||||
return userSolution?.solutions.length === exercise.questions!.length;
|
return userSolution?.solutions.length === exercise.questions!.length;
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import { checkAccess } from "@/utils/permissions";
|
|||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
|
import { FiEdit, FiArrowRight } from 'react-icons/fi';
|
||||||
|
import { HiArrowRight } from "react-icons/hi";
|
||||||
|
import { BiEdit } from "react-icons/bi";
|
||||||
|
|
||||||
const searchFields = [["module"], ["id"], ["createdBy"]];
|
const searchFields = [["module"], ["id"], ["createdBy"]];
|
||||||
|
|
||||||
@@ -105,7 +108,7 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch({type: "INIT_EXAM", payload: {exams: [exam], modules: [module]}})
|
dispatch({ type: "INIT_EXAM", payload: { exams: [exam], modules: [module] } })
|
||||||
|
|
||||||
router.push("/exam");
|
router.push("/exam");
|
||||||
};
|
};
|
||||||
@@ -236,7 +239,7 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
|||||||
{row.original.private ? <BsCircle /> : <BsBan />}
|
{row.original.private ? <BsCircle /> : <BsBan />}
|
||||||
</button>
|
</button>
|
||||||
{checkAccess(user, ["admin", "developer", "mastercorporate"]) && (
|
{checkAccess(user, ["admin", "developer", "mastercorporate"]) && (
|
||||||
<button data-tip="Edit owners" onClick={() => setSelectedExam(row.original)} className="cursor-pointer tooltip">
|
<button data-tip="Edit exam" onClick={() => setSelectedExam(row.original)} className="cursor-pointer tooltip">
|
||||||
<BsPencil />
|
<BsPencil />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -265,12 +268,54 @@ export default function ExamList({ user, entities }: { user: User; entities: Ent
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleExamEdit = () => {
|
||||||
|
router.push(`/generation?id=${selectedExam!.id}&module=${selectedExam!.module}`);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full h-full">
|
<div className="flex flex-col gap-4 w-full h-full">
|
||||||
{renderSearch()}
|
{renderSearch()}
|
||||||
<Modal isOpen={!!selectedExam} title={`Edit Exam Owners - ${selectedExam?.id}`} onClose={() => setSelectedExam(undefined)}>
|
<Modal isOpen={!!selectedExam} onClose={() => setSelectedExam(undefined)} maxWidth="max-w-xl">
|
||||||
{!!selectedExam ? (
|
{!!selectedExam ? (
|
||||||
<ExamOwnerSelector options={filteredCorporates} exam={selectedExam} onSave={(owners) => updateExam(selectedExam, { owners })} />
|
<>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<BiEdit className="w-5 h-5 text-gray-600" />
|
||||||
|
<span className="text-gray-600 font-medium">Ready to Edit</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4 mb-3">
|
||||||
|
<p className="font-medium mb-1">
|
||||||
|
Exam ID: {selectedExam.id}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-500 text-sm">
|
||||||
|
Click 'Next' to proceed to the exam editor.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between gap-4 mt-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setSelectedExam(undefined)}
|
||||||
|
className="w-32"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={handleExamEdit}
|
||||||
|
className="w-32 text-white flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
Proceed
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/*<ExamOwnerSelector options={filteredCorporates} exam={selectedExam} onSave={(owners) => updateExam(selectedExam, { owners })} />*/}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div />
|
<div />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -345,7 +345,6 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
|
|||||||
setUserSolutions(userSolutions);
|
setUserSolutions(userSolutions);
|
||||||
}
|
}
|
||||||
setShuffles([]);
|
setShuffles([]);
|
||||||
console.log(exam);
|
|
||||||
if (index === undefined) {
|
if (index === undefined) {
|
||||||
setFlags({ reviewAll: true });
|
setFlags({ reviewAll: true });
|
||||||
setModuleIndex(0);
|
setModuleIndex(0);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { requestUser } from "@/utils/api";
|
|||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import { getExam, getExams } from "@/utils/exams.be";
|
import { getExam, getExams } from "@/utils/exams.be";
|
||||||
import { Exam, Exercise, InteractiveSpeakingExercise, ListeningPart, SpeakingExercise } from "@/interfaces/exam";
|
import { Exam, Exercise, InteractiveSpeakingExercise, ListeningPart, SpeakingExercise } from "@/interfaces/exam";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { isAdmin } from "@/utils/users";
|
import { isAdmin } from "@/utils/users";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -47,26 +47,38 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, query })
|
|||||||
|
|
||||||
if (Object.keys(permissions).every(p => !permissions[p as Module])) return redirect("/")
|
if (Object.keys(permissions).every(p => !permissions[p as Module])) return redirect("/")
|
||||||
|
|
||||||
const { id, module } = query as { id?: string, module?: Module }
|
const { id, module: examModule } = query as { id?: string, module?: Module }
|
||||||
if (!id || !module) return { props: serialize({ user, permissions }) };
|
if (!id || !examModule) return { props: serialize({ user, permissions }) };
|
||||||
|
|
||||||
if (!permissions[module]) return redirect("/generation")
|
//if (!permissions[module]) return redirect("/generation")
|
||||||
|
|
||||||
const exam = await getExam(module, id)
|
const exam = await getExam(examModule, id)
|
||||||
if (!exam) return redirect("/generation")
|
if (!exam) return redirect("/generation")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ user, exam, permissions }),
|
props: serialize({ id, user, exam, examModule, permissions }),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
export default function Generation({ user, exam, permissions }: { user: User; exam?: Exam, permissions: Permission }) {
|
export default function Generation({ id, user, exam, examModule, permissions }: { id: string, user: User; exam?: Exam, examModule?: Module, permissions: Permission }) {
|
||||||
const { title, currentModule, modules, dispatch } = useExamEditorStore();
|
const { title, currentModule, modules, dispatch } = useExamEditorStore();
|
||||||
|
const [examLevelParts, setExamLevelParts] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
const updateRoot = (updates: Partial<ExamEditorStore>) => {
|
const updateRoot = (updates: Partial<ExamEditorStore>) => {
|
||||||
dispatch({ type: 'UPDATE_ROOT', payload: { updates } });
|
dispatch({ type: 'UPDATE_ROOT', payload: { updates } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id && exam && examModule) {
|
||||||
|
if (examModule === "level" && exam.module === "level") {
|
||||||
|
setExamLevelParts(exam.parts.length);
|
||||||
|
}
|
||||||
|
updateRoot({currentModule: examModule})
|
||||||
|
dispatch({ type: "INIT_EXAM_EDIT", payload: { exam, id, examModule } })
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [id, exam, module])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAvatars = async () => {
|
const fetchAvatars = async () => {
|
||||||
const response = await axios.get("/api/exam/avatars");
|
const response = await axios.get("/api/exam/avatars");
|
||||||
@@ -118,15 +130,11 @@ export default function Generation({ user, exam, permissions }: { user: User; ex
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
dispatch({type: 'FULL_RESET'});
|
dispatch({ type: 'FULL_RESET' });
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (exam) { }
|
|
||||||
}, [exam])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -150,6 +158,7 @@ export default function Generation({ user, exam, permissions }: { user: User; ex
|
|||||||
label="Title"
|
label="Title"
|
||||||
onChange={(title) => updateRoot({ title })}
|
onChange={(title) => updateRoot({ title })}
|
||||||
roundness="xl"
|
roundness="xl"
|
||||||
|
value={title}
|
||||||
defaultValue={title}
|
defaultValue={title}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -194,7 +203,7 @@ export default function Generation({ user, exam, permissions }: { user: User; ex
|
|||||||
))}
|
))}
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
<ExamEditor />
|
<ExamEditor levelParts={examLevelParts} />
|
||||||
</Layout>
|
</Layout>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ExamPart, ModuleState, SectionState } from "./types"
|
|||||||
import { levelPart, listeningSection, readingPart, speakingTask, writingTask } from "@/stores/examEditor/sections"
|
import { levelPart, listeningSection, readingPart, speakingTask, writingTask } from "@/stores/examEditor/sections"
|
||||||
|
|
||||||
|
|
||||||
const defaultSettings = (module: Module) => {
|
export const defaultSettings = (module: Module) => {
|
||||||
const baseSettings = {
|
const baseSettings = {
|
||||||
category: '',
|
category: '',
|
||||||
introOption: { label: 'None', value: 'None' },
|
introOption: { label: 'None', value: 'None' },
|
||||||
@@ -77,7 +77,7 @@ const defaultSettings = (module: Module) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sectionLabels = (module: Module) => {
|
export const sectionLabels = (module: Module, levelParts?: number) => {
|
||||||
switch (module) {
|
switch (module) {
|
||||||
case 'reading':
|
case 'reading':
|
||||||
return Array.from({ length: 3 }, (_, index) => ({
|
return Array.from({ length: 3 }, (_, index) => ({
|
||||||
@@ -131,7 +131,6 @@ const defaultSection = (module: Module, sectionId: number) => {
|
|||||||
export const defaultSectionSettings = (module: Module, sectionId: number, part?: ExamPart) => {
|
export const defaultSectionSettings = (module: Module, sectionId: number, part?: ExamPart) => {
|
||||||
return {
|
return {
|
||||||
sectionId: sectionId,
|
sectionId: sectionId,
|
||||||
sectionLabel: "",
|
|
||||||
settings: defaultSettings(module),
|
settings: defaultSettings(module),
|
||||||
state: part !== undefined ? part : defaultSection(module, sectionId),
|
state: part !== undefined ? part : defaultSection(module, sectionId),
|
||||||
generating: undefined,
|
generating: undefined,
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import defaultModuleSettings from "../defaults";
|
import { Exam, ExerciseOnlyExam, PartExam } from "@/interfaces/exam";
|
||||||
import ExamEditorStore from "../types";
|
import defaultModuleSettings, { defaultSectionSettings, defaultSettings, sectionLabels } from "../defaults";
|
||||||
|
import ExamEditorStore, { SectionState } from "../types";
|
||||||
import { MODULE_ACTIONS, ModuleActions, moduleReducer } from "./moduleReducer";
|
import { MODULE_ACTIONS, ModuleActions, moduleReducer } from "./moduleReducer";
|
||||||
import { SECTION_ACTIONS, SectionActions, sectionReducer } from "./sectionReducer";
|
import { SECTION_ACTIONS, SectionActions, sectionReducer } from "./sectionReducer";
|
||||||
|
import { Module } from "@/interfaces";
|
||||||
|
import { updateExamWithUserSolutions } from "@/stores/exam/utils";
|
||||||
|
import { defaultExamUserSolutions } from "@/utils/exams";
|
||||||
|
|
||||||
type UpdateRoot = {
|
type UpdateRoot = {
|
||||||
type: 'UPDATE_ROOT';
|
type: 'UPDATE_ROOT';
|
||||||
@@ -9,9 +13,9 @@ type UpdateRoot = {
|
|||||||
updates: Partial<ExamEditorStore>
|
updates: Partial<ExamEditorStore>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
type FullReset = { type: 'FULL_RESET' };
|
type RootActions = { type: 'FULL_RESET' } | { type: "INIT_EXAM_EDIT", payload: { exam: Exam; examModule: Module; id: string } };
|
||||||
|
|
||||||
export type Action = ModuleActions | SectionActions | UpdateRoot | FullReset;
|
export type Action = ModuleActions | SectionActions | UpdateRoot | RootActions;
|
||||||
|
|
||||||
export const rootReducer = (
|
export const rootReducer = (
|
||||||
state: ExamEditorStore,
|
state: ExamEditorStore,
|
||||||
@@ -63,7 +67,64 @@ export const rootReducer = (
|
|||||||
listening: defaultModuleSettings("listening", 30),
|
listening: defaultModuleSettings("listening", 30),
|
||||||
level: defaultModuleSettings("level", 60)
|
level: defaultModuleSettings("level", 60)
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
case 'INIT_EXAM_EDIT': {
|
||||||
|
const { exam, id, examModule } = action.payload;
|
||||||
|
let typedExam;
|
||||||
|
let examState;
|
||||||
|
|
||||||
|
|
||||||
|
if (["reading", "listening", "level"].includes(examModule)) {
|
||||||
|
typedExam = updateExamWithUserSolutions(exam, defaultExamUserSolutions(exam)) as PartExam;
|
||||||
|
examState = typedExam.parts.map((part, index) => {
|
||||||
|
return {
|
||||||
|
...defaultSectionSettings(examModule, index + 1, part),
|
||||||
|
sectionId: index + 1,
|
||||||
|
settings: {
|
||||||
|
...defaultSettings(examModule),
|
||||||
|
category: part.category,
|
||||||
|
introOption: { label: 'Custom', value: 'Custom' },
|
||||||
|
currentIntro: part.intro
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
typedExam = updateExamWithUserSolutions(exam, defaultExamUserSolutions(exam)) as ExerciseOnlyExam;
|
||||||
|
examState = typedExam.exercises.map((exercise, index) => {
|
||||||
|
return {
|
||||||
|
...defaultSectionSettings(examModule, index + 1),
|
||||||
|
sectionId: index + 1,
|
||||||
|
settings: {
|
||||||
|
...defaultSettings(examModule),
|
||||||
|
category: exercise.category,
|
||||||
|
introOption: { label: 'Custom', value: 'Custom' },
|
||||||
|
currentIntro: exercise.intro
|
||||||
|
},
|
||||||
|
state: exercise,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: id,
|
||||||
|
modules: {
|
||||||
|
...state.modules,
|
||||||
|
[examModule]: {
|
||||||
|
...defaultModuleSettings(examModule, exam.minTimer),
|
||||||
|
examLabel: exam.label,
|
||||||
|
difficulty: exam.difficulty,
|
||||||
|
isPrivate: exam.private,
|
||||||
|
sections: examState,
|
||||||
|
importModule: false,
|
||||||
|
sectionLabels:
|
||||||
|
exam.module === "level" ?
|
||||||
|
sectionLabels(examModule, exam.parts.length) :
|
||||||
|
sectionLabels(examModule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user