import QuestionsModal from "@/components/QuestionsModal"; import { renderExercise } from "@/components/Exercises"; import Button from "@/components/Low/Button"; 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 { countExercises } from "@/utils/moduleUtils"; import clsx from "clsx"; import { use, useEffect, useMemo, useState } from "react"; import TextComponent from "./TextComponent"; import PartDivider from "./PartDivider"; import Timer from "@/components/Medium/Timer"; import shuffleExamExercise from "./Shuffle"; import { Tab } from "@headlessui/react"; import Modal from "@/components/Modal"; interface Props { exam: LevelExam; showSolutions?: boolean; onFinish: (userSolutions: UserSolution[]) => void; editing?: boolean; partDividers?: boolean; } const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => { return Array.isArray(words) && words.every( word => word && typeof word === 'object' && 'id' in word && 'options' in word ); } export default function Level({ exam, showSolutions = false, onFinish, editing = false }: Props) { const levelBgColor = "bg-ielts-level-light"; const { userSolutions, hasExamEnded, partIndex, exerciseIndex, questionIndex, shuffles, currentSolution, setBgColor, setUserSolutions, setHasExamEnded, setPartIndex, setExerciseIndex, setQuestionIndex, setShuffles, setCurrentSolution } = useExamStore((state) => state); // In case client want to switch back const textRenderDisabled = true; const [showSubmissionModal, setShowSubmissionModal] = useState(false); const [showQuestionsModal, setShowQuestionsModal] = useState(false); 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>(new Set(showSolutions ? exam.parts.map((_, index) => index) : [0])); const [questionModalKwargs, setQuestionModalKwargs] = useState<{ type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined; }>({ type: "blankQuestions", onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } } }); const [currentExercise, setCurrentExercise] = useState(undefined); const [showPartDivider, setShowPartDivider] = useState(typeof exam.parts[0].intro === "string" && !showSolutions); const [startNow, setStartNow] = useState(true && !showSolutions); 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 scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); const [contextWords, setContextWords] = useState<{ match: string, originalLine: string }[] | undefined>(undefined); const [contextWordLines, setContextWordLines] = useState(undefined); const [totalLines, setTotalLines] = useState(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 => ({ exerciseID: solution.exercise, shuffles: solution.shuffleMaps || [] })); setShuffles(solutionShuffles); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const getExercise = () => { let exercise = exam.parts[partIndex]?.exercises[exerciseIndex]; exercise = { ...exercise, userSolutions: userSolutions.find((x) => x.exercise == exercise.id)?.solutions || [], }; exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles); return exercise; }; useEffect(() => { setCurrentExercise(getExercise()); // eslint-disable-next-line react-hooks/exhaustive-deps }, [partIndex, exerciseIndex, questionIndex]); const next = () => { setNextExerciseCalled(true); } const nextExercise = () => { scrollToTop(); if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) { setExerciseIndex(exerciseIndex + 1); setCurrentSolutionSet(false); return; } if (partIndex + 1 === exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions && !continueAnyways) { modalKwargs(); setShowQuestionsModal(true); return; } if (partIndex + 1 < exam.parts.length && !hasExamEnded) { if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.has(partIndex + 1)) { modalKwargs(); setShowQuestionsModal(true); return; } if (!showSolutions && exam.parts[0].intro && !seenParts.has(partIndex + 1)) { setShowPartDivider(true); setBgColor(levelBgColor); } setSeenParts(prev => new Set(prev).add(partIndex + 1)); if (partIndex < exam.parts.length - 1 && exam.parts[partIndex + 1].context && !textRenderDisabled) { setTextRender(true); } setPartIndex(partIndex + 1); setExerciseIndex(0); setQuestionIndex(0); setCurrentSolutionSet(false); return; } if (partIndex + 1 === exam.parts.length && exerciseIndex === exam.parts[partIndex].exercises.length - 1 && !continueAnyways) { modalKwargs(); setShowQuestionsModal(true); } setHasExamEnded(false); setCurrentSolutionSet(false); if (typeof showSolutionsSave !== "undefined") { onFinish(showSolutionsSave); } else { onFinish(userSolutions); } } useEffect(() => { if (nextExerciseCalled && currentSolutionSet) { nextExercise(); setNextExerciseCalled(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [nextExerciseCalled, currentSolutionSet]) const previousExercise = (solution?: UserSolution) => { scrollToTop(); if (exam.parts[partIndex].context && questionIndex === 0 && !textRender && !textRenderDisabled) { setTextRender(true); return; } if (questionIndex == 0) { setPartIndex(partIndex - 1); if (!seenParts.has(partIndex - 1)) { setBgColor(levelBgColor); setShowPartDivider(true); setQuestionIndex(0); setSeenParts(prev => new Set(prev).add(partIndex - 1)); return; } const lastExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1; const lastExercise = exam.parts[partIndex - 1].exercises[lastExerciseIndex]; setExerciseIndex(lastExerciseIndex); if (lastExercise.type === "multipleChoice") { setQuestionIndex(lastExercise.questions.length - 1) } else { setQuestionIndex(0) } return; } setExerciseIndex(exerciseIndex - 1); if (exerciseIndex - 1 === -1) { setPartIndex(partIndex - 1); const lastPartExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1; const previousExercise = exam.parts[partIndex - 1].exercises[lastPartExerciseIndex]; if (previousExercise.type === "multipleChoice") { setQuestionIndex(previousExercise.questions.length - 1) } } }; const calculateExerciseIndex = () => { return exam.parts.reduce((acc, curr, index) => { if (index < partIndex) { return acc + countExercises(curr.exercises) } return acc; }, 0) + (questionIndex + 1); }; const renderText = () => ( <>
<>
{textRender && !textRenderDisabled ? ( <>

Please read the following excerpt attentively, you will then be asked questions about the text you've read.

You will be allowed to read the text while doing the exercises ) : (

Answer the questions on the right based on what you've read.

)}
{exam.parts[partIndex].context && }
{textRender && !textRenderDisabled && (
)} ); const partLabel = () => { const partCategory = exam.parts[partIndex].category ? ` (${exam.parts[partIndex].category})` : ''; if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words)) return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${currentExercise.words[currentExercise.words.length - 1].id})${partCategory}\n\n${currentExercise.prompt}` if (currentExercise?.type === "multipleChoice") { return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${currentExercise.questions[currentExercise.questions.length - 1].id})${partCategory}\n\n${currentExercise.prompt}` } if (typeof exam.parts[partIndex].context === "string") { const nextExercise = exam.parts[partIndex].exercises[0] as MultipleChoiceExercise; return `Part ${partIndex + 1} (Questions ${nextExercise.questions[0].id} - ${nextExercise.questions[nextExercise.questions.length - 1].id})${partCategory}\n\n${nextExercise.prompt}` } } 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+)\?$/; const findMatch = (index: number) => { if (currentExercise && currentExercise.type === "multipleChoice" && currentExercise!.questions[index]) { const match = currentExercise!.questions[index].prompt.match(regex); if (match) { return { match: match[1], originalLine: match[2] } } } return; } // if the client for some whatever random reason decides // to add more questions update this const numberOfQuestions = 2; if (exam.parts[partIndex].context) { const hits = Array.from({ length: numberOfQuestions }).reduce<{ match: string, originalLine: string }[]>((acc, _, i) => { const result = findMatch(questionIndex + i); if (!!result) { acc.push(result); } return acc; }, []); if (hits.length > 0) { setContextWords(hits) } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentExercise, questionIndex, totalLines]); useEffect(() => { if ( exerciseIndex !== -1 && currentExercise && currentExercise.type === "multipleChoice" && exam.parts[partIndex].context && contextWordLines ) { if (contextWordLines.length > 0) { contextWordLines.forEach((n, i) => { if (contextWords && contextWords[i] && n !== -1) { const updatedPrompt = currentExercise!.questions[questionIndex + i].prompt.replace( `in line ${contextWords[i].originalLine}`, `in line ${n}` ); currentExercise!.questions[questionIndex + i].prompt = updatedPrompt; } }) setChangedPrompt(true); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [contextWordLines]); useEffect(() => { if (continueAnyways) { setContinueAnyways(false); nextExercise(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [continueAnyways]); const modalKwargs = () => { const kwargs: { type: "module" | "blankQuestions" | "submit", unanswered: boolean, onClose: (next?: boolean) => void; } = { type: "blankQuestions", unanswered: false, onClose: function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } } }; if (partIndex === exam.parts.length - 1) { kwargs.type = "submit" kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestion(partIndex)); kwargs.onClose = function (x: boolean | undefined) { if (x) { setShowSubmissionModal(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } }; } setQuestionModalKwargs(kwargs); } const mcNavKwargs = { userSolutions: userSolutions, exam: exam, partIndex: partIndex, showSolutions: showSolutions, "setExerciseIndex": setExerciseIndex, "setPartIndex": setPartIndex, "runOnClick": setQuestionIndex } const memoizedRender = useMemo(() => { setChangedPrompt(false); return ( <> {textRender && !textRenderDisabled ? renderText() : <> {exam.parts[partIndex].context && renderText()} {(showSolutions || editing) ? currentExercise && renderSolution(currentExercise, nextExercise, previousExercise) : currentExercise && renderExercise(currentExercise, exam.id, next, previousExercise) } } ) // eslint-disable-next-line react-hooks/exhaustive-deps }, [textRender, currentExercise, changedPrompt]); return ( <>
{ }} title={"Confirm Submission"} > <>

Are you sure you want to proceed with the submission?

{ !(partIndex === 0 && questionIndex === 0 && (showPartDivider || startNow)) && } {(showPartDivider || startNow) ? { setShowPartDivider(false); setStartNow(false); setBgColor("bg-white"); }} /> : ( <> {exam.parts[0].intro && (
{exam.parts.map((_, index) => { /* // If client wants to revert uncomment and remove the added if statement if (!seenParts.has(index)) { e.preventDefault(); } else { */ setExerciseIndex(0); setQuestionIndex(0); if (!seenParts.has(index)) { setShowPartDivider(true); setBgColor(levelBgColor); setSeenParts(prev => new Set(prev).add(index)); } }} className={({ selected }) => clsx( "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-level/80", "ring-white ring-opacity-60 focus:outline-none", "transition duration-300 ease-in-out hover:bg-white/70", selected && "bg-white shadow", // seenParts.includes(index) ? "hover:bg-white/70" : "cursor-not-allowed" ) } >{`Part ${index + 1}`} ) }
)} x.exercises))} disableTimer={showSolutions || editing} showTimer={false} {...mcNavKwargs} />
{memoizedRender}
)}
); }