- 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:
Tiago Ribeiro
2023-08-11 14:23:09 +01:00
parent 5099721b9b
commit db54d58bab
18 changed files with 407 additions and 48 deletions

View File

@@ -83,7 +83,7 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
))} ))}
</div> </div>
{answers.map((solution, index) => ( {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>
</div> </div>

View 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>
</>
);
}

View File

@@ -4,6 +4,7 @@ import {
MatchSentencesExercise, MatchSentencesExercise,
MultipleChoiceExercise, MultipleChoiceExercise,
SpeakingExercise, SpeakingExercise,
TrueFalseExercise,
UserSolution, UserSolution,
WriteBlanksExercise, WriteBlanksExercise,
WritingExercise, WritingExercise,
@@ -14,6 +15,7 @@ import MultipleChoice from "./MultipleChoice";
import WriteBlanks from "./WriteBlanks"; import WriteBlanks from "./WriteBlanks";
import Writing from "./Writing"; import Writing from "./Writing";
import Speaking from "./Speaking"; import Speaking from "./Speaking";
import TrueFalse from "./TrueFalse";
const MatchSentences = dynamic(() => import("@/components/Exercises/MatchSentences"), {ssr: false}); const MatchSentences = dynamic(() => import("@/components/Exercises/MatchSentences"), {ssr: false});
@@ -26,6 +28,8 @@ export const renderExercise = (exercise: Exercise, onNext: (userSolutions: UserS
switch (exercise.type) { switch (exercise.type) {
case "fillBlanks": case "fillBlanks":
return <FillBlanks {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} />; return <FillBlanks {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} />;
case "trueFalse":
return <TrueFalse {...(exercise as TrueFalseExercise)} onNext={onNext} onBack={onBack} />;
case "matchSentences": case "matchSentences":
return <MatchSentences {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} />; return <MatchSentences {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} />;
case "multipleChoice": case "multipleChoice":

View File

@@ -13,19 +13,19 @@ interface Props {
export default function Button({color = "purple", variant = "solid", disabled = false, className, children, onClick}: 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}} = { const colorClassNames: {[key in typeof color]: {[key in typeof variant]: string}} = {
purple: { 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: 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", "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: { 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: 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", "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: { 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: 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",
}, },
}; };

View File

@@ -58,7 +58,7 @@ export default function Sidebar({path, navDisabled = false}: Props) {
tabIndex={1} tabIndex={1}
onClick={logout} onClick={logout}
className={clsx( 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", "absolute bottom-8",
)}> )}>
<RiLogoutBoxFill size={20} /> <RiLogoutBoxFill size={20} />

View File

@@ -5,7 +5,15 @@ import {CommonProps} from ".";
import {Fragment} from "react"; import {Fragment} from "react";
import Button from "../Low/Button"; 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) => { const renderLines = (line: string) => {
return ( return (
<span> <span>
@@ -42,8 +50,8 @@ export default function FillBlanksSolutions({prompt, solutions, text, userSoluti
<> <>
<button <button
className={clsx( className={clsx(
"rounded-full hover:text-white hover:bg-mti-orange transition duration-300 ease-in-out my-1 mr-1", "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-orange-light", userSolution && "px-5 py-2 text-center text-white bg-mti-rose-light",
)}> )}>
{userSolution.solution} {userSolution.solution}
</button> </button>
@@ -92,18 +100,25 @@ export default function FillBlanksSolutions({prompt, solutions, text, userSoluti
Unanswered Unanswered
</div> </div>
<div className="flex gap-2 items-center"> <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 Wrong
</div> </div>
</div> </div>
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <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 Back
</Button> </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 Next
</Button> </Button>
</div> </div>

View File

@@ -9,7 +9,24 @@ import {Fragment} from "react";
import Button from "../Low/Button"; import Button from "../Low/Button";
import Xarrow from "react-xarrows"; 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 ( return (
<> <>
<div className="flex flex-col gap-4 mt-4 h-full mb-20"> <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", "transition duration-300 ease-in-out",
!userSolutions.find((x) => x.question === id) && "!bg-mti-red", !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-purple",
userSolutions.find((x) => x.question === id)?.option !== solution && "bg-mti-orange", userSolutions.find((x) => x.question === id)?.option !== solution && "bg-mti-rose",
)}> )}>
{id} {id}
</button> </button>
@@ -62,10 +79,10 @@ export default function MatchSentencesSolutions({options, prompt, sentences, use
end={sentence.solution} end={sentence.solution}
lineColor={ lineColor={
!userSolutions.find((x) => x.question === sentence.id) !userSolutions.find((x) => x.question === sentence.id)
? "#0696ff" ? "#CC5454"
: userSolutions.find((x) => x.question === sentence.id)?.option === sentence.solution : userSolutions.find((x) => x.question === sentence.id)?.option === sentence.solution
? "#307912" ? "#7872BF"
: "#FF6000" : "#CC5454"
} }
showHead={false} 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 className="w-4 h-4 rounded-full bg-mti-red" /> Unanswered
</div> </div>
<div className="flex gap-2 items-center"> <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>
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <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 Back
</Button> </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 Next
</Button> </Button>
</div> </div>

View File

@@ -21,7 +21,7 @@ function Question({
return "!border-mti-purple-light !text-mti-purple-light"; 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 ( 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 [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 = () => { const next = () => {
if (questionIndex === questions.length - 1) { if (questionIndex === questions.length - 1) {
onNext(); onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type});
} else { } else {
setQuestionIndex((prev) => prev + 1); setQuestionIndex((prev) => prev + 1);
} }
@@ -67,7 +75,7 @@ export default function MultipleChoice({prompt, questions, userSolutions, onNext
const back = () => { const back = () => {
if (questionIndex === 0) { if (questionIndex === 0) {
onBack(); onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type});
} else { } else {
setQuestionIndex((prev) => prev - 1); setQuestionIndex((prev) => prev - 1);
} }
@@ -95,7 +103,7 @@ export default function MultipleChoice({prompt, questions, userSolutions, onNext
Unanswered Unanswered
</div> </div>
<div className="flex gap-2 items-center"> <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 Wrong
</div> </div>
</div> </div>

View File

@@ -8,7 +8,7 @@ import axios from "axios";
const Waveform = dynamic(() => import("../Waveform"), {ssr: false}); 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>(); const [solutionURL, setSolutionURL] = useState<string>();
useEffect(() => { useEffect(() => {
@@ -71,11 +71,31 @@ export default function Speaking({title, text, prompts, userSolutions, onNext, o
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <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 Back
</Button> </Button>
<Button
<Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full"> 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 Next
</Button> </Button>
</div> </div>

View 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>
</>
);
}

View File

@@ -49,7 +49,7 @@ function Blank({
<span className="inline-flex gap-2"> <span className="inline-flex gap-2">
{userSolution && !isUserSolutionCorrect() && ( {userSolution && !isUserSolutionCorrect() && (
<input <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} placeholder={id}
onChange={(e) => setUserInput(e.target.value)} onChange={(e) => setUserInput(e.target.value)}
value={userSolution} value={userSolution}
@@ -69,6 +69,7 @@ function Blank({
export default function WriteBlanksSolutions({ export default function WriteBlanksSolutions({
id, id,
type,
prompt, prompt,
maxWords, maxWords,
solutions, solutions,
@@ -77,6 +78,20 @@ export default function WriteBlanksSolutions({
onNext, onNext,
onBack, onBack,
}: WriteBlanksExercise & CommonProps) { }: 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) => { const renderLines = (line: string) => {
return ( return (
<span className="text-base leading-5"> <span className="text-base leading-5">
@@ -120,18 +135,25 @@ export default function WriteBlanksSolutions({
Unanswered Unanswered
</div> </div>
<div className="flex gap-2 items-center"> <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 Wrong
</div> </div>
</div> </div>
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <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 Back
</Button> </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 Next
</Button> </Button>
</div> </div>

View File

@@ -10,7 +10,7 @@ import {toast} from "react-toastify";
import Button from "../Low/Button"; import Button from "../Low/Button";
import {Dialog, Transition} from "@headlessui/react"; 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); const [isModalOpen, setIsModalOpen] = useState(false);
return ( return (
@@ -96,11 +96,17 @@ export default function Writing({id, prompt, attachment, userSolutions, onNext,
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <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 Back
</Button> </Button>
<Button
<Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full"> 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 Next
</Button> </Button>
</div> </div>

View File

@@ -4,6 +4,8 @@ import {
MatchSentencesExercise, MatchSentencesExercise,
MultipleChoiceExercise, MultipleChoiceExercise,
SpeakingExercise, SpeakingExercise,
TrueFalseExercise,
UserSolution,
WriteBlanksExercise, WriteBlanksExercise,
WritingExercise, WritingExercise,
} from "@/interfaces/exam"; } from "@/interfaces/exam";
@@ -11,20 +13,23 @@ import dynamic from "next/dynamic";
import FillBlanks from "./FillBlanks"; import FillBlanks from "./FillBlanks";
import MultipleChoice from "./MultipleChoice"; import MultipleChoice from "./MultipleChoice";
import Speaking from "./Speaking"; import Speaking from "./Speaking";
import TrueFalseSolution from "./TrueFalse";
import WriteBlanks from "./WriteBlanks"; import WriteBlanks from "./WriteBlanks";
import Writing from "./Writing"; import Writing from "./Writing";
const MatchSentences = dynamic(() => import("@/components/Solutions/MatchSentences"), {ssr: false}); const MatchSentences = dynamic(() => import("@/components/Solutions/MatchSentences"), {ssr: false});
export interface CommonProps { export interface CommonProps {
onNext: () => void; onNext: (userSolutions: UserSolution) => void;
onBack: () => void; onBack: (userSolutions: UserSolution) => void;
} }
export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: () => void) => { export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: () => void) => {
switch (exercise.type) { switch (exercise.type) {
case "fillBlanks": case "fillBlanks":
return <FillBlanks {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} />; return <FillBlanks {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} />;
case "trueFalse":
return <TrueFalseSolution {...(exercise as TrueFalseExercise)} onNext={onNext} onBack={onBack} />;
case "matchSentences": case "matchSentences":
return <MatchSentences {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} />; return <MatchSentences {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} />;
case "multipleChoice": case "multipleChoice":

View File

@@ -155,9 +155,9 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<div className="w-3 h-3 bg-mti-orange-light rounded-full mt-1" /> <div className="w-3 h-3 bg-mti-rose-light rounded-full mt-1" />
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-mti-orange-light"> <span className="text-mti-rose-light">
{(selectedScore.total - selectedScore.correct).toString().padStart(2, "0")} {(selectedScore.total - selectedScore.correct).toString().padStart(2, "0")}
</span> </span>
<span className="text-lg">Wrong</span> <span className="text-lg">Wrong</span>

View File

@@ -63,6 +63,7 @@ export interface SpeakingExam {
export type Exercise = export type Exercise =
| FillBlanksExercise | FillBlanksExercise
| TrueFalseExercise
| MatchSentencesExercise | MatchSentencesExercise
| MultipleChoiceExercise | MultipleChoiceExercise
| WriteBlanksExercise | WriteBlanksExercise
@@ -122,6 +123,20 @@ export interface FillBlanksExercise {
}[]; }[];
} }
export interface TrueFalseExercise {
type: "trueFalse";
id: string;
prompt: string; // *EXAMPLE: "Select the appropriate option."
questions: TrueFalseQuestion[];
userSolutions: {id: string; solution: "true" | "false" | "not_given"}[];
}
export interface TrueFalseQuestion {
id: string; // *EXAMPLE: "1"
prompt: string; // *EXAMPLE: "What does her briefcase look like?"
solution: "true" | "false" | "not_given"; // *EXAMPLE: "True"
}
export interface WriteBlanksExercise { export interface WriteBlanksExercise {
prompt: string; // *EXAMPLE: "Complete the notes below by writing NO MORE THAN THREE WORDS in the spaces provided." prompt: string; // *EXAMPLE: "Complete the notes below by writing NO MORE THAN THREE WORDS in the spaces provided."
maxWords: number; // *EXAMPLE: 3 - The maximum amount of words allowed per blank, 0 for unlimited maxWords: number; // *EXAMPLE: 3 - The maximum amount of words allowed per blank, 0 for unlimited

View File

@@ -54,7 +54,7 @@ export default function Login() {
<main className="w-full h-[100vh] flex bg-white text-black"> <main className="w-full h-[100vh] flex bg-white text-black">
<ToastContainer /> <ToastContainer />
<section className="h-full w-fit min-w-fit relative"> <section className="h-full w-fit min-w-fit relative">
<div className="absolute h-full w-full bg-mti-orange-light z-10 bg-opacity-50" /> <div className="absolute h-full w-full bg-mti-rose-light z-10 bg-opacity-50" />
<img src="/people-talking-tablet.png" alt="People smiling looking at a tablet" className="h-full aspect-auto" /> <img src="/people-talking-tablet.png" alt="People smiling looking at a tablet" className="h-full aspect-auto" />
</section> </section>
<section className="h-full w-full flex flex-col items-center justify-center gap-2"> <section className="h-full w-full flex flex-col items-center justify-center gap-2">
@@ -79,9 +79,7 @@ export default function Login() {
</div> </div>
<span>Remember my password</span> <span>Remember my password</span>
</div> </div>
<Link href="/forgot-password" className="text-mti-purple-light text-xs"> <span className="text-mti-purple-light text-xs">Forgot Password?</span>
Forgot Password?
</Link>
</div> </div>
<Button className="mt-8 w-full" color="purple" disabled={isLoading}> <Button className="mt-8 w-full" color="purple" disabled={isLoading}>
{!isLoading && "Login"} {!isLoading && "Login"}

View File

@@ -164,7 +164,7 @@ export default function History({user}: {user: User}) {
"flex flex-col gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300", "flex flex-col gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300",
correct / total >= 0.7 && "hover:border-mti-purple", correct / total >= 0.7 && "hover:border-mti-purple",
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red", correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
correct / total < 0.3 && "hover:border-mti-orange", correct / total < 0.3 && "hover:border-mti-rose",
)} )}
onClick={selectExam} onClick={selectExam}
role="button"> role="button">
@@ -174,7 +174,7 @@ export default function History({user}: {user: User}) {
className={clsx( className={clsx(
correct / total >= 0.7 && "text-mti-purple", correct / total >= 0.7 && "text-mti-purple",
correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red", correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red",
correct / total < 0.3 && "text-mti-orange", correct / total < 0.3 && "text-mti-rose",
)}> )}>
Level{" "} Level{" "}
{(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)} {(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)}

View File

@@ -56,7 +56,7 @@ export default function Register() {
<main className="w-full h-[100vh] flex bg-white text-black"> <main className="w-full h-[100vh] flex bg-white text-black">
<ToastContainer /> <ToastContainer />
<section className="h-full w-fit min-w-fit relative"> <section className="h-full w-fit min-w-fit relative">
<div className="absolute h-full w-full bg-mti-orange-light z-10 bg-opacity-50" /> <div className="absolute h-full w-full bg-mti-rose-light z-10 bg-opacity-50" />
<img src="/people-talking-tablet.png" alt="People smiling looking at a tablet" className="h-full aspect-auto" /> <img src="/people-talking-tablet.png" alt="People smiling looking at a tablet" className="h-full aspect-auto" />
</section> </section>
<section className="h-full w-full flex flex-col items-center justify-center gap-12"> <section className="h-full w-full flex flex-col items-center justify-center gap-12">