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 && (