Merge remote-tracking branch 'origin/develop' into feature/ExamGenRework

This commit is contained in:
Carlos-Mesquita
2024-11-12 14:28:51 +00:00
27 changed files with 836 additions and 755 deletions

BIN
public/blue-stock-photo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

View File

@@ -11,6 +11,7 @@ import MCDropdown from "./MCDropdown";
const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
id, id,
type, type,
isPractice = false,
prompt, prompt,
solutions, solutions,
text, text,
@@ -20,6 +21,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
preview, preview,
onNext, onNext,
onBack, onBack,
disableProgressButtons = false
}) => { }) => {
const examState = useExamStore((state) => state); const examState = useExamStore((state) => state);
@@ -39,6 +41,11 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles; const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles;
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers, disableProgressButtons])
const excludeWordMCType = (x: any) => { const excludeWordMCType = (x: any) => {
return typeof x === "string" ? x : (x as { letter: string; word: string }); return typeof x === "string" ? x : (x as { letter: string; word: string });
}; };
@@ -163,13 +170,12 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers]); }, [answers]);
return ( const progressButtons = () => (
<div className="flex flex-col gap-4">
<div className="flex justify-between w-full gap-8"> <div className="flex justify-between w-full gap-8">
<Button <Button
color="purple" color="purple"
variant="outline" variant="outline"
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })} onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps, isPractice })}
className="max-w-[200px] w-full" className="max-w-[200px] w-full"
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}> disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
Previous Page Previous Page
@@ -178,14 +184,19 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
<Button <Button
color="purple" color="purple"
onClick={() => { onClick={() => {
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps }); onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps, isPractice });
}} }}
className="max-w-[200px] self-end w-full"> className="max-w-[200px] self-end w-full">
Next Page Next Page
</Button> </Button>
</div> </div>
)
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20"> return (
<div className="flex flex-col gap-4">
{!disableProgressButtons && progressButtons()}
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", !disableProgressButtons && "mb-20")}>
{variant !== "mc" && ( {variant !== "mc" && (
<span className="text-sm w-full leading-6"> <span className="text-sm w-full leading-6">
{prompt.split("\\n").map((line, index) => ( {prompt.split("\\n").map((line, index) => (
@@ -224,25 +235,8 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
</div> </div>
)} )}
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button
color="purple"
variant="outline"
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })}
className="max-w-[200px] w-full"
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
Previous Page
</Button>
<Button {!disableProgressButtons && progressButtons()}
color="purple"
onClick={() => {
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
}}
className="max-w-[200px] self-end w-full">
Next Page
</Button>
</div>
</div> </div>
); );
}; };

View File

@@ -24,6 +24,7 @@ export default function InteractiveSpeaking({
userSolutions, userSolutions,
onNext, onNext,
onBack, onBack,
isPractice = false,
preview = false preview = false
}: InteractiveSpeakingExercise & CommonProps) { }: InteractiveSpeakingExercise & CommonProps) {
const [recordingDuration, setRecordingDuration] = useState(0); const [recordingDuration, setRecordingDuration] = useState(0);
@@ -54,6 +55,7 @@ export default function InteractiveSpeaking({
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer], solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
score: { correct: 100, total: 100, missing: 0 }, score: { correct: 100, total: 100, missing: 0 },
type, type,
isPractice
}); });
}; };
@@ -76,6 +78,7 @@ export default function InteractiveSpeaking({
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer], solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
score: { correct: 100, total: 100, missing: 0 }, score: { correct: 100, total: 100, missing: 0 },
type, type,
isPractice
}); });
}; };
@@ -146,6 +149,7 @@ export default function InteractiveSpeaking({
module: "speaking", module: "speaking",
exam: examID, exam: examID,
type, type,
isPractice
}, },
]); ]);

View File

@@ -63,7 +63,18 @@ function DraggableOptionArea({option}: {option: MatchSentenceExerciseOption}) {
); );
} }
export default function MatchSentences({id, options, type, prompt, sentences, userSolutions, onNext, onBack}: MatchSentencesExercise & CommonProps) { export default function MatchSentences({
id,
options,
type,
prompt,
sentences,
userSolutions,
onNext,
onBack,
isPractice = false,
disableProgressButtons = false
}: MatchSentencesExercise & CommonProps) {
const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions); const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions);
const hasExamEnded = useExamStore((state) => state.hasExamEnded); const hasExamEnded = useExamStore((state) => state.hasExamEnded);
@@ -71,7 +82,7 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution); const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
useEffect(() => { useEffect(() => {
setCurrentSolution({exercise: id, solutions: answers, score: calculateScore(), type}); setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers, setAnswers]); }, [answers, setAnswers]);
@@ -95,30 +106,39 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
}; };
useEffect(() => { useEffect(() => {
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type}); if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers, disableProgressButtons])
useEffect(() => {
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasExamEnded]); }, [hasExamEnded]);
return ( const progressButtons = () => (
<div className="flex flex-col gap-4 mt-4">
<div className="flex justify-between w-full gap-8"> <div className="flex justify-between w-full gap-8">
<Button <Button
color="purple" color="purple"
variant="outline" variant="outline"
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})} onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice })}
className="max-w-[200px] w-full"> className="max-w-[200px] w-full">
Back Back
</Button> </Button>
<Button <Button
color="purple" color="purple"
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})} onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice })}
className="max-w-[200px] self-end w-full"> className="max-w-[200px] self-end w-full">
Next Next
</Button> </Button>
</div> </div>
)
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20"> return (
<div className="flex flex-col gap-4 mt-4">
{!disableProgressButtons && progressButtons()}
<div className={clsx("flex flex-col gap-4 mt-4", !disableProgressButtons && "mb-20")}>
<span className="text-sm w-full leading-6"> <span className="text-sm w-full leading-6">
{prompt.split("\\n").map((line, index) => ( {prompt.split("\\n").map((line, index) => (
<Fragment key={index}> <Fragment key={index}>
@@ -151,22 +171,7 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
</DndContext> </DndContext>
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> {!disableProgressButtons && progressButtons()}
<Button
color="purple"
variant="outline"
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}
className="max-w-[200px] w-full">
Back
</Button>
<Button
color="purple"
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
</div> </div>
); );
} }

View File

@@ -72,8 +72,18 @@ function Question({
); );
} }
export default function MultipleChoice({id, prompt, type, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) { export default function MultipleChoice({
const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions); id,
prompt,
type,
questions,
userSolutions,
isPractice = false,
onNext,
onBack,
disableProgressButtons = false
}: MultipleChoiceExercise & CommonProps) {
const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions || []);
const { questionIndex, exerciseIndex, exam, shuffles, hasExamEnded, partIndex, setQuestionIndex, setCurrentSolution } = useExamStore( const { questionIndex, exerciseIndex, exam, shuffles, hasExamEnded, partIndex, setQuestionIndex, setCurrentSolution } = useExamStore(
(state) => state, (state) => state,
@@ -84,7 +94,7 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
useEffect(() => { useEffect(() => {
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type}); if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasExamEnded]); }, [hasExamEnded]);
@@ -93,7 +103,7 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
}; };
useEffect(() => { useEffect(() => {
setCurrentSolution({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps}); setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers, setAnswers]); }, [answers, setAnswers]);
@@ -130,9 +140,14 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
return { total, correct, missing }; return { total, correct, missing };
}; };
useEffect(() => {
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers, disableProgressButtons])
const next = () => { const next = () => {
if (questionIndex + 1 >= questions.length - 1) { if (questionIndex + 1 >= questions.length - 1) {
onNext({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps}); onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps, isPractice });
} else { } else {
setQuestionIndex(questionIndex + 2); setQuestionIndex(questionIndex + 2);
} }
@@ -141,7 +156,7 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
const back = () => { const back = () => {
if (questionIndex === 0) { if (questionIndex === 0) {
onBack({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps}); onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps, isPractice });
} else { } else {
if (exam?.module === "level" && typeof exam.parts[0].intro !== "undefined" && questionIndex === 0) return; if (exam?.module === "level" && typeof exam.parts[0].intro !== "undefined" && questionIndex === 0) return;
setQuestionIndex(questionIndex - 2); setQuestionIndex(questionIndex - 2);
@@ -150,8 +165,7 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
scrollToTop(); scrollToTop();
}; };
return ( const progressButtons = () => (
<div className="flex flex-col gap-4">
<div className="flex justify-between w-full gap-8"> <div className="flex justify-between w-full gap-8">
<Button <Button
color="purple" color="purple"
@@ -172,10 +186,23 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
: "Next"} : "Next"}
</Button> </Button>
</div> </div>
)
<div className="flex flex-col gap-4 mt-4 mb-20"> const renderAllQuestions = () =>
questions.map(question => (
<div
key={question.id} className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
<Question
{...question}
userSolution={answers.find((x) => question.id === x.question)?.option}
onSelectOption={(option) => onSelectOption(option, question)}
/>
</div>
))
const renderTwoQuestions = () => (
<>
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8"> <div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
{/*<span className="text-xl font-semibold mb-2">{"Select the appropriate option."}</span>*/}
{questionIndex < questions.length && ( {questionIndex < questions.length && (
<Question <Question
{...questions[questionIndex]} {...questions[questionIndex]}
@@ -194,28 +221,18 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
/> />
</div> </div>
)} )}
</>
)
return (
<div className="flex flex-col gap-4">
{!disableProgressButtons && progressButtons()}
<div className={clsx("flex flex-col gap-4 mt-4", !disableProgressButtons && "mb-20")}>
{disableProgressButtons ? renderAllQuestions() : renderTwoQuestions()}
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> {!disableProgressButtons && progressButtons()}
<Button
color="purple"
variant="outline"
onClick={back}
className="max-w-[200px] w-full"
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
Back
</Button>
<Button color="purple" onClick={next} className="max-w-[200px] self-end w-full">
{exam &&
exam.module === "level" &&
partIndex === exam.parts.length - 1 &&
exerciseIndex === exam.parts[partIndex].exercises.length - 1 &&
questionIndex + 1 >= questions.length - 1
? "Submit"
: "Next"}
</Button>
</div>
</div> </div>
); );
} }

