141 lines
5.0 KiB
TypeScript
141 lines
5.0 KiB
TypeScript
import { renderExercise } from "@/components/Exercises";
|
|
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
|
import { renderSolution } from "@/components/Solutions";
|
|
import { UserSolution, SpeakingExam, SpeakingExercise, InteractiveSpeakingExercise, Exercise } from "@/interfaces/exam";
|
|
import useExamStore, { usePersistentExamStore } from "@/stores/exam";
|
|
import { countExercises } from "@/utils/moduleUtils";
|
|
import { convertCamelCaseToReadable } from "@/utils/string";
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import PartDivider from "./Navigation/SectionDivider";
|
|
import { ExamProps } from "./types";
|
|
import useExamTimer from "@/hooks/useExamTimer";
|
|
import useExamNavigation from "./Navigation/useExamNavigation";
|
|
import ProgressButtons from "./components/ProgressButtons";
|
|
import { calculateExerciseIndexSpeaking } from "./utils/calculateExerciseIndex";
|
|
import SectionNavbar from "./Navigation/SectionNavbar";
|
|
|
|
|
|
const Speaking: React.FC<ExamProps<SpeakingExam>> = ({ exam, showSolutions = false, preview = false }) => {
|
|
const updateTimers = useExamTimer(exam.module, preview || showSolutions);
|
|
const userSolutionRef = useRef<(() => UserSolution) | null>(null);
|
|
const [solutionWasUpdated, setSolutionWasUpdated] = useState(false);
|
|
|
|
const examState = useExamStore((state) => state);
|
|
const persistentExamState = usePersistentExamStore((state) => state);
|
|
|
|
const {
|
|
exerciseIndex, userSolutions, flags,
|
|
timeSpentCurrentModule, questionIndex,
|
|
setBgColor, setUserSolutions, setTimeIsUp,
|
|
dispatch,
|
|
} = !preview ? examState : persistentExamState;
|
|
|
|
const { finalizeModule, timeIsUp } = flags;
|
|
|
|
const timer = useRef(exam.minTimer - timeSpentCurrentModule / 60);
|
|
|
|
const {
|
|
nextExercise, previousExercise,
|
|
showPartDivider, setShowPartDivider,
|
|
setSeenParts, seenParts, setIsBetweenParts
|
|
} = useExamNavigation({ exam, module: "speaking", showSolutions, preview, disableBetweenParts: true });
|
|
|
|
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])
|
|
|
|
|
|
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: "speaking", exam: exam.id }]);
|
|
setSolutionWasUpdated(false);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [solutionWasUpdated])
|
|
|
|
|
|
const currentExercise = useMemo<Exercise>(() => {
|
|
const exercise = exam.exercises[exerciseIndex];
|
|
return {
|
|
...exercise,
|
|
variant: exerciseIndex < 2 && exercise.type === "interactiveSpeaking" ? "initial" : undefined,
|
|
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
|
} as SpeakingExercise | InteractiveSpeakingExercise;
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [exerciseIndex]);
|
|
|
|
|
|
const progressButtons = useMemo(() =>
|
|
// Do not remove the ()=> in handle next
|
|
<ProgressButtons handlePrevious={previousExercise} handleNext={() => nextExercise()} />
|
|
, [nextExercise, previousExercise]);
|
|
|
|
|
|
const handlePartDividerClick = () => {
|
|
setShowPartDivider(false);
|
|
setBgColor("bg-white");
|
|
setSeenParts((prev) => new Set(prev).add(exerciseIndex));
|
|
}
|
|
|
|
const memoizedExerciseIndex = useMemo(() =>
|
|
calculateExerciseIndexSpeaking(exam, exerciseIndex, questionIndex)
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
, [exerciseIndex, questionIndex]
|
|
);
|
|
|
|
|
|
return (
|
|
<>
|
|
{(showPartDivider) ?
|
|
<PartDivider
|
|
module="speaking"
|
|
sectionLabel="Speaking"
|
|
defaultTitle="Speaking exam"
|
|
section={exam.exercises[exerciseIndex]}
|
|
sectionIndex={exerciseIndex}
|
|
onNext={handlePartDividerClick}
|
|
/> : (
|
|
<>
|
|
{exam.exercises.length > 1 && <SectionNavbar
|
|
module="speaking"
|
|
sectionLabel="Part"
|
|
seenParts={seenParts}
|
|
setShowPartDivider={setShowPartDivider}
|
|
setSeenParts={setSeenParts}
|
|
preview={preview}
|
|
/>}
|
|
<div className="flex flex-col h-full w-full gap-8 items-center">
|
|
<ModuleTitle
|
|
label={convertCamelCaseToReadable(exam.exercises[exerciseIndex].type)}
|
|
minTimer={timer.current}
|
|
exerciseIndex={memoizedExerciseIndex}
|
|
module="speaking"
|
|
totalExercises={countExercises(exam.exercises)}
|
|
disableTimer={showSolutions || preview}
|
|
preview={preview}
|
|
/>
|
|
{!showPartDivider && !showSolutions && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons)}
|
|
{showSolutions && renderSolution(currentExercise, progressButtons, progressButtons)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default Speaking;
|