218 lines
7.7 KiB
TypeScript
218 lines
7.7 KiB
TypeScript
import { Exercise, ReadingExam, UserSolution } from "@/interfaces/exam";
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import clsx from "clsx";
|
|
import { convertCamelCaseToReadable } from "@/utils/string";
|
|
import { renderExercise } from "@/components/Exercises";
|
|
import { renderSolution } from "@/components/Solutions";
|
|
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
|
import BlankQuestionsModal from "@/components/QuestionsModal";
|
|
import { countExercises } from "@/utils/moduleUtils";
|
|
import PartDivider from "./Navigation/SectionDivider";
|
|
import ReadingPassage from "./components/ReadingPassage";
|
|
//import ReadingPassageModal from "./components/ReadingPassageModal";
|
|
import { calculateExerciseIndex } from "./utils/calculateExerciseIndex";
|
|
import useExamNavigation from "./Navigation/useExamNavigation";
|
|
import { ExamProps } from "./types";
|
|
import useExamStore, { usePersistentExamStore } from "@/stores/exam";
|
|
import useExamTimer from "@/hooks/useExamTimer";
|
|
import SectionNavbar from "./Navigation/SectionNavbar";
|
|
import ProgressButtons from "./components/ProgressButtons";
|
|
import PracticeModal from "@/components/PracticeModal";
|
|
|
|
const Reading: React.FC<ExamProps<ReadingExam>> = ({ exam, showSolutions = false, preview = false }) => {
|
|
const updateTimers = useExamTimer(exam.module, preview || showSolutions);
|
|
const userSolutionRef = useRef<(() => UserSolution) | null>(null);
|
|
const [solutionWasUpdated, setSolutionWasUpdated] = useState(false);
|
|
|
|
const [showBlankModal, setShowBlankModal] = useState(false);
|
|
//const [showTextModal, setShowTextModal] = useState(false);
|
|
const [isTextMinimized, setIsTextMinimzed] = useState(false);
|
|
|
|
const examState = useExamStore((state) => state);
|
|
const persistentExamState = usePersistentExamStore((state) => state);
|
|
|
|
const {
|
|
exerciseIndex, partIndex, questionIndex,
|
|
userSolutions, flags, timeSpentCurrentModule,
|
|
setBgColor, setUserSolutions, setTimeIsUp,
|
|
dispatch,
|
|
} = !preview ? examState : persistentExamState;
|
|
|
|
|
|
const timer = useRef(exam.minTimer - timeSpentCurrentModule / 60);
|
|
|
|
const { finalizeModule, timeIsUp } = flags;
|
|
|
|
const {
|
|
nextExercise, previousExercise,
|
|
showPartDivider, setShowPartDivider,
|
|
seenParts, setSeenParts,
|
|
isBetweenParts, setIsBetweenParts,
|
|
startNow
|
|
} = useExamNavigation({ exam, module: "reading", showBlankModal, setShowBlankModal, showSolutions, preview, disableBetweenParts: showSolutions });
|
|
|
|
const hasPractice = useMemo(() => {
|
|
if (partIndex > -1 && partIndex < exam.parts.length) {
|
|
return exam.parts[partIndex].exercises.some(e => e.isPractice)
|
|
}
|
|
return false
|
|
}, [partIndex, exam.parts])
|
|
|
|
useEffect(() => {
|
|
if (finalizeModule || timeIsUp) {
|
|
updateTimers();
|
|
|
|
if (timeIsUp) {
|
|
setTimeIsUp(false);
|
|
}
|
|
dispatch({ type: "FINALIZE_MODULE", payload: { updateTimers: false } })
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [finalizeModule, timeIsUp])
|
|
|
|
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);
|
|
};
|
|
}, []);
|
|
|
|
const registerSolution = useCallback((updateSolution: () => UserSolution) => {
|
|
userSolutionRef.current = updateSolution;
|
|
setSolutionWasUpdated(true);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (solutionWasUpdated && userSolutionRef.current) {
|
|
const solution = userSolutionRef.current();
|
|
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "reading", exam: exam.id }]);
|
|
setSolutionWasUpdated(false);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [solutionWasUpdated])
|
|
|
|
const currentExercise = useMemo<Exercise>(() => {
|
|
const exercise = exam.parts[partIndex].exercises[exerciseIndex];
|
|
return {
|
|
...exercise,
|
|
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [partIndex, exerciseIndex]);
|
|
|
|
|
|
const confirmFinishModule = (keepGoing?: boolean) => {
|
|
if (!keepGoing) {
|
|
setShowBlankModal(false);
|
|
return;
|
|
} else {
|
|
nextExercise(true);
|
|
setShowBlankModal(false);
|
|
}
|
|
};
|
|
|
|
const memoizedExerciseIndex = useMemo(() =>
|
|
calculateExerciseIndex(exam, partIndex, exerciseIndex, questionIndex)
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
, [partIndex, exerciseIndex, questionIndex]
|
|
);
|
|
|
|
const handlePartDividerClick = () => {
|
|
setShowPartDivider(false);
|
|
setBgColor("bg-white");
|
|
setSeenParts((prev) => new Set(prev).add(partIndex));
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (partIndex !== 0 && !showSolutions) {
|
|
setIsBetweenParts(true);
|
|
}
|
|
}, [partIndex, setIsBetweenParts, showSolutions])
|
|
|
|
|
|
const progressButtons = useMemo(() =>
|
|
// Do not remove the ()=> in handle next
|
|
<ProgressButtons handlePrevious={previousExercise} handleNext={() => nextExercise()} />
|
|
, [nextExercise, previousExercise]);
|
|
|
|
return (
|
|
<>
|
|
{showPartDivider ?
|
|
<div className="flex justify-center items-center h-full">
|
|
<PartDivider
|
|
module="reading"
|
|
sectionLabel="Part"
|
|
defaultTitle="Reading exam"
|
|
section={exam.parts[partIndex]}
|
|
sectionIndex={partIndex}
|
|
onNext={() => handlePartDividerClick()}
|
|
/>
|
|
</div> : (
|
|
<>
|
|
<PracticeModal key={partIndex} open={hasPractice} />
|
|
<div className="flex flex-col h-full w-full gap-8">
|
|
<BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} />
|
|
{/*<ReadingPassageModal text={exam.parts[partIndex].text} isOpen={showTextModal} onClose={() => setShowTextModal(false)} />*/}
|
|
{exam.parts.length > 1 && <SectionNavbar
|
|
module="reading"
|
|
sectionLabel="Part"
|
|
seenParts={seenParts}
|
|
setShowPartDivider={setShowPartDivider}
|
|
setSeenParts={setSeenParts}
|
|
setIsBetweenParts={setIsBetweenParts}
|
|
preview={preview}
|
|
/>}
|
|
<ModuleTitle
|
|
minTimer={timer.current}
|
|
exerciseIndex={memoizedExerciseIndex}
|
|
module="reading"
|
|
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
|
disableTimer={showSolutions || preview}
|
|
label={convertCamelCaseToReadable(exam.parts[partIndex].exercises[exerciseIndex].type)}
|
|
preview={preview}
|
|
/>
|
|
<div
|
|
className={clsx(
|
|
"mb-20 w-full",
|
|
((startNow || isBetweenParts) && !showSolutions) ? "flex flex-col gap-2" : "grid grid-cols-2 gap-4",
|
|
)}>
|
|
<ReadingPassage
|
|
exam={exam}
|
|
partIndex={partIndex}
|
|
exerciseType={currentExercise.type}
|
|
isTextMinimized={isTextMinimized}
|
|
setIsTextMinimized={setIsTextMinimzed}
|
|
/>
|
|
{!startNow && !showPartDivider && !showSolutions && !isBetweenParts && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons)}
|
|
{showSolutions && renderSolution(currentExercise, progressButtons, progressButtons)}
|
|
</div>
|
|
{/*exerciseIndex > -1 && partIndex > -1 && exerciseIndex < exam.parts[partIndex].exercises.length && (
|
|
<Button
|
|
color="purple"
|
|
variant="outline"
|
|
onClick={() => setShowTextModal(true)}
|
|
className="max-w-[200px] self-end w-full absolute bottom-[31px] right-64">
|
|
Read text
|
|
</Button>
|
|
)*/}
|
|
</div>
|
|
{((startNow || isBetweenParts) && !showPartDivider && !showSolutions) &&
|
|
<ProgressButtons
|
|
hidePrevious={partIndex == 0 && isBetweenParts || startNow}
|
|
nextLabel={startNow ? "Start now" : "Next Page"}
|
|
handlePrevious={previousExercise}
|
|
handleNext={() => nextExercise()} />
|
|
}
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default Reading;
|