Files
encoach_ui_odoo19/src/exams/Reading.tsx
Tiago Ribeiro 61d1bbbe13 ENCOA-268
2024-12-12 16:14:54 +00:00

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;