import {MultipleChoiceExercise, ReadingExam, ReadingPart, UserSolution} from "@/interfaces/exam"; import {Fragment, useEffect, useState} from "react"; import Icon from "@mdi/react"; import {mdiArrowRight, mdiNotebook} from "@mdi/js"; import clsx from "clsx"; import {infoButtonStyle} from "@/constants/buttonStyles"; import {convertCamelCaseToReadable} from "@/utils/string"; import {Dialog, Transition} from "@headlessui/react"; import {renderExercise} from "@/components/Exercises"; import {renderSolution} from "@/components/Solutions"; import {Panel} from "primereact/panel"; import {Steps} from "primereact/steps"; import {BsAlarm, BsBook, BsChevronDown, BsChevronUp, BsClock, BsStopwatch} from "react-icons/bs"; import ProgressBar from "@/components/Low/ProgressBar"; import ModuleTitle from "@/components/Medium/ModuleTitle"; import {Divider} from "primereact/divider"; import Button from "@/components/Low/Button"; import BlankQuestionsModal from "@/components/QuestionsModal"; import useExamStore, { usePersistentExamStore } from "@/stores/examStore"; import {defaultUserSolutions} from "@/utils/exams"; import {countExercises} from "@/utils/moduleUtils"; interface Props { exam: ReadingExam; showSolutions?: boolean; preview?: boolean; onFinish: (userSolutions: UserSolution[]) => void; } const numberToLetter = (number: number) => (number + 9).toString(36).toUpperCase(); function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: string; content: string; onClose: () => void}) { return (
{title}

{content.split("\\n").map((line, index) => ( {line}
))}

); } function TextComponent({part, exerciseType}: {part: ReadingPart; exerciseType: string}) { return (

{part.text.title}

{part.text.content .split(/\n|(\\n)/g) .filter((x) => x && x.length > 0 && x !== "\\n") .map((line, index) => ( {exerciseType === "matchSentences" && (
{numberToLetter(index + 1)}

{line}

)} {exerciseType !== "matchSentences" &&

{line}

}
))}
); } export default function Reading({exam, showSolutions = false, preview = false, onFinish}: Props) { const [showTextModal, setShowTextModal] = useState(false); const [showBlankModal, setShowBlankModal] = useState(false); const [multipleChoicesDone, setMultipleChoicesDone] = useState<{id: string; amount: number}[]>([]); const [isTextMinimized, setIsTextMinimzed] = useState(false); const [exerciseType, setExerciseType] = useState(""); const examState = useExamStore((state) => state); const persistentExamState = usePersistentExamStore((state) => state); const { hasExamEnded, userSolutions, exerciseIndex, partIndex, questionIndex: storeQuestionIndex, setBgColor, setUserSolutions, setHasExamEnded, setExerciseIndex, setPartIndex, setQuestionIndex: setStoreQuestionIndex } = !preview ? examState : persistentExamState; const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); useEffect(() => { if (showSolutions) setExerciseIndex(-1); }, [setExerciseIndex, showSolutions]); useEffect(() => { const previousParts = exam.parts.filter((_, index) => index < partIndex); let previousMultipleChoice = previousParts.flatMap((x) => x.exercises).filter((x) => x.type === "multipleChoice") as MultipleChoiceExercise[]; if (partIndex > -1 && exerciseIndex > -1) { const previousPartExercises = exam.parts[partIndex].exercises.filter((_, index) => index < exerciseIndex); const partMultipleChoice = previousPartExercises.filter((x) => x.type === "multipleChoice") as MultipleChoiceExercise[]; previousMultipleChoice = [...previousMultipleChoice, ...partMultipleChoice]; } setMultipleChoicesDone(previousMultipleChoice.map((x) => ({id: x.id, amount: x.questions.length - 1}))); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { const listener = (e: KeyboardEvent) => { if (e.key === "F3" || ((e.ctrlKey || e.metaKey) && e.key === "f")) { e.preventDefault(); } }; document.addEventListener("keydown", listener); return () => { document.removeEventListener("keydown", listener); }; }, []); useEffect(() => { if (hasExamEnded && exerciseIndex === -1) { setExerciseIndex(exerciseIndex + 1); } }, [hasExamEnded, exerciseIndex, setExerciseIndex]); const confirmFinishModule = (keepGoing?: boolean) => { if (!keepGoing) { setShowBlankModal(false); return; } onFinish(userSolutions); }; const nextExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "reading", exam: exam.id}]); } if (storeQuestionIndex > 0) { const exercise = getExercise(); setExerciseType(exercise.type); setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise.id), {id: exercise.id, amount: storeQuestionIndex}]); } setStoreQuestionIndex(0); 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; } setHasExamEnded(false); if (solution) { onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "reading", exam: exam.id}]); } else { onFinish(userSolutions); } }; const previousExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "reading", exam: exam.id}]); } setStoreQuestionIndex(0); setExerciseIndex(exerciseIndex - 1); }; const getExercise = () => { const exercise = exam.parts[partIndex].exercises[exerciseIndex]; return { ...exercise, userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [], }; }; useEffect(() => { if (partIndex > -1 && exerciseIndex > -1) { const exercise = getExercise(); setExerciseType(exercise.type); setMultipleChoicesDone((prev) => prev.filter((x) => x.id !== exercise.id)); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [exerciseIndex, partIndex]); const calculateExerciseIndex = () => { if (partIndex === -1) return 0; 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 = () => (
{!isTextMinimized && ( <>

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
)} {isTextMinimized && Reading Passage}
); return ( <>
{partIndex > -1 && setShowTextModal(false)} />} x.exercises))} disableTimer={showSolutions} label={exerciseIndex === -1 ? undefined : convertCamelCaseToReadable(exam.parts[partIndex].exercises[exerciseIndex].type)} />
-1 && !isTextMinimized && "grid grid-cols-2 gap-4", exerciseIndex > -1 && isTextMinimized && "flex flex-col gap-2", )}> {partIndex > -1 && renderText()} {exerciseIndex > -1 && partIndex > -1 && exerciseIndex < exam.parts[partIndex].exercises.length && !showSolutions && renderExercise(getExercise(), exam.id, nextExercise, previousExercise, undefined, preview)} {exerciseIndex > -1 && partIndex > -1 && exerciseIndex < exam.parts[partIndex].exercises.length && showSolutions && renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)}
{exerciseIndex > -1 && partIndex > -1 && exerciseIndex < exam.parts[partIndex].exercises.length && ( )}
{exerciseIndex === -1 && partIndex > 0 && (
)} {exerciseIndex === -1 && partIndex === 0 && ( )} ); }