Navigation rework, added prompt edit to components that were missing

This commit is contained in:
Carlos-Mesquita
2024-11-25 16:50:46 +00:00
parent e9b7bd14cc
commit 114da173be
105 changed files with 3761 additions and 3728 deletions

View File

@@ -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;