From 1fc439cb25e49371e4aeb7f71bb2e3c46dc073c2 Mon Sep 17 00:00:00 2001 From: Carlos-Mesquita Date: Tue, 26 Nov 2024 14:33:49 +0000 Subject: [PATCH] Fixed some navigation issues and updated Listening --- src/components/Exercises/FillBlanks/index.tsx | 2 +- .../Exercises/MatchSentences/index.tsx | 2 +- .../Exercises/WriteBlanks/index.tsx | 2 +- src/components/Solutions/FillBlanks.tsx | 2 +- src/components/Solutions/MatchSentences.tsx | 2 +- src/components/Solutions/MultipleChoice.tsx | 2 +- src/components/Solutions/TrueFalse.tsx | 2 +- src/components/Solutions/WriteBlanks.tsx | 2 +- src/exams/Finish.tsx | 6 +- src/exams/Listening.tsx | 103 ++--- src/exams/Listening.tsx.bak | 372 ------------------ src/exams/Navigation/useExamNavigation.tsx | 106 ++++- src/exams/Reading.tsx | 19 +- src/pages/(exam)/ExamPage.tsx | 10 +- src/stores/exam/reducers/index.ts | 31 +- 15 files changed, 183 insertions(+), 480 deletions(-) delete mode 100644 src/exams/Listening.tsx.bak diff --git a/src/components/Exercises/FillBlanks/index.tsx b/src/components/Exercises/FillBlanks/index.tsx index 9060bba2..075725f3 100644 --- a/src/components/Exercises/FillBlanks/index.tsx +++ b/src/components/Exercises/FillBlanks/index.tsx @@ -167,7 +167,7 @@ const FillBlanks: React.FC = ({ return (
{headerButtons} -
+
{variant !== "mc" && ( {prompt.split("\\n").map((line, index) => ( diff --git a/src/components/Exercises/MatchSentences/index.tsx b/src/components/Exercises/MatchSentences/index.tsx index 1e6c51d9..99694f29 100644 --- a/src/components/Exercises/MatchSentences/index.tsx +++ b/src/components/Exercises/MatchSentences/index.tsx @@ -53,7 +53,7 @@ const MatchSentences: React.FC = ({ return (
{headerButtons} -
+
{prompt.split("\\n").map((line, index) => ( diff --git a/src/components/Exercises/WriteBlanks/index.tsx b/src/components/Exercises/WriteBlanks/index.tsx index 805d32b6..57fa7404 100644 --- a/src/components/Exercises/WriteBlanks/index.tsx +++ b/src/components/Exercises/WriteBlanks/index.tsx @@ -66,7 +66,7 @@ const WriteBlanks: React.FC = ({ return (
{headerButtons} -
+
{prompt.split("\\n").map((line, index) => ( diff --git a/src/components/Solutions/FillBlanks.tsx b/src/components/Solutions/FillBlanks.tsx index 66b1265f..3449efdc 100644 --- a/src/components/Solutions/FillBlanks.tsx +++ b/src/components/Solutions/FillBlanks.tsx @@ -116,7 +116,7 @@ const FillBlanksSolutions: React.FC = ({ id, s return (
{headerButtons} -
+
{correctUserSolutions && text.split("\\n").map((line, index) => ( diff --git a/src/components/Solutions/MatchSentences.tsx b/src/components/Solutions/MatchSentences.tsx index 73c29e1a..3f177a46 100644 --- a/src/components/Solutions/MatchSentences.tsx +++ b/src/components/Solutions/MatchSentences.tsx @@ -57,7 +57,7 @@ export default function MatchSentencesSolutions({ return (
{headerButtons} -
+
{prompt.split("\\n").map((line, index) => ( diff --git a/src/components/Solutions/MultipleChoice.tsx b/src/components/Solutions/MultipleChoice.tsx index 322ab741..32e184b7 100644 --- a/src/components/Solutions/MultipleChoice.tsx +++ b/src/components/Solutions/MultipleChoice.tsx @@ -153,7 +153,7 @@ export default function MultipleChoice({ id, type, prompt, questions, userSoluti
{headerButtons} -
+
{(!headerButtons || !footerButtons) ? renderAllQuestions() : renderTwoQuestions()}
diff --git a/src/components/Solutions/TrueFalse.tsx b/src/components/Solutions/TrueFalse.tsx index 6de189dd..e137b552 100644 --- a/src/components/Solutions/TrueFalse.tsx +++ b/src/components/Solutions/TrueFalse.tsx @@ -30,7 +30,7 @@ export default function TrueFalseSolution({ prompt, type, id, questions, userSol
{headerButtons} -
+
{prompt.split("\\n").map((line, index) => ( diff --git a/src/components/Solutions/WriteBlanks.tsx b/src/components/Solutions/WriteBlanks.tsx index 467700e3..837ca9de 100644 --- a/src/components/Solutions/WriteBlanks.tsx +++ b/src/components/Solutions/WriteBlanks.tsx @@ -87,7 +87,7 @@ export default function WriteBlanksSolutions({
{headerButtons} -
+
{prompt.split("\\n").map((line, index) => ( diff --git a/src/exams/Finish.tsx b/src/exams/Finish.tsx index ddea088f..ff67ee33 100644 --- a/src/exams/Finish.tsx +++ b/src/exams/Finish.tsx @@ -61,8 +61,8 @@ export default function Finish({ user, practiceScores, scores, modules, informat const aiUsage = Math.round(ai_usage(solutions) * 100); - const entity = useMemo(() => assignment?.entity || user.entities[0]?.id || "", [assignment?.entity, user.entities]) - const { gradingSystem } = useGradingSystem(entity); + //const entity = useMemo(() => assignment?.entity || user.entities[0]?.id || "", [assignment?.entity, user.entities]) + //const { gradingSystem } = useGradingSystem(entity); const router = useRouter() @@ -104,7 +104,7 @@ export default function Finish({ user, practiceScores, scores, modules, informat const showLevel = (level: number) => { if (selectedModule === "level") { - const label = getGradingLabel(level, gradingSystem?.steps || []); + const label = getGradingLabel(level, []); return (
{label} diff --git a/src/exams/Listening.tsx b/src/exams/Listening.tsx index 7c433175..d5ea0d0e 100644 --- a/src/exams/Listening.tsx +++ b/src/exams/Listening.tsx @@ -3,14 +3,9 @@ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "rea import { renderExercise } from "@/components/Exercises"; import { renderSolution } from "@/components/Solutions"; import ModuleTitle from "@/components/Medium/ModuleTitle"; -import AudioPlayer from "@/components/Low/AudioPlayer"; -import Button from "@/components/Low/Button"; import BlankQuestionsModal from "@/components/QuestionsModal"; import useExamStore, { usePersistentExamStore } from "@/stores/exam"; import PartDivider from "./Navigation/SectionDivider"; -import { Dialog, Transition } from "@headlessui/react"; -import { capitalize } from "lodash"; -import { mapBy } from "@/utils"; import ScriptModal from "./components/ScriptModal"; import { ExamProps } from "./types"; import useExamTimer from "@/hooks/useExamTimer"; @@ -19,8 +14,6 @@ import RenderAudioInstructionsPlayer from "./components/RenderAudioInstructionsP import RenderAudioPlayer from "./components/RenderAudioPlayer"; import SectionNavbar from "./Navigation/SectionNavbar"; import ProgressButtons from "./components/ProgressButtons"; -import { countExercises } from "@/utils/moduleUtils"; -import { calculateExerciseIndex } from "./utils/calculateExerciseIndex"; const Listening: React.FC> = ({ exam, showSolutions = false, preview = false }) => { @@ -36,9 +29,8 @@ const Listening: React.FC> = ({ exam, showSolutions = f const persistentExamState = usePersistentExamStore((state) => state); const { - exerciseIndex, partIndex, assignment, + partIndex, assignment, userSolutions, flags, timeSpentCurrentModule, - questionIndex, setBgColor, setUserSolutions, setTimeIsUp, dispatch } = !preview ? examState : persistentExamState; @@ -47,13 +39,16 @@ const Listening: React.FC> = ({ exam, showSolutions = f const timer = useRef(exam.minTimer - timeSpentCurrentModule / 60); - const [isFirstTimeRender, setIsFirstTimeRender] = useState(partIndex === 0 && exerciseIndex == 0 && !showSolutions); - const { nextExercise, previousExercise, showPartDivider, setShowPartDivider, - seenParts, setSeenParts - } = useExamNavigation({ exam, module: "listening", showBlankModal, setShowBlankModal, showSolutions, preview, disableBetweenParts: true }); + seenParts, setSeenParts, + isBetweenParts, startNow + } = useExamNavigation({ + exam, module: "listening", showBlankModal, + setShowBlankModal, showSolutions, preview, + allPartExercisesRender: true, disableBetweenParts: true + }); useEffect(() => { @@ -80,14 +75,23 @@ const Listening: React.FC> = ({ exam, showSolutions = f // eslint-disable-next-line react-hooks/exhaustive-deps }, [solutionWasUpdated]) - const currentExercise = useMemo(() => { - const exercise = exam.parts[partIndex].exercises[exerciseIndex]; - return { + + const renderPartExercises = useMemo(() => { + const exercises = exam.parts[partIndex].exercises; + const formattedExercises = exercises.map(exercise => ({ ...exercise, userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [], - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [partIndex, exerciseIndex]); + })) + + return ( +
+ {formattedExercises.map(e => showSolutions + ? renderSolution(e) + : (!startNow && !showPartDivider && !isBetweenParts && !showSolutions) && renderExercise(e, exam.id, registerSolution, preview))} +
+ ) + }, [partIndex, startNow, showPartDivider, isBetweenParts, showSolutions]); + const confirmFinishModule = (keepGoing?: boolean) => { @@ -100,18 +104,10 @@ const Listening: React.FC> = ({ exam, showSolutions = f } }; - const memoizedExerciseIndex = useMemo(() => - calculateExerciseIndex(exam, partIndex, exerciseIndex, questionIndex) - - // eslint-disable-next-line react-hooks/exhaustive-deps - , [partIndex, exerciseIndex, questionIndex] - ); - const handlePartDividerClick = () => { setShowPartDivider(false); setBgColor("bg-white"); setSeenParts((prev) => new Set(prev).add(partIndex)); - if (isFirstTimeRender) setIsFirstTimeRender(false); } useEffect(() => { @@ -123,6 +119,21 @@ const Listening: React.FC> = ({ exam, showSolutions = f nextExercise()} /> , [nextExercise, previousExercise]); + const memoizedRenderAudioPlayer = useMemo(() => + , [partIndex, assignment, timesListened, setShowTextModal, setTimesListened]) + + const memoizedInstructions = useMemo(()=> + + , []) + return ( <> {showPartDivider ? @@ -136,13 +147,13 @@ const Listening: React.FC> = ({ exam, showSolutions = f /> : ( <> - {!isFirstTimeRender && exam.parts[partIndex].script && + {!startNow && exam.parts[partIndex].script && setShowTextModal(false)} /> }
{exam.parts.length > 1 && > = ({ exam, showSolutions = f /> } x.exercises))} + totalExercises={exam.parts.length} disableTimer={showSolutions || preview} - indexLabel="Exercise" + indexLabel="Part" preview={preview} /> {/* Audio Player for the Instructions */} - {isFirstTimeRender && } + {startNow && memoizedInstructions} {/* Part's audio player */} - {!isFirstTimeRender && - } + {(!startNow && isBetweenParts) && memoizedRenderAudioPlayer} {/* Exercise renderer */} - {!isFirstTimeRender && !showPartDivider && !showSolutions && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons)} - {showSolutions && renderSolution(currentExercise, progressButtons, progressButtons)} + <> + {(!startNow && !showPartDivider) && progressButtons} + {renderPartExercises} + {(!startNow && !showPartDivider && !isBetweenParts) && progressButtons} +
- {((isFirstTimeRender) && !showPartDivider && !showSolutions) && + {((startNow) && !showPartDivider && !showSolutions) && isFirstTimeRender ? setIsFirstTimeRender(false) : nextExercise()} /> + handleNext={() => nextExercise()} /> } ) } diff --git a/src/exams/Listening.tsx.bak b/src/exams/Listening.tsx.bak deleted file mode 100644 index 636210e4..00000000 --- a/src/exams/Listening.tsx.bak +++ /dev/null @@ -1,372 +0,0 @@ -import { ListeningExam, MultipleChoiceExercise, Script, UserSolution } from "@/interfaces/exam"; -import { Fragment, useEffect, useState } from "react"; -import { renderExercise } from "@/components/Exercises"; -import { renderSolution } from "@/components/Solutions"; -import ModuleTitle from "@/components/Medium/ModuleTitle"; -import AudioPlayer from "@/components/Low/AudioPlayer"; -import Button from "@/components/Low/Button"; -import BlankQuestionsModal from "@/components/QuestionsModal"; -import useExamStore, { usePersistentExamStore } from "@/stores/examStore"; -import PartDivider from "./Navigation/SectionDivider"; -import { Dialog, Transition } from "@headlessui/react"; -import { capitalize } from "lodash"; -import { mapBy } from "@/utils"; - -interface Props { - exam: ListeningExam; - showSolutions?: boolean; - preview?: boolean; - onFinish: (userSolutions: UserSolution[]) => void; -} - -function ScriptModal({ isOpen, script, onClose }: { isOpen: boolean; script: Script; onClose: () => void }) { - return ( - - - -
- - -
-
- - -
-

- {typeof script === "string" && script.split("\\n").map((line, index) => ( - - {line} -
-
- ))} - - {typeof script === "object" && script.map((line, index) => ( - - {line.name} ({capitalize(line.gender)}): {line.text} -
-
-
- ))} -

-
- -
- -
-
-
-
-
-
-
- ); -} - -const INSTRUCTIONS_AUDIO_SRC = - "https://firebasestorage.googleapis.com/v0/b/storied-phalanx-349916.appspot.com/o/generic_listening_intro_v2.mp3?alt=media&token=16769f5f-1e9b-4a72-86a9-45a6f0fa9f82"; - -export default function Listening({ exam, showSolutions = false, preview = false, onFinish }: Props) { - const listeningBgColor = "bg-ielts-listening-light"; - - const [showTextModal, setShowTextModal] = useState(false); - const [timesListened, setTimesListened] = useState(0); - const [showBlankModal, setShowBlankModal] = useState(false); - const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]); - - const [seenParts, setSeenParts] = useState>(new Set(showSolutions ? exam.parts.map((_, index) => index) : [])); - const [showPartDivider, setShowPartDivider] = useState(typeof exam.parts[0].intro === "string" && exam.parts[0].intro !== ""); - - const examState = useExamStore((state) => state); - const persistentExamState = usePersistentExamStore((state) => state); - - const { - hasExamEnded, - userSolutions, - exerciseIndex, - partIndex, - questionIndex: storeQuestionIndex, - setBgColor, - setUserSolutions, - setHasExamEnded, - setExerciseIndex, - setPartIndex, - setQuestionIndex: setStoreQuestionIndex - } = !preview ? examState : persistentExamState; - - const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); - - useEffect(() => { - if (!showSolutions && exam.parts[partIndex]?.intro !== undefined && exam.parts[partIndex]?.intro !== "" && !seenParts.has(exerciseIndex)) { - setShowPartDivider(true); - setBgColor(listeningBgColor); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [partIndex]); - - useEffect(() => { - if (showSolutions) return setExerciseIndex(0); - }, [setExerciseIndex, showSolutions]); - - useEffect(() => { - if (partIndex === -1 && exam.variant === "partial") { - setPartIndex(0); - } - }, [partIndex, exam, setPartIndex]); - - useEffect(() => { - const previousParts = exam.parts.filter((_, index) => index < partIndex); - let previousMultipleChoice = previousParts.flatMap((x) => x.exercises).filter((x) => x.type === "multipleChoice") as MultipleChoiceExercise[]; - - if (partIndex > -1 && exerciseIndex > -1) { - const previousPartExercises = exam.parts[partIndex].exercises.filter((_, index) => index < exerciseIndex); - const partMultipleChoice = previousPartExercises.filter((x) => x.type === "multipleChoice") as MultipleChoiceExercise[]; - - previousMultipleChoice = [...previousMultipleChoice, ...partMultipleChoice]; - } - - setMultipleChoicesDone(previousMultipleChoice.map((x) => ({ id: x.id, amount: x.questions.length - 1 }))); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (hasExamEnded) onFinish(userSolutions) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hasExamEnded]); - - const confirmFinishModule = (keepGoing?: boolean) => { - if (!keepGoing) { - setShowBlankModal(false); - return; - } - - onFinish(userSolutions); - }; - - const nextExercise = (solution?: UserSolution) => { - if (solution) - setUserSolutions([ - ...userSolutions.filter((x) => x.exercise !== solution.exercise), - { ...solution, module: "listening", exam: exam.id } - ]); - }; - - const previousExercise = (solution?: UserSolution) => { - scrollToTop(); - if (solution) - setUserSolutions([ - ...userSolutions.filter((x) => x.exercise !== solution.exercise), - { ...solution, module: "listening", exam: exam.id } - ]); - - setPartIndex(partIndex - 1) - }; - - const nextPart = () => { - scrollToTop() - if (partIndex + 1 < exam.parts.length && !hasExamEnded) { - setPartIndex(partIndex + 1); - setExerciseIndex(0); - return; - } - - if (!showSolutions && !hasExamEnded) { - const exercises = partIndex < exam.parts.length ? exam.parts[partIndex].exercises : [] - const exerciseIDs = mapBy(exercises, 'id') - - const hasMissing = userSolutions.filter(x => exerciseIDs.includes(x.exercise)).map(x => x.score.missing).some(x => x > 0) - - if (hasMissing) return setShowBlankModal(true); - - } - - setHasExamEnded(false); - onFinish(userSolutions); - } - - const renderPartExercises = () => { - const exercises = partIndex > -1 ? exam.parts[partIndex].exercises : [] - const formattedExercises = exercises.map(exercise => ({ - ...exercise, - userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [], - })) - - return ( -
- {formattedExercises.map(e => showSolutions - ? renderSolution(e, nextExercise, previousExercise, undefined, true) - : renderExercise(e, exam.id, nextExercise, previousExercise, undefined, true))} -
- ) - } - - const renderAudioInstructionsPlayer = () => ( -
-
-

Please listen to the instructions audio attentively.

-
-
- -
-
- ); - - const renderAudioPlayer = () => ( -
- {exam?.parts[partIndex]?.audio?.source ? ( - <> -
-
-

Please listen to the following audio attentively.

- - {(() => { - const audioRepeatTimes = exam?.parts[partIndex]?.audio?.repeatableTimes; - return audioRepeatTimes && audioRepeatTimes > 0 - ? `You will only be allowed to listen to the audio ${audioRepeatTimes - timesListened} time(s).` - : "You may listen to the audio as many times as you would like."; - })()} - -
- {partIndex > -1 && !examState.assignment && !!exam.parts[partIndex].script && ( - - )} -
-
- setTimesListened((prev) => prev + 1)} - disabled={exam?.parts[partIndex]?.audio?.repeatableTimes != null && - timesListened === exam.parts[partIndex]?.audio?.repeatableTimes} - disablePause - /> -
- - ) : ( - This section will be displayed the audio once it has been generated. - )} - -
- ); - - const progressButtons = () => ( -
- - - -
- ) - - return ( - <> - {showPartDivider ? - { setShowPartDivider(false); setBgColor("bg-white"); setSeenParts((prev) => new Set(prev).add(exerciseIndex)) }} - /> : ( - <> - - {partIndex > -1 && exam.parts[partIndex].script && - setShowTextModal(false)} /> - } -
- - - {/* Audio Player for the Instructions */} - {partIndex === -1 && renderAudioInstructionsPlayer()} - - {/* Part's audio player */} - {partIndex > -1 && renderAudioPlayer()} - - {/* Exercise renderer */} - - {exerciseIndex > -1 && partIndex > -1 && ( - <> - {progressButtons()} - {renderPartExercises()} - {progressButtons()} - - )} -
- - {exerciseIndex === -1 && partIndex > -1 && exam.variant !== "partial" && ( -
- - - -
- )} - - {partIndex === -1 && exam.variant !== "partial" && ( - - )} - {exerciseIndex === -1 && partIndex === 0 && exam.variant === "partial" && ( - - )} - ) - } - - ); -} \ No newline at end of file diff --git a/src/exams/Navigation/useExamNavigation.tsx b/src/exams/Navigation/useExamNavigation.tsx index 31f6892c..bd042cbd 100644 --- a/src/exams/Navigation/useExamNavigation.tsx +++ b/src/exams/Navigation/useExamNavigation.tsx @@ -17,10 +17,13 @@ type UseExamNavigation = (props: { showSolutions: boolean; preview: boolean; disableBetweenParts?: boolean; + allPartExercisesRender?: boolean; + modalBetweenParts?: boolean; }) => { showPartDivider: boolean; seenParts: Set; isBetweenParts: boolean; + startNow: boolean; nextExercise: (isBetweenParts?: boolean) => void; previousExercise: (isBetweenParts?: boolean) => void; setShowPartDivider: React.Dispatch>; @@ -36,6 +39,8 @@ const useExamNavigation: UseExamNavigation = ({ showSolutions, preview, disableBetweenParts = false, + allPartExercisesRender = false, + modalBetweenParts = false, }) => { const examState = useExamStore((state) => state); @@ -50,7 +55,10 @@ const useExamNavigation: UseExamNavigation = ({ dispatch, } = !preview ? examState : persistentExamState; - const [isBetweenParts, setIsBetweenParts] = useState(partIndex !== 0 && exerciseIndex == 0 && !disableBetweenParts); + const [isBetweenParts, setIsBetweenParts] = useState(module === "reading" + ? (partIndex !== 0 && exerciseIndex === 0 && !disableBetweenParts) + : (exerciseIndex === 0 && !disableBetweenParts) + ); const isPartExam = ["reading", "listening", "level"].includes(exam.module); const [seenParts, setSeenParts] = useState>( @@ -63,6 +71,14 @@ const useExamNavigation: UseExamNavigation = ({ ) ); const [showPartDivider, setShowPartDivider] = useState(hasDivider(exam, 0)); + const [startNow, setStartNow] = useState(!showPartDivider && !showSolutions); + + // when navbar is used + useEffect(()=> { + if(startNow && partIndex !== 0) { + setStartNow(false); + } + } , [partIndex, startNow]) useEffect(() => { if (!showSolutions && hasDivider(exam, isPartExam ? partIndex : exerciseIndex) && !seenParts.has(partIndex)) { @@ -95,14 +111,42 @@ const useExamNavigation: UseExamNavigation = ({ const nextPartExam = (keepGoing: boolean) => { const partExam = (exam as PartExam); - const reachedFinalExercise = exerciseIndex + 1 === partExam.parts[partIndex].exercises.length; - const currentExercise = partExam.parts[partIndex].exercises[exerciseIndex]; + if (startNow) { + setStartNow(false); + return; + } if (isBetweenParts) { setIsBetweenParts(false); return; } + if (allPartExercisesRender) { + if (partIndex < partExam.parts.length - 1) { + if (!disableBetweenParts) setIsBetweenParts(true); + setPartIndex(partIndex + 1); + return; + } + + if (!answeredEveryQuestion(exam as PartExam, userSolutions) && !keepGoing && setShowBlankModal && !showSolutions && !preview) { + if (modalKwargs) modalKwargs(); + setShowBlankModal(true); + return; + } + + if (preview) { + setPartIndex(0); + } else if (!showSolutions) { + dispatch({ type: "FINALIZE_MODULE", payload: { updateTimers: true } }); + } else { + dispatch({ type: "FINALIZE_MODULE_SOLUTIONS" }); + } + return; + } + + const reachedFinalExercise = exerciseIndex + 1 === partExam.parts[partIndex].exercises.length; + const currentExercise = partExam.parts[partIndex].exercises[exerciseIndex]; + if (currentExercise.type === "multipleChoice") { const nextQuestionIndex = questionIndex + MC_PER_PAGE; if (nextQuestionIndex < currentExercise.questions!.length) { @@ -110,11 +154,19 @@ const useExamNavigation: UseExamNavigation = ({ return; } } + if (!reachedFinalExercise) { setExerciseIndex(exerciseIndex + 1); setQuestionIndex(0); return; } + + console.log(modalBetweenParts); + if (modalBetweenParts && !answeredEveryQuestion(exam as PartExam, userSolutions) && !keepGoing && setShowBlankModal && !showSolutions && !preview) { + if (modalKwargs) modalKwargs(); + setShowBlankModal(true); + return; + } if (partIndex < partExam.parts.length - 1) { if (!disableBetweenParts) setIsBetweenParts(true); @@ -124,37 +176,58 @@ const useExamNavigation: UseExamNavigation = ({ return; } - if (!answeredEveryQuestion(exam as PartExam, userSolutions) && !keepGoing && setShowBlankModal && !showSolutions && !preview) { - if (modalKwargs) modalKwargs() + + if (!modalBetweenParts && !answeredEveryQuestion(exam as PartExam, userSolutions) && !keepGoing && setShowBlankModal && !showSolutions && !preview) { + if (modalKwargs) modalKwargs(); setShowBlankModal(true); return; } + if (preview) { setPartIndex(0); setExerciseIndex(0); setQuestionIndex(0); - } - - if (!showSolutions) { + } else if (!showSolutions) { dispatch({ type: "FINALIZE_MODULE", payload: { updateTimers: true } }); } else { dispatch({ type: "FINALIZE_MODULE_SOLUTIONS" }); } - } + }; const previousPartExam = () => { + + if (partIndex === 0 && exerciseIndex === 0 && !startNow) { + setStartNow(true); + return; + } + + if (!disableBetweenParts && isBetweenParts) { + setIsBetweenParts(false); + } + + if (allPartExercisesRender) { + if (isBetweenParts && partIndex !== 0) { + setPartIndex(partIndex - 1); + setIsBetweenParts(false); + return; + } + if (disableBetweenParts) { + if (partIndex !== 0) { + setPartIndex(partIndex - 1); + } + } else { + setIsBetweenParts(true); + } + return; + } + const currentExercise = (exam as PartExam).parts[partIndex].exercises[exerciseIndex]; - if (currentExercise.type === "multipleChoice" && questionIndex > 0) { + if (currentExercise.type === "multipleChoice" && questionIndex > 0 && !allPartExercisesRender) { setQuestionIndex(Math.max(0, questionIndex - MC_PER_PAGE)); return; } - - if (exerciseIndex === 0 && !disableBetweenParts) { - setIsBetweenParts(true); - return; - } - + if (exerciseIndex !== 0) { setExerciseIndex(exerciseIndex - 1); setQuestionIndex(0); @@ -222,6 +295,7 @@ const useExamNavigation: UseExamNavigation = ({ setSeenParts, isBetweenParts, setIsBetweenParts, + startNow }; } diff --git a/src/exams/Reading.tsx b/src/exams/Reading.tsx index 46eec1dd..e9d7f094 100644 --- a/src/exams/Reading.tsx +++ b/src/exams/Reading.tsx @@ -34,20 +34,20 @@ const Reading: React.FC> = ({ exam, showSolutions = false exerciseIndex, partIndex, questionIndex, userSolutions, flags, timeSpentCurrentModule, setBgColor, setUserSolutions, setTimeIsUp, - dispatch + dispatch, } = !preview ? examState : persistentExamState; const timer = useRef(exam.minTimer - timeSpentCurrentModule / 60); const { finalizeModule, timeIsUp } = flags; - const [isFirstTimeRender, setIsFirstTimeRender] = useState(partIndex === 0 && exerciseIndex == 0 && !showSolutions); const { nextExercise, previousExercise, showPartDivider, setShowPartDivider, seenParts, setSeenParts, - isBetweenParts, setIsBetweenParts + isBetweenParts, setIsBetweenParts, + startNow } = useExamNavigation({ exam, module: "reading", showBlankModal, setShowBlankModal, showSolutions, preview, disableBetweenParts: showSolutions }); useEffect(() => { @@ -118,7 +118,6 @@ const Reading: React.FC> = ({ exam, showSolutions = false setShowPartDivider(false); setBgColor("bg-white"); setSeenParts((prev) => new Set(prev).add(partIndex)); - if (isFirstTimeRender) setIsFirstTimeRender(false); } useEffect(() => { @@ -171,7 +170,7 @@ const Reading: React.FC> = ({ exam, showSolutions = false
> = ({ exam, showSolutions = false isTextMinimized={isTextMinimized} setIsTextMinimized={setIsTextMinimzed} /> - {!isFirstTimeRender && !showPartDivider && !showSolutions && !isBetweenParts && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons)} + {!startNow && !showPartDivider && !showSolutions && !isBetweenParts && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons)} {showSolutions && renderSolution(currentExercise, progressButtons, progressButtons)}
{/*exerciseIndex > -1 && partIndex > -1 && exerciseIndex < exam.parts[partIndex].exercises.length && ( @@ -193,12 +192,12 @@ const Reading: React.FC> = ({ exam, showSolutions = false )*/}
- {((isFirstTimeRender || isBetweenParts) && !showPartDivider && !showSolutions) && + {((startNow || isBetweenParts) && !showPartDivider && !showSolutions) && isFirstTimeRender ? setIsFirstTimeRender(false) : nextExercise()} /> + handleNext={() => nextExercise()} /> } )} diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx index 52753dba..0182a4b9 100644 --- a/src/pages/(exam)/ExamPage.tsx +++ b/src/pages/(exam)/ExamPage.tsx @@ -193,9 +193,9 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar = useEffect(() => { if (flags.finalizeExam && moduleIndex !== -1) { - setModuleIndex(-1); + setModuleIndex(-1); } - }, [flags.finalizeExam, moduleIndex, setModuleIndex]); + }, [flags, moduleIndex, setModuleIndex]); useEffect(() => { if (flags.finalizeExam && !flags.pendingEvaluation && pendingExercises.length === 0) { @@ -215,11 +215,11 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar = await axios.get("/api/stats/update"); setShowSolutions(true); setFlags({ finalizeExam: false }); - dispatch({type: "UPDATE_EXAMS"}) + dispatch({ type: "UPDATE_EXAMS" }) })(); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [saveStats, setFlags, setModuleIndex, evaluated, pendingExercises, setUserSolutions]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [saveStats, setFlags, setModuleIndex, evaluated, pendingExercises, setUserSolutions, flags]); const aggregateScoresByModule = (isPractice?: boolean): { diff --git a/src/stores/exam/reducers/index.ts b/src/stores/exam/reducers/index.ts index ac368610..656d9c57 100644 --- a/src/stores/exam/reducers/index.ts +++ b/src/stores/exam/reducers/index.ts @@ -111,7 +111,17 @@ export const rootReducer = ( } else { // then check whether there are more modules in the exam, if there are // setup the next module - if (state.moduleIndex + 1 < state.selectedModules.length) { + if (state.moduleIndex === state.selectedModules.length - 1) { + return { + showSolutions: true, + flags: { + ...state.flags, + finalizeModule: false, + finalizeExam: true, + pendingEvaluation: hasUnevaluatedSolutions, + } + } + } else if (state.moduleIndex < state.selectedModules.length - 1) { return { moduleIndex: state.moduleIndex + 1, partIndex: 0, @@ -123,27 +133,14 @@ export const rootReducer = ( finalizeModule: false, } } - } else { - // if there are no modules left, flag finalizeExam - // so that the stats are uploaded in ExamPage - // and the Finish view is set there, no need to - // dispatch another init - return { - showSolutions: true, - flags: { - ...state.flags, - finalizeModule: false, - finalizeExam: true, - pendingEvaluation: hasUnevaluatedSolutions, - } - } } } } case 'FINALIZE_MODULE_SOLUTIONS': { if (state.flags.reviewAll) { - const notLastModule = state.moduleIndex < state.selectedModules.length; + const notLastModule = state.moduleIndex < state.selectedModules.length - 1; const moduleIndex = notLastModule ? state.moduleIndex + 1 : -1; + if (notLastModule) { return { questionIndex: 0, @@ -160,12 +157,12 @@ export const rootReducer = ( moduleIndex: -1 } } - } else { return { moduleIndex: -1 } } + } case 'UPDATE_EXAMS': { const exams = state.exams.map((e) => updateExamWithUserSolutions(e, state.userSolutions));