From 1315e0b280d14319db20e63b6b788220974fad9a Mon Sep 17 00:00:00 2001 From: Carlos Mesquita Date: Thu, 22 Aug 2024 22:02:37 +0100 Subject: [PATCH 1/4] Shuffles fixed --- src/components/Exercises/FillBlanks/index.tsx | 9 +- src/components/Exercises/MultipleChoice.tsx | 43 ++--- src/components/Solutions/FillBlanks.tsx | 1 - src/components/Solutions/MultipleChoice.tsx | 52 +++--- src/exams/Level/Shuffle.ts | 155 ++++++++++++++++++ src/exams/Level/index.tsx | 121 ++------------ src/interfaces/exam.ts | 7 +- src/interfaces/user.ts | 2 +- src/pages/(exam)/ExamPage.tsx | 68 ++++---- src/stores/examStore.ts | 12 +- 10 files changed, 265 insertions(+), 205 deletions(-) create mode 100644 src/exams/Level/Shuffle.ts diff --git a/src/components/Exercises/FillBlanks/index.tsx b/src/components/Exercises/FillBlanks/index.tsx index 96316496..9fec8f82 100644 --- a/src/components/Exercises/FillBlanks/index.tsx +++ b/src/components/Exercises/FillBlanks/index.tsx @@ -20,9 +20,10 @@ const FillBlanks: React.FC = ({ onNext, onBack, }) => { - const { shuffleMaps, exam, partIndex, questionIndex, exerciseIndex } = useExamStore((state) => state); + const { shuffles, exam, partIndex, questionIndex, exerciseIndex } = useExamStore((state) => state); const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions); const hasExamEnded = useExamStore((state) => state.hasExamEnded); + const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles; const [currentMCSelection, setCurrentMCSelection] = useState<{ id: string, selection: FillBlanksMCOption }>(); @@ -77,7 +78,7 @@ const FillBlanks: React.FC = ({ }; const renderLines = (line: string) => { return ( -
+
{reactStringReplace(line, /({{\d+}})/g, (match) => { const id = match.replaceAll(/[\{\}]/g, ""); const userSolution = answers.find((x) => x.id === id); @@ -127,10 +128,10 @@ const FillBlanks: React.FC = ({ const getShuffles = () => { let shuffle = {}; - if (shuffleMaps.length !== 0) { + if (shuffleMaps) { shuffle = { shuffleMaps: shuffleMaps.filter((map) => - answers.some(answer => answer.id === map.id) + answers.some(answer => answer.id === map.questionID) ) } } diff --git a/src/components/Exercises/MultipleChoice.tsx b/src/components/Exercises/MultipleChoice.tsx index e72896a0..e4e95d76 100644 --- a/src/components/Exercises/MultipleChoice.tsx +++ b/src/components/Exercises/MultipleChoice.tsx @@ -1,5 +1,5 @@ /* eslint-disable @next/next/no-img-element */ -import { MultipleChoiceExercise, MultipleChoiceQuestion } from "@/interfaces/exam"; +import { MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap } from "@/interfaces/exam"; import useExamStore from "@/stores/examStore"; import clsx from "clsx"; import { useEffect, useState } from "react"; @@ -77,13 +77,15 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti const { questionIndex, exam, - shuffleMaps, + shuffles, hasExamEnded, userSolutions: storeUserSolutions, setQuestionIndex, setUserSolutions } = useExamStore((state) => state); + const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles; + const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); useEffect(() => { @@ -104,6 +106,16 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), { option, question: question.id }]); }; + const getShuffledSolution = (originalSolution: string, questionShuffleMap: ShuffleMap) => { + for (const [newPosition, originalPosition] of Object.entries(questionShuffleMap.map)) { + if (originalPosition === originalSolution) { + return newPosition; + } + } + return originalSolution; + } + + const calculateScore = () => { const total = questions.length; const correct = answers.filter((x) => { @@ -112,34 +124,25 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti }); let isSolutionCorrect; - if (shuffleMaps.length == 0) { + if (!shuffleMaps) { isSolutionCorrect = matchingQuestion?.solution === x.option; } else { - const shuffleMap = shuffleMaps.find((map) => map.id == x.question) - isSolutionCorrect = shuffleMap?.map[x.option] == matchingQuestion?.solution; + const shuffleMap = shuffleMaps.find((map) => map.questionID == x.question); + if (shuffleMap) { + isSolutionCorrect = getShuffledSolution(x.option, shuffleMap) == matchingQuestion?.solution; + }else { + isSolutionCorrect = matchingQuestion?.solution === x.option; + } } return isSolutionCorrect || false; }).length; const missing = total - correct; - return { total, correct, missing }; }; - const getShuffles = () => { - let shuffle = {}; - if (shuffleMaps.length !== 0) { - shuffle = { - shuffleMaps: shuffleMaps.filter((map) => - answers.some(answer => answer.question === map.id) - ) - } - } - return shuffle; - } - const next = () => { if (questionIndex === questions.length - 1) { - onNext({ exercise: id, solutions: answers, score: calculateScore(), type, ...getShuffles() }); + onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps }); } else { setQuestionIndex(questionIndex + 1); } @@ -148,7 +151,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti const back = () => { if (questionIndex === 0) { - onBack({ exercise: id, solutions: answers, score: calculateScore(), type, ...getShuffles() }); + onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps }); } else { setQuestionIndex(questionIndex - 1); } diff --git a/src/components/Solutions/FillBlanks.tsx b/src/components/Solutions/FillBlanks.tsx index 4f2aea4b..32e9c15d 100644 --- a/src/components/Solutions/FillBlanks.tsx +++ b/src/components/Solutions/FillBlanks.tsx @@ -28,7 +28,6 @@ export default function FillBlanksSolutions({ const total = text.match(/({{\d+}})/g)?.length || 0; const correct = correctUserSolutions!.filter((x) => { const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution; - console.log(solution); if (!solution) return false; const option = words.find((w) => { diff --git a/src/components/Solutions/MultipleChoice.tsx b/src/components/Solutions/MultipleChoice.tsx index 320163e4..6f29e9ab 100644 --- a/src/components/Solutions/MultipleChoice.tsx +++ b/src/components/Solutions/MultipleChoice.tsx @@ -6,6 +6,7 @@ import { useEffect, useState } from "react"; import reactStringReplace from "react-string-replace"; import { CommonProps } from "."; import Button from "../Low/Button"; +import { v4 } from "uuid"; function Question({ id, @@ -17,34 +18,12 @@ function Question({ }: MultipleChoiceQuestion & { userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean }) { const { userSolutions } = useExamStore((state) => state); - const getShuffledOptions = (options: { id: string, text: string }[], questionShuffleMap: ShuffleMap) => { - const shuffledOptions = ['A', 'B', 'C', 'D'].map(newId => { - const originalId = questionShuffleMap.map[newId]; - const originalOption = options.find(option => option.id === originalId); - return { - id: newId, - text: originalOption!.text - }; - }); - return shuffledOptions; - } - - const getShuffledSolution = (originalSolution: string, questionShuffleMap: ShuffleMap) => { - for (const [newPosition, originalPosition] of Object.entries(questionShuffleMap.map)) { - if (originalPosition === originalSolution) { - return newPosition; - } - } - return originalSolution; - } - const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => { if (foundMap) return foundMap; - return userSolution.shuffleMaps?.find(map => map.id === id) || null; + return userSolution.shuffleMaps?.find(map => map.questionID === id) || null; }, null as ShuffleMap | null); - const questionOptions = questionShuffleMap ? getShuffledOptions(options as { id: string, text: string }[], questionShuffleMap) : options; - const newSolution = questionShuffleMap ? getShuffledSolution(solution, questionShuffleMap) : solution; + const newSolution = questionShuffleMap ? questionShuffleMap?.map[solution] : solution; const renderPrompt = (prompt: string) => { return reactStringReplace(prompt, /(.*?<\/u>)/g, (match) => { @@ -70,15 +49,15 @@ function Question({ {isNaN(Number(id)) ? ( {renderPrompt(prompt).filter((x) => x?.toString() !== "")} ) : ( - + <> - {id} - {renderPrompt(prompt).filter((x) => x?.toString() !== "")} + {id} - {renderPrompt(prompt).filter((x) => x?.toString() !== "")} )}
{variant === "image" && - questionOptions.map((option) => ( + options.map((option) => (
))} {variant === "text" && - questionOptions.map((option) => ( + options.map((option) => (
@@ -106,14 +85,23 @@ function Question({ export default function MultipleChoice({ id, type, prompt, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) { const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state); + const stats = useExamStore((state) => state.userSolutions); const calculateScore = () => { const total = questions.length; + const questionShuffleMap = stats.find((x) => x.exercise == id)?.shuffleMaps; const correct = userSolutions.filter( - (x) => questions.find((y) => y.id.toString() === x.question.toString())?.solution === x.option || false, + (x) => { + if (questionShuffleMap) { + const shuffleMap = questionShuffleMap.find((y) => y.questionID === x.question) + const originalSol = questions.find((y) => y.id.toString() === x.question.toString())?.solution!; + return x.option == shuffleMap?.map[originalSol] + } else { + return questions.find((y) => y.id.toString() === x.question.toString())?.solution === x.option || false + } + }, ).length; const missing = total - userSolutions.filter((x) => questions.find((y) => y.id.toString() === x.question.toString())).length; - return { total, correct, missing }; }; @@ -159,12 +147,12 @@ export default function MultipleChoice({ id, type, prompt, questions, userSoluti Wrong
-
+
diff --git a/src/components/Exercises/MultipleChoice.tsx b/src/components/Exercises/MultipleChoice.tsx index e4e95d76..1a5d294f 100644 --- a/src/components/Exercises/MultipleChoice.tsx +++ b/src/components/Exercises/MultipleChoice.tsx @@ -81,7 +81,8 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti hasExamEnded, userSolutions: storeUserSolutions, setQuestionIndex, - setUserSolutions + setUserSolutions, + setCurrentSolution } = useExamStore((state) => state); const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles; @@ -106,6 +107,11 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), { option, question: question.id }]); }; + useEffect(()=> { + setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [answers]) + const getShuffledSolution = (originalSolution: string, questionShuffleMap: ShuffleMap) => { for (const [newPosition, originalPosition] of Object.entries(questionShuffleMap.map)) { if (originalPosition === originalSolution) { diff --git a/src/components/Low/Button.tsx b/src/components/Low/Button.tsx index 99de3d4b..8f5b8f15 100644 --- a/src/components/Low/Button.tsx +++ b/src/components/Low/Button.tsx @@ -9,6 +9,7 @@ interface Props { className?: string; disabled?: boolean; isLoading?: boolean; + padding?: string; onClick?: () => void; type?: "button" | "reset" | "submit"; } @@ -21,6 +22,7 @@ export default function Button({ className, children, type, + padding = "py-4 px-6", onClick, }: Props) { const colorClassNames: {[key in typeof color]: {[key in typeof variant]: string}} = { @@ -61,7 +63,8 @@ export default function Button({ type={type} onClick={onClick} className={clsx( - "py-4 px-6 rounded-full transition ease-in-out duration-300 disabled:cursor-not-allowed cursor-pointer", + "rounded-full transition ease-in-out duration-300 disabled:cursor-not-allowed cursor-pointer", + padding, colorClassNames[color][variant], className, )} diff --git a/src/components/Medium/ModuleTitle.tsx b/src/components/Medium/ModuleTitle.tsx index c529a227..04dc84e9 100644 --- a/src/components/Medium/ModuleTitle.tsx +++ b/src/components/Medium/ModuleTitle.tsx @@ -1,13 +1,17 @@ -import {Module} from "@/interfaces"; -import useExamStore from "@/stores/examStore"; -import {moduleLabels} from "@/utils/moduleUtils"; +import { Module } from "@/interfaces"; +import { moduleLabels } from "@/utils/moduleUtils"; import clsx from "clsx"; -import {motion} from "framer-motion"; -import {ReactNode, useEffect, useState} from "react"; -import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch} from "react-icons/bs"; +import { Fragment, ReactNode, useCallback, useState } from "react"; +import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch } from "react-icons/bs"; import ProgressBar from "../Low/ProgressBar"; -import TimerEndedModal from "../TimerEndedModal"; import Timer from "./Timer"; +import { Exam, LevelExam, MultipleChoiceExercise, ShuffleMap, UserSolution } from "@/interfaces/exam"; +import { BsFillGrid3X3GapFill } from "react-icons/bs"; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import Button from "../Low/Button"; +import { Dialog, Transition } from "@headlessui/react"; +import useExamStore from "@/stores/examStore"; +import Modal from "../Modal"; interface Props { minTimer: number; @@ -18,13 +22,32 @@ interface Props { disableTimer?: boolean; partLabel?: string; showTimer?: boolean; + showSolutions?: boolean; + runOnClick?: ((questionIndex: number) => void) | undefined; } -export default function ModuleTitle({ - minTimer, module, label, exerciseIndex, totalExercises, disableTimer = false, partLabel, showTimer = true +export default function ModuleTitle({ + minTimer, + module, + label, + exerciseIndex, + totalExercises, + disableTimer = false, + partLabel, + showTimer = true, + showSolutions = false, + runOnClick = undefined }: Props) { + const { + userSolutions, + partIndex, + exam + } = useExamStore((state) => state); + const examExerciseIndex = useExamStore((state) => state.exerciseIndex) - const moduleIcon: {[key in Module]: ReactNode} = { + const [isOpen, setIsOpen] = useState(false); + + const moduleIcon: { [key in Module]: ReactNode } = { reading: , listening: , writing: , @@ -32,6 +55,78 @@ export default function ModuleTitle({ level: , }; + const isMultipleChoiceLevelExercise = () => { + if (exam?.module === 'level' && typeof partIndex === "number" && partIndex > -1) { + const currentExercise = (exam as LevelExam).parts[partIndex].exercises[examExerciseIndex]; + return currentExercise && currentExercise.type === 'multipleChoice'; + } + return false; + }; + + const renderMCQuestionGrid = () => { + if (!isMultipleChoiceLevelExercise() && !userSolutions) return null; + + const currentExercise = (exam as LevelExam).parts[partIndex!].exercises[examExerciseIndex] as MultipleChoiceExercise; + const userSolution = userSolutions!.find((x) => x.exercise == currentExercise.id)!; + const answeredQuestions = new Set(userSolution.solutions.map(sol => sol.question)); + const exerciseOffset = currentExercise.questions[0].id; + const lastExercise = exerciseOffset + (currentExercise.questions.length - 1); + + const getQuestionColor = (questionId: string, solution: string, userQuestionSolution: string | undefined) => { + const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => { + if (foundMap) return foundMap; + return userSolution.shuffleMaps?.find(map => map.questionID.toString() === questionId.toString()) || null; + }, null as ShuffleMap | null); + const newSolution = questionShuffleMap ? questionShuffleMap?.map[solution] : solution; + + if (!userSolutions) return ""; + + if (!userQuestionSolution) { + return "!bg-mti-gray-davy !border--mti-gray-davy !text-mti-gray-davy !text-white hover:!bg-gray-700"; + } + + return userQuestionSolution === newSolution ? + "!bg-mti-purple-light !text-mti-purple-light !text-white hover:!bg-mti-purple-dark" : + "!bg-mti-rose-light !border-mti-rose-light !text-mti-rose-light !text-white hover:!bg-mti-rose-dark"; + } + return ( + <> +

{`Part ${partIndex + 1} (Questions ${exerciseOffset} - ${lastExercise})`}

+
+ {currentExercise.questions.map((_, index) => { + const questionNumber = exerciseOffset + index; + const isAnswered = answeredQuestions.has(questionNumber); + const solution = currentExercise.questions.find((x) => x.id == questionNumber)!.solution; + + const userQuestionSolution = currentExercise.userSolutions?.find((x) => x.question == questionNumber)?.option; + return ( + + ); + })} +
+

+ Click a question number to jump to that question +

+ + ); + }; + return ( <> {showTimer && } @@ -67,8 +162,24 @@ export default function ModuleTitle({
+ {isMultipleChoiceLevelExercise() && ( + <> + + setIsOpen(false)} + className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white shadow-xl transition-all" + > + <> + {renderMCQuestionGrid()} + + + + )} ); -} +} \ No newline at end of file diff --git a/src/components/Medium/Timer.tsx b/src/components/Medium/Timer.tsx index 86f1b68b..58ce4751 100644 --- a/src/components/Medium/Timer.tsx +++ b/src/components/Medium/Timer.tsx @@ -51,7 +51,7 @@ const Timer: React.FC = ({minTimer, disableTimer, standalone = false}) => void; } -export default function QuestionsModal({ isOpen, onClose, blankQuestions = true, finishingWhat = "module" }: Props) { +export default function QuestionsModal({ isOpen, onClose, type = "module", unanswered = false }: Props) { return ( onClose(false)} className="relative z-50"> @@ -34,11 +34,11 @@ export default function QuestionsModal({ isOpen, onClose, blankQuestions = true, leaveTo="opacity-0 scale-95">
- {blankQuestions ? ( + {type === "module" && ( <> Questions Unanswered - Please note that you are finishing the current {finishingWhat} and once you proceed to the next {finishingWhat}, you will no longer be + Please note that you are finishing the current module and once you proceed to the next module, you will no longer be able to change the answers of the current one, including your unanswered questions.

Are you sure you want to continue without completing those questions? @@ -52,14 +52,16 @@ export default function QuestionsModal({ isOpen, onClose, blankQuestions = true,
- ): ( + )} + {type === "blankQuestions" && ( <> - Confirm Submission + Questions Unanswered - Please note that you are finishing the current {finishingWhat} and once you proceed to the next {finishingWhat}, you will no longer be - able to review the answers of the current one.
+ You have left some questions unanswered in the current part.

- Are you sure you want to continue? + If you wish to continue, you can still access this part later using the navigation bar at the top.
+
+ Do you want to proceed to the next part, or would you like to go back and complete the unanswered questions in the current part?
)} + {type === "submit" && ( + <> + Confirm Submission + + {unanswered ? ( + <> + By clicking "Submit," you are finalizing your exam with some questions left unanswered. Once you submit, you will not be able to review or change any of your answers, including the unanswered ones.
+
+ Are you sure you want to submit and complete the exam with unanswered questions? + + ) : ( + <> + By clicking "Submit," you are finalizing your exam. Once you submit, you will not be able to review or change any of your answers.
+
+ Are you sure you want to submit and complete the exam? + + )} +
+
+ + +
+ + )} diff --git a/src/components/Solutions/FillBlanks.tsx b/src/components/Solutions/FillBlanks.tsx index 32e9c15d..92dee6b5 100644 --- a/src/components/Solutions/FillBlanks.tsx +++ b/src/components/Solutions/FillBlanks.tsx @@ -1,4 +1,4 @@ -import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam"; +import { FillBlanksExercise, FillBlanksMCOption, ShuffleMap } from "@/interfaces/exam"; import clsx from "clsx"; import reactStringReplace from "react-string-replace"; import { CommonProps } from "."; @@ -16,13 +16,13 @@ export default function FillBlanksSolutions({ onNext, onBack, }: FillBlanksExercise & CommonProps) { - - // next and back was all messed up and still don't know why, anyways const storeUserSolutions = useExamStore((state) => state.userSolutions); const correctUserSolutions = storeUserSolutions.find( (solution) => solution.exercise === id )?.solutions; + + const shuffles = useExamStore((state) => state.shuffles); const calculateScore = () => { const total = text.match(/({{\d+}})/g)?.length || 0; @@ -65,16 +65,18 @@ export default function FillBlanksSolutions({ return ( {reactStringReplace(line, /({{\d+}})/g, (match) => { - const id = match.replaceAll(/[\{\}]/g, ""); - const userSolution = correctUserSolutions!.find((x) => x.id.toString() === id.toString()); - const answerSolution = solutions.find(sol => sol.id.toString() === id.toString())!.solution; + const questionId = match.replaceAll(/[\{\}]/g, ""); + const userSolution = correctUserSolutions!.find((x) => x.id.toString() === questionId.toString()); + const answerSolution = solutions.find(sol => sol.id.toString() === questionId.toString())!.solution; + const questionShuffleMap = shuffles.find((x) => x.exerciseID == id)?.shuffles.find((y) => y.questionID == questionId); + const newAnswerSolution = questionShuffleMap ? questionShuffleMap.map[answerSolution].toLowerCase() : answerSolution.toLowerCase(); if (!userSolution) { let answerText; if (typeCheckWordsMC(words)) { - const options = words.find((x) => x.id.toString() === id.toString()); + const options = words.find((x) => x.id.toString() === questionId.toString()); const correctKey = Object.keys(options!.options).find(key => - key.toLowerCase() === answerSolution.toLowerCase() + key.toLowerCase() === newAnswerSolution ); answerText = options!.options[correctKey as keyof typeof options]; } else { @@ -97,7 +99,7 @@ export default function FillBlanksSolutions({ : 'letter' in w ? w.letter.toLowerCase() === userSolution.solution.toLowerCase() : 'options' in w - ? w.id === userSolution.id + ? w.id === userSolution.questionId : false ); @@ -113,10 +115,10 @@ export default function FillBlanksSolutions({ let correct; let solutionText; if (typeCheckWordsMC(words)) { - const options = words.find((x) => x.id.toString() === id.toString()); + const options = words.find((x) => x.id.toString() === questionId.toString()); if (options) { const correctKey = Object.keys(options.options).find(key => - key.toLowerCase() === answerSolution.toLowerCase() + key.toLowerCase() === newAnswerSolution ); correct = userSolution.solution == options.options[correctKey as keyof typeof options.options]; solutionText = options.options[correctKey as keyof typeof options.options] || answerSolution; diff --git a/src/exams/Level/index.tsx b/src/exams/Level/index.tsx index a913b2d9..bba8695f 100644 --- a/src/exams/Level/index.tsx +++ b/src/exams/Level/index.tsx @@ -13,6 +13,7 @@ import TextComponent from "./TextComponent"; import PartDivider from "./PartDivider"; import Timer from "@/components/Medium/Timer"; import shuffleExamExercise from "./Shuffle"; +import { Tab } from "@headlessui/react"; interface Props { exam: LevelExam; @@ -33,6 +34,17 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = const levelBgColor = "bg-ielts-level-light"; const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]); const [showQuestionsModal, setShowQuestionsModal] = useState(false); + const [continueAnyways, setContinueAnyways] = useState(false); + + const [seenParts, setSeenParts] = useState(showSolutions ? exam.parts.map((_, index) => index) : [0]); + const [lastSolution, setLastSolution] = useState(undefined); + + const [questionModalKwargs, setQuestionModalKwargs] = useState<{ + type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined; + }>({ + type: "blankQuestions", + onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } } + }); const { setBgColor } = useExamStore((state) => state); const { userSolutions, setUserSolutions } = useExamStore((state) => state); @@ -48,7 +60,28 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = const [contextWord, setContextWord] = useState(undefined); const [contextWordLine, setContextWordLine] = useState(undefined); + const [currentSolution, setCurrentSolution] = useExamStore((state) => [state.currentSolution, state.setCurrentSolution]) + const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined) + + useEffect(() => { + if (typeof currentSolution !== "undefined") { + setUserSolutions([...userSolutions.filter((x) => x.exercise !== currentSolution.exercise), { ...currentSolution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]); + setCurrentSolution(undefined); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentSolution]) + + useEffect(()=> { + if (showSolutions) { + const solutionShuffles = userSolutions.map(solution => ({ + exerciseID: solution.exercise, + shuffles: solution.shuffleMaps || [] + })); + setShuffles(solutionShuffles); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + },[]); useEffect(() => { if (hasExamEnded && exerciseIndex === -1) { @@ -56,27 +89,21 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = } }, [hasExamEnded, exerciseIndex, setExerciseIndex]); - const getExercise = () => { + const getExercise = () => {; let exercise = exam.parts[partIndex]?.exercises[exerciseIndex]; if (!exercise) return undefined; - exercise = { ...exercise, userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [], }; - if (showSolutions) { - setShuffles([...shuffles.filter((x) => x.exerciseID !== exercise.id), { exerciseID: exercise.id, shuffles: userSolutions.find((x) => x.exercise === exercise.id)!.shuffleMaps!}]); - } else { - exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles); - } + + exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles); + return exercise; }; useEffect(() => { - - if (exerciseIndex !== -1) { - setCurrentExercise(getExercise()); - } + setCurrentExercise(getExercise()); // eslint-disable-next-line react-hooks/exhaustive-deps }, [partIndex, exerciseIndex, exam.parts[partIndex].context]); @@ -108,24 +135,31 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = const nextExercise = (solution?: UserSolution) => { scrollToTop(); - if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]); - } - - /*if (storeQuestionIndex > 0 || currentExercise?.type == "fillBlanks") { - setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex }]); - }*/ if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) { setExerciseIndex(exerciseIndex + 1); return; } - if (partIndex + 1 < exam.parts.length && !hasExamEnded && (showQuestionsModal || showSolutions)) { + if (partIndex + 1 === exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions && !continueAnyways) { + setLastSolution(solution); + modalKwargs(); + setShowQuestionsModal(true); + return; + } + + if (partIndex + 1 < exam.parts.length && !hasExamEnded) { + if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions) { + modalKwargs(); + setShowQuestionsModal(true); + return; + } + if (!showSolutions && exam.parts[0].intro) { setShowPartDivider(true); setBgColor(levelBgColor); } + setSeenParts((prev) => [...prev, partIndex + 1]) setPartIndex(partIndex + 1); setExerciseIndex(!!exam.parts[partIndex + 1].context ? -1 : 0); setStoreQuestionIndex(0); @@ -133,28 +167,9 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = return; } - if (partIndex + 1 < exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions) { - setShowQuestionsModal(true); - return; - } - - if ( - solution && - ![...userSolutions.filter((x) => x.exercise !== solution?.exercise).map((x) => x.score.missing), solution?.score.missing].every( - (x) => x === 0, - ) && - !showSolutions && - !editing && - !hasExamEnded - ) { - setShowQuestionsModal(true); - return; - } - setHasExamEnded(false); - - if (solution) { - onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]); + if (typeof showSolutionsSave !== "undefined") { + onFinish(showSolutionsSave); } else { onFinish(userSolutions); } @@ -200,19 +215,28 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = }, [exerciseIndex]) const calculateExerciseIndex = () => { - if (partIndex === 0) { + if (exam.parts[0].intro) { + return exam.parts.reduce((acc, curr, index) => { + if (index < partIndex) { + return acc + countExercises(curr.exercises) + } + return acc; + }, 0) + (storeQuestionIndex + 1); + } else { + if (partIndex === 0) { + return ( + (exerciseIndex === -1 ? 0 : exerciseIndex + 1) + storeQuestionIndex //+ multipleChoicesDone.reduce((acc, curr) => acc + curr.amount, 0) + ); + } + const exercisesPerPart = exam.parts.map((x) => x.exercises.length); + const exercisesDone = exercisesPerPart.filter((_, index) => index < partIndex).reduce((acc, curr) => curr + acc, 0); return ( - (exerciseIndex === -1 ? 0 : exerciseIndex + 1) + storeQuestionIndex //+ multipleChoicesDone.reduce((acc, curr) => acc + curr.amount, 0) + exercisesDone + + (exerciseIndex === -1 ? 0 : exerciseIndex + 1) + + storeQuestionIndex + + multipleChoicesDone.reduce((acc, curr) => { return acc + curr.amount }, 0) ); } - const exercisesPerPart = exam.parts.map((x) => x.exercises.length); - const exercisesDone = exercisesPerPart.filter((_, index) => index < partIndex).reduce((acc, curr) => curr + acc, 0); - return ( - exercisesDone + - (exerciseIndex === -1 ? 0 : exerciseIndex + 1) + - storeQuestionIndex - + multipleChoicesDone.reduce((acc, curr) => { return acc + curr.amount }, 0) - ); }; const renderText = () => ( @@ -247,8 +271,8 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = } } - const modalKwargs = () => { - const allSolutionsCorrectLength = exam.parts[partIndex].exercises.every((exercise) => { + const answeredEveryQuestion = (partIndex: number) => { + return exam.parts[partIndex].exercises.every((exercise) => { const userSolution = userSolutions.find(x => x.exercise === exercise.id); if (exercise.type === "multipleChoice") { return userSolution?.solutions.length === exercise.questions.length; @@ -258,27 +282,81 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = } return false; }); - - return { - blankQuestions: !allSolutionsCorrectLength, - finishingWhat: "part", - onClose: partIndex !== exam.parts.length - 1 ? ( - function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } } - ) : function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); onFinish(userSolutions); } else { setShowQuestionsModal(false) } } - } - } + useEffect(() => { + if (continueAnyways) { + nextExercise(lastSolution); + setContinueAnyways(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [continueAnyways]); + + const modalKwargs = () => { + const kwargs: { type: "module" | "blankQuestions" | "submit", unanswered: boolean, onClose: (next?: boolean) => void; } = { + type: "blankQuestions", + unanswered: false, + onClose: function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } } + }; + + if (partIndex === exam.parts.length - 1) { + kwargs.type = "submit" + kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestion(partIndex)); + kwargs.onClose = function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } }; + } + setQuestionModalKwargs(kwargs); + } + + const mcNavKwargs = { + userSolutions: userSolutions, + exam: exam, + partIndex: partIndex, + showSolutions: showSolutions, + "setExerciseIndex": setExerciseIndex, + "setPartIndex": setPartIndex, + "runOnClick": setStoreQuestionIndex + } + + return ( <>
- + { !(partIndex === 0 && storeQuestionIndex === 0 && showPartDivider) && } {exam.parts[0].intro && showPartDivider ? { setShowPartDivider(false); setBgColor("bg-white") }} /> : ( <> + {exam.parts[0].intro && ( +
+ + + {exam.parts.map((_, index) => + { + if (!seenParts.includes(index)) { + e.preventDefault(); + } else { + setExerciseIndex(0); + setStoreQuestionIndex(0); + } + }} + className={({ selected }) => + clsx( + "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-level/80", + "ring-white ring-opacity-60 focus:outline-none", + "transition duration-300 ease-in-out", + selected && "bg-white shadow", + seenParts.includes(index) ? "hover:bg-white/70" : "cursor-not-allowed" + ) + } + >{`Part ${index + 1}`} + ) + } + + +
+ )} x.exercises))} disableTimer={showSolutions || editing} showTimer={false} + {...mcNavKwargs} />
void; setShuffles: (shuffles: Shuffles[]) => void; setBgColor: (bgColor: string) => void; + setCurrentSolution: (currentSolution: UserSolution | undefined) => void; reset: () => void; } @@ -58,7 +60,8 @@ export const initialState: ExamState = { questionIndex: 0, inactivity: 0, shuffles: [], - bgColor: "bg-white" + bgColor: "bg-white", + currentSolution: undefined }; const useExamStore = create((set) => ({ @@ -80,6 +83,7 @@ const useExamStore = create((set) => ({ setInactivity: (inactivity: number) => set(() => ({inactivity})), setShuffles: (shuffles: Shuffles[]) => set(() => ({shuffles})), setBgColor: (bgColor) => set(() => ({bgColor})), + setCurrentSolution: (currentSolution: UserSolution | undefined) => set(() => ({currentSolution})), reset: () => set(() => initialState), })); From 2146ef1a923490272ff77f8e7213512a217a900e Mon Sep 17 00:00:00 2001 From: Carlos Mesquita Date: Sat, 24 Aug 2024 00:54:55 +0100 Subject: [PATCH 4/4] Found the bug --- src/components/Exercises/FillBlanks/index.tsx | 71 +++--- src/components/Exercises/MultipleChoice.tsx | 16 +- src/components/Low/Button.tsx | 2 +- src/components/QuestionsModal.tsx | 41 ++-- src/exams/Level/TextComponent.tsx | 15 +- src/exams/Level/index.tsx | 223 +++++++++++------- 6 files changed, 221 insertions(+), 147 deletions(-) diff --git a/src/components/Exercises/FillBlanks/index.tsx b/src/components/Exercises/FillBlanks/index.tsx index 27717d8b..909860c9 100644 --- a/src/components/Exercises/FillBlanks/index.tsx +++ b/src/components/Exercises/FillBlanks/index.tsx @@ -44,39 +44,39 @@ const FillBlanks: React.FC = ({ }, [hasExamEnded]); - let correctWords: any; + let correctWords: any; if (exam && exam.module === "level" && exam.parts[partIndex].exercises[exerciseIndex].type === "fillBlanks") { correctWords = (exam.parts[partIndex].exercises[exerciseIndex] as FillBlanksExercise).words; - } + } - const calculateScore = () => { - const total = text.match(/({{\d+}})/g)?.length || 0; - const correct = answers!.filter((x) => { - const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution; - if (!solution) return false; - const option = correctWords!.find((w: any) => { - if (typeof w === "string") { - return w.toLowerCase() === x.solution.toLowerCase(); - } else if ('letter' in w) { - return w.word.toLowerCase() === x.solution.toLowerCase(); - } else { - return w.id.toString() === x.id.toString(); - } - }); - if (!option) return false; + const calculateScore = () => { + const total = text.match(/({{\d+}})/g)?.length || 0; + const correct = answers!.filter((x) => { + const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution; + if (!solution) return false; + const option = correctWords!.find((w: any) => { + if (typeof w === "string") { + return w.toLowerCase() === x.solution.toLowerCase(); + } else if ('letter' in w) { + return w.word.toLowerCase() === x.solution.toLowerCase(); + } else { + return w.id.toString() === x.id.toString(); + } + }); + if (!option) return false; - if (typeof option === "string") { - return solution.toLowerCase() === option.toLowerCase(); - } else if ('letter' in option) { - return solution.toLowerCase() === option.word.toLowerCase(); - } else if ('options' in option) { - return option.options[solution as keyof typeof option.options] == x.solution; - } - return false; - }).length; - const missing = total - answers!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length; + if (typeof option === "string") { + return solution.toLowerCase() === option.toLowerCase(); + } else if ('letter' in option) { + return solution.toLowerCase() === option.word.toLowerCase(); + } else if ('options' in option) { + return option.options[solution as keyof typeof option.options] == x.solution; + } + return false; + }).length; + const missing = total - answers!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length; return { total, correct, missing }; - }; + }; const renderLines = (line: string) => { return (
@@ -125,9 +125,13 @@ const FillBlanks: React.FC = ({ const onSelection = (questionID: string, value: string) => { setAnswers((prev) => [...prev.filter((x) => x.id !== questionID), { id: questionID, solution: value }]); - setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps }); } + useEffect(() => { + setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [answers]) + return ( <>
@@ -214,15 +218,18 @@ const FillBlanks: React.FC = ({ onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })} className="max-w-[200px] w-full" disabled={ - exam && typeof partIndex !== "undefined" && exam.module === "level" && - typeof exam.parts[0].intro === "string" && questionIndex === 0} + exam && exam.module === "level" && + typeof exam.parts[0].intro === "string" && + partIndex === 0 && + questionIndex === 0 + } > Back diff --git a/src/components/Exercises/MultipleChoice.tsx b/src/components/Exercises/MultipleChoice.tsx index 1a5d294f..e158643b 100644 --- a/src/components/Exercises/MultipleChoice.tsx +++ b/src/components/Exercises/MultipleChoice.tsx @@ -24,7 +24,7 @@ function Question({ const renderPrompt = (prompt: string) => { return reactStringReplace(prompt, /(.*?<\/u>)/g, (match) => { const word = match.replaceAll("", "").replaceAll("", ""); - return word.length > 0 ? {word} : null; + return word.length > 0 ? {word} : null; }); }; @@ -49,7 +49,7 @@ function Question({ "flex flex-col items-center border border-mti-gray-platinum p-4 px-8 rounded-xl gap-4 cursor-pointer bg-white relative select-none", userSolution === option.id.toString() && "border-mti-purple-light", )}> - {option.id.toString()} + {option.id.toString()} {`Option
))} @@ -79,6 +79,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti exam, shuffles, hasExamEnded, + partIndex, userSolutions: storeUserSolutions, setQuestionIndex, setUserSolutions, @@ -107,7 +108,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), { option, question: question.id }]); }; - useEffect(()=> { + useEffect(() => { setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [answers]) @@ -136,7 +137,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti const shuffleMap = shuffleMaps.find((map) => map.questionID == x.question); if (shuffleMap) { isSolutionCorrect = getShuffledSolution(x.option, shuffleMap) == matchingQuestion?.solution; - }else { + } else { isSolutionCorrect = matchingQuestion?.solution === x.option; } } @@ -159,6 +160,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti if (questionIndex === 0) { onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps }); } else { + if (exam?.module === "level" && typeof exam.parts[0].intro !== "undefined" && questionIndex === 0) return; setQuestionIndex(questionIndex - 1); } @@ -181,7 +183,11 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
diff --git a/src/components/Low/Button.tsx b/src/components/Low/Button.tsx index 8f5b8f15..32b1d708 100644 --- a/src/components/Low/Button.tsx +++ b/src/components/Low/Button.tsx @@ -63,7 +63,7 @@ export default function Button({ type={type} onClick={onClick} className={clsx( - "rounded-full transition ease-in-out duration-300 disabled:cursor-not-allowed cursor-pointer", + "rounded-full transition ease-in-out duration-300 disabled:cursor-not-allowed cursor-pointer select-none", padding, colorClassNames[color][variant], className, diff --git a/src/components/QuestionsModal.tsx b/src/components/QuestionsModal.tsx index 9b8ca06b..c1e14ecf 100644 --- a/src/components/QuestionsModal.tsx +++ b/src/components/QuestionsModal.tsx @@ -1,5 +1,5 @@ import { Dialog, Transition } from "@headlessui/react"; -import { Fragment } from "react"; +import { Fragment, useEffect, useState } from "react"; import Button from "./Low/Button"; interface Props { @@ -10,6 +10,19 @@ interface Props { } export default function QuestionsModal({ isOpen, onClose, type = "module", unanswered = false }: Props) { + const [isClosing, setIsClosing] = useState(false); + + const blockMultipleClicksClose = (x: boolean) => { + if (!isClosing) { + setIsClosing(true); + onClose(x); + } + + setTimeout(() => { + setIsClosing(false); + }, 400); + } + return ( onClose(false)} className="relative z-50"> @@ -44,10 +57,10 @@ export default function QuestionsModal({ isOpen, onClose, type = "module", unans Are you sure you want to continue without completing those questions?
- -
@@ -56,18 +69,16 @@ export default function QuestionsModal({ isOpen, onClose, type = "module", unans {type === "blankQuestions" && ( <> Questions Unanswered - - You have left some questions unanswered in the current part.
-
- If you wish to continue, you can still access this part later using the navigation bar at the top.
-
- Do you want to proceed to the next part, or would you like to go back and complete the unanswered questions in the current part? -
-
- -
@@ -92,10 +103,10 @@ export default function QuestionsModal({ isOpen, onClose, type = "module", unans )}
- -
diff --git a/src/exams/Level/TextComponent.tsx b/src/exams/Level/TextComponent.tsx index 367427c8..fc3fa216 100644 --- a/src/exams/Level/TextComponent.tsx +++ b/src/exams/Level/TextComponent.tsx @@ -7,7 +7,7 @@ interface Props { setContextWordLine: React.Dispatch> } -const TextComponent: React.FC = ({part, contextWord, setContextWordLine}) => { +const TextComponent: React.FC = ({ part, contextWord, setContextWordLine }) => { const textRef = useRef(null); const calculateLineNumbers = () => { @@ -130,14 +130,11 @@ const TextComponent: React.FC = ({part, contextWord, setContextWordLine}) }*/ return ( -
-
-
-
- {part.context!.split('\n\n').map((line, index) => { - return

{index + 1}{line}

- })} -
+
+
+ {part.context!.split('\n\n').map((line, index) => { + return

{index + 1}{line}

+ })}
); diff --git a/src/exams/Level/index.tsx b/src/exams/Level/index.tsx index bba8695f..b88aa109 100644 --- a/src/exams/Level/index.tsx +++ b/src/exams/Level/index.tsx @@ -32,12 +32,32 @@ const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => { export default function Level({ exam, showSolutions = false, onFinish, editing = false }: Props) { const levelBgColor = "bg-ielts-level-light"; + + const { + userSolutions, + hasExamEnded, + partIndex, + exerciseIndex, + questionIndex, + shuffles, + currentSolution, + setBgColor, + setUserSolutions, + setHasExamEnded, + setPartIndex, + setExerciseIndex, + setQuestionIndex, + setShuffles, + setCurrentSolution + } = useExamStore((state) => state); + + const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]); const [showQuestionsModal, setShowQuestionsModal] = useState(false); const [continueAnyways, setContinueAnyways] = useState(false); + const [textRender, setTextRender] = useState(false); const [seenParts, setSeenParts] = useState(showSolutions ? exam.parts.map((_, index) => index) : [0]); - const [lastSolution, setLastSolution] = useState(undefined); const [questionModalKwargs, setQuestionModalKwargs] = useState<{ type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined; @@ -46,72 +66,70 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } } }); - const { setBgColor } = useExamStore((state) => state); - const { userSolutions, setUserSolutions } = useExamStore((state) => state); - const { hasExamEnded, setHasExamEnded } = useExamStore((state) => state); - const { partIndex, setPartIndex } = useExamStore((state) => state); - const { exerciseIndex, setExerciseIndex } = useExamStore((state) => state); - const [storeQuestionIndex, setStoreQuestionIndex] = useExamStore((state) => [state.questionIndex, state.setQuestionIndex]); - const [shuffles, setShuffles] = useExamStore((state) => [state.shuffles, state.setShuffles]) - const [currentExercise, setCurrentExercise] = useState(); + + + const [currentExercise, setCurrentExercise] = useState(exam.parts[0].exercises[0]); const [showPartDivider, setShowPartDivider] = useState(typeof exam.parts[0].intro === "string" && !showSolutions); const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); const [contextWord, setContextWord] = useState(undefined); const [contextWordLine, setContextWordLine] = useState(undefined); - const [currentSolution, setCurrentSolution] = useExamStore((state) => [state.currentSolution, state.setCurrentSolution]) const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined) useEffect(() => { if (typeof currentSolution !== "undefined") { setUserSolutions([...userSolutions.filter((x) => x.exercise !== currentSolution.exercise), { ...currentSolution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentSolution, exam.id, exam.shuffle, shuffles, currentExercise]) + + useEffect(() => { + if (typeof currentSolution !== "undefined") { setCurrentSolution(undefined); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentSolution]) + }, [currentSolution]); - useEffect(()=> { + useEffect(() => { if (showSolutions) { const solutionShuffles = userSolutions.map(solution => ({ exerciseID: solution.exercise, shuffles: solution.shuffleMaps || [] - })); + })); setShuffles(solutionShuffles); } // eslint-disable-next-line react-hooks/exhaustive-deps - },[]); + }, []); - useEffect(() => { - if (hasExamEnded && exerciseIndex === -1) { - setExerciseIndex(exerciseIndex + 1); - } - }, [hasExamEnded, exerciseIndex, setExerciseIndex]); - - const getExercise = () => {; + const getExercise = () => { let exercise = exam.parts[partIndex]?.exercises[exerciseIndex]; - if (!exercise) return undefined; exercise = { ...exercise, userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [], }; - exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles); - + return exercise; }; useEffect(() => { setCurrentExercise(getExercise()); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [partIndex, exerciseIndex, exam.parts[partIndex].context]); + }, [partIndex, exerciseIndex, questionIndex]); useEffect(() => { const regex = /.*?['"](.*?)['"] in line (\d+)\?$/; - if (exerciseIndex !== -1 && currentExercise && currentExercise.type === "multipleChoice" && currentExercise.questions[storeQuestionIndex].prompt) { - const match = currentExercise.questions[storeQuestionIndex].prompt.match(regex); + if ( + exerciseIndex !== -1 && currentExercise && + currentExercise.type === "multipleChoice" && + currentExercise.questions[questionIndex] && + currentExercise.questions[questionIndex].prompt && + exam.parts[partIndex].context + ) { + const match = currentExercise.questions[questionIndex].prompt.match(regex); if (match) { const word = match[1]; const originalLineNumber = match[2]; @@ -120,18 +138,18 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = setContextWord(word); } - const updatedPrompt = currentExercise.questions[storeQuestionIndex].prompt.replace( + const updatedPrompt = currentExercise.questions[questionIndex].prompt.replace( `in line ${originalLineNumber}`, `in line ${contextWordLine || originalLineNumber}` ); - currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt; + currentExercise.questions[questionIndex].prompt = updatedPrompt; } else { setContextWord(undefined); } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentExercise, storeQuestionIndex]); + }, [currentExercise, questionIndex]); const nextExercise = (solution?: UserSolution) => { scrollToTop(); @@ -142,28 +160,30 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = } if (partIndex + 1 === exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions && !continueAnyways) { - setLastSolution(solution); modalKwargs(); setShowQuestionsModal(true); return; } if (partIndex + 1 < exam.parts.length && !hasExamEnded) { - if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions) { + if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.includes(partIndex + 1)) { modalKwargs(); setShowQuestionsModal(true); return; } - if (!showSolutions && exam.parts[0].intro) { + if (!showSolutions && exam.parts[0].intro && !seenParts.includes(partIndex + 1)) { setShowPartDivider(true); setBgColor(levelBgColor); } setSeenParts((prev) => [...prev, partIndex + 1]) + if (partIndex < exam.parts.length - 1 && exam.parts[partIndex + 1].context) { + setTextRender(true); + } setPartIndex(partIndex + 1); - setExerciseIndex(!!exam.parts[partIndex + 1].context ? -1 : 0); - setStoreQuestionIndex(0); - setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex }]); + setExerciseIndex(0); + setQuestionIndex(0); + setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : questionIndex }]); return; } @@ -177,8 +197,24 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = const previousExercise = (solution?: UserSolution) => { scrollToTop(); - if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]); + + if (exam.parts[partIndex].context && questionIndex === 0 && !textRender) { + setTextRender(true); + return; + } + + if (questionIndex == 0) { + setPartIndex(partIndex - 1); + const lastExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1; + const lastExercise = exam.parts[partIndex - 1].exercises[lastExerciseIndex]; + setExerciseIndex(lastExerciseIndex); + + if (lastExercise.type === "multipleChoice") { + setQuestionIndex(lastExercise.questions.length - 1) + } else { + setQuestionIndex(0) + } + return; } setExerciseIndex(exerciseIndex - 1); @@ -187,7 +223,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = const lastPartExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1; const previousExercise = exam.parts[partIndex - 1].exercises[lastPartExerciseIndex]; if (previousExercise.type === "multipleChoice") { - setStoreQuestionIndex(previousExercise.questions.length - 1) + setQuestionIndex(previousExercise.questions.length - 1) } const multipleChoiceQuestionsDone = []; for (let i = 0; i < exam.parts.length; i++) { @@ -207,13 +243,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = }; - useEffect(() => { - if (exerciseIndex === -1) { - nextExercise() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [exerciseIndex]) - const calculateExerciseIndex = () => { if (exam.parts[0].intro) { return exam.parts.reduce((acc, curr, index) => { @@ -221,11 +250,11 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = return acc + countExercises(curr.exercises) } return acc; - }, 0) + (storeQuestionIndex + 1); + }, 0) + (questionIndex + 1); } else { if (partIndex === 0) { return ( - (exerciseIndex === -1 ? 0 : exerciseIndex + 1) + storeQuestionIndex //+ multipleChoicesDone.reduce((acc, curr) => acc + curr.amount, 0) + (exerciseIndex === -1 ? 0 : exerciseIndex + 1) + questionIndex //+ multipleChoicesDone.reduce((acc, curr) => acc + curr.amount, 0) ); } const exercisesPerPart = exam.parts.map((x) => x.exercises.length); @@ -233,28 +262,56 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = return ( exercisesDone + (exerciseIndex === -1 ? 0 : exerciseIndex + 1) + - storeQuestionIndex + questionIndex + multipleChoicesDone.reduce((acc, curr) => { return acc + curr.amount }, 0) ); } }; const renderText = () => ( -
- <> -
-

- Please read the following excerpt attentively, you will then be asked questions about the text you've read. -

- You will be allowed to read the text while doing the exercises + <> +
+ <> +
+ {textRender ? ( + <> +

+ Please read the following excerpt attentively, you will then be asked questions about the text you've read. +

+ You will be allowed to read the text while doing the exercises + + ) : ( +

+ Answer the questions on the right based on what you've read. +

+ )} +
+ {exam.parts[partIndex].context && + } +
+ +
+ {textRender && ( +
+ + +
- - -
+ )} + ); const partLabel = () => { @@ -286,8 +343,8 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = useEffect(() => { if (continueAnyways) { - nextExercise(lastSolution); setContinueAnyways(false); + nextExercise(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [continueAnyways]); @@ -314,7 +371,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = showSolutions: showSolutions, "setExerciseIndex": setExerciseIndex, "setPartIndex": setPartIndex, - "runOnClick": setStoreQuestionIndex + "runOnClick": setQuestionIndex } @@ -323,7 +380,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
{ - !(partIndex === 0 && storeQuestionIndex === 0 && showPartDivider) && + !(partIndex === 0 && questionIndex === 0 && showPartDivider) && } {exam.parts[0].intro && showPartDivider ? { setShowPartDivider(false); setBgColor("bg-white") }} /> : ( @@ -338,7 +395,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = e.preventDefault(); } else { setExerciseIndex(0); - setStoreQuestionIndex(0); + setQuestionIndex(0); } }} className={({ selected }) => @@ -370,24 +427,20 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
-1 && exerciseIndex > -1 && !!exam.parts[partIndex].context && "grid grid-cols-2 gap-4", + !!exam.parts[partIndex].context && !textRender && "grid grid-cols-2 gap-4", )}> - {partIndex > -1 && !!exam.parts[partIndex].context && renderText()} - {exerciseIndex > -1 && - partIndex > -1 && - exerciseIndex < exam.parts[partIndex].exercises.length && - !showSolutions && - !editing && - currentExercise && - renderExercise(currentExercise, exam.id, nextExercise, previousExercise)} - - {exerciseIndex > -1 && - partIndex > -1 && - exerciseIndex < exam.parts[partIndex].exercises.length && - (showSolutions || editing) && - currentExercise && - renderSolution(currentExercise, nextExercise, previousExercise)} + {textRender ? + renderText() : + <> + {exam.parts[partIndex].context && renderText()} + {(showSolutions || editing) ? + renderSolution(currentExercise, nextExercise, previousExercise) + : + renderExercise(currentExercise, exam.id, nextExercise, previousExercise) + } + + }
{/*exerciseIndex === -1 && partIndex > 0 && (
@@ -400,8 +453,8 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = }} className="max-w-[200px] w-full" disabled={ - exam && typeof partIndex !== "undefined" && exam.module === "level" && - typeof exam.parts[0].intro === "string" && storeQuestionIndex === 0} + exam && typeof partIndex !== "undefined" && exam.module === "level" && + typeof exam.parts[0].intro === "string" && questionIndex === 0} > Back