Exam generation rework, batch user tables, fastapi endpoint switch
This commit is contained in:
@@ -1,38 +0,0 @@
|
||||
import Button from "@/components/Low/Button";
|
||||
import { Module } from "@/interfaces";
|
||||
import { LevelPart, UserSolution } from "@/interfaces/exam";
|
||||
import clsx from "clsx";
|
||||
import { ReactNode } from "react";
|
||||
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs";
|
||||
|
||||
interface Props {
|
||||
partIndex: number;
|
||||
part: LevelPart // for now
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
const PartDivider: React.FC<Props> = ({ partIndex, part, onNext }) => {
|
||||
|
||||
const moduleIcon: { [key in Module]: ReactNode } = {
|
||||
reading: <BsBook className="text-ielts-reading w-6 h-6" />,
|
||||
listening: <BsHeadphones className="text-ielts-listening w-6 h-6" />,
|
||||
writing: <BsPen className="text-ielts-writing w-6 h-6" />,
|
||||
speaking: <BsMegaphone className="text-ielts-speaking w-6 h-6" />,
|
||||
level: <BsClipboard className="text-white w-6 h-6" />,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx("flex flex-col h-fit border bg-white rounded-3xl p-12 gap-8", part.intro ? "w-3/6" : "items-center my-auto")}>
|
||||
{/** only level for now */}
|
||||
<div className="flex flex-row gap-4 items-center"><div className="w-12 h-12 bg-ielts-level flex items-center justify-center rounded-lg">{moduleIcon["level"]}</div><p className="text-3xl">{part.intro ? `Part ${partIndex + 1}` : "Placement Test"}</p></div>
|
||||
{part.intro && part.intro.split('\\n\\n').map((x, index) => <p key={`line-${index}`} className="text-2xl text-clip" dangerouslySetInnerHTML={{__html: x.replace('that is not correct', 'that is <span class="font-bold"><u>not correct</u></span>')}}></p>)}
|
||||
<div className="flex items-center justify-center mt-4">
|
||||
<Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full text-2xl">
|
||||
{partIndex === 0 ? `Start now`: `Start Part ${partIndex + 1}`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PartDivider;
|
||||
@@ -147,9 +147,3 @@ function fisherYatesShuffle<T>(array: T[]): T[] {
|
||||
}
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
|
||||
return Array.isArray(words) && words.every(
|
||||
word => word && typeof word === 'object' && 'id' in word && 'options' in word
|
||||
);
|
||||
}
|
||||
@@ -6,34 +6,33 @@ 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 { countExercises } from "@/utils/moduleUtils";
|
||||
import clsx from "clsx";
|
||||
import { use, useEffect, useMemo, useState } from "react";
|
||||
import TextComponent from "./TextComponent";
|
||||
import PartDivider from "./PartDivider";
|
||||
import PartDivider from "../Navigation/SectionDivider";
|
||||
import Timer from "@/components/Medium/Timer";
|
||||
import shuffleExamExercise from "./Shuffle";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import Modal from "@/components/Modal";
|
||||
import { typeCheckWordsMC } from "@/utils/type.check";
|
||||
import SectionNavbar from "../Navigation/SectionNavbar";
|
||||
|
||||
interface Props {
|
||||
exam: LevelExam;
|
||||
showSolutions?: boolean;
|
||||
onFinish: (userSolutions: UserSolution[]) => void;
|
||||
editing?: boolean;
|
||||
preview?: 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) {
|
||||
export default function Level({ exam, showSolutions = false, onFinish, preview = false }: Props) {
|
||||
const levelBgColor = "bg-ielts-level-light";
|
||||
|
||||
const examState = useExamStore((state) => state);
|
||||
const persistentExamState = usePersistentExamStore((state) => state);
|
||||
|
||||
const {
|
||||
userSolutions,
|
||||
hasExamEnded,
|
||||
@@ -50,7 +49,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
setQuestionIndex,
|
||||
setShuffles,
|
||||
setCurrentSolution
|
||||
} = useExamStore((state) => state);
|
||||
} = !preview ? examState : persistentExamState;
|
||||
|
||||
// In case client want to switch back
|
||||
const textRenderDisabled = true;
|
||||
@@ -74,7 +73,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
|
||||
const [currentExercise, setCurrentExercise] = useState<Exercise | undefined>(undefined);
|
||||
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && !showSolutions);
|
||||
const [startNow, setStartNow] = useState<boolean>(true && !showSolutions);
|
||||
const [startNow, setStartNow] = useState<boolean>(!showSolutions);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentExercise === undefined && partIndex === 0 && exerciseIndex === 0) {
|
||||
@@ -175,7 +174,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
return;
|
||||
}
|
||||
|
||||
if (partIndex + 1 === exam.parts.length && exerciseIndex === exam.parts[partIndex].exercises.length - 1 && !continueAnyways) {
|
||||
if (partIndex + 1 === exam.parts.length && exerciseIndex === exam.parts[partIndex].exercises.length - 1 && !continueAnyways && !showSolutions) {
|
||||
modalKwargs();
|
||||
setShowQuestionsModal(true);
|
||||
}
|
||||
@@ -414,9 +413,9 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
exam: exam,
|
||||
partIndex: partIndex,
|
||||
showSolutions: showSolutions,
|
||||
"setExerciseIndex": setExerciseIndex,
|
||||
"setPartIndex": setPartIndex,
|
||||
"runOnClick": setQuestionIndex
|
||||
setExerciseIndex: setExerciseIndex,
|
||||
setPartIndex: setPartIndex,
|
||||
runOnClick: setQuestionIndex
|
||||
}
|
||||
|
||||
|
||||
@@ -427,8 +426,8 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
{textRender && !textRenderDisabled ?
|
||||
renderText() :
|
||||
<>
|
||||
{exam.parts[partIndex].context && renderText()}
|
||||
{(showSolutions || editing) ?
|
||||
{exam.parts[partIndex]?.context && renderText()}
|
||||
{(showSolutions) ?
|
||||
currentExercise && renderSolution(currentExercise, nextExercise, previousExercise) :
|
||||
currentExercise && renderExercise(currentExercise, exam.id, next, previousExercise)
|
||||
}
|
||||
@@ -465,64 +464,55 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
!(partIndex === 0 && questionIndex === 0 && (showPartDivider || startNow)) &&
|
||||
<Timer minTimer={exam.minTimer} disableTimer={showSolutions} standalone={true} />
|
||||
}
|
||||
{(showPartDivider || startNow) ? <PartDivider part={exam.parts[partIndex]} partIndex={partIndex} onNext={() => { setShowPartDivider(false); setStartNow(false); setBgColor("bg-white"); }} /> : (
|
||||
<>
|
||||
{exam.parts[0].intro && (
|
||||
<div className="w-full">
|
||||
<Tab.Group className="w-[90%]" selectedIndex={partIndex} onChange={setPartIndex}>
|
||||
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-level/20 p-1">
|
||||
{exam.parts.map((_, index) =>
|
||||
<Tab key={index} onClick={(e) => {
|
||||
/*
|
||||
// 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}`}</Tab>
|
||||
)
|
||||
{(showPartDivider || startNow) ?
|
||||
<PartDivider
|
||||
module="level"
|
||||
sectionLabel="Part"
|
||||
defaultTitle="Placement Test"
|
||||
section={exam.parts[partIndex]}
|
||||
sectionIndex={partIndex}
|
||||
onNext={() => { setShowPartDivider(false); setStartNow(false); setBgColor("bg-white"); }}
|
||||
/> : (
|
||||
<>
|
||||
{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));
|
||||
}
|
||||
}
|
||||
</Tab.List>
|
||||
</Tab.Group>
|
||||
} />
|
||||
)}
|
||||
<ModuleTitle
|
||||
examLabel={exam.label}
|
||||
partLabel={partLabel()}
|
||||
minTimer={exam.minTimer}
|
||||
exerciseIndex={calculateExerciseIndex()}
|
||||
module="level"
|
||||
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
||||
disableTimer={showSolutions}
|
||||
showTimer={false}
|
||||
{...mcNavKwargs}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
"mb-20 w-full",
|
||||
!!exam.parts[partIndex].context && !textRender && "grid grid-cols-2 gap-4",
|
||||
)}>
|
||||
{memoizedRender}
|
||||
</div>
|
||||
)}
|
||||
<ModuleTitle
|
||||
examLabel={exam.label}
|
||||
partLabel={partLabel()}
|
||||
minTimer={exam.minTimer}
|
||||
exerciseIndex={calculateExerciseIndex()}
|
||||
module="level"
|
||||
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
||||
disableTimer={showSolutions || editing}
|
||||
showTimer={false}
|
||||
{...mcNavKwargs}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
"mb-20 w-full",
|
||||
!!exam.parts[partIndex].context && !textRender && "grid grid-cols-2 gap-4",
|
||||
)}>
|
||||
{memoizedRender}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
50
src/exams/Navigation/SectionDivider.tsx
Normal file
50
src/exams/Navigation/SectionDivider.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import Button from "@/components/Low/Button";
|
||||
import { Module } from "@/interfaces";
|
||||
import { LevelPart, ListeningPart, ReadingPart, SpeakingExercise, UserSolution, WritingExercise } from "@/interfaces/exam";
|
||||
import clsx from "clsx";
|
||||
import { ReactNode } from "react";
|
||||
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs";
|
||||
|
||||
interface Props {
|
||||
sectionIndex: number;
|
||||
sectionLabel: string;
|
||||
defaultTitle: string;
|
||||
module: Module;
|
||||
section: LevelPart | ReadingPart | ListeningPart | WritingExercise | SpeakingExercise;
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
const PartDivider: React.FC<Props> = ({ sectionIndex, sectionLabel, section, module, defaultTitle, onNext }) => {
|
||||
const iconStyle = "text-white w-6 h-6";
|
||||
const moduleIcon: { [key in Module]: ReactNode } = {
|
||||
reading: <BsBook className={iconStyle} />,
|
||||
listening: <BsHeadphones className={iconStyle} />,
|
||||
writing: <BsPen className={iconStyle} />,
|
||||
speaking: <BsMegaphone className={iconStyle} />,
|
||||
level: <BsClipboard className={iconStyle} />,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx("flex flex-col h-fit border bg-white rounded-3xl p-12 gap-8", section.intro ? "w-3/6" : "items-center my-auto")}>
|
||||
<div className="flex flex-row gap-4 items-center">
|
||||
<div className={`w-12 h-12 bg-ielts-${module} flex items-center justify-center rounded-lg`}>{moduleIcon[module]}</div>
|
||||
<p className="text-3xl">{section.intro ? `${sectionLabel} ${sectionIndex + 1}` : defaultTitle}</p>
|
||||
</div>
|
||||
{section.intro && section.intro.split('\\n\\n').map((x, index) => <p key={`line-${index}`} className="text-2xl text-clip" dangerouslySetInnerHTML={{ __html: x.replace('that is not correct', 'that is <span class="font-bold"><u>not correct</u></span>') }}></p>)}
|
||||
<div className="flex items-center justify-center mt-4">
|
||||
<button
|
||||
onClick={() => onNext()}
|
||||
className={clsx(
|
||||
"max-w-[200px] self-end w-full text-2xl text-white",
|
||||
"rounded-full transition ease-in-out duration-300 disabled:cursor-not-allowed cursor-pointer select-none",
|
||||
"py-4 px-6",
|
||||
`bg-ielts-${module} hover:bg-ielts-${module}/70`
|
||||
)}>
|
||||
{sectionIndex === 0 ? `Start now` : `Start ${sectionLabel} ${sectionIndex + 1}`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PartDivider;
|
||||
42
src/exams/Navigation/SectionNavbar.tsx
Normal file
42
src/exams/Navigation/SectionNavbar.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Module } from "@/interfaces";
|
||||
import { LevelPart, ListeningPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam";
|
||||
import { Tab, TabGroup, TabList } from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
interface Props {
|
||||
module: Module;
|
||||
sections: LevelPart[] | ReadingPart[] | ListeningPart[] | WritingExercise[] | SpeakingExercise[];
|
||||
sectionIndex: number;
|
||||
sectionLabel: string;
|
||||
setSectionIndex: (index: number) => void;
|
||||
onClick: (index: number) => void;
|
||||
}
|
||||
|
||||
const SectionNavbar: React.FC<Props> = ({module, sections, sectionIndex, sectionLabel, setSectionIndex, onClick}) => {
|
||||
return (
|
||||
|
||||
<div className="w-full">
|
||||
<TabGroup className="w-[90%]" selectedIndex={sectionIndex} onChange={setSectionIndex}>
|
||||
<TabList className={`flex space-x-1 rounded-xl bg-ielts-${module}/20 p-1`}>
|
||||
{sections.map((_, index) =>
|
||||
<Tab key={index} onClick={() => onClick(index)}
|
||||
className={({ selected }) =>
|
||||
clsx(
|
||||
`w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-${module}/80`,
|
||||
"ring-white ring-opacity-60 focus:outline-none",
|
||||
"transition duration-300 ease-in-out hover:bg-white/70",
|
||||
selected && "bg-white shadow",
|
||||
)
|
||||
}
|
||||
>{`${sectionLabel} ${index + 1}`}</Tab>
|
||||
)
|
||||
}
|
||||
</TabList>
|
||||
</TabGroup>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default SectionNavbar;
|
||||
@@ -16,13 +16,14 @@ import ModuleTitle from "@/components/Medium/ModuleTitle";
|
||||
import {Divider} from "primereact/divider";
|
||||
import Button from "@/components/Low/Button";
|
||||
import BlankQuestionsModal from "@/components/QuestionsModal";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import useExamStore, { usePersistentExamStore } from "@/stores/examStore";
|
||||
import {defaultUserSolutions} from "@/utils/exams";
|
||||
import {countExercises} from "@/utils/moduleUtils";
|
||||
|
||||
interface Props {
|
||||
exam: ReadingExam;
|
||||
showSolutions?: boolean;
|
||||
preview?: boolean;
|
||||
onFinish: (userSolutions: UserSolution[]) => void;
|
||||
}
|
||||
|
||||
@@ -105,18 +106,30 @@ function TextComponent({part, exerciseType}: {part: ReadingPart; exerciseType: s
|
||||
);
|
||||
}
|
||||
|
||||
export default function Reading({exam, showSolutions = false, onFinish}: Props) {
|
||||
export default function Reading({exam, showSolutions = false, preview = false, onFinish}: Props) {
|
||||
const [showTextModal, setShowTextModal] = useState(false);
|
||||
const [showBlankModal, setShowBlankModal] = useState(false);
|
||||
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{id: string; amount: number}[]>([]);
|
||||
const [isTextMinimized, setIsTextMinimzed] = useState(false);
|
||||
const [exerciseType, setExerciseType] = useState("");
|
||||
|
||||
const {userSolutions, setUserSolutions} = useExamStore((state) => state);
|
||||
const {hasExamEnded, setHasExamEnded} = useExamStore((state) => state);
|
||||
const {partIndex, setPartIndex} = useExamStore((state) => state);
|
||||
const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state);
|
||||
const [storeQuestionIndex, setStoreQuestionIndex] = useExamStore((state) => [state.questionIndex, state.setQuestionIndex]);
|
||||
const examState = useExamStore((state) => state);
|
||||
const persistentExamState = usePersistentExamStore((state) => state);
|
||||
|
||||
const {
|
||||
hasExamEnded,
|
||||
userSolutions,
|
||||
exerciseIndex,
|
||||
partIndex,
|
||||
questionIndex: storeQuestionIndex,
|
||||
setBgColor,
|
||||
setUserSolutions,
|
||||
setHasExamEnded,
|
||||
setExerciseIndex,
|
||||
setPartIndex,
|
||||
setQuestionIndex: setStoreQuestionIndex
|
||||
} = !preview ? examState : persistentExamState;
|
||||
|
||||
|
||||
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
||||
|
||||
@@ -308,7 +321,7 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props)
|
||||
partIndex > -1 &&
|
||||
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
||||
!showSolutions &&
|
||||
renderExercise(getExercise(), exam.id, nextExercise, previousExercise)}
|
||||
renderExercise(getExercise(), exam.id, nextExercise, previousExercise, undefined, preview)}
|
||||
|
||||
{exerciseIndex > -1 &&
|
||||
partIndex > -1 &&
|
||||
|
||||
@@ -1,34 +1,52 @@
|
||||
import {renderExercise} from "@/components/Exercises";
|
||||
import { renderExercise } from "@/components/Exercises";
|
||||
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
||||
import {renderSolution} from "@/components/Solutions";
|
||||
import {infoButtonStyle} from "@/constants/buttonStyles";
|
||||
import {UserSolution, WritingExam} from "@/interfaces/exam";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import {defaultUserSolutions} from "@/utils/exams";
|
||||
import {countExercises} from "@/utils/moduleUtils";
|
||||
import {mdiArrowRight} from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import clsx from "clsx";
|
||||
import {Fragment, useEffect, useState} from "react";
|
||||
import {toast} from "react-toastify";
|
||||
import { renderSolution } from "@/components/Solutions";
|
||||
import { UserSolution, WritingExam } from "@/interfaces/exam";
|
||||
import useExamStore, { usePersistentExamStore } from "@/stores/examStore";
|
||||
import { countExercises } from "@/utils/moduleUtils";
|
||||
import PartDivider from "./Navigation/SectionDivider";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
exam: WritingExam;
|
||||
showSolutions?: boolean;
|
||||
preview?: boolean;
|
||||
onFinish: (userSolutions: UserSolution[]) => void;
|
||||
}
|
||||
|
||||
export default function Writing({exam, showSolutions = false, onFinish}: Props) {
|
||||
const {userSolutions, setUserSolutions} = useExamStore((state) => state);
|
||||
const {hasExamEnded, setHasExamEnded} = useExamStore((state) => state);
|
||||
const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state);
|
||||
export default function Writing({ exam, showSolutions = false, preview = false, onFinish }: Props) {
|
||||
const writingBgColor = "bg-ielts-writing-light";
|
||||
|
||||
const examState = useExamStore((state) => state);
|
||||
const persistentExamState = usePersistentExamStore((state) => state);
|
||||
|
||||
const {
|
||||
userSolutions,
|
||||
exerciseIndex,
|
||||
setBgColor,
|
||||
setUserSolutions,
|
||||
setHasExamEnded,
|
||||
setExerciseIndex,
|
||||
} = !preview ? examState : persistentExamState;
|
||||
|
||||
const [seenParts, setSeenParts] = useState<Set<number>>(new Set(showSolutions ? exam.exercises.map((_, index) => index) : []));
|
||||
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.exercises[0].intro === "string" && exam.exercises[0].intro !== "");
|
||||
|
||||
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
||||
|
||||
useEffect(() => {
|
||||
if (!showSolutions && exam.exercises[exerciseIndex]?.intro !== undefined && exam.exercises[exerciseIndex]?.intro !== "" && !seenParts.has(exerciseIndex)) {
|
||||
setShowPartDivider(true);
|
||||
setBgColor(writingBgColor);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [exerciseIndex]);
|
||||
|
||||
|
||||
const nextExercise = (solution?: UserSolution) => {
|
||||
scrollToTop();
|
||||
if (solution) {
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "writing", exam: exam.id}]);
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "writing", exam: exam.id }]);
|
||||
}
|
||||
|
||||
if (exerciseIndex + 1 < exam.exercises.length) {
|
||||
@@ -41,7 +59,7 @@ export default function Writing({exam, showSolutions = false, onFinish}: Props)
|
||||
setHasExamEnded(false);
|
||||
|
||||
if (solution) {
|
||||
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "writing", exam: exam.id}]);
|
||||
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "writing", exam: exam.id }]);
|
||||
} else {
|
||||
onFinish(userSolutions);
|
||||
}
|
||||
@@ -50,7 +68,7 @@ export default function Writing({exam, showSolutions = false, onFinish}: Props)
|
||||
const previousExercise = (solution?: UserSolution) => {
|
||||
scrollToTop();
|
||||
if (solution) {
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "writing", exam: exam.id}]);
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "writing", exam: exam.id }]);
|
||||
}
|
||||
|
||||
if (exerciseIndex > 0) {
|
||||
@@ -68,23 +86,34 @@ export default function Writing({exam, showSolutions = false, onFinish}: Props)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col h-full w-full gap-8 items-center">
|
||||
<ModuleTitle
|
||||
minTimer={exam.minTimer}
|
||||
exerciseIndex={exerciseIndex + 1}
|
||||
{(showPartDivider) ?
|
||||
<PartDivider
|
||||
module="writing"
|
||||
totalExercises={countExercises(exam.exercises)}
|
||||
disableTimer={showSolutions}
|
||||
/>
|
||||
{exerciseIndex > -1 &&
|
||||
exerciseIndex < exam.exercises.length &&
|
||||
!showSolutions &&
|
||||
renderExercise(getExercise(), exam.id, nextExercise, previousExercise)}
|
||||
{exerciseIndex > -1 &&
|
||||
exerciseIndex < exam.exercises.length &&
|
||||
showSolutions &&
|
||||
renderSolution(exam.exercises[exerciseIndex], nextExercise, previousExercise)}
|
||||
</div>
|
||||
sectionLabel="Task"
|
||||
defaultTitle="Writing exam"
|
||||
section={exam.exercises[exerciseIndex]}
|
||||
sectionIndex={exerciseIndex}
|
||||
onNext={() => { setShowPartDivider(false); setBgColor("bg-white"); setSeenParts((prev) => new Set(prev).add(exerciseIndex))}}
|
||||
/> : (
|
||||
<div className="flex flex-col h-full w-full gap-8 items-center">
|
||||
<ModuleTitle
|
||||
minTimer={exam.minTimer}
|
||||
exerciseIndex={exerciseIndex + 1}
|
||||
module="writing"
|
||||
totalExercises={countExercises(exam.exercises)}
|
||||
disableTimer={showSolutions || preview}
|
||||
/>
|
||||
{exerciseIndex > -1 &&
|
||||
exerciseIndex < exam.exercises.length &&
|
||||
!showSolutions &&
|
||||
renderExercise(getExercise(), exam.id, nextExercise, previousExercise, preview)}
|
||||
{exerciseIndex > -1 &&
|
||||
exerciseIndex < exam.exercises.length &&
|
||||
showSolutions &&
|
||||
renderSolution(exam.exercises[exerciseIndex], nextExercise, previousExercise)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user