View File

@@ -14,7 +14,7 @@ const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mo
ssr: false, ssr: false,
}); });
export default function Speaking({ id, title, text, video_url, type, prompts, suffix, userSolutions, onNext, onBack, preview = false }: SpeakingExercise & CommonProps) { export default function Speaking({ id, title, text, video_url, type, prompts, suffix, userSolutions, isPractice = false, onNext, onBack, preview = false }: SpeakingExercise & CommonProps) {
const [recordingDuration, setRecordingDuration] = useState(0); const [recordingDuration, setRecordingDuration] = useState(0);
const [isRecording, setIsRecording] = useState(false); const [isRecording, setIsRecording] = useState(false);
const [mediaBlob, setMediaBlob] = useState<string>(); const [mediaBlob, setMediaBlob] = useState<string>();
@@ -81,7 +81,7 @@ export default function Speaking({ id, title, text, video_url, type, prompts, su
exercise: id, exercise: id,
solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [], solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [],
score: { correct: 0, total: 100, missing: 0 }, score: { correct: 0, total: 100, missing: 0 },
type, type, isPractice
}); });
}; };
@@ -90,7 +90,7 @@ export default function Speaking({ id, title, text, video_url, type, prompts, su
exercise: id, exercise: id,
solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [], solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [],
score: { correct: 0, total: 100, missing: 0 }, score: { correct: 0, total: 100, missing: 0 },
type, type, isPractice
}); });
}; };

View File

@@ -1,17 +1,28 @@
import { TrueFalseExercise } from "@/interfaces/exam"; import { TrueFalseExercise } from "@/interfaces/exam";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import clsx from "clsx";
import { Fragment, useEffect, useState } from "react"; import { Fragment, useEffect, useState } from "react";
import { CommonProps } from "."; import { CommonProps } from ".";
import Button from "../Low/Button"; import Button from "../Low/Button";
export default function TrueFalse({id, type, prompt, questions, userSolutions, onNext, onBack}: TrueFalseExercise & CommonProps) { export default function TrueFalse({
id,
type,
prompt,
questions,
userSolutions,
isPractice = false,
onNext,
onBack,
disableProgressButtons = false
}: TrueFalseExercise & CommonProps) {
const [answers, setAnswers] = useState<{ id: string; solution: "true" | "false" | "not_given" }[]>(userSolutions); const [answers, setAnswers] = useState<{ id: string; solution: "true" | "false" | "not_given" }[]>(userSolutions);
const hasExamEnded = useExamStore((state) => state.hasExamEnded); const hasExamEnded = useExamStore((state) => state.hasExamEnded);
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution); const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
useEffect(() => { useEffect(() => {
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type}); if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasExamEnded]); }, [hasExamEnded]);
@@ -30,7 +41,7 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
}; };
useEffect(() => { useEffect(() => {
setCurrentSolution({exercise: id, solutions: answers, score: calculateScore(), type}); setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers, setAnswers]); }, [answers, setAnswers]);
@@ -44,26 +55,35 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
setAnswers((prev) => [...prev.filter((x) => x.id !== questionId), { id: questionId, solution }]); setAnswers((prev) => [...prev.filter((x) => x.id !== questionId), { id: questionId, solution }]);
}; };
return ( useEffect(() => {
<div className="flex flex-col gap-4 mt-4"> if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers, disableProgressButtons])
const progressButtons = () => (
<div className="flex justify-between w-full gap-8"> <div className="flex justify-between w-full gap-8">
<Button <Button
color="purple" color="purple"
variant="outline" variant="outline"
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})} onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice })}
className="max-w-[200px] w-full"> className="max-w-[200px] w-full">
Back Back
</Button> </Button>
<Button <Button
color="purple" color="purple"
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})} onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice })}
className="max-w-[200px] self-end w-full"> className="max-w-[200px] self-end w-full">
Next Next
</Button> </Button>
</div> </div>
)
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20"> return (
<div className="flex flex-col gap-4 mt-4">
{!disableProgressButtons && progressButtons()}
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", !disableProgressButtons && "mb-20")}>
<span className="text-sm w-full leading-6"> <span className="text-sm w-full leading-6">
{prompt.split("\\n").map((line, index) => ( {prompt.split("\\n").map((line, index) => (
<Fragment key={index}> <Fragment key={index}>
@@ -123,22 +143,7 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
</div> </div>
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> {!disableProgressButtons && progressButtons()}
<Button
color="purple"
variant="outline"
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}
className="max-w-[200px] w-full">
Back
</Button>
<Button
color="purple"
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
</div> </div>
); );
} }

View File

@@ -46,13 +46,25 @@ function Blank({
); );
} }
export default function WriteBlanks({id, prompt, type, maxWords, solutions, userSolutions, text, onNext, onBack}: WriteBlanksExercise & CommonProps) { export default function WriteBlanks({
id,
prompt,
type,
maxWords,
solutions,
userSolutions,
isPractice = false,
text,
onNext,
onBack,
disableProgressButtons = false
}: WriteBlanksExercise & CommonProps) {
const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions); const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions);
const { hasExamEnded, setCurrentSolution } = useExamStore((state) => state); const { hasExamEnded, setCurrentSolution } = useExamStore((state) => state);
useEffect(() => { useEffect(() => {
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type}); if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasExamEnded]); }, [hasExamEnded]);
@@ -71,10 +83,15 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user
}; };
useEffect(() => { useEffect(() => {
setCurrentSolution({exercise: id, solutions: answers, score: calculateScore(), type}); setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers, setAnswers]); }, [answers, setAnswers]);
useEffect(() => {
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers, disableProgressButtons])
const renderLines = (line: string) => { const renderLines = (line: string) => {
return ( return (
<span className="text-base leading-5"> <span className="text-base leading-5">
@@ -91,26 +108,30 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user
); );
}; };
return ( const progressButtons = () => (
<div className="flex flex-col gap-4">
<div className="flex justify-between w-full gap-8"> <div className="flex justify-between w-full gap-8">
<Button <Button
color="purple" color="purple"
variant="outline" variant="outline"
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})} onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice })}
className="max-w-[200px] w-full"> className="max-w-[200px] w-full">
Back Back
</Button> </Button>
<Button <Button
color="purple" color="purple"
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})} onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice })}
className="max-w-[200px] self-end w-full"> className="max-w-[200px] self-end w-full">
Next Next
</Button> </Button>
</div> </div>
)
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20"> return (
<div className="flex flex-col gap-4">
{!disableProgressButtons && progressButtons()}
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", !disableProgressButtons && "mb-20")}>
<span className="text-sm w-full leading-6"> <span className="text-sm w-full leading-6">
{prompt.split("\\n").map((line, index) => ( {prompt.split("\\n").map((line, index) => (
<span key={index}> <span key={index}>
@@ -129,22 +150,7 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user
</span> </span>
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> {!disableProgressButtons && progressButtons()}
<Button
color="purple"
variant="outline"
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}
className="max-w-[200px] w-full">
Back
</Button>
<Button
color="purple"
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
</div> </div>
); );
} }

View File

@@ -16,6 +16,7 @@ export default function Writing({
wordCounter, wordCounter,
attachment, attachment,
userSolutions, userSolutions,
isPractice = false,
onNext, onNext,
onBack, onBack,
enableNavigation = false enableNavigation = false
@@ -66,7 +67,7 @@ export default function Writing({
useEffect(() => { useEffect(() => {
if (hasExamEnded) if (hasExamEnded)
onNext({exercise: id, solutions: [{id, solution: inputText}], score: {correct: 100, total: 100, missing: 0}, type, module: "writing"}); onNext({ exercise: id, solutions: [{ id, solution: inputText }], score: { correct: 100, total: 100, missing: 0 }, type, module: "writing", isPractice });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasExamEnded]); }, [hasExamEnded]);
@@ -91,7 +92,7 @@ export default function Writing({
color="purple" color="purple"
variant="outline" variant="outline"
onClick={() => onClick={() =>
onBack({exercise: id, solutions: [{id, solution: inputText}], score: {correct: 100, total: 100, missing: 0}, type}) onBack({ exercise: id, solutions: [{ id, solution: inputText }], score: { correct: 100, total: 100, missing: 0 }, type, isPractice })
} }
className="max-w-[200px] self-end w-full"> className="max-w-[200px] self-end w-full">
Back Back
@@ -105,7 +106,7 @@ export default function Writing({
solutions: [{ id, solution: inputText.replaceAll(/\s{2,}/g, " ") }], solutions: [{ id, solution: inputText.replaceAll(/\s{2,}/g, " ") }],
score: { correct: 100, total: 100, missing: 0 }, score: { correct: 100, total: 100, missing: 0 },
type, type,
module: "writing", module: "writing", isPractice
}) })
} }
className="max-w-[200px] self-end w-full"> className="max-w-[200px] self-end w-full">
@@ -177,7 +178,7 @@ export default function Writing({
color="purple" color="purple"
variant="outline" variant="outline"
onClick={() => onClick={() =>
onBack({exercise: id, solutions: [{id, solution: inputText}], score: {correct: 100, total: 100, missing: 0}, type}) onBack({ exercise: id, solutions: [{ id, solution: inputText }], score: { correct: 100, total: 100, missing: 0 }, type, isPractice })
} }
className="max-w-[200px] self-end w-full"> className="max-w-[200px] self-end w-full">
Back Back
@@ -191,7 +192,7 @@ export default function Writing({
solutions: [{ id, solution: inputText.replaceAll(/\s{2,}/g, " ") }], solutions: [{ id, solution: inputText.replaceAll(/\s{2,}/g, " ") }],
score: { correct: 100, total: 100, missing: 0 }, score: { correct: 100, total: 100, missing: 0 },
type, type,
module: "writing", module: "writing", isPractice
}) })
} }
className="max-w-[200px] self-end w-full"> className="max-w-[200px] self-end w-full">

View File

@@ -26,6 +26,7 @@ export interface CommonProps {
onNext: (userSolutions: UserSolution) => void; onNext: (userSolutions: UserSolution) => void;
onBack: (userSolutions: UserSolution) => void; onBack: (userSolutions: UserSolution) => void;
enableNavigation?: boolean; enableNavigation?: boolean;
disableProgressButtons?: boolean
preview?: boolean; preview?: boolean;
} }
@@ -35,19 +36,20 @@ export const renderExercise = (
onNext: (userSolutions: UserSolution) => void, onNext: (userSolutions: UserSolution) => void,
onBack: (userSolutions: UserSolution) => void, onBack: (userSolutions: UserSolution) => void,
enableNavigation?: boolean, enableNavigation?: boolean,
disableProgressButtons?: boolean,
preview?: boolean, preview?: boolean,
) => { ) => {
switch (exercise.type) { switch (exercise.type) {
case "fillBlanks": case "fillBlanks":
return <FillBlanks key={exercise.id} {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />; return <FillBlanks disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
case "trueFalse": case "trueFalse":
return <TrueFalse key={exercise.id} {...(exercise as TrueFalseExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />; return <TrueFalse disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as TrueFalseExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
case "matchSentences": case "matchSentences":
return <MatchSentences key={exercise.id} {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview}/>; return <MatchSentences disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
case "multipleChoice": case "multipleChoice":
return <MultipleChoice key={exercise.id} {...(exercise as MultipleChoiceExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />; return <MultipleChoice disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as MultipleChoiceExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
case "writeBlanks": case "writeBlanks":
return <WriteBlanks key={exercise.id} {...(exercise as WriteBlanksExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />; return <WriteBlanks disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as WriteBlanksExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
case "writing": case "writing":
return <Writing key={exercise.id} {...(exercise as WritingExercise)} onNext={onNext} onBack={onBack} examID={examID} enableNavigation={enableNavigation} preview={preview} />; return <Writing key={exercise.id} {...(exercise as WritingExercise)} onNext={onNext} onBack={onBack} examID={examID} enableNavigation={enableNavigation} preview={preview} />;
case "speaking": case "speaking":

View File

@@ -6,6 +6,7 @@ import { sortByModuleName } from "@/utils/moduleUtils";
import clsx from "clsx"; import clsx from "clsx";
import moment from "moment"; import moment from "moment";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useMemo } from "react";
import Button from "../Low/Button"; import Button from "../Low/Button";
import ModuleBadge from "../ModuleBadge"; import ModuleBadge from "../ModuleBadge";
@@ -20,6 +21,8 @@ interface Props {
export default function AssignmentCard({ user, assignment, session, startAssignment, resumeAssignment }: Props) { export default function AssignmentCard({ user, assignment, session, startAssignment, resumeAssignment }: Props) {
const router = useRouter() const router = useRouter()
const hasBeenSubmitted = useMemo(() => assignment.results.map((r) => r.user).includes(user.id), [assignment.results, user.id])
return ( return (
<div <div
className={clsx( className={clsx(
@@ -45,7 +48,7 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
<ModuleBadge className="scale-110 w-full" key={module} module={module} /> <ModuleBadge className="scale-110 w-full" key={module} module={module} />
))} ))}
</div> </div>
{futureAssignmentFilter(assignment) && ( {futureAssignmentFilter(assignment) && !hasBeenSubmitted && (
<Button <Button
color="rose" color="rose"
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl" className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
@@ -54,7 +57,7 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
Not yet started Not yet started
</Button> </Button>
)} )}
{activeAssignmentFilter(assignment) && !assignment.results.map((r) => r.user).includes(user.id) && ( {activeAssignmentFilter(assignment) && !hasBeenSubmitted && (
<> <>
<div <div
className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden" className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden"
@@ -94,7 +97,7 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
)} )}
</> </>
)} )}
{assignment.results.map((r) => r.user).includes(user.id) && ( {hasBeenSubmitted && (
<Button <Button
color="green" color="green"
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl" className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"

View File

@@ -23,6 +23,7 @@ interface Props {
showSolutions?: boolean; showSolutions?: boolean;
currentExercise?: Exercise; currentExercise?: Exercise;
runOnClick?: ((questionIndex: number) => void) | undefined; runOnClick?: ((questionIndex: number) => void) | undefined;
indexLabel?: string
} }
export default function ModuleTitle({ export default function ModuleTitle({
@@ -36,7 +37,8 @@ export default function ModuleTitle({
partLabel, partLabel,
showTimer = true, showTimer = true,
showSolutions = false, showSolutions = false,
runOnClick = undefined runOnClick = undefined,
indexLabel = "Question"
}: Props) { }: Props) {
const { exam, partIndex, exerciseIndex: examExerciseIndex, userSolutions } = useExamStore((state) => state); const { exam, partIndex, exerciseIndex: examExerciseIndex, userSolutions } = useExamStore((state) => state);
@@ -88,7 +90,7 @@ export default function ModuleTitle({
{examLabel ? examLabel : (module === "level" ? "Placement Test" : `${moduleLabels[module]} exam${label ? ` - ${label}` : ''}`)} {examLabel ? examLabel : (module === "level" ? "Placement Test" : `${moduleLabels[module]} exam${label ? ` - ${label}` : ''}`)}
</span> </span>
<span className="text-sm font-semibold self-end"> <span className="text-sm font-semibold self-end">
Question {exerciseIndex}/{totalExercises} {indexLabel} {exerciseIndex}/{totalExercises}
</span> </span>
</div> </div>
<ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" /> <ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" />

View File

@@ -54,7 +54,7 @@ const aggregateScoresByModule = (stats: Stat[]): {module: Module; total: number;
}, },
}; };
stats.forEach((x) => { stats.filter(x => !x.isPractice).forEach((x) => {
scores[x.module!] = { scores[x.module!] = {
total: scores[x.module!].total + x.score.total, total: scores[x.module!].total + x.score.total,
correct: scores[x.module!].correct + x.score.correct, correct: scores[x.module!].correct + x.score.correct,

View File

@@ -7,7 +7,7 @@ import Button from "../Low/Button";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import { typeCheckWordsMC } from "@/utils/type.check"; import { typeCheckWordsMC } from "@/utils/type.check";
export default function FillBlanksSolutions({id, type, prompt, solutions, words, text, onNext, onBack}: FillBlanksExercise & CommonProps) { export default function FillBlanksSolutions({ id, type, prompt, solutions, words, text, onNext, onBack, disableProgressButtons = false }: FillBlanksExercise & CommonProps) {
const storeUserSolutions = useExamStore((state) => state.userSolutions); const storeUserSolutions = useExamStore((state) => state.userSolutions);
const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state); const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state);
@@ -149,8 +149,7 @@ export default function FillBlanksSolutions({id, type, prompt, solutions, words,
); );
}; };
return ( const progressButtons = () => (
<div className="flex flex-col gap-4">
<div className="flex justify-between w-full gap-8"> <div className="flex justify-between w-full gap-8">
<Button <Button
color="purple" color="purple"
@@ -168,8 +167,13 @@ export default function FillBlanksSolutions({id, type, prompt, solutions, words,
Next Next
</Button> </Button>
</div> </div>
)
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20"> return (
<div className="flex flex-col gap-4">
{!disableProgressButtons && progressButtons()}
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", !disableProgressButtons && "mb-20")}>
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6"> <span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
{correctUserSolutions && {correctUserSolutions &&
text.split("\\n").map((line, index) => ( text.split("\\n").map((line, index) => (
@@ -195,23 +199,7 @@ export default function FillBlanksSolutions({id, type, prompt, solutions, words,
</div> </div>
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> {!disableProgressButtons && progressButtons()}
<Button
color="purple"
variant="outline"
onClick={() => onBack({exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type})}
className="max-w-[200px] w-full"
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
Back
</Button>
<Button
color="purple"
onClick={() => onNext({exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type})}
className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
</div> </div>
); );
} }

View File

@@ -61,6 +61,7 @@ export default function MatchSentencesSolutions({
userSolutions, userSolutions,
onNext, onNext,
onBack, onBack,
disableProgressButtons = false
}: MatchSentencesExercise & CommonProps) { }: MatchSentencesExercise & CommonProps) {
const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state); const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state);
@@ -74,8 +75,7 @@ export default function MatchSentencesSolutions({
return { total, correct, missing }; return { total, correct, missing };
}; };
return ( const progressButtons = () => (
<div className="flex flex-col gap-4 mt-4">
<div className="flex justify-between w-full gap-8"> <div className="flex justify-between w-full gap-8">
<Button <Button
color="purple" color="purple"
@@ -93,8 +93,13 @@ export default function MatchSentencesSolutions({
Next Next
</Button> </Button>
</div> </div>
)
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20"> return (
<div className="flex flex-col gap-4 mt-4">
{!disableProgressButtons && progressButtons()}
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", !disableProgressButtons && "mb-20")}>
<span className="text-sm w-full leading-6"> <span className="text-sm w-full leading-6">
{prompt.split("\\n").map((line, index) => ( {prompt.split("\\n").map((line, index) => (
<Fragment key={index}> <Fragment key={index}>
@@ -128,23 +133,7 @@ export default function MatchSentencesSolutions({
</div> </div>
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> {!disableProgressButtons && progressButtons()}
<Button
color="purple"
variant="outline"
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
className="max-w-[200px] w-full"
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
Back
</Button>
<Button
color="purple"
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
</div> </div>
); );
} }

View File

@@ -89,7 +89,7 @@ function Question({
); );
} }
export default function MultipleChoice({id, type, prompt, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) { export default function MultipleChoice({ id, type, prompt, questions, userSolutions, onNext, onBack, disableProgressButtons = false }: MultipleChoiceExercise & CommonProps) {
const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state); const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state);
const stats = useExamStore((state) => state.userSolutions); const stats = useExamStore((state) => state.userSolutions);
@@ -126,8 +126,7 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
} }
}; };
return ( const progressButtons = () => (
<div className="flex flex-col gap-4">
<div className="flex justify-between w-full gap-8"> <div className="flex justify-between w-full gap-8">
<Button <Button
color="purple" color="purple"
@@ -142,12 +141,23 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
Next Next
</Button> </Button>
</div> </div>
)
<div className="flex flex-col gap-4 w-full h-full mb-20 mt-4"> const renderAllQuestions = () =>
<div className="flex flex-col gap-4 mt-2"> questions.map(question => (
<div
key={question.id} className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
<Question
{...question}
userSolution={userSolutions.find((x) => question.id === x.question)?.option}
/>
</div>
))
const renderTwoQuestions = () => (
<>
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8"> <div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
{/*<span className="text-xl font-semibold">{prompt}</span>*/} {questionIndex < questions.length && (
{userSolutions && questionIndex < questions.length && (
<Question <Question
{...questions[questionIndex]} {...questions[questionIndex]}
userSolution={userSolutions.find((x) => questions[questionIndex].id === x.question)?.option} userSolution={userSolutions.find((x) => questions[questionIndex].id === x.question)?.option}
@@ -155,7 +165,7 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
)} )}
</div> </div>
{userSolutions && questionIndex + 1 < questions.length && ( {questionIndex + 1 < questions.length && (
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8"> <div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
<Question <Question
{...questions[questionIndex + 1]} {...questions[questionIndex + 1]}
@@ -163,7 +173,15 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
/> />
</div> </div>
)} )}
</div> </>
)
return (
<div className="flex flex-col gap-4">
{!disableProgressButtons && progressButtons()}
<div className={clsx("flex flex-col gap-4 mt-4", !disableProgressButtons && "mb-20")}>
{disableProgressButtons ? renderAllQuestions() : renderTwoQuestions()}
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
@@ -181,20 +199,7 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
</div> </div>
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> {!disableProgressButtons && progressButtons()}
<Button
color="purple"
variant="outline"
onClick={back}
className="max-w-[200px] w-full"
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
Back
</Button>
<Button color="purple" onClick={next} className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
</div> </div>
); );
} }

View File

@@ -8,7 +8,7 @@ import useExamStore from "@/stores/examStore";
type Solution = "true" | "false" | "not_given"; type Solution = "true" | "false" | "not_given";
export default function TrueFalseSolution({prompt, type, id, questions, userSolutions, onNext, onBack}: TrueFalseExercise & CommonProps) { export default function TrueFalseSolution({ prompt, type, id, questions, userSolutions, onNext, onBack, disableProgressButtons = false }: TrueFalseExercise & CommonProps) {
const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state); const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state);
const calculateScore = () => { const calculateScore = () => {
@@ -39,8 +39,7 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
return "gray"; return "gray";
}; };
return ( const progressButtons = () => (
<div className="flex flex-col gap-4 mt-4">
<div className="flex justify-between w-full gap-8"> <div className="flex justify-between w-full gap-8">
<Button <Button
color="purple" color="purple"
@@ -58,8 +57,13 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
Next Next
</Button> </Button>
</div> </div>
)
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20"> return (
<div className="flex flex-col gap-4 mt-4">
{!disableProgressButtons && progressButtons()}
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", !disableProgressButtons && "mb-20")}>
<span className="text-sm w-full leading-6"> <span className="text-sm w-full leading-6">
{prompt.split("\\n").map((line, index) => ( {prompt.split("\\n").map((line, index) => (
<Fragment key={index}> <Fragment key={index}>
@@ -137,23 +141,7 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
</div> </div>
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> {!disableProgressButtons && progressButtons()}
<Button
color="purple"
variant="outline"
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
className="max-w-[200px] w-full"
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
Back
</Button>
<Button
color="purple"
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
</div> </div>
); );
} }

View File

@@ -71,6 +71,7 @@ export default function WriteBlanksSolutions({
text, text,
onNext, onNext,
onBack, onBack,
disableProgressButtons = false
}: WriteBlanksExercise & CommonProps) { }: WriteBlanksExercise & CommonProps) {
const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state); const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state);
@@ -104,8 +105,7 @@ export default function WriteBlanksSolutions({
); );
}; };
return ( const progressButtons = () => (
<div className="flex flex-col gap-4">
<div className="flex justify-between w-full gap-8"> <div className="flex justify-between w-full gap-8">
<Button <Button
color="purple" color="purple"
@@ -123,8 +123,13 @@ export default function WriteBlanksSolutions({
Next Next
</Button> </Button>
</div> </div>
)
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20"> return (
<div className="flex flex-col gap-4">
{!disableProgressButtons && progressButtons()}
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", !disableProgressButtons && "mb-20")}>
<span className="text-sm w-full leading-6"> <span className="text-sm w-full leading-6">
{prompt.split("\\n").map((line, index) => ( {prompt.split("\\n").map((line, index) => (
<Fragment key={index}> <Fragment key={index}>
@@ -158,23 +163,7 @@ export default function WriteBlanksSolutions({
</div> </div>
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> {!disableProgressButtons && progressButtons()}
<Button
color="purple"
variant="outline"
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
className="max-w-[200px] w-full"
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
Back
</Button>
<Button
color="purple"
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
</div> </div>
); );
} }

View File

@@ -24,20 +24,22 @@ const MatchSentences = dynamic(() => import("@/components/Solutions/MatchSentenc
export interface CommonProps { export interface CommonProps {
onNext: (userSolutions: UserSolution) => void; onNext: (userSolutions: UserSolution) => void;
onBack: (userSolutions: UserSolution) => void; onBack: (userSolutions: UserSolution) => void;
disableProgressButtons?: boolean,
} }
export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: () => void, updateIndex?: (internalIndex: number) => void) => { export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: () => void, updateIndex?: (internalIndex: number) => void,
disableProgressButtons?: boolean) => {
switch (exercise.type) { switch (exercise.type) {
case "fillBlanks": case "fillBlanks":
return <FillBlanks key={exercise.id} {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} />; return <FillBlanks disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} />;
case "trueFalse": case "trueFalse":
return <TrueFalseSolution key={exercise.id} {...(exercise as TrueFalseExercise)} onNext={onNext} onBack={onBack} />; return <TrueFalseSolution disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as TrueFalseExercise)} onNext={onNext} onBack={onBack} />;
case "matchSentences": case "matchSentences":
return <MatchSentences key={exercise.id} {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} />; return <MatchSentences disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} />;
case "multipleChoice": case "multipleChoice":
return <MultipleChoice key={exercise.id} {...(exercise as MultipleChoiceExercise)} onNext={onNext} onBack={onBack} />; return <MultipleChoice disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as MultipleChoiceExercise)} onNext={onNext} onBack={onBack} />;
case "writeBlanks": case "writeBlanks":
return <WriteBlanks key={exercise.id} {...(exercise as WriteBlanksExercise)} onNext={onNext} onBack={onBack} />; return <WriteBlanks disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as WriteBlanksExercise)} onNext={onNext} onBack={onBack} />;
case "writing": case "writing":
return <Writing key={exercise.id} {...(exercise as WritingExercise)} onNext={onNext} onBack={onBack} />; return <Writing key={exercise.id} {...(exercise as WritingExercise)} onNext={onNext} onBack={onBack} />;
case "speaking": case "speaking":

View File

@@ -326,11 +326,9 @@ export default function Finish({ user, scores, modules, information, solutions,
)} )}
</div> </div>
<Link href={destination || "/"} className="w-full max-w-[200px] self-end"> <Button onClick={() => destination === "/exam" ? router.reload() : router.push(destination || "/")} color="purple" className="w-full max-w-[200px] self-end">
<Button color="purple" className="w-full max-w-[200px] self-end">
Dashboard Dashboard
</Button> </Button>
</Link>
</div> </div>
)} )}
</> </>

View File

@@ -1,5 +1,5 @@
import { ListeningExam, MultipleChoiceExercise, UserSolution } from "@/interfaces/exam"; import { ListeningExam, MultipleChoiceExercise, Script, UserSolution } from "@/interfaces/exam";
import { useEffect, useState } from "react"; import { Fragment, useEffect, useState } from "react";
import { renderExercise } from "@/components/Exercises"; import { renderExercise } from "@/components/Exercises";
import { renderSolution } from "@/components/Solutions"; import { renderSolution } from "@/components/Solutions";
import ModuleTitle from "@/components/Medium/ModuleTitle"; import ModuleTitle from "@/components/Medium/ModuleTitle";
@@ -9,6 +9,9 @@ import BlankQuestionsModal from "@/components/QuestionsModal";
import useExamStore, { usePersistentExamStore } from "@/stores/examStore"; import useExamStore, { usePersistentExamStore } from "@/stores/examStore";
import { countExercises } from "@/utils/moduleUtils"; import { countExercises } from "@/utils/moduleUtils";
import PartDivider from "./Navigation/SectionDivider"; import PartDivider from "./Navigation/SectionDivider";
import { Dialog, Transition } from "@headlessui/react";
import { capitalize } from "lodash";
import { mapBy } from "@/utils";
interface Props { interface Props {
exam: ListeningExam; exam: ListeningExam;
@@ -17,17 +20,76 @@ interface Props {
onFinish: (userSolutions: UserSolution[]) => void; onFinish: (userSolutions: UserSolution[]) => void;
} }
function ScriptModal({ isOpen, script, onClose }: { isOpen: boolean; script: Script; onClose: () => void }) {
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95">
<Dialog.Panel className="w-full relative max-w-4xl transform rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<div className="mt-2 overflow-auto mb-28">
<p className="text-sm">
{typeof script === "string" && script.split("\\n").map((line, index) => (
<Fragment key={index}>
{line}
<br />
</Fragment>
))}
{typeof script === "object" && script.map((line, index) => (
<span key={index}>
<b>{line.name} ({capitalize(line.gender)})</b>: {line.text}
<br />
<br />
</span>
))}
</p>
</div>
<div className="absolute bottom-8 right-8 max-w-[200px] self-end w-full">
<Button color="purple" variant="outline" className="max-w-[200px] self-end w-full" onClick={onClose}>
Close
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
);
}
const INSTRUCTIONS_AUDIO_SRC = const INSTRUCTIONS_AUDIO_SRC =
"https://firebasestorage.googleapis.com/v0/b/storied-phalanx-349916.appspot.com/o/generic_listening_intro_v2.mp3?alt=media&token=16769f5f-1e9b-4a72-86a9-45a6f0fa9f82"; "https://firebasestorage.googleapis.com/v0/b/storied-phalanx-349916.appspot.com/o/generic_listening_intro_v2.mp3?alt=media&token=16769f5f-1e9b-4a72-86a9-45a6f0fa9f82";
export default function Listening({ exam, showSolutions = false, preview = false, onFinish }: Props) { export default function Listening({ exam, showSolutions = false, preview = false, onFinish }: Props) {
const listeningBgColor = "bg-ielts-listening-light"; const listeningBgColor = "bg-ielts-listening-light";
const [showTextModal, setShowTextModal] = useState(false);
const [timesListened, setTimesListened] = useState(0); const [timesListened, setTimesListened] = useState(0);
const [showBlankModal, setShowBlankModal] = useState(false); const [showBlankModal, setShowBlankModal] = useState(false);
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]); const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
const [seenParts, setSeenParts] = useState<Set<number>>(new Set(showSolutions ? exam.parts.map((_, index) => index) : [])); const [seenParts, setSeenParts] = useState<Set<number>>(new Set(showSolutions ? exam.parts.map((_, index) => index) : []));
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && exam.parts[0].intro !== ""); const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && exam.parts[0].intro !== "");
@@ -99,75 +161,35 @@ export default function Listening({ exam, showSolutions = false, preview = false
}; };
const nextExercise = (solution?: UserSolution) => { const nextExercise = (solution?: UserSolution) => {
scrollToTop(); if (solution)
if (solution) { setUserSolutions([
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "listening", exam: exam.id }]); ...userSolutions.filter((x) => x.exercise !== solution.exercise),
} { ...solution, module: "listening", exam: exam.id }
if (storeQuestionIndex > 0) { ]);
const exercise = getExercise(); };
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise.id), { id: exercise.id, amount: storeQuestionIndex }]);
}
setStoreQuestionIndex(0);
if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) { const previousExercise = (solution?: UserSolution) => { };
setExerciseIndex(exerciseIndex + 1);
return;
}
const nextPart = () => {
if (partIndex + 1 < exam.parts.length && !hasExamEnded) { if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
setPartIndex(partIndex + 1); setPartIndex(partIndex + 1);
setTimesListened(0); setExerciseIndex(0);
setExerciseIndex(showSolutions ? 0 : -1);
return; return;
} }
if ( if (!showSolutions && !hasExamEnded) {
solution && const exercises = partIndex < exam.parts.length ? exam.parts[partIndex].exercises : []
![...userSolutions.filter((x) => x.exercise !== solution?.exercise).map((x) => x.score.missing), solution?.score.missing].every( const exerciseIDs = mapBy(exercises, 'id')
(x) => x === 0,
) && const hasMissing = userSolutions.filter(x => exerciseIDs.includes(x.exercise)).map(x => x.score.missing).some(x => x > 0)
!showSolutions &&
!hasExamEnded && if (hasMissing) return setShowBlankModal(true);
!preview
) {
setShowBlankModal(true);
return;
} }
setHasExamEnded(false); setHasExamEnded(false);
if (solution) {
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "listening", exam: exam.id }]);
} else {
onFinish(userSolutions); onFinish(userSolutions);
} }
};
const previousExercise = (solution?: UserSolution) => {
scrollToTop();
if (solution) {
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "listening", exam: exam.id }]);
}
setStoreQuestionIndex(0);
setExerciseIndex(exerciseIndex - 1);
};
const getExercise = () => {
const exercise = exam.parts[partIndex].exercises[exerciseIndex];
return {
...exercise,
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
};
};
useEffect(() => {
if (partIndex > -1 && exerciseIndex > -1) {
const exercise = getExercise();
setMultipleChoicesDone((prev) => prev.filter((x) => x.id !== exercise.id));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [exerciseIndex, partIndex]);
const calculateExerciseIndex = () => { const calculateExerciseIndex = () => {
if (partIndex === -1) return 0; if (partIndex === -1) return 0;
@@ -186,6 +208,22 @@ export default function Listening({ exam, showSolutions = false, preview = false
); );
}; };
const renderPartExercises = () => {
const exercises = partIndex > -1 ? exam.parts[partIndex].exercises : []
const formattedExercises = exercises.map(exercise => ({
...exercise,
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
}))
return (
<div className="flex flex-col gap-4">
{formattedExercises.map(e => showSolutions
? renderSolution(e, nextExercise, previousExercise, undefined, true)
: renderExercise(e, exam.id, nextExercise, previousExercise, undefined, true))}
</div>
)
}
const renderAudioInstructionsPlayer = () => ( const renderAudioInstructionsPlayer = () => (
<div className="flex flex-col gap-8 w-full bg-mti-gray-seasalt rounded-xl py-8 px-16"> <div className="flex flex-col gap-8 w-full bg-mti-gray-seasalt rounded-xl py-8 px-16">
<div className="flex flex-col w-full gap-2"> <div className="flex flex-col w-full gap-2">
@@ -201,6 +239,7 @@ export default function Listening({ exam, showSolutions = false, preview = false
<div className="flex flex-col gap-8 w-full bg-mti-gray-seasalt rounded-xl py-8 px-16"> <div className="flex flex-col gap-8 w-full bg-mti-gray-seasalt rounded-xl py-8 px-16">
{exam?.parts[partIndex]?.audio?.source ? ( {exam?.parts[partIndex]?.audio?.source ? (
<> <>
<div className="w-full items-start flex justify-between">
<div className="flex flex-col w-full gap-2"> <div className="flex flex-col w-full gap-2">
<h4 className="text-xl font-semibold">Please listen to the following audio attentively.</h4> <h4 className="text-xl font-semibold">Please listen to the following audio attentively.</h4>
<span className="text-base"> <span className="text-base">
@@ -212,6 +251,17 @@ export default function Listening({ exam, showSolutions = false, preview = false
})()} })()}
</span> </span>
</div> </div>
{partIndex > -1 && !examState.assignment && !!exam.parts[partIndex].script && (
<Button
onClick={() => setShowTextModal(true)}
variant="outline"
color="gray"
className="w-full max-w-[200px]"
>
View Transcript
</Button>
)}
</div>
<div className="rounded-xl flex flex-col gap-4 items-center w-full h-fit"> <div className="rounded-xl flex flex-col gap-4 items-center w-full h-fit">
<AudioPlayer <AudioPlayer
key={partIndex} key={partIndex}
@@ -231,6 +281,25 @@ export default function Listening({ exam, showSolutions = false, preview = false
</div> </div>
); );
const progressButtons = () => (
<div className="flex justify-between w-full gap-8">
<Button
color="purple"
variant="outline"
onClick={previousExercise}
className="max-w-[200px] w-full">
Back
</Button>
<Button
color="purple"
onClick={nextPart}
className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
)
return ( return (
<> <>
{showPartDivider ? {showPartDivider ?
@@ -244,14 +313,19 @@ export default function Listening({ exam, showSolutions = false, preview = false
/> : ( /> : (
<> <>
<BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} /> <BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} />
{partIndex > -1 && exam.parts[partIndex].script &&
<ScriptModal script={exam.parts[partIndex].script!} isOpen={showTextModal} onClose={() => setShowTextModal(false)} />
}
<div className="flex flex-col h-full w-full gap-8 justify-between"> <div className="flex flex-col h-full w-full gap-8 justify-between">
<ModuleTitle <ModuleTitle
exerciseIndex={calculateExerciseIndex()} exerciseIndex={partIndex + 1}
minTimer={exam.minTimer} minTimer={exam.minTimer}
module="listening" module="listening"
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))} totalExercises={exam.parts.length}
disableTimer={showSolutions || preview} disableTimer={showSolutions || preview}
indexLabel="Part"
/> />
{/* Audio Player for the Instructions */} {/* Audio Player for the Instructions */}
{partIndex === -1 && renderAudioInstructionsPlayer()} {partIndex === -1 && renderAudioInstructionsPlayer()}
@@ -259,18 +333,14 @@ export default function Listening({ exam, showSolutions = false, preview = false
{partIndex > -1 && renderAudioPlayer()} {partIndex > -1 && renderAudioPlayer()}
{/* Exercise renderer */} {/* Exercise renderer */}
{exerciseIndex > -1 &&
partIndex > -1 &&
exerciseIndex < exam.parts[partIndex].exercises.length &&
!showSolutions &&
renderExercise(getExercise(), exam.id, nextExercise, previousExercise)}
{/* Solution renderer */} {exerciseIndex > -1 && partIndex > -1 && (
{exerciseIndex > -1 && <>
partIndex > -1 && {progressButtons()}
exerciseIndex < exam.parts[partIndex].exercises.length && {renderPartExercises()}
showSolutions && {progressButtons()}
renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)} </>
)}
</div> </div>
{exerciseIndex === -1 && partIndex > -1 && exam.variant !== "partial" && ( {exerciseIndex === -1 && partIndex > -1 && exam.variant !== "partial" && (
@@ -295,12 +365,12 @@ export default function Listening({ exam, showSolutions = false, preview = false
)} )}
{partIndex === -1 && exam.variant !== "partial" && ( {partIndex === -1 && exam.variant !== "partial" && (
<Button color="purple" onClick={() => setPartIndex(0)} className="max-w-[200px] self-end w-full justify-self-end"> <Button color="purple" onClick={() => nextPart()} className="max-w-[200px] self-end w-full justify-self-end">
Start now Start now
</Button> </Button>
)} )}
{exerciseIndex === -1 && partIndex === 0 && exam.variant === "partial" && ( {exerciseIndex === -1 && partIndex === 0 && exam.variant === "partial" && (
<Button color="purple" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full justify-self-end"> <Button color="purple" onClick={() => nextPart()} className="max-w-[200px] self-end w-full justify-self-end">
Start now Start now
</Button> </Button>
)} )}

View File

@@ -90,6 +90,7 @@ export interface UserSolution {
exercise: string; exercise: string;
isDisabled?: boolean; isDisabled?: boolean;
shuffleMaps?: ShuffleMap[]; shuffleMaps?: ShuffleMap[];
isPractice?: boolean
} }
export interface WritingExam extends ExamBase { export interface WritingExam extends ExamBase {
@@ -165,6 +166,7 @@ export interface WritingExercise extends Section {
}[]; }[];
topic?: string; topic?: string;
variant?: string; variant?: string;
isPractice?: boolean
} }
export interface AIDetectionAttributes { export interface AIDetectionAttributes {
@@ -199,6 +201,7 @@ export interface SpeakingExercise extends Section {
evaluation?: SpeakingEvaluation; evaluation?: SpeakingEvaluation;
}[]; }[];
topic?: string; topic?: string;
isPractice?: boolean
} }
export interface InteractiveSpeakingExercise extends Section { export interface InteractiveSpeakingExercise extends Section {
@@ -218,6 +221,7 @@ export interface InteractiveSpeakingExercise extends Section {
first_topic?: string; first_topic?: string;
second_topic?: string; second_topic?: string;
variant?: "initial" | "final"; variant?: "initial" | "final";
isPractice?: boolean
} }
export interface FillBlanksMCOption { export interface FillBlanksMCOption {
@@ -246,6 +250,7 @@ export interface FillBlanksExercise {
solution: string; // *EXAMPLE: "preserve" solution: string; // *EXAMPLE: "preserve"
}[]; }[];
variant?: string; variant?: string;
isPractice?: boolean
} }
export interface TrueFalseExercise { export interface TrueFalseExercise {
@@ -254,6 +259,7 @@ export interface TrueFalseExercise {
prompt: string; // *EXAMPLE: "Select the appropriate option." prompt: string; // *EXAMPLE: "Select the appropriate option."
questions: TrueFalseQuestion[]; questions: TrueFalseQuestion[];
userSolutions: { id: string; solution: "true" | "false" | "not_given" }[]; userSolutions: { id: string; solution: "true" | "false" | "not_given" }[];
isPractice?: boolean
} }
export interface TrueFalseQuestion { export interface TrueFalseQuestion {
@@ -277,6 +283,7 @@ export interface WriteBlanksExercise {
solution: string; solution: string;
}[]; }[];
variant?: string; variant?: string;
isPractice?: boolean
} }
export interface MatchSentencesExercise { export interface MatchSentencesExercise {
@@ -288,6 +295,7 @@ export interface MatchSentencesExercise {
allowRepetition: boolean; allowRepetition: boolean;
options: MatchSentenceExerciseOption[]; options: MatchSentenceExerciseOption[];
variant?: string; variant?: string;
isPractice?: boolean
} }
export interface MatchSentenceExerciseSentence { export interface MatchSentenceExerciseSentence {
@@ -312,6 +320,7 @@ export interface MultipleChoiceExercise {
title: string; title: string;
content: string; content: string;
} }
isPractice?: boolean
} }
export interface MultipleChoiceQuestion { export interface MultipleChoiceQuestion {

View File

@@ -142,6 +142,7 @@ export interface Stat {
path: string; path: string;
version: string; version: string;
}; };
isPractice?: boolean
} }
export interface Group { export interface Group {

View File

@@ -11,7 +11,6 @@ import Reading from "@/exams/Reading";
import Selection from "@/exams/Selection"; import Selection from "@/exams/Selection";
import Speaking from "@/exams/Speaking"; import Speaking from "@/exams/Speaking";
import Writing from "@/exams/Writing"; import Writing from "@/exams/Writing";
import useUser from "@/hooks/useUser";
import { Exam, LevelExam, UserSolution, Variant } from "@/interfaces/exam"; import { Exam, LevelExam, UserSolution, Variant } from "@/interfaces/exam";
import { Stat, User } from "@/interfaces/user"; import { Stat, User } from "@/interfaces/user";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
@@ -21,12 +20,7 @@ import axios from "axios";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { toast, ToastContainer } from "react-toastify"; import { toast, ToastContainer } from "react-toastify";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import useSessions from "@/hooks/useSessions";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import clsx from "clsx";
import useGradingSystem from "@/hooks/useGrading";
import { Assignment } from "@/interfaces/results";
import { mapBy } from "@/utils";
interface Props { interface Props {
page: "exams" | "exercises"; page: "exams" | "exercises";
@@ -214,7 +208,6 @@ export default function ExamPage({ page, user, destination = "/exam", hideSideba
}, [setModuleIndex, showSolutions]); }, [setModuleIndex, showSolutions]);
useEffect(() => { useEffect(() => {
console.log(selectedModules)
if (selectedModules.length > 0 && exams.length > 0 && moduleIndex < selectedModules.length) { if (selectedModules.length > 0 && exams.length > 0 && moduleIndex < selectedModules.length) {
const nextExam = exams[moduleIndex]; const nextExam = exams[moduleIndex];
@@ -264,6 +257,7 @@ export default function ExamPage({ page, user, destination = "/exam", hideSideba
isDisabled: solution.isDisabled, isDisabled: solution.isDisabled,
shuffleMaps: solution.shuffleMaps, shuffleMaps: solution.shuffleMaps,
...(assignment ? { assignment: assignment.id } : {}), ...(assignment ? { assignment: assignment.id } : {}),
isPractice: solution.isPractice
})); }));
axios axios
@@ -422,7 +416,7 @@ export default function ExamPage({ page, user, destination = "/exam", hideSideba
}, },
}; };
userSolutions.forEach((x) => { userSolutions.filter(x => !x.isPractice).forEach((x) => {
const examModule = const examModule =
x.module || (x.type === "writing" ? "writing" : x.type === "speaking" || x.type === "interactiveSpeaking" ? "speaking" : undefined); x.module || (x.type === "writing" ? "writing" : x.type === "speaking" || x.type === "interactiveSpeaking" ? "speaking" : undefined);

View File

@@ -2,7 +2,7 @@
import { User } from "@/interfaces/user"; import { User } from "@/interfaces/user";
import { toast, ToastContainer } from "react-toastify"; import { toast, ToastContainer } from "react-toastify";
import axios from "axios"; import axios from "axios";
import {FormEvent, useEffect, useState} from "react"; import { FormEvent, useEffect, useMemo, useState } from "react";
import Head from "next/head"; import Head from "next/head";
import useUser from "@/hooks/useUser"; import useUser from "@/hooks/useUser";
import { Divider } from "primereact/divider"; import { Divider } from "primereact/divider";
@@ -30,13 +30,14 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) =
}; };
}, sessionOptions); }, sessionOptions);
export default function Login({ destination }: { destination: string }) { export default function Login({ destination = "/" }: { destination?: string }) {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [rememberPassword, setRememberPassword] = useState(false); const [rememberPassword, setRememberPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const router = useRouter(); const router = useRouter();
const isOfficialExamLogin = useMemo(() => destination.startsWith("/official-exam"), [destination])
const { user, mutateUser } = useUser({ const { user, mutateUser } = useUser({
redirectTo: destination, redirectTo: destination,
@@ -110,14 +111,25 @@ export default function Login({ destination }: { destination: string }) {
<main className="flex h-[100vh] w-full bg-white text-black"> <main className="flex h-[100vh] w-full bg-white text-black">
<ToastContainer /> <ToastContainer />
<section className="relative hidden h-full w-fit min-w-fit lg:flex"> <section className="relative hidden h-full w-fit min-w-fit lg:flex">
{/* <div className="bg-mti-rose-light absolute z-10 h-full w-full bg-opacity-50" /> */} {!isOfficialExamLogin && (
<img src="/red-stock-photo.jpg" alt="People smiling looking at a tablet" className="aspect-auto h-full" /> <img src="/red-stock-photo.jpg" alt="People smiling looking at a tablet" className="aspect-auto h-full" />
)}
{isOfficialExamLogin && (
<img src="/purple-stock-photo.png" alt="People smiling looking at a tablet" className="aspect-auto h-full" />
)}
</section> </section>
<section className="flex h-full w-full flex-col items-center justify-center gap-2"> <section className="flex h-full w-full flex-col items-center justify-center gap-2">
<div className={clsx("flex flex-col items-center", !user && "mb-4")}> <div className={clsx("flex flex-col items-center", !user && "mb-4")}>
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-36 lg:w-56" /> <img src="/logo_title.png" alt="EnCoach's Logo" className="w-36 lg:w-56" />
{!isOfficialExamLogin && (
<>
<h1 className="text-2xl font-bold lg:text-4xl">Login to your account</h1> <h1 className="text-2xl font-bold lg:text-4xl">Login to your account</h1>
<p className="text-mti-gray-cool self-start text-sm font-normal lg:text-base">with your registered Email Address</p> <p className="text-mti-gray-cool self-start text-sm font-normal lg:text-base">with your registered Email Address</p>
</>
)}
{isOfficialExamLogin && (
<h1 className="text-2xl font-bold lg:text-4xl">Welcome to the Official Exams Portal</h1>
)}
</div> </div>
<Divider className="max-w-xs lg:max-w-md" /> <Divider className="max-w-xs lg:max-w-md" />
{!user && ( {!user && (

View File

@@ -43,12 +43,10 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
const entities = await getEntitiesWithRoles(checkAccess(user, ["admin", "developer"]) ? undefined : entityIDs) const entities = await getEntitiesWithRoles(checkAccess(user, ["admin", "developer"]) ? undefined : entityIDs)
const users = await (checkAccess(user, ["admin", "developer"]) ? getUsers() : getEntitiesUsers(mapBy(entities, 'id'))) const users = await (checkAccess(user, ["admin", "developer"]) ? getUsers() : getEntitiesUsers(mapBy(entities, 'id')))
const groups = await (checkAccess(user, ["admin", "developer"]) ? getGroups() : getGroupsByEntities(mapBy(entities, 'id')))
const assignments = await (checkAccess(user, ["admin", "developer"]) ? getAssignments() : getEntitiesAssignments(mapBy(entities, 'id'))) const assignments = await (checkAccess(user, ["admin", "developer"]) ? getAssignments() : getEntitiesAssignments(mapBy(entities, 'id')))
const gradingSystems = await Promise.all(entityIDs.map(getGradingSystemByEntity))
return { return {
props: serialize({user, users, assignments, entities, gradingSystems}), props: serialize({ user, users, assignments, entities }),
}; };
}, sessionOptions); }, sessionOptions);
@@ -58,13 +56,12 @@ interface Props {
user: User; user: User;
users: User[]; users: User[];
assignments: Assignment[]; assignments: Assignment[];
gradingSystems: Grading[]
entities: EntityWithRoles[] entities: EntityWithRoles[]
} }
const MAX_TRAINING_EXAMS = 10; const MAX_TRAINING_EXAMS = 10;
export default function History({user, users, assignments, entities, gradingSystems}: Props) { export default function History({ user, users, assignments, entities }: Props) {
const router = useRouter(); const router = useRouter();
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [ const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
state.selectedUser, state.selectedUser,