From 490c5ad7d31a5bfac9d12f6b46c3dac031b8d0c1 Mon Sep 17 00:00:00 2001 From: Carlos-Mesquita Date: Mon, 2 Dec 2024 17:16:12 +0000 Subject: [PATCH] ENCOA-253, ENCOA-248, ENCOA-246 --- .../ExamEditor/SettingsEditor/index.tsx | 2 +- src/components/ExamEditor/index.tsx | 27 +++++++------ .../Medium/ModuleTitle/MCQuestionGrid.tsx | 10 +++-- src/components/Popouts/Exam.tsx | 33 +++++++++------ src/exams/Level/index.tsx | 6 +-- src/exams/Navigation/useExamNavigation.tsx | 40 +++++++++++++------ src/interfaces/exam.ts | 2 +- src/pages/api/exam/[module]/index.ts | 21 ++++++---- 8 files changed, 90 insertions(+), 51 deletions(-) diff --git a/src/components/ExamEditor/SettingsEditor/index.tsx b/src/components/ExamEditor/SettingsEditor/index.tsx index afb2bccb..fc9da308 100644 --- a/src/components/ExamEditor/SettingsEditor/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/index.tsx @@ -112,7 +112,7 @@ const SettingsEditor: React.FC = ({ onChange={(o) => onIntroOptionChange({ value: o!.value, label: o!.label })} value={localSettings.introOption} /> - {localSettings.introOption.value !== "None" && ( + {localSettings.introOption && localSettings.introOption.value !== "None" && ( = ({ levelParts = 0 }) => { importModule } = useExamEditorStore(state => state.modules[currentModule]); - const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== undefined ? levelParts : 1); + const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== 0 ? 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}` - })) + 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]) diff --git a/src/components/Medium/ModuleTitle/MCQuestionGrid.tsx b/src/components/Medium/ModuleTitle/MCQuestionGrid.tsx index abe905e4..e218ad63 100644 --- a/src/components/Medium/ModuleTitle/MCQuestionGrid.tsx +++ b/src/components/Medium/ModuleTitle/MCQuestionGrid.tsx @@ -2,8 +2,9 @@ import Button from "@/components/Low/Button"; import Modal from "@/components/Modal"; import { Exam, LevelExam, MultipleChoiceExercise, ShuffleMap } from "@/interfaces/exam"; import useExamStore, { usePersistentExamStore } from "@/stores/exam"; +import { defaultExamUserSolutions } from "@/utils/exams"; import clsx from "clsx"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { BsFillGrid3X3GapFill } from "react-icons/bs"; interface Props { @@ -18,7 +19,7 @@ const MCQuestionGrid: React.FC = ({ exam, showSolutions, runOnClick, prev const examState = useExamStore((state) => state); const persistentExamState = usePersistentExamStore((state) => state); - + const { userSolutions, partIndex: sectionIndex, @@ -27,7 +28,10 @@ const MCQuestionGrid: React.FC = ({ exam, showSolutions, runOnClick, prev const currentExercise = useMemo(() => (exam as LevelExam).parts[sectionIndex!].exercises[exerciseIndex] as MultipleChoiceExercise, [exam, exerciseIndex, sectionIndex]) const userSolution = useMemo(() => userSolutions!.find((x) => x.exercise.toString() == currentExercise.id.toString())!, [currentExercise.id, userSolutions]) - const answeredQuestions = useMemo(() => new Set(userSolution.solutions.map(sol => sol.question.toString())), [userSolution.solutions]) + const answeredQuestions = useMemo(() => + userSolution ? new Set(userSolution.solutions.map(sol => sol.question.toString())) : new Set(), + [userSolution] + ); const exerciseOffset = useMemo(() => Number(currentExercise.questions[0].id), [currentExercise.questions]) const lastExercise = useMemo(() => exerciseOffset + (currentExercise.questions.length - 1), [currentExercise.questions.length, exerciseOffset]); diff --git a/src/components/Popouts/Exam.tsx b/src/components/Popouts/Exam.tsx index a878291e..59509c11 100644 --- a/src/components/Popouts/Exam.tsx +++ b/src/components/Popouts/Exam.tsx @@ -8,29 +8,38 @@ import { LevelExam, ListeningExam, ReadingExam, SpeakingExam, WritingExam } from import { User } from "@/interfaces/user"; import { usePersistentExamStore } from "@/stores/exam"; import clsx from "clsx"; +import { useEffect } from "react"; // TODO: perms const Popout: React.FC<{ user: User }> = ({ user }) => { - const state = usePersistentExamStore((state) => state); + const { bgColor, exam, partIndex, setUserSolutions } = usePersistentExamStore(); usePersistentStorage(usePersistentExamStore); + + useEffect(() => { + return () => { + setUserSolutions([]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + return ( -
+
- {state.exam?.module == "level" && state.exam.parts && state.partIndex >= 0 && - + {exam?.module == "level" && exam.parts && partIndex >= 0 && + } - {state.exam?.module == "writing" && state.exam.exercises && state.partIndex >= 0 && - + {exam?.module == "writing" && exam.exercises && partIndex >= 0 && + } - {state.exam?.module == "reading" && state.exam.parts.length > 0 && - + {exam?.module == "reading" && exam.parts.length > 0 && + } - {state.exam?.module == "listening" && state.exam.parts.length > 0 && - + {exam?.module == "listening" && exam.parts.length > 0 && + } - {state.exam?.module == "speaking" && state.exam.exercises.length > 0 && - + {exam?.module == "speaking" && exam.exercises.length > 0 && + }
diff --git a/src/exams/Level/index.tsx b/src/exams/Level/index.tsx index d462731b..bfd8d739 100644 --- a/src/exams/Level/index.tsx +++ b/src/exams/Level/index.tsx @@ -92,7 +92,7 @@ const Level: React.FC> = ({ exam, showSolutions = false, pr const { nextExercise, previousExercise, showPartDivider, setShowPartDivider, - seenParts, setSeenParts, startNow + seenParts, setSeenParts, startNow, setStartNow } = useExamNavigation( { exam, module: "level", showBlankModal: showQuestionsModal, @@ -361,14 +361,14 @@ const Level: React.FC> = ({ exam, showSolutions = false, pr (!showPartDivider && !startNow) && } - {(showPartDivider || startNow) ? + {(showPartDivider || (startNow && partIndex === 0)) ? { setShowPartDivider(false); setBgColor("bg-white"); setSeenParts(prev => new Set(prev).add(partIndex)); }} + onNext={() => { setShowPartDivider(false); setStartNow(false); setBgColor("bg-white"); setSeenParts(prev => new Set(prev).add(partIndex)); }} /> : ( <> {exam.parts.length > 1 && >; setSeenParts: React.Dispatch>>; setIsBetweenParts: React.Dispatch>; + setStartNow: React.Dispatch>; }; const useExamNavigation: UseExamNavigation = ({ @@ -50,8 +51,7 @@ const useExamNavigation: UseExamNavigation = ({ exerciseIndex, setExerciseIndex, partIndex, setPartIndex, questionIndex, setQuestionIndex, - userSolutions, setModuleIndex, - setBgColor, + userSolutions, setBgColor, dispatch, } = !preview ? examState : persistentExamState; @@ -74,19 +74,34 @@ const useExamNavigation: UseExamNavigation = ({ const [startNow, setStartNow] = useState(!showPartDivider && !showSolutions); // when navbar is used - useEffect(()=> { - if(startNow && !showPartDivider && partIndex !== 0) { + useEffect(() => { + if (startNow && !showPartDivider && partIndex !== 0) { setStartNow(false); } - } , [partIndex, startNow, showPartDivider]) + }, [partIndex, startNow, showPartDivider]) useEffect(() => { - if (!showSolutions && hasDivider(exam, isPartExam ? partIndex : exerciseIndex) && !seenParts.has(partIndex)) { - setShowPartDivider(true); + if ( + ( + module === "level" ? + (startNow || hasDivider(exam, isPartExam ? partIndex : exerciseIndex)) + : + (hasDivider(exam, isPartExam ? partIndex : exerciseIndex)) + ) + && !showSolutions && !seenParts.has(partIndex) + + ) { + if ( + // partIndex === 0 -> startNow + (module === "level" && (partIndex === 0 || hasDivider(exam, isPartExam ? partIndex : exerciseIndex))) || + module !== "level" + ) { + setShowPartDivider(true); + } setBgColor(`bg-ielts-${module}-light`); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [partIndex]); + }, [partIndex, startNow]); const nextExercise = (keepGoing: boolean = false) => { scrollToTop(); @@ -160,7 +175,7 @@ const useExamNavigation: UseExamNavigation = ({ setQuestionIndex(0); return; } - + if (modalBetweenParts && !seenParts.has(partIndex + 1) && !answeredEveryQuestionInPart(exam as PartExam, partIndex, userSolutions) && !keepGoing && setShowBlankModal && !showSolutions && !preview) { if (modalKwargs) modalKwargs(); setShowBlankModal(true); @@ -197,7 +212,7 @@ const useExamNavigation: UseExamNavigation = ({ const previousPartExam = () => { if (!showPartDivider && partIndex === 0 && exerciseIndex === 0 && questionIndex === 0 && !startNow) { - setStartNow(true); + if (module !== "level") setStartNow(true); return; } @@ -226,7 +241,7 @@ const useExamNavigation: UseExamNavigation = ({ setQuestionIndex(Math.max(0, questionIndex - MC_PER_PAGE)); return; } - + if (exerciseIndex !== 0) { setExerciseIndex(exerciseIndex - 1); setQuestionIndex(0); @@ -294,7 +309,8 @@ const useExamNavigation: UseExamNavigation = ({ setSeenParts, isBetweenParts, setIsBetweenParts, - startNow + startNow, + setStartNow }; } diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index 8a469c12..0c3b9329 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -5,7 +5,7 @@ export type Variant = "full" | "partial"; export type InstructorGender = "male" | "female" | "varied"; export type Difficulty = "easy" | "medium" | "hard"; -interface ExamBase { +export interface ExamBase { id: string; module: Module; minTimer: number; diff --git a/src/pages/api/exam/[module]/index.ts b/src/pages/api/exam/[module]/index.ts index 6bd24165..0e8d7eef 100644 --- a/src/pages/api/exam/[module]/index.ts +++ b/src/pages/api/exam/[module]/index.ts @@ -3,7 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import client from "@/lib/mongodb"; import { withIronSessionApiRoute } from "iron-session/next"; import { sessionOptions } from "@/lib/session"; -import { Exam, InstructorGender, Variant } from "@/interfaces/exam"; +import { Exam, ExamBase, InstructorGender, Variant } from "@/interfaces/exam"; import { getExams } from "@/utils/exams.be"; import { Module } from "@/interfaces"; import { getUserCorporate } from "@/utils/groups.be"; @@ -60,18 +60,25 @@ async function POST(req: NextApiRequest, res: NextApiResponse) { }; await session.withTransaction(async () => { - const docSnap = await db.collection(module).findOne({ id: req.body.id }, { session }); + const docSnap = await db.collection(module).findOne({ id: req.body.id }, { session }); - if (docSnap) { + // Check whether the id of the exam matches another exam with different + // owners, throw exception if there is, else allow editing + const ownersSet = new Set(docSnap?.owners || []); + if (docSnap?.owners?.length === exam.owners.lenght && exam.owners.every((e: string) => ownersSet.has(e))) { throw new Error("Name already exists"); } - await db.collection(module).insertOne( - { id: req.body.id, ...exam }, - { session } + await db.collection(module).updateOne( + { id: req.body.id }, + { $set: { id: req.body.id, ...exam } }, + { + upsert: true, + session + } ); }); - + res.status(200).json(exam); } catch (error) {