Navigation rework, added prompt edit to components that were missing
This commit is contained in:
@@ -5,11 +5,10 @@ import ModuleTitle from "@/components/Medium/ModuleTitle";
|
||||
import { renderSolution } from "@/components/Solutions";
|
||||
import { Module } from "@/interfaces";
|
||||
import { Exercise, FillBlanksMCOption, LevelExam, MultipleChoiceExercise, UserSolution } from "@/interfaces/exam";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import { usePersistentExamStore } from "@/stores/examStore";
|
||||
import useExamStore, { usePersistentExamStore } from "@/stores/exam";
|
||||
import { countExercises } from "@/utils/moduleUtils";
|
||||
import clsx from "clsx";
|
||||
import { use, useEffect, useMemo, useState } from "react";
|
||||
import { use, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import TextComponent from "./TextComponent";
|
||||
import PartDivider from "../Navigation/SectionDivider";
|
||||
import Timer from "@/components/Medium/Timer";
|
||||
@@ -19,39 +18,44 @@ import Modal from "@/components/Modal";
|
||||
import { typeCheckWordsMC } from "@/utils/type.check";
|
||||
import SectionNavbar from "../Navigation/SectionNavbar";
|
||||
import AudioPlayer from "@/components/Low/AudioPlayer";
|
||||
import { ExamProps } from "../types";
|
||||
import {answeredEveryQuestionInPart} from "../utils/answeredEveryQuestion";
|
||||
import useExamTimer from "@/hooks/useExamTimer";
|
||||
import ProgressButtons from "../components/ProgressButtons";
|
||||
|
||||
interface Props {
|
||||
exam: LevelExam;
|
||||
showSolutions?: boolean;
|
||||
onFinish: (userSolutions: UserSolution[]) => void;
|
||||
preview?: boolean;
|
||||
partDividers?: boolean;
|
||||
}
|
||||
|
||||
export default function Level({ exam, showSolutions = false, onFinish, preview = false }: Props) {
|
||||
const Level: React.FC<ExamProps<LevelExam>> = ({ exam, showSolutions = false, preview = false }) => {
|
||||
const levelBgColor = "bg-ielts-level-light";
|
||||
|
||||
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 {
|
||||
userSolutions,
|
||||
hasExamEnded,
|
||||
partIndex,
|
||||
exerciseIndex,
|
||||
questionIndex,
|
||||
shuffles,
|
||||
currentSolution,
|
||||
setTimeIsUp,
|
||||
setBgColor,
|
||||
setUserSolutions,
|
||||
setHasExamEnded,
|
||||
setPartIndex,
|
||||
setExerciseIndex,
|
||||
setQuestionIndex,
|
||||
setShuffles,
|
||||
setCurrentSolution
|
||||
flags,
|
||||
timeSpentCurrentModule,
|
||||
dispatch,
|
||||
} = !preview ? examState : persistentExamState;
|
||||
|
||||
const { finalizeModule, timeIsUp } = flags;
|
||||
|
||||
const timer = useRef(exam.minTimer - timeSpentCurrentModule / 60);
|
||||
|
||||
// In case client want to switch back
|
||||
const textRenderDisabled = true;
|
||||
|
||||
@@ -61,8 +65,6 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
||||
const [continueAnyways, setContinueAnyways] = useState(false);
|
||||
const [textRender, setTextRender] = useState(false);
|
||||
const [changedPrompt, setChangedPrompt] = useState(false);
|
||||
const [nextExerciseCalled, setNextExerciseCalled] = useState(false);
|
||||
const [currentSolutionSet, setCurrentSolutionSet] = useState(false);
|
||||
|
||||
const [seenParts, setSeenParts] = useState<Set<number>>(new Set(showSolutions ? exam.parts.map((_, index) => index) : [0]));
|
||||
|
||||
@@ -72,8 +74,6 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
||||
type: "blankQuestions",
|
||||
onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } }
|
||||
});
|
||||
|
||||
const [currentExercise, setCurrentExercise] = useState<Exercise | undefined>(undefined);
|
||||
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && !showSolutions);
|
||||
const [startNow, setStartNow] = useState<boolean>(!showSolutions);
|
||||
|
||||
@@ -86,12 +86,10 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [exerciseIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentExercise === undefined && partIndex === 0 && exerciseIndex === 0) {
|
||||
setCurrentExercise(exam.parts[0].exercises[0]);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentExercise, partIndex, exerciseIndex]);
|
||||
const registerSolution = useCallback((updateSolution: () => UserSolution) => {
|
||||
userSolutionRef.current = updateSolution;
|
||||
setSolutionWasUpdated(true);
|
||||
}, []);
|
||||
|
||||
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
||||
|
||||
@@ -99,23 +97,6 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
||||
const [contextWordLines, setContextWordLines] = useState<number[] | undefined>(undefined);
|
||||
const [totalLines, setTotalLines] = useState<number>(0);
|
||||
|
||||
const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof currentSolution !== "undefined") {
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== currentSolution.exercise), { ...currentSolution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]);
|
||||
setCurrentSolutionSet(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentSolution, exam.id, exam.shuffle, shuffles, currentExercise])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof currentSolution !== "undefined") {
|
||||
setCurrentSolution(undefined);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentSolution]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showSolutions) {
|
||||
const solutionShuffles = userSolutions.map(solution => ({
|
||||
@@ -127,42 +108,53 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const getExercise = () => {
|
||||
let exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
|
||||
|
||||
const currentExercise = useMemo<Exercise>(() => {
|
||||
let exercise = exam.parts[partIndex].exercises[exerciseIndex];
|
||||
exercise = {
|
||||
...exercise,
|
||||
userSolutions: userSolutions.find((x) => x.exercise == exercise.id)?.solutions || [],
|
||||
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
||||
};
|
||||
exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles);
|
||||
return exercise;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [partIndex, exerciseIndex]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentExercise(getExercise());
|
||||
if (solutionWasUpdated && userSolutionRef.current) {
|
||||
const solution = userSolutionRef.current();
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]);
|
||||
setSolutionWasUpdated(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [partIndex, exerciseIndex, questionIndex]);
|
||||
}, [solutionWasUpdated]);
|
||||
|
||||
const next = () => {
|
||||
setNextExerciseCalled(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 nextExercise = () => {
|
||||
scrollToTop();
|
||||
|
||||
if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) {
|
||||
if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length) {
|
||||
setExerciseIndex(exerciseIndex + 1);
|
||||
setCurrentSolutionSet(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (partIndex + 1 === exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions && !continueAnyways) {
|
||||
if (partIndex + 1 === exam.parts.length && !showQuestionsModal && !showSolutions && !continueAnyways) {
|
||||
modalKwargs();
|
||||
setShowQuestionsModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
|
||||
if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.has(partIndex + 1)) {
|
||||
if (partIndex + 1 < exam.parts.length) {
|
||||
if (!answeredEveryQuestionInPart(exam, partIndex, userSolutions) && !continueAnyways && !showSolutions && !seenParts.has(partIndex + 1)) {
|
||||
modalKwargs();
|
||||
setShowQuestionsModal(true);
|
||||
return;
|
||||
@@ -181,7 +173,6 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
||||
setPartIndex(partIndex + 1);
|
||||
setExerciseIndex(0);
|
||||
setQuestionIndex(0);
|
||||
setCurrentSolutionSet(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -190,24 +181,14 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
||||
setShowQuestionsModal(true);
|
||||
}
|
||||
|
||||
setHasExamEnded(false);
|
||||
setCurrentSolutionSet(false);
|
||||
if (typeof showSolutionsSave !== "undefined") {
|
||||
onFinish(showSolutionsSave);
|
||||
} else {
|
||||
onFinish(userSolutions);
|
||||
}
|
||||
if (!showSolutions) {
|
||||
dispatch({ type: "FINALIZE_MODULE", payload: { updateTimers: true } })
|
||||
} else {
|
||||
dispatch({ type: "FINALIZE_MODULE_SOLUTIONS"})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (nextExerciseCalled && currentSolutionSet) {
|
||||
nextExercise();
|
||||
setNextExerciseCalled(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nextExerciseCalled, currentSolutionSet])
|
||||
|
||||
const previousExercise = (solution?: UserSolution) => {
|
||||
const previousExercise = () => {
|
||||
scrollToTop();
|
||||
|
||||
if (exam.parts[partIndex].context && questionIndex === 0 && !textRender && !textRenderDisabled) {
|
||||
@@ -353,25 +334,6 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
||||
}
|
||||
}
|
||||
|
||||
const answeredEveryQuestion = (partIndex: number) => {
|
||||
return exam.parts[partIndex].exercises.every((exercise) => {
|
||||
const userSolution = userSolutions.find(x => x.exercise === exercise.id);
|
||||
switch (exercise.type) {
|
||||
case 'multipleChoice':
|
||||
return userSolution?.solutions.length === exercise.questions.length;
|
||||
case 'fillBlanks':
|
||||
return userSolution?.solutions.length === exercise.words.length;
|
||||
case 'writeBlanks':
|
||||
return userSolution?.solutions.length === exercise.solutions.length;
|
||||
case 'matchSentences':
|
||||
return userSolution?.solutions.length === exercise.sentences.length;
|
||||
case 'trueFalse':
|
||||
return userSolution?.solutions.length === exercise.questions.length;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
|
||||
|
||||
@@ -408,8 +370,7 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
exerciseIndex !== -1 && currentExercise &&
|
||||
currentExercise.type === "multipleChoice" &&
|
||||
currentExercise && currentExercise.type === "multipleChoice" &&
|
||||
exam.parts[partIndex].context && contextWordLines
|
||||
) {
|
||||
if (contextWordLines.length > 0) {
|
||||
@@ -446,7 +407,7 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
||||
|
||||
if (partIndex === exam.parts.length - 1) {
|
||||
kwargs.type = "submit"
|
||||
kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestion(partIndex));
|
||||
kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestionInPart(exam, partIndex, userSolutions));
|
||||
kwargs.onClose = function (x: boolean | undefined) { if (x) { setShowSubmissionModal(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } };
|
||||
}
|
||||
setQuestionModalKwargs(kwargs);
|
||||
@@ -462,6 +423,7 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
||||
runOnClick: setQuestionIndex
|
||||
}
|
||||
|
||||
const progressButtons = <ProgressButtons handlePrevious={previousExercise} handleNext={nextExercise} />;
|
||||
|
||||
const memoizedRender = useMemo(() => {
|
||||
setChangedPrompt(false);
|
||||
@@ -473,8 +435,8 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
||||
{exam.parts[partIndex]?.context && renderText()}
|
||||
{exam.parts[partIndex]?.audio && renderAudioPlayer()}
|
||||
{(showSolutions) ?
|
||||
currentExercise && renderSolution(currentExercise, nextExercise, previousExercise) :
|
||||
currentExercise && renderExercise(currentExercise, exam.id, next, previousExercise)
|
||||
currentExercise && renderSolution(currentExercise, progressButtons, progressButtons) :
|
||||
currentExercise && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons)
|
||||
}
|
||||
</>
|
||||
}
|
||||
@@ -520,34 +482,24 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
||||
onNext={() => { setShowPartDivider(false); setStartNow(false); setBgColor("bg-white"); setSeenParts(prev => new Set(prev).add(partIndex)); }}
|
||||
/> : (
|
||||
<>
|
||||
{exam.parts[0].intro && (
|
||||
<SectionNavbar
|
||||
module="level"
|
||||
sections={exam.parts}
|
||||
sectionLabel="Part"
|
||||
sectionIndex={partIndex}
|
||||
setSectionIndex={setPartIndex}
|
||||
onClick={
|
||||
(index: number) => {
|
||||
setExerciseIndex(0);
|
||||
setQuestionIndex(0);
|
||||
if (!seenParts.has(index)) {
|
||||
setShowPartDivider(true);
|
||||
setBgColor(levelBgColor);
|
||||
setSeenParts(prev => new Set(prev).add(index));
|
||||
}
|
||||
}
|
||||
} />
|
||||
)}
|
||||
<SectionNavbar
|
||||
module="level"
|
||||
sectionLabel="Part"
|
||||
seenParts={seenParts}
|
||||
setShowPartDivider={setShowPartDivider}
|
||||
setSeenParts={setSeenParts}
|
||||
preview={preview}
|
||||
/>
|
||||
<ModuleTitle
|
||||
examLabel={exam.label}
|
||||
partLabel={partLabel()}
|
||||
minTimer={exam.minTimer}
|
||||
minTimer={timer.current}
|
||||
exerciseIndex={calculateExerciseIndex()}
|
||||
module="level"
|
||||
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
||||
disableTimer={showSolutions}
|
||||
showTimer={false}
|
||||
preview={preview}
|
||||
{...mcNavKwargs}
|
||||
/>
|
||||
<div
|
||||
@@ -563,3 +515,5 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Level;
|
||||
|
||||
Reference in New Issue
Block a user