- Added a new type of exercise
- Updated all solutions to solve a huge bug where after reviewing, it would reset the score
This commit is contained in:
@@ -83,7 +83,7 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
|
||||
))}
|
||||
</div>
|
||||
{answers.map((solution, index) => (
|
||||
<Xarrow key={index} start={solution.question} end={solution.option} lineColor="#307912" showHead={false} />
|
||||
<Xarrow key={index} start={solution.question} end={solution.option} lineColor="#7872BF" showHead={false} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
111
src/components/Exercises/TrueFalse.tsx
Normal file
111
src/components/Exercises/TrueFalse.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import {TrueFalseExercise} from "@/interfaces/exam";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import {Fragment, useEffect, useState} from "react";
|
||||
import {CommonProps} from ".";
|
||||
import Button from "../Low/Button";
|
||||
|
||||
export default function TrueFalse({id, type, prompt, questions, userSolutions, onNext, onBack}: TrueFalseExercise & CommonProps) {
|
||||
const [answers, setAnswers] = useState<{id: string; solution: "true" | "false" | "not_given"}[]>(userSolutions);
|
||||
|
||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hasExamEnded]);
|
||||
|
||||
const calculateScore = () => {
|
||||
const total = questions.length || 0;
|
||||
const correct = answers.filter((x) => questions.find((y) => x.id === y.id)?.solution === x.solution.toLowerCase() || false).length;
|
||||
const missing = total - answers.filter((x) => questions.find((y) => x.id === y.id)).length;
|
||||
|
||||
return {total, correct, missing};
|
||||
};
|
||||
|
||||
const toggleAnswer = (solution: "true" | "false" | "not_given", questionId: string) => {
|
||||
const answer = answers.find((x) => x.id === questionId);
|
||||
if (answer && answer.solution === solution) {
|
||||
setAnswers((prev) => prev.filter((x) => x.id !== questionId));
|
||||
return;
|
||||
}
|
||||
|
||||
setAnswers((prev) => [...prev.filter((x) => x.id !== questionId), {id: questionId, solution}]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-4 mt-4 h-full mb-20">
|
||||
<span className="text-sm w-full leading-6">
|
||||
{prompt.split("\\n").map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
<div className="flex flex-col gap-6 mb-4">
|
||||
<p>For each of the questions below, select</p>
|
||||
<div className="pl-8 flex gap-8">
|
||||
<span className="flex flex-col gap-4">
|
||||
<span className="font-bold italic">TRUE</span>
|
||||
<span className="font-bold italic">FALSE</span>
|
||||
<span className="font-bold italic">NOT GIVEN</span>
|
||||
</span>
|
||||
<span className="flex flex-col gap-4">
|
||||
<span>if the statement agrees with the information</span>
|
||||
<span>if the statement contradicts with the information</span>
|
||||
<span>if there is no information on this</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm w-full leading-6">You can click a selected option again to deselect it.</span>
|
||||
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-8">
|
||||
{questions.map((question, index) => (
|
||||
<div key={question.id} className="flex flex-col gap-4">
|
||||
<span>
|
||||
{index + 1}. {question.prompt}
|
||||
</span>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant={answers.find((x) => x.id === question.id)?.solution === "true" ? "solid" : "outline"}
|
||||
onClick={() => toggleAnswer("true", question.id)}
|
||||
className="!py-2">
|
||||
True
|
||||
</Button>
|
||||
<Button
|
||||
variant={answers.find((x) => x.id === question.id)?.solution === "false" ? "solid" : "outline"}
|
||||
onClick={() => toggleAnswer("false", question.id)}
|
||||
className="!py-2">
|
||||
False
|
||||
</Button>
|
||||
<Button
|
||||
variant={answers.find((x) => x.id === question.id)?.solution === "not_given" ? "solid" : "outline"}
|
||||
onClick={() => toggleAnswer("not_given", question.id)}
|
||||
className="!py-2">
|
||||
Not Given
|
||||
</Button>
|
||||
</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})}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
MatchSentencesExercise,
|
||||
MultipleChoiceExercise,
|
||||
SpeakingExercise,
|
||||
TrueFalseExercise,
|
||||
UserSolution,
|
||||
WriteBlanksExercise,
|
||||
WritingExercise,
|
||||
@@ -14,6 +15,7 @@ import MultipleChoice from "./MultipleChoice";
|
||||
import WriteBlanks from "./WriteBlanks";
|
||||
import Writing from "./Writing";
|
||||
import Speaking from "./Speaking";
|
||||
import TrueFalse from "./TrueFalse";
|
||||
|
||||
const MatchSentences = dynamic(() => import("@/components/Exercises/MatchSentences"), {ssr: false});
|
||||
|
||||
@@ -26,6 +28,8 @@ export const renderExercise = (exercise: Exercise, onNext: (userSolutions: UserS
|
||||
switch (exercise.type) {
|
||||
case "fillBlanks":
|
||||
return <FillBlanks {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
||||
case "trueFalse":
|
||||
return <TrueFalse {...(exercise as TrueFalseExercise)} onNext={onNext} onBack={onBack} />;
|
||||
case "matchSentences":
|
||||
return <MatchSentences {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} />;
|
||||
case "multipleChoice":
|
||||
|
||||
@@ -13,19 +13,19 @@ interface Props {
|
||||
export default function Button({color = "purple", variant = "solid", disabled = false, className, children, onClick}: Props) {
|
||||
const colorClassNames: {[key in typeof color]: {[key in typeof variant]: string}} = {
|
||||
purple: {
|
||||
solid: "bg-mti-purple-light text-white hover:bg-mti-purple disabled:text-mti-purple disabled:bg-mti-purple-ultralight selection:bg-mti-purple-dark",
|
||||
solid: "bg-mti-purple-light text-white border border-mti-purple-light hover:bg-mti-purple disabled:text-mti-purple disabled:bg-mti-purple-ultralight selection:bg-mti-purple-dark",
|
||||
outline:
|
||||
"bg-transparent text-mti-purple-light border border-mti-purple-light hover:bg-mti-purple-light disabled:text-mti-purple disabled:bg-mti-purple-ultralight disabled:border-none selection:bg-mti-purple-dark hover:text-white selection:text-white",
|
||||
},
|
||||
red: {
|
||||
solid: "bg-mti-red-light text-white hover:bg-mti-red disabled:text-mti-red disabled:bg-mti-red-ultralight selection:bg-mti-red-dark",
|
||||
solid: "bg-mti-red-light text-white border border-mti-red-light hover:bg-mti-red disabled:text-mti-red disabled:bg-mti-red-ultralight selection:bg-mti-red-dark",
|
||||
outline:
|
||||
"bg-transparent text-mti-red-light border border-mti-red-light hover:bg-mti-red-light disabled:text-mti-red disabled:bg-mti-red-ultralight disabled:border-none selection:bg-mti-red-dark hover:text-white selection:text-white",
|
||||
},
|
||||
rose: {
|
||||
solid: "bg-mti-orange-light text-white hover:bg-mti-orange disabled:text-mti-orange disabled:bg-mti-orange-ultralight selection:bg-mti-orange-dark",
|
||||
solid: "bg-mti-rose-light text-white border border-mti-rose-light hover:bg-mti-rose disabled:text-mti-rose disabled:bg-mti-rose-ultralight selection:bg-mti-rose-dark",
|
||||
outline:
|
||||
"bg-transparent text-mti-orange-light border border-mti-orange-light hover:bg-mti-orange-light disabled:text-mti-orange disabled:bg-mti-orange-ultralight disabled:border-none selection:bg-mti-orange-dark hover:text-white selection:text-white",
|
||||
"bg-transparent text-mti-rose-light border border-mti-rose-light hover:bg-mti-rose-light disabled:text-mti-rose disabled:bg-mti-rose-ultralight disabled:border-none selection:bg-mti-rose-dark hover:text-white selection:text-white",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function Sidebar({path, navDisabled = false}: Props) {
|
||||
tabIndex={1}
|
||||
onClick={logout}
|
||||
className={clsx(
|
||||
"p-4 px-8 rounded-full flex gap-4 items-center cursor-pointer text-black hover:text-mti-orange transition duration-300 ease-in-out",
|
||||
"p-4 px-8 rounded-full flex gap-4 items-center cursor-pointer text-black hover:text-mti-rose transition duration-300 ease-in-out",
|
||||
"absolute bottom-8",
|
||||
)}>
|
||||
<RiLogoutBoxFill size={20} />
|
||||
|
||||
@@ -5,7 +5,15 @@ import {CommonProps} from ".";
|
||||
import {Fragment} from "react";
|
||||
import Button from "../Low/Button";
|
||||
|
||||
export default function FillBlanksSolutions({prompt, solutions, text, userSolutions, onNext, onBack}: FillBlanksExercise & CommonProps) {
|
||||
export default function FillBlanksSolutions({id, type, prompt, solutions, text, userSolutions, onNext, onBack}: FillBlanksExercise & CommonProps) {
|
||||
const calculateScore = () => {
|
||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||
const correct = userSolutions.filter((x) => solutions.find((y) => x.id === y.id)?.solution === x.solution.toLowerCase() || false).length;
|
||||
const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id === y.id)).length;
|
||||
|
||||
return {total, correct, missing};
|
||||
};
|
||||
|
||||
const renderLines = (line: string) => {
|
||||
return (
|
||||
<span>
|
||||
@@ -42,8 +50,8 @@ export default function FillBlanksSolutions({prompt, solutions, text, userSoluti
|
||||
<>
|
||||
<button
|
||||
className={clsx(
|
||||
"rounded-full hover:text-white hover:bg-mti-orange transition duration-300 ease-in-out my-1 mr-1",
|
||||
userSolution && "px-5 py-2 text-center text-white bg-mti-orange-light",
|
||||
"rounded-full hover:text-white hover:bg-mti-rose transition duration-300 ease-in-out my-1 mr-1",
|
||||
userSolution && "px-5 py-2 text-center text-white bg-mti-rose-light",
|
||||
)}>
|
||||
{userSolution.solution}
|
||||
</button>
|
||||
@@ -92,18 +100,25 @@ export default function FillBlanksSolutions({prompt, solutions, text, userSoluti
|
||||
Unanswered
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="w-4 h-4 rounded-full bg-mti-orange" />
|
||||
<div className="w-4 h-4 rounded-full bg-mti-rose" />
|
||||
Wrong
|
||||
</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} className="max-w-[200px] w-full">
|
||||
<Button
|
||||
color="purple"
|
||||
variant="outline"
|
||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||
className="max-w-[200px] w-full">
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full">
|
||||
<Button
|
||||
color="purple"
|
||||
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,24 @@ import {Fragment} from "react";
|
||||
import Button from "../Low/Button";
|
||||
import Xarrow from "react-xarrows";
|
||||
|
||||
export default function MatchSentencesSolutions({options, prompt, sentences, userSolutions, onNext, onBack}: MatchSentencesExercise & CommonProps) {
|
||||
export default function MatchSentencesSolutions({
|
||||
id,
|
||||
type,
|
||||
options,
|
||||
prompt,
|
||||
sentences,
|
||||
userSolutions,
|
||||
onNext,
|
||||
onBack,
|
||||
}: MatchSentencesExercise & CommonProps) {
|
||||
const calculateScore = () => {
|
||||
const total = sentences.length;
|
||||
const correct = userSolutions.filter((x) => sentences.find((y) => y.id === x.question)?.solution === x.option || false).length;
|
||||
const missing = total - userSolutions.filter((x) => sentences.find((y) => y.id === x.question)).length;
|
||||
|
||||
return {total, correct, missing};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-4 mt-4 h-full mb-20">
|
||||
@@ -33,7 +50,7 @@ export default function MatchSentencesSolutions({options, prompt, sentences, use
|
||||
"transition duration-300 ease-in-out",
|
||||
!userSolutions.find((x) => x.question === id) && "!bg-mti-red",
|
||||
userSolutions.find((x) => x.question === id)?.option === solution && "bg-mti-purple",
|
||||
userSolutions.find((x) => x.question === id)?.option !== solution && "bg-mti-orange",
|
||||
userSolutions.find((x) => x.question === id)?.option !== solution && "bg-mti-rose",
|
||||
)}>
|
||||
{id}
|
||||
</button>
|
||||
@@ -62,10 +79,10 @@ export default function MatchSentencesSolutions({options, prompt, sentences, use
|
||||
end={sentence.solution}
|
||||
lineColor={
|
||||
!userSolutions.find((x) => x.question === sentence.id)
|
||||
? "#0696ff"
|
||||
? "#CC5454"
|
||||
: userSolutions.find((x) => x.question === sentence.id)?.option === sentence.solution
|
||||
? "#307912"
|
||||
: "#FF6000"
|
||||
? "#7872BF"
|
||||
: "#CC5454"
|
||||
}
|
||||
showHead={false}
|
||||
/>
|
||||
@@ -79,17 +96,24 @@ export default function MatchSentencesSolutions({options, prompt, sentences, use
|
||||
<div className="w-4 h-4 rounded-full bg-mti-red" /> Unanswered
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="w-4 h-4 rounded-full bg-mti-orange" /> Wrong
|
||||
<div className="w-4 h-4 rounded-full bg-mti-rose" /> Wrong
|
||||
</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()} className="max-w-[200px] w-full">
|
||||
<Button
|
||||
color="purple"
|
||||
variant="outline"
|
||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||
className="max-w-[200px] w-full">
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full">
|
||||
<Button
|
||||
color="purple"
|
||||
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ function Question({
|
||||
return "!border-mti-purple-light !text-mti-purple-light";
|
||||
}
|
||||
|
||||
return userSolution === option ? "!border-mti-orange-light !text-mti-orange-light" : "";
|
||||
return userSolution === option ? "!border-mti-rose-light !text-mti-rose-light" : "";
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -54,12 +54,20 @@ function Question({
|
||||
);
|
||||
}
|
||||
|
||||
export default function MultipleChoice({prompt, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
|
||||
export default function MultipleChoice({id, type, prompt, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
|
||||
const [questionIndex, setQuestionIndex] = useState(0);
|
||||
|
||||
const calculateScore = () => {
|
||||
const total = questions.length;
|
||||
const correct = userSolutions.filter((x) => questions.find((y) => y.id === x.question)?.solution === x.option || false).length;
|
||||
const missing = total - userSolutions.filter((x) => questions.find((y) => y.id === x.question)).length;
|
||||
|
||||
return {total, correct, missing};
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
if (questionIndex === questions.length - 1) {
|
||||
onNext();
|
||||
onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type});
|
||||
} else {
|
||||
setQuestionIndex((prev) => prev + 1);
|
||||
}
|
||||
@@ -67,7 +75,7 @@ export default function MultipleChoice({prompt, questions, userSolutions, onNext
|
||||
|
||||
const back = () => {
|
||||
if (questionIndex === 0) {
|
||||
onBack();
|
||||
onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type});
|
||||
} else {
|
||||
setQuestionIndex((prev) => prev - 1);
|
||||
}
|
||||
@@ -95,7 +103,7 @@ export default function MultipleChoice({prompt, questions, userSolutions, onNext
|
||||
Unanswered
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="w-4 h-4 rounded-full bg-mti-orange" />
|
||||
<div className="w-4 h-4 rounded-full bg-mti-rose" />
|
||||
Wrong
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ import axios from "axios";
|
||||
|
||||
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
|
||||
|
||||
export default function Speaking({title, text, prompts, userSolutions, onNext, onBack}: SpeakingExercise & CommonProps) {
|
||||
export default function Speaking({id, type, title, text, prompts, userSolutions, onNext, onBack}: SpeakingExercise & CommonProps) {
|
||||
const [solutionURL, setSolutionURL] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -71,11 +71,31 @@ export default function Speaking({title, text, prompts, userSolutions, onNext, o
|
||||
</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} className="max-w-[200px] w-full">
|
||||
<Button
|
||||
color="purple"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
onBack({
|
||||
exercise: id,
|
||||
solutions: userSolutions,
|
||||
score: {correct: 1, total: 1, missing: 0},
|
||||
type,
|
||||
})
|
||||
}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full">
|
||||
<Button
|
||||
color="purple"
|
||||
onClick={() =>
|
||||
onNext({
|
||||
exercise: id,
|
||||
solutions: userSolutions,
|
||||
score: {correct: 1, total: 1, missing: 0},
|
||||
type,
|
||||
})
|
||||
}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
131
src/components/Solutions/TrueFalse.tsx
Normal file
131
src/components/Solutions/TrueFalse.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import {FillBlanksExercise, TrueFalseExercise} from "@/interfaces/exam";
|
||||
import clsx from "clsx";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import {CommonProps} from ".";
|
||||
import {Fragment} from "react";
|
||||
import Button from "../Low/Button";
|
||||
|
||||
type Solution = "true" | "false" | "not_given";
|
||||
|
||||
export default function TrueFalseSolution({prompt, type, id, questions, userSolutions, onNext, onBack}: TrueFalseExercise & CommonProps) {
|
||||
const calculateScore = () => {
|
||||
const total = questions.length || 0;
|
||||
const correct = userSolutions.filter((x) => questions.find((y) => x.id === y.id)?.solution === x.solution.toLowerCase() || false).length;
|
||||
const missing = total - userSolutions.filter((x) => questions.find((y) => x.id === y.id)).length;
|
||||
|
||||
return {total, correct, missing};
|
||||
};
|
||||
|
||||
const getButtonColor = (buttonSolution: Solution, solution: Solution, userSolution: Solution | undefined) => {
|
||||
if (buttonSolution !== userSolution && buttonSolution !== solution) return "purple";
|
||||
|
||||
if (userSolution) {
|
||||
if (userSolution === buttonSolution && solution === buttonSolution) {
|
||||
return "purple";
|
||||
}
|
||||
|
||||
if (solution === buttonSolution) {
|
||||
return "purple";
|
||||
}
|
||||
|
||||
return "rose";
|
||||
}
|
||||
|
||||
return "red";
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-4 mt-4 h-full mb-20">
|
||||
<span className="text-sm w-full leading-6">
|
||||
{prompt.split("\\n").map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
<div className="flex flex-col gap-6 mb-4">
|
||||
<p>For each of the questions below, select</p>
|
||||
<div className="pl-8 flex gap-8">
|
||||
<span className="flex flex-col gap-4">
|
||||
<span className="font-bold italic">TRUE</span>
|
||||
<span className="font-bold italic">FALSE</span>
|
||||
<span className="font-bold italic">NOT GIVEN</span>
|
||||
</span>
|
||||
<span className="flex flex-col gap-4">
|
||||
<span>if the statement agrees with the information</span>
|
||||
<span>if the statement contradicts with the information</span>
|
||||
<span>if there is no information on this</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm w-full leading-6">You can click a selected option again to deselect it.</span>
|
||||
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-8">
|
||||
{questions.map((question, index) => {
|
||||
const userSolution = userSolutions.find((x) => x.id === question.id);
|
||||
|
||||
return (
|
||||
<div key={question.id} className="flex flex-col gap-4">
|
||||
<span>
|
||||
{index + 1}. {question.prompt}
|
||||
</span>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant={question.solution === "true" || userSolution?.solution === "true" ? "solid" : "outline"}
|
||||
className="!py-2"
|
||||
color={getButtonColor("true", question.solution, userSolution?.solution)}>
|
||||
True
|
||||
</Button>
|
||||
<Button
|
||||
variant={question.solution === "false" || userSolution?.solution === "false" ? "solid" : "outline"}
|
||||
className="!py-2"
|
||||
color={getButtonColor("false", question.solution, userSolution?.solution)}>
|
||||
False
|
||||
</Button>
|
||||
<Button
|
||||
variant={question.solution === "not_given" || userSolution?.solution === "not_given" ? "solid" : "outline"}
|
||||
className="!py-2"
|
||||
color={getButtonColor("not_given", question.solution, userSolution?.solution)}>
|
||||
Not Given
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="w-4 h-4 rounded-full bg-mti-purple" />
|
||||
Correct
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="w-4 h-4 rounded-full bg-mti-red" />
|
||||
Unanswered
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="w-4 h-4 rounded-full bg-mti-rose" />
|
||||
Wrong
|
||||
</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: userSolutions, score: calculateScore(), type})}
|
||||
className="max-w-[200px] w-full">
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -49,7 +49,7 @@ function Blank({
|
||||
<span className="inline-flex gap-2">
|
||||
{userSolution && !isUserSolutionCorrect() && (
|
||||
<input
|
||||
className="py-2 px-3 rounded-2xl w-48 focus:outline-none my-2 bg-mti-orange-ultralight text-mti-orange-light"
|
||||
className="py-2 px-3 rounded-2xl w-48 focus:outline-none my-2 bg-mti-rose-ultralight text-mti-rose-light"
|
||||
placeholder={id}
|
||||
onChange={(e) => setUserInput(e.target.value)}
|
||||
value={userSolution}
|
||||
@@ -69,6 +69,7 @@ function Blank({
|
||||
|
||||
export default function WriteBlanksSolutions({
|
||||
id,
|
||||
type,
|
||||
prompt,
|
||||
maxWords,
|
||||
solutions,
|
||||
@@ -77,6 +78,20 @@ export default function WriteBlanksSolutions({
|
||||
onNext,
|
||||
onBack,
|
||||
}: WriteBlanksExercise & CommonProps) {
|
||||
const calculateScore = () => {
|
||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||
const correct = userSolutions.filter(
|
||||
(x) =>
|
||||
solutions
|
||||
.find((y) => x.id === y.id)
|
||||
?.solution.map((y) => y.toLowerCase())
|
||||
.includes(x.solution.toLowerCase()) || false,
|
||||
).length;
|
||||
const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id === y.id)).length;
|
||||
|
||||
return {total, correct, missing};
|
||||
};
|
||||
|
||||
const renderLines = (line: string) => {
|
||||
return (
|
||||
<span className="text-base leading-5">
|
||||
@@ -120,18 +135,25 @@ export default function WriteBlanksSolutions({
|
||||
Unanswered
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="w-4 h-4 rounded-full bg-mti-orange" />
|
||||
<div className="w-4 h-4 rounded-full bg-mti-rose" />
|
||||
Wrong
|
||||
</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()} className="max-w-[200px] w-full">
|
||||
<Button
|
||||
color="purple"
|
||||
variant="outline"
|
||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||
className="max-w-[200px] w-full">
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full">
|
||||
<Button
|
||||
color="purple"
|
||||
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import {toast} from "react-toastify";
|
||||
import Button from "../Low/Button";
|
||||
import {Dialog, Transition} from "@headlessui/react";
|
||||
|
||||
export default function Writing({id, prompt, attachment, userSolutions, onNext, onBack}: WritingExercise & CommonProps) {
|
||||
export default function Writing({id, type, prompt, attachment, userSolutions, onNext, onBack}: WritingExercise & CommonProps) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -96,11 +96,17 @@ export default function Writing({id, prompt, attachment, userSolutions, onNext,
|
||||
</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} className="max-w-[200px] w-full">
|
||||
<Button
|
||||
color="purple"
|
||||
variant="outline"
|
||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: {correct: 1, total: 1, missing: 0}, type})}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full">
|
||||
<Button
|
||||
color="purple"
|
||||
onClick={() => onNext({exercise: id, solutions: userSolutions, score: {correct: 1, total: 1, missing: 0}, type})}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
MatchSentencesExercise,
|
||||
MultipleChoiceExercise,
|
||||
SpeakingExercise,
|
||||
TrueFalseExercise,
|
||||
UserSolution,
|
||||
WriteBlanksExercise,
|
||||
WritingExercise,
|
||||
} from "@/interfaces/exam";
|
||||
@@ -11,20 +13,23 @@ import dynamic from "next/dynamic";
|
||||
import FillBlanks from "./FillBlanks";
|
||||
import MultipleChoice from "./MultipleChoice";
|
||||
import Speaking from "./Speaking";
|
||||
import TrueFalseSolution from "./TrueFalse";
|
||||
import WriteBlanks from "./WriteBlanks";
|
||||
import Writing from "./Writing";
|
||||
|
||||
const MatchSentences = dynamic(() => import("@/components/Solutions/MatchSentences"), {ssr: false});
|
||||
|
||||
export interface CommonProps {
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
onNext: (userSolutions: UserSolution) => void;
|
||||
onBack: (userSolutions: UserSolution) => void;
|
||||
}
|
||||
|
||||
export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: () => void) => {
|
||||
switch (exercise.type) {
|
||||
case "fillBlanks":
|
||||
return <FillBlanks {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
||||
case "trueFalse":
|
||||
return <TrueFalseSolution {...(exercise as TrueFalseExercise)} onNext={onNext} onBack={onBack} />;
|
||||
case "matchSentences":
|
||||
return <MatchSentences {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} />;
|
||||
case "multipleChoice":
|
||||
|
||||
Reference in New Issue
Block a user