Found the bug

This commit is contained in:
Carlos Mesquita
2024-08-24 00:54:55 +01:00
parent f0f38b335f
commit 2146ef1a92
6 changed files with 221 additions and 147 deletions

View File

@@ -125,9 +125,13 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
const onSelection = (questionID: string, value: string) => { const onSelection = (questionID: string, value: string) => {
setAnswers((prev) => [...prev.filter((x) => x.id !== questionID), { id: questionID, solution: value }]); 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 ( return (
<> <>
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20"> <div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
@@ -214,15 +218,18 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })} onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })}
className="max-w-[200px] w-full" className="max-w-[200px] w-full"
disabled={ disabled={
exam && typeof partIndex !== "undefined" && exam.module === "level" && exam && exam.module === "level" &&
typeof exam.parts[0].intro === "string" && questionIndex === 0} typeof exam.parts[0].intro === "string" &&
partIndex === 0 &&
questionIndex === 0
}
> >
Back Back
</Button> </Button>
<Button <Button
color="purple" color="purple"
onClick={() => {onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })}} onClick={() => { onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps }) }}
className="max-w-[200px] self-end w-full"> className="max-w-[200px] self-end w-full">
Next Next
</Button> </Button>

View File

@@ -24,7 +24,7 @@ function Question({
const renderPrompt = (prompt: string) => { const renderPrompt = (prompt: string) => {
return reactStringReplace(prompt, /(<u>.*?<\/u>)/g, (match) => { return reactStringReplace(prompt, /(<u>.*?<\/u>)/g, (match) => {
const word = match.replaceAll("<u>", "").replaceAll("</u>", ""); const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
return word.length > 0 ? <u>{word}</u> : null; return word.length > 0 ? <u key={v4()}>{word}</u> : 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", "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", userSolution === option.id.toString() && "border-mti-purple-light",
)}> )}>
<span className={clsx("text-sm", userSolution !== option.id.toString() && "opacity-50")}>{option.id.toString()}</span> <span key={v4()} className={clsx("text-sm", userSolution !== option.id.toString() && "opacity-50")}>{option.id.toString()}</span>
<img src={option.src!} alt={`Option ${option.id.toString()}`} /> <img src={option.src!} alt={`Option ${option.id.toString()}`} />
</div> </div>
))} ))}
@@ -79,6 +79,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
exam, exam,
shuffles, shuffles,
hasExamEnded, hasExamEnded,
partIndex,
userSolutions: storeUserSolutions, userSolutions: storeUserSolutions,
setQuestionIndex, setQuestionIndex,
setUserSolutions, 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 }]); 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 }); setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers]) }, [answers])
@@ -136,7 +137,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
const shuffleMap = shuffleMaps.find((map) => map.questionID == x.question); const shuffleMap = shuffleMaps.find((map) => map.questionID == x.question);
if (shuffleMap) { if (shuffleMap) {
isSolutionCorrect = getShuffledSolution(x.option, shuffleMap) == matchingQuestion?.solution; isSolutionCorrect = getShuffledSolution(x.option, shuffleMap) == matchingQuestion?.solution;
}else { } else {
isSolutionCorrect = matchingQuestion?.solution === x.option; isSolutionCorrect = matchingQuestion?.solution === x.option;
} }
} }
@@ -159,6 +160,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
if (questionIndex === 0) { if (questionIndex === 0) {
onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps }); onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
} else { } else {
if (exam?.module === "level" && typeof exam.parts[0].intro !== "undefined" && questionIndex === 0) return;
setQuestionIndex(questionIndex - 1); setQuestionIndex(questionIndex - 1);
} }
@@ -181,7 +183,11 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button color="purple" variant="outline" onClick={back} className="max-w-[200px] w-full" <Button color="purple" variant="outline" onClick={back} className="max-w-[200px] w-full"
disabled={ disabled={
exam && 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 Back
</Button> </Button>

View File

@@ -63,7 +63,7 @@ export default function Button({
type={type} type={type}
onClick={onClick} onClick={onClick}
className={clsx( 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, padding,
colorClassNames[color][variant], colorClassNames[color][variant],
className, className,

View File

@@ -1,5 +1,5 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { Fragment } from "react"; import { Fragment, useEffect, useState } from "react";
import Button from "./Low/Button"; import Button from "./Low/Button";
interface Props { interface Props {
@@ -10,6 +10,19 @@ interface Props {
} }
export default function QuestionsModal({ isOpen, onClose, type = "module", unanswered = false }: 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 ( return (
<Transition show={isOpen} as={Fragment}> <Transition show={isOpen} as={Fragment}>
<Dialog onClose={() => onClose(false)} className="relative z-50"> <Dialog onClose={() => 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? Are you sure you want to continue without completing those questions?
</span> </span>
<div className="w-full flex justify-between mt-8"> <div className="w-full flex justify-between mt-8">
<Button color="purple" onClick={() => onClose(false)} variant="outline" className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={() => blockMultipleClicksClose(false)} variant="outline" className="max-w-[200px] self-end w-full">
Go Back Go Back
</Button> </Button>
<Button color="purple" onClick={() => onClose(true)} className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={() => blockMultipleClicksClose(true)} className="max-w-[200px] self-end w-full">
Continue Continue
</Button> </Button>
</div> </div>
@@ -56,18 +69,16 @@ export default function QuestionsModal({ isOpen, onClose, type = "module", unans
{type === "blankQuestions" && ( {type === "blankQuestions" && (
<> <>
<Dialog.Title className="font-bold text-xl">Questions Unanswered</Dialog.Title> <Dialog.Title className="font-bold text-xl">Questions Unanswered</Dialog.Title>
<span> <div className="flex flex-col text-lg gap-2">
You have left some questions unanswered in the current part. <br /> <p>You have left some questions unanswered in the current part.</p>
<br /> <p>If you wish to continue, you can still access this part later using the navigation bar at the top or the &quot;Back&quot; button.</p>
If you wish to continue, you can still access this part later using the navigation bar at the top. <br /> <p>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?</p>
<br /> </div>
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? <div className="w-full flex justify-between mt-6">
</span> <Button color="purple" onClick={() => blockMultipleClicksClose(false)} variant="outline" className="max-w-[200px] self-end w-full">
<div className="w-full flex justify-between mt-8">
<Button color="purple" onClick={() => onClose(false)} variant="outline" className="max-w-[200px] self-end w-full">
Go Back Go Back
</Button> </Button>
<Button color="purple" onClick={() => onClose(true)} className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={() => blockMultipleClicksClose(true)} className="max-w-[200px] self-end w-full">
Continue Continue
</Button> </Button>
</div> </div>
@@ -92,10 +103,10 @@ export default function QuestionsModal({ isOpen, onClose, type = "module", unans
)} )}
</span> </span>
<div className="w-full flex justify-between mt-8"> <div className="w-full flex justify-between mt-8">
<Button color="purple" onClick={() => onClose(false)} variant="outline" className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={() => blockMultipleClicksClose(false)} variant="outline" className="max-w-[200px] self-end w-full">
Go Back Go Back
</Button> </Button>
<Button color="purple" onClick={() => onClose(true)} className="max-w-[200px] self-end w-full"> <Button color="purple" onClick={() => blockMultipleClicksClose(true)} className="max-w-[200px] self-end w-full">
Submit Submit
</Button> </Button>
</div> </div>

View File

@@ -7,7 +7,7 @@ interface Props {
setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>> setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>>
} }
const TextComponent: React.FC<Props> = ({part, contextWord, setContextWordLine}) => { const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine }) => {
const textRef = useRef<HTMLDivElement>(null); const textRef = useRef<HTMLDivElement>(null);
const calculateLineNumbers = () => { const calculateLineNumbers = () => {
@@ -130,8 +130,6 @@ const TextComponent: React.FC<Props> = ({part, contextWord, setContextWordLine})
}*/ }*/
return ( return (
<div className="flex flex-col gap-2 w-full">
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
<div className="flex mt-2"> <div className="flex mt-2">
<div ref={textRef} className="h-fit ml-2 flex flex-col gap-4"> <div ref={textRef} className="h-fit ml-2 flex flex-col gap-4">
{part.context!.split('\n\n').map((line, index) => { {part.context!.split('\n\n').map((line, index) => {
@@ -139,7 +137,6 @@ const TextComponent: React.FC<Props> = ({part, contextWord, setContextWordLine})
})} })}
</div> </div>
</div> </div>
</div>
); );
} }

View File

@@ -32,12 +32,32 @@ const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
export default function Level({ exam, showSolutions = false, onFinish, editing = false }: Props) { export default function Level({ exam, showSolutions = false, onFinish, editing = false }: Props) {
const levelBgColor = "bg-ielts-level-light"; 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 [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
const [showQuestionsModal, setShowQuestionsModal] = useState(false); const [showQuestionsModal, setShowQuestionsModal] = useState(false);
const [continueAnyways, setContinueAnyways] = useState(false); const [continueAnyways, setContinueAnyways] = useState(false);
const [textRender, setTextRender] = useState(false);
const [seenParts, setSeenParts] = useState<number[]>(showSolutions ? exam.parts.map((_, index) => index) : [0]); const [seenParts, setSeenParts] = useState<number[]>(showSolutions ? exam.parts.map((_, index) => index) : [0]);
const [lastSolution, setLastSolution] = useState<UserSolution | undefined>(undefined);
const [questionModalKwargs, setQuestionModalKwargs] = useState<{ const [questionModalKwargs, setQuestionModalKwargs] = useState<{
type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined; type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined;
@@ -46,33 +66,33 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } } 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 [currentExercise, setCurrentExercise] = useState<Exercise>(exam.parts[0].exercises[0]);
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<Exercise>();
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && !showSolutions); const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && !showSolutions);
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
const [contextWord, setContextWord] = useState<string | undefined>(undefined); const [contextWord, setContextWord] = useState<string | undefined>(undefined);
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined); const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
const [currentSolution, setCurrentSolution] = useExamStore((state) => [state.currentSolution, state.setCurrentSolution])
const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined) const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined)
useEffect(() => { useEffect(() => {
if (typeof currentSolution !== "undefined") { 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!] : [] }]); 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); setCurrentSolution(undefined);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentSolution]) }, [currentSolution]);
useEffect(()=> { useEffect(() => {
if (showSolutions) { if (showSolutions) {
const solutionShuffles = userSolutions.map(solution => ({ const solutionShuffles = userSolutions.map(solution => ({
exerciseID: solution.exercise, exerciseID: solution.exercise,
@@ -81,22 +101,14 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
setShuffles(solutionShuffles); setShuffles(solutionShuffles);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
},[]); }, []);
useEffect(() => { const getExercise = () => {
if (hasExamEnded && exerciseIndex === -1) {
setExerciseIndex(exerciseIndex + 1);
}
}, [hasExamEnded, exerciseIndex, setExerciseIndex]);
const getExercise = () => {;
let exercise = exam.parts[partIndex]?.exercises[exerciseIndex]; let exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
if (!exercise) return undefined;
exercise = { exercise = {
...exercise, ...exercise,
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [], userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
}; };
exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles); exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles);
return exercise; return exercise;
@@ -105,13 +117,19 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
useEffect(() => { useEffect(() => {
setCurrentExercise(getExercise()); setCurrentExercise(getExercise());
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [partIndex, exerciseIndex, exam.parts[partIndex].context]); }, [partIndex, exerciseIndex, questionIndex]);
useEffect(() => { useEffect(() => {
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/; const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
if (exerciseIndex !== -1 && currentExercise && currentExercise.type === "multipleChoice" && currentExercise.questions[storeQuestionIndex].prompt) { if (
const match = currentExercise.questions[storeQuestionIndex].prompt.match(regex); 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) { if (match) {
const word = match[1]; const word = match[1];
const originalLineNumber = match[2]; const originalLineNumber = match[2];
@@ -120,18 +138,18 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
setContextWord(word); setContextWord(word);
} }
const updatedPrompt = currentExercise.questions[storeQuestionIndex].prompt.replace( const updatedPrompt = currentExercise.questions[questionIndex].prompt.replace(
`in line ${originalLineNumber}`, `in line ${originalLineNumber}`,
`in line ${contextWordLine || originalLineNumber}` `in line ${contextWordLine || originalLineNumber}`
); );
currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt; currentExercise.questions[questionIndex].prompt = updatedPrompt;
} else { } else {
setContextWord(undefined); setContextWord(undefined);
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentExercise, storeQuestionIndex]); }, [currentExercise, questionIndex]);
const nextExercise = (solution?: UserSolution) => { const nextExercise = (solution?: UserSolution) => {
scrollToTop(); scrollToTop();
@@ -142,28 +160,30 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
} }
if (partIndex + 1 === exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions && !continueAnyways) { if (partIndex + 1 === exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions && !continueAnyways) {
setLastSolution(solution);
modalKwargs(); modalKwargs();
setShowQuestionsModal(true); setShowQuestionsModal(true);
return; return;
} }
if (partIndex + 1 < exam.parts.length && !hasExamEnded) { if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions) { if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.includes(partIndex + 1)) {
modalKwargs(); modalKwargs();
setShowQuestionsModal(true); setShowQuestionsModal(true);
return; return;
} }
if (!showSolutions && exam.parts[0].intro) { if (!showSolutions && exam.parts[0].intro && !seenParts.includes(partIndex + 1)) {
setShowPartDivider(true); setShowPartDivider(true);
setBgColor(levelBgColor); setBgColor(levelBgColor);
} }
setSeenParts((prev) => [...prev, partIndex + 1]) setSeenParts((prev) => [...prev, partIndex + 1])
if (partIndex < exam.parts.length - 1 && exam.parts[partIndex + 1].context) {
setTextRender(true);
}
setPartIndex(partIndex + 1); setPartIndex(partIndex + 1);
setExerciseIndex(!!exam.parts[partIndex + 1].context ? -1 : 0); setExerciseIndex(0);
setStoreQuestionIndex(0); setQuestionIndex(0);
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex }]); setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : questionIndex }]);
return; return;
} }
@@ -177,8 +197,24 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
const previousExercise = (solution?: UserSolution) => { const previousExercise = (solution?: UserSolution) => {
scrollToTop(); 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); 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 lastPartExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1;
const previousExercise = exam.parts[partIndex - 1].exercises[lastPartExerciseIndex]; const previousExercise = exam.parts[partIndex - 1].exercises[lastPartExerciseIndex];
if (previousExercise.type === "multipleChoice") { if (previousExercise.type === "multipleChoice") {
setStoreQuestionIndex(previousExercise.questions.length - 1) setQuestionIndex(previousExercise.questions.length - 1)
} }
const multipleChoiceQuestionsDone = []; const multipleChoiceQuestionsDone = [];
for (let i = 0; i < exam.parts.length; i++) { 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 = () => { const calculateExerciseIndex = () => {
if (exam.parts[0].intro) { if (exam.parts[0].intro) {
return exam.parts.reduce((acc, curr, index) => { 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 + countExercises(curr.exercises)
} }
return acc; return acc;
}, 0) + (storeQuestionIndex + 1); }, 0) + (questionIndex + 1);
} else { } else {
if (partIndex === 0) { if (partIndex === 0) {
return ( 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); const exercisesPerPart = exam.parts.map((x) => x.exercises.length);
@@ -233,28 +262,56 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
return ( return (
exercisesDone + exercisesDone +
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) + (exerciseIndex === -1 ? 0 : exerciseIndex + 1) +
storeQuestionIndex questionIndex
+ multipleChoicesDone.reduce((acc, curr) => { return acc + curr.amount }, 0) + multipleChoicesDone.reduce((acc, curr) => { return acc + curr.amount }, 0)
); );
} }
}; };
const renderText = () => ( const renderText = () => (
<>
<div className={clsx("flex flex-col gap-6 w-full bg-mti-gray-seasalt rounded-xl mt-4 relative py-8 px-16")}> <div className={clsx("flex flex-col gap-6 w-full bg-mti-gray-seasalt rounded-xl mt-4 relative py-8 px-16")}>
<> <>
<div className="flex flex-col w-full gap-2"> <div className="flex flex-col w-full gap-2">
{textRender ? (
<>
<h4 className="text-xl font-semibold"> <h4 className="text-xl font-semibold">
Please read the following excerpt attentively, you will then be asked questions about the text you&apos;ve read. Please read the following excerpt attentively, you will then be asked questions about the text you&apos;ve read.
</h4> </h4>
<span className="text-base">You will be allowed to read the text while doing the exercises</span> <span className="text-base">You will be allowed to read the text while doing the exercises</span>
</div> </>
) : (
<h4 className="text-xl font-semibold">
Answer the questions on the right based on what you&apos;ve read.
</h4>
)}
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
{exam.parts[partIndex].context &&
<TextComponent <TextComponent
part={exam.parts[partIndex]} part={exam.parts[partIndex]}
contextWord={contextWord} contextWord={contextWord}
setContextWordLine={setContextWordLine} setContextWordLine={setContextWordLine}
/> />}
</div>
</> </>
</div> </div>
{textRender && (
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button
color="purple"
variant="outline"
className="max-w-[200px] w-full"
onClick={() => { setTextRender(false); previousExercise(); }}
>
Back
</Button>
<Button color="purple" onClick={() => setTextRender(false)} className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
)}
</>
); );
const partLabel = () => { const partLabel = () => {
@@ -286,8 +343,8 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
useEffect(() => { useEffect(() => {
if (continueAnyways) { if (continueAnyways) {
nextExercise(lastSolution);
setContinueAnyways(false); setContinueAnyways(false);
nextExercise();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [continueAnyways]); }, [continueAnyways]);
@@ -314,7 +371,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
showSolutions: showSolutions, showSolutions: showSolutions,
"setExerciseIndex": setExerciseIndex, "setExerciseIndex": setExerciseIndex,
"setPartIndex": setPartIndex, "setPartIndex": setPartIndex,
"runOnClick": setStoreQuestionIndex "runOnClick": setQuestionIndex
} }
@@ -323,7 +380,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
<div className={clsx("flex flex-col h-full w-full gap-8 items-center", showPartDivider && "justify-center")}> <div className={clsx("flex flex-col h-full w-full gap-8 items-center", showPartDivider && "justify-center")}>
<QuestionsModal isOpen={showQuestionsModal} {...questionModalKwargs} /> <QuestionsModal isOpen={showQuestionsModal} {...questionModalKwargs} />
{ {
!(partIndex === 0 && storeQuestionIndex === 0 && showPartDivider) && !(partIndex === 0 && questionIndex === 0 && showPartDivider) &&
<Timer minTimer={exam.minTimer} disableTimer={showSolutions} standalone={true} /> <Timer minTimer={exam.minTimer} disableTimer={showSolutions} standalone={true} />
} }
{exam.parts[0].intro && showPartDivider ? <PartDivider part={exam.parts[partIndex]} partIndex={partIndex} onNext={() => { setShowPartDivider(false); setBgColor("bg-white") }} /> : ( {exam.parts[0].intro && showPartDivider ? <PartDivider part={exam.parts[partIndex]} partIndex={partIndex} onNext={() => { setShowPartDivider(false); setBgColor("bg-white") }} /> : (
@@ -338,7 +395,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
e.preventDefault(); e.preventDefault();
} else { } else {
setExerciseIndex(0); setExerciseIndex(0);
setStoreQuestionIndex(0); setQuestionIndex(0);
} }
}} }}
className={({ selected }) => className={({ selected }) =>
@@ -370,24 +427,20 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
<div <div
className={clsx( className={clsx(
"mb-20 w-full", "mb-20 w-full",
partIndex > -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 && {textRender ?
partIndex > -1 && renderText() :
exerciseIndex < exam.parts[partIndex].exercises.length && <>
!showSolutions && {exam.parts[partIndex].context && renderText()}
!editing && {(showSolutions || editing) ?
currentExercise && renderSolution(currentExercise, nextExercise, previousExercise)
renderExercise(currentExercise, exam.id, nextExercise, previousExercise)} :
renderExercise(currentExercise, exam.id, nextExercise, previousExercise)
{exerciseIndex > -1 && }
partIndex > -1 && </>
exerciseIndex < exam.parts[partIndex].exercises.length && }
(showSolutions || editing) &&
currentExercise &&
renderSolution(currentExercise, nextExercise, previousExercise)}
</div> </div>
{/*exerciseIndex === -1 && partIndex > 0 && ( {/*exerciseIndex === -1 && partIndex > 0 && (
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
@@ -401,7 +454,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
className="max-w-[200px] w-full" className="max-w-[200px] w-full"
disabled={ disabled={
exam && typeof partIndex !== "undefined" && exam.module === "level" && exam && typeof partIndex !== "undefined" && exam.module === "level" &&
typeof exam.parts[0].intro === "string" && storeQuestionIndex === 0} typeof exam.parts[0].intro === "string" && questionIndex === 0}
> >
Back Back
</Button> </Button>