From 2146ef1a923490272ff77f8e7213512a217a900e Mon Sep 17 00:00:00 2001 From: Carlos Mesquita Date: Sat, 24 Aug 2024 00:54:55 +0100 Subject: [PATCH] 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