diff --git a/src/components/Exercises/InteractiveSpeaking.tsx b/src/components/Exercises/InteractiveSpeaking.tsx
index bd56190d..577a0891 100644
--- a/src/components/Exercises/InteractiveSpeaking.tsx
+++ b/src/components/Exercises/InteractiveSpeaking.tsx
@@ -21,7 +21,6 @@ export default function InteractiveSpeaking({
type,
prompts,
userSolutions,
- updateIndex,
onNext,
onBack,
}: InteractiveSpeakingExercise & CommonProps) {
@@ -111,10 +110,6 @@ export default function InteractiveSpeaking({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userSolutions, mediaBlob, answers]);
- useEffect(() => {
- if (updateIndex) updateIndex(questionIndex);
- }, [questionIndex, updateIndex]);
-
useEffect(() => {
if (hasExamEnded) {
const answer = {
diff --git a/src/components/Exercises/MultipleChoice.tsx b/src/components/Exercises/MultipleChoice.tsx
index ebaaf856..9cfd780b 100644
--- a/src/components/Exercises/MultipleChoice.tsx
+++ b/src/components/Exercises/MultipleChoice.tsx
@@ -55,16 +55,7 @@ function Question({
);
}
-export default function MultipleChoice({
- id,
- prompt,
- type,
- questions,
- userSolutions,
- updateIndex,
- onNext,
- onBack,
-}: MultipleChoiceExercise & CommonProps) {
+export default function MultipleChoice({id, prompt, type, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions);
const {questionIndex, setQuestionIndex} = useExamStore((state) => state);
@@ -83,10 +74,6 @@ export default function MultipleChoice({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasExamEnded]);
- useEffect(() => {
- if (updateIndex) updateIndex(questionIndex);
- }, [questionIndex, updateIndex]);
-
const onSelectOption = (option: string) => {
const question = questions[questionIndex];
setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), {option, question: question.id}]);
diff --git a/src/components/Exercises/index.tsx b/src/components/Exercises/index.tsx
index 2b1a623b..b1a6dc1b 100644
--- a/src/components/Exercises/index.tsx
+++ b/src/components/Exercises/index.tsx
@@ -23,7 +23,6 @@ const MatchSentences = dynamic(() => import("@/components/Exercises/MatchSentenc
export interface CommonProps {
examID?: string;
- updateIndex?: (internalIndex: number) => void;
onNext: (userSolutions: UserSolution) => void;
onBack: (userSolutions: UserSolution) => void;
}
@@ -33,7 +32,6 @@ export const renderExercise = (
examID: string,
onNext: (userSolutions: UserSolution) => void,
onBack: (userSolutions: UserSolution) => void,
- updateIndex?: (internalIndex: number) => void,
) => {
switch (exercise.type) {
case "fillBlanks":
@@ -43,16 +41,7 @@ export const renderExercise = (
case "matchSentences":
return ;
case "multipleChoice":
- return (
-
- );
+ return ;
case "writeBlanks":
return ;
case "writing":
@@ -65,7 +54,6 @@ export const renderExercise = (
key={exercise.id}
{...(exercise as InteractiveSpeakingExercise)}
examID={examID}
- updateIndex={updateIndex}
onNext={onNext}
onBack={onBack}
/>
diff --git a/src/components/Solutions/MultipleChoice.tsx b/src/components/Solutions/MultipleChoice.tsx
index ecbf0025..ec5667db 100644
--- a/src/components/Solutions/MultipleChoice.tsx
+++ b/src/components/Solutions/MultipleChoice.tsx
@@ -1,5 +1,6 @@
/* eslint-disable @next/next/no-img-element */
import {MultipleChoiceExercise, MultipleChoiceQuestion} from "@/interfaces/exam";
+import useExamStore from "@/stores/examStore";
import clsx from "clsx";
import {useEffect, useState} from "react";
import {CommonProps} from ".";
@@ -61,17 +62,8 @@ function Question({
);
}
-export default function MultipleChoice({
- id,
- type,
- prompt,
- questions,
- userSolutions,
- updateIndex,
- onNext,
- onBack,
-}: MultipleChoiceExercise & CommonProps) {
- const [questionIndex, setQuestionIndex] = useState(0);
+export default function MultipleChoice({id, type, prompt, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
+ const {questionIndex, setQuestionIndex} = useExamStore((state) => state);
const calculateScore = () => {
const total = questions.length;
@@ -83,15 +75,11 @@ export default function MultipleChoice({
return {total, correct, missing};
};
- useEffect(() => {
- if (updateIndex) updateIndex(questionIndex);
- }, [questionIndex, updateIndex]);
-
const next = () => {
if (questionIndex === questions.length - 1) {
onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type});
} else {
- setQuestionIndex((prev) => prev + 1);
+ setQuestionIndex(questionIndex + 1);
}
};
@@ -99,7 +87,7 @@ export default function MultipleChoice({
if (questionIndex === 0) {
onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type});
} else {
- setQuestionIndex((prev) => prev - 1);
+ setQuestionIndex(questionIndex - 1);
}
};
diff --git a/src/components/Solutions/index.tsx b/src/components/Solutions/index.tsx
index 2926703a..6e5c243d 100644
--- a/src/components/Solutions/index.tsx
+++ b/src/components/Solutions/index.tsx
@@ -22,7 +22,6 @@ import Writing from "./Writing";
const MatchSentences = dynamic(() => import("@/components/Solutions/MatchSentences"), {ssr: false});
export interface CommonProps {
- updateIndex?: (internalIndex: number) => void;
onNext: (userSolutions: UserSolution) => void;
onBack: (userSolutions: UserSolution) => void;
}
@@ -36,15 +35,7 @@ export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: (
case "matchSentences":
return ;
case "multipleChoice":
- return (
-
- );
+ return ;
case "writeBlanks":
return ;
case "writing":
diff --git a/src/exams/Finish.tsx b/src/exams/Finish.tsx
index 1c3820c4..ef677c97 100644
--- a/src/exams/Finish.tsx
+++ b/src/exams/Finish.tsx
@@ -78,7 +78,7 @@ export default function Finish({user, scores, modules, information, isLoading, o
const getTotalExercises = () => {
const exam = exams.find((x) => x.module === selectedModule)!;
- if (exam.module === "reading" || exam.module === "listening") {
+ if (exam.module === "reading" || exam.module === "listening" || exam.module === "level") {
return exam.parts.flatMap((x) => x.exercises).length;
}
diff --git a/src/exams/Level.tsx b/src/exams/Level.tsx
index b31d1ee0..09beb76e 100644
--- a/src/exams/Level.tsx
+++ b/src/exams/Level.tsx
@@ -1,8 +1,10 @@
+import BlankQuestionsModal from "@/components/BlankQuestionsModal";
import {renderExercise} from "@/components/Exercises";
+import Button from "@/components/Low/Button";
import ModuleTitle from "@/components/Medium/ModuleTitle";
import {renderSolution} from "@/components/Solutions";
import {infoButtonStyle} from "@/constants/buttonStyles";
-import {LevelExam, UserSolution, WritingExam} from "@/interfaces/exam";
+import {LevelExam, LevelPart, UserSolution, WritingExam} from "@/interfaces/exam";
import useExamStore from "@/stores/examStore";
import {defaultUserSolutions} from "@/utils/exams";
import {countExercises} from "@/utils/moduleUtils";
@@ -10,6 +12,7 @@ import {mdiArrowRight} from "@mdi/js";
import Icon from "@mdi/react";
import clsx from "clsx";
import {Fragment, useEffect, useState} from "react";
+import {BsChevronDown, BsChevronUp} from "react-icons/bs";
import {toast} from "react-toastify";
interface Props {
@@ -18,36 +21,84 @@ interface Props {
onFinish: (userSolutions: UserSolution[]) => void;
}
+function TextComponent({part}: {part: LevelPart}) {
+ return (
+
+
+ {!!part.context &&
+ part.context
+ .split(/\n|(\\n)/g)
+ .filter((x) => x && x.length > 0 && x !== "\\n")
+ .map((line, index) => (
+
+ {line}
+
+ ))}
+
+ );
+}
+
export default function Level({exam, showSolutions = false, onFinish}: Props) {
- const [questionIndex, setQuestionIndex] = useState(0);
- const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
- const [exerciseIndex, setExerciseIndex] = useState(0);
+ const [multipleChoicesDone, setMultipleChoicesDone] = useState<{id: string; amount: number}[]>([]);
+ const [showBlankModal, setShowBlankModal] = useState(false);
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]);
- useEffect(() => {
- setCurrentQuestionIndex(0);
- }, [questionIndex]);
+ const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
useEffect(() => {
if (hasExamEnded && exerciseIndex === -1) {
- setExerciseIndex((prev) => prev + 1);
+ setExerciseIndex(exerciseIndex + 1);
}
- }, [hasExamEnded, exerciseIndex]);
+ }, [hasExamEnded, exerciseIndex, setExerciseIndex]);
- const nextExercise = (solution?: UserSolution) => {
- if (solution) {
- setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]);
- }
- setQuestionIndex((prev) => prev + currentQuestionIndex);
-
- if (exerciseIndex + 1 < exam.exercises.length) {
- setExerciseIndex((prev) => prev + 1);
+ const confirmFinishModule = (keepGoing?: boolean) => {
+ if (!keepGoing) {
+ setShowBlankModal(false);
return;
}
- if (exerciseIndex >= exam.exercises.length) return;
+ onFinish(userSolutions);
+ };
+
+ const nextExercise = (solution?: UserSolution) => {
+ scrollToTop();
+ if (solution) {
+ setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]);
+ }
+
+ if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) {
+ setExerciseIndex(exerciseIndex + 1);
+ return;
+ }
+
+ if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
+ setPartIndex(partIndex + 1);
+ setExerciseIndex(showSolutions ? 0 : -1);
+ return;
+ }
+
+ if (
+ solution &&
+ ![...userSolutions.filter((x) => x.exercise !== solution?.exercise).map((x) => x.score.missing), solution?.score.missing].every(
+ (x) => x === 0,
+ ) &&
+ !showSolutions &&
+ !hasExamEnded
+ ) {
+ setShowBlankModal(true);
+ return;
+ }
+
+ if (storeQuestionIndex > 0) {
+ const exercise = getExercise();
+ setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise.id), {id: exercise.id, amount: storeQuestionIndex}]);
+ }
+ setStoreQuestionIndex(0);
setHasExamEnded(false);
@@ -59,41 +110,102 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
};
const previousExercise = (solution?: UserSolution) => {
+ scrollToTop();
if (solution) {
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]);
}
+ setStoreQuestionIndex(0);
- if (exerciseIndex > 0) {
- setExerciseIndex((prev) => prev - 1);
- }
+ setExerciseIndex(exerciseIndex - 1);
};
const getExercise = () => {
- const exercise = exam.exercises[exerciseIndex];
+ const exercise = exam.parts[partIndex].exercises[exerciseIndex];
return {
...exercise,
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
};
};
+ const calculateExerciseIndex = () => {
+ 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 (
+ exercisesDone +
+ (exerciseIndex === -1 ? 0 : exerciseIndex + 1) +
+ storeQuestionIndex +
+ multipleChoicesDone.reduce((acc, curr) => 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
+
+
+ >
+
+ );
+
return (
<>
+
x.exercises))}
disableTimer={showSolutions}
/>
- {exerciseIndex > -1 &&
- exerciseIndex < exam.exercises.length &&
- !showSolutions &&
- renderExercise(getExercise(), exam.id, nextExercise, previousExercise, setCurrentQuestionIndex)}
- {exerciseIndex > -1 &&
- exerciseIndex < exam.exercises.length &&
- showSolutions &&
- renderSolution(exam.exercises[exerciseIndex], nextExercise, previousExercise, setCurrentQuestionIndex)}
+ -1 && !!exam.parts[partIndex].context && "grid grid-cols-2 gap-4")}>
+ {partIndex > -1 && !!exam.parts[partIndex].context && renderText()}
+
+ {exerciseIndex > -1 &&
+ partIndex > -1 &&
+ exerciseIndex < exam.parts[partIndex].exercises.length &&
+ !showSolutions &&
+ renderExercise(getExercise(), exam.id, nextExercise, previousExercise)}
+
+ {exerciseIndex > -1 &&
+ partIndex > -1 &&
+ exerciseIndex < exam.parts[partIndex].exercises.length &&
+ showSolutions &&
+ renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)}
+
+ {exerciseIndex === -1 && partIndex > 0 && (
+
+
+
+
+
+ )}
+ {exerciseIndex === -1 && partIndex === 0 && (
+
+ )}
>
);
diff --git a/src/exams/Listening.tsx b/src/exams/Listening.tsx
index c7884c3a..24cb4859 100644
--- a/src/exams/Listening.tsx
+++ b/src/exams/Listening.tsx
@@ -19,8 +19,6 @@ const INSTRUCTIONS_AUDIO_SRC =
"https://firebasestorage.googleapis.com/v0/b/storied-phalanx-349916.appspot.com/o/generic_listening_intro_v2.mp3?alt=media&token=16769f5f-1e9b-4a72-86a9-45a6f0fa9f82";
export default function Listening({exam, showSolutions = false, onFinish}: Props) {
- const [questionIndex, setQuestionIndex] = useState(0);
- const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [timesListened, setTimesListened] = useState(0);
const [showBlankModal, setShowBlankModal] = useState(false);
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{id: string; amount: number}[]>([]);
@@ -64,10 +62,6 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
}
}, [hasExamEnded, exerciseIndex, setExerciseIndex]);
- useEffect(() => {
- setCurrentQuestionIndex(0);
- }, [questionIndex]);
-
const confirmFinishModule = (keepGoing?: boolean) => {
if (!keepGoing) {
setShowBlankModal(false);
@@ -220,14 +214,14 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
partIndex > -1 &&
exerciseIndex < exam.parts[partIndex].exercises.length &&
!showSolutions &&
- renderExercise(getExercise(), exam.id, nextExercise, previousExercise, setCurrentQuestionIndex)}
+ renderExercise(getExercise(), exam.id, nextExercise, previousExercise)}
{/* Solution renderer */}
{exerciseIndex > -1 &&
partIndex > -1 &&
exerciseIndex < exam.parts[partIndex].exercises.length &&
showSolutions &&
- renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise, setCurrentQuestionIndex)}
+ renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)}
{exerciseIndex === -1 && partIndex > -1 && exam.variant !== "partial" && (
diff --git a/src/exams/Reading.tsx b/src/exams/Reading.tsx
index 7059d8ee..f2c1ec53 100644
--- a/src/exams/Reading.tsx
+++ b/src/exams/Reading.tsx
@@ -155,10 +155,6 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props)
};
}, []);
- useEffect(() => {
- setCurrentQuestionIndex(0);
- }, [questionIndex]);
-
useEffect(() => {
if (hasExamEnded && exerciseIndex === -1) {
setExerciseIndex(exerciseIndex + 1);
@@ -314,13 +310,13 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props)
partIndex > -1 &&
exerciseIndex < exam.parts[partIndex].exercises.length &&
!showSolutions &&
- renderExercise(getExercise(), exam.id, nextExercise, previousExercise, setCurrentQuestionIndex)}
+ renderExercise(getExercise(), exam.id, nextExercise, previousExercise)}
{exerciseIndex > -1 &&
partIndex > -1 &&
exerciseIndex < exam.parts[partIndex].exercises.length &&
showSolutions &&
- renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise, setCurrentQuestionIndex)}
+ renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)}
{exerciseIndex > -1 && partIndex > -1 && exerciseIndex < exam.parts[partIndex].exercises.length && (