From db54d58bab20425aa6ca74ddab2f3d4aba577112 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Fri, 11 Aug 2023 14:23:09 +0100 Subject: [PATCH] - Added a new type of exercise - Updated all solutions to solve a huge bug where after reviewing, it would reset the score --- src/components/Exercises/MatchSentences.tsx | 2 +- src/components/Exercises/TrueFalse.tsx | 111 +++++++++++++++++ src/components/Exercises/index.tsx | 4 + src/components/Low/Button.tsx | 8 +- src/components/Sidebar.tsx | 2 +- src/components/Solutions/FillBlanks.tsx | 27 +++- src/components/Solutions/MatchSentences.tsx | 40 ++++-- src/components/Solutions/MultipleChoice.tsx | 18 ++- src/components/Solutions/Speaking.tsx | 28 ++++- src/components/Solutions/TrueFalse.tsx | 131 ++++++++++++++++++++ src/components/Solutions/WriteBlanks.tsx | 30 ++++- src/components/Solutions/Writing.tsx | 14 ++- src/components/Solutions/index.tsx | 9 +- src/exams/Finish.tsx | 4 +- src/interfaces/exam.ts | 15 +++ src/pages/login.tsx | 6 +- src/pages/record.tsx | 4 +- src/pages/register.tsx | 2 +- 18 files changed, 407 insertions(+), 48 deletions(-) create mode 100644 src/components/Exercises/TrueFalse.tsx create mode 100644 src/components/Solutions/TrueFalse.tsx diff --git a/src/components/Exercises/MatchSentences.tsx b/src/components/Exercises/MatchSentences.tsx index 389da576..a74ca9b8 100644 --- a/src/components/Exercises/MatchSentences.tsx +++ b/src/components/Exercises/MatchSentences.tsx @@ -83,7 +83,7 @@ export default function MatchSentences({id, options, type, prompt, sentences, us ))} {answers.map((solution, index) => ( - + ))} diff --git a/src/components/Exercises/TrueFalse.tsx b/src/components/Exercises/TrueFalse.tsx new file mode 100644 index 00000000..1da90a60 --- /dev/null +++ b/src/components/Exercises/TrueFalse.tsx @@ -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 ( + <> +
+ + {prompt.split("\\n").map((line, index) => ( + + {line} +
+
+ ))} +
+
+

For each of the questions below, select

+
+ + TRUE + FALSE + NOT GIVEN + + + if the statement agrees with the information + if the statement contradicts with the information + if there is no information on this + +
+
+ You can click a selected option again to deselect it. +
+ {questions.map((question, index) => ( +
+ + {index + 1}. {question.prompt} + +
+ + + +
+
+ ))} +
+
+ +
+ + + +
+ + ); +} diff --git a/src/components/Exercises/index.tsx b/src/components/Exercises/index.tsx index 232dda24..e4d1e845 100644 --- a/src/components/Exercises/index.tsx +++ b/src/components/Exercises/index.tsx @@ -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 ; + case "trueFalse": + return ; case "matchSentences": return ; case "multipleChoice": diff --git a/src/components/Low/Button.tsx b/src/components/Low/Button.tsx index e6033680..cc288052 100644 --- a/src/components/Low/Button.tsx +++ b/src/components/Low/Button.tsx @@ -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", }, }; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 4e5ccf43..0cfb8e60 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -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", )}> diff --git a/src/components/Solutions/FillBlanks.tsx b/src/components/Solutions/FillBlanks.tsx index 4750aef8..49494f82 100644 --- a/src/components/Solutions/FillBlanks.tsx +++ b/src/components/Solutions/FillBlanks.tsx @@ -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 ( @@ -42,8 +50,8 @@ export default function FillBlanksSolutions({prompt, solutions, text, userSoluti <> @@ -92,18 +100,25 @@ export default function FillBlanksSolutions({prompt, solutions, text, userSoluti Unanswered
-
+
Wrong
- -
diff --git a/src/components/Solutions/MatchSentences.tsx b/src/components/Solutions/MatchSentences.tsx index 0e11f804..f38a858b 100644 --- a/src/components/Solutions/MatchSentences.tsx +++ b/src/components/Solutions/MatchSentences.tsx @@ -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 ( <>
@@ -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} @@ -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
Unanswered
-
Wrong +
Wrong
- -
diff --git a/src/components/Solutions/MultipleChoice.tsx b/src/components/Solutions/MultipleChoice.tsx index 4c76d43b..d301ea93 100644 --- a/src/components/Solutions/MultipleChoice.tsx +++ b/src/components/Solutions/MultipleChoice.tsx @@ -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
-
+
Wrong
diff --git a/src/components/Solutions/Speaking.tsx b/src/components/Solutions/Speaking.tsx index 458e2a9a..b62c0d83 100644 --- a/src/components/Solutions/Speaking.tsx +++ b/src/components/Solutions/Speaking.tsx @@ -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(); useEffect(() => { @@ -71,11 +71,31 @@ export default function Speaking({title, text, prompts, userSolutions, onNext, o
- - -
diff --git a/src/components/Solutions/TrueFalse.tsx b/src/components/Solutions/TrueFalse.tsx new file mode 100644 index 00000000..f16b5aac --- /dev/null +++ b/src/components/Solutions/TrueFalse.tsx @@ -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 ( + <> +
+ + {prompt.split("\\n").map((line, index) => ( + + {line} +
+
+ ))} +
+
+

For each of the questions below, select

+
+ + TRUE + FALSE + NOT GIVEN + + + if the statement agrees with the information + if the statement contradicts with the information + if there is no information on this + +
+
+ You can click a selected option again to deselect it. +
+ {questions.map((question, index) => { + const userSolution = userSolutions.find((x) => x.id === question.id); + + return ( +
+ + {index + 1}. {question.prompt} + +
+ + + +
+
+ ); + })} +
+
+
+
+ Correct +
+
+
+ Unanswered +
+
+
+ Wrong +
+
+
+ +
+ + + +
+ + ); +} diff --git a/src/components/Solutions/WriteBlanks.tsx b/src/components/Solutions/WriteBlanks.tsx index 7eff1bc7..6fa860b4 100644 --- a/src/components/Solutions/WriteBlanks.tsx +++ b/src/components/Solutions/WriteBlanks.tsx @@ -49,7 +49,7 @@ function Blank({ {userSolution && !isUserSolutionCorrect() && ( 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 ( @@ -120,18 +135,25 @@ export default function WriteBlanksSolutions({ Unanswered
-
+
Wrong
- -
diff --git a/src/components/Solutions/Writing.tsx b/src/components/Solutions/Writing.tsx index 1cd7dc61..ef12a7d9 100644 --- a/src/components/Solutions/Writing.tsx +++ b/src/components/Solutions/Writing.tsx @@ -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,
- - -
diff --git a/src/components/Solutions/index.tsx b/src/components/Solutions/index.tsx index f77e41f0..225b3ace 100644 --- a/src/components/Solutions/index.tsx +++ b/src/components/Solutions/index.tsx @@ -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 ; + case "trueFalse": + return ; case "matchSentences": return ; case "multipleChoice": diff --git a/src/exams/Finish.tsx b/src/exams/Finish.tsx index 37514577..93fba474 100644 --- a/src/exams/Finish.tsx +++ b/src/exams/Finish.tsx @@ -155,9 +155,9 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
-
+
- + {(selectedScore.total - selectedScore.correct).toString().padStart(2, "0")} Wrong diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index d4af81bf..fa606959 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -63,6 +63,7 @@ export interface SpeakingExam { export type Exercise = | FillBlanksExercise + | TrueFalseExercise | MatchSentencesExercise | MultipleChoiceExercise | 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 { 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 diff --git a/src/pages/login.tsx b/src/pages/login.tsx index d33844e1..c3f51ba1 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -54,7 +54,7 @@ export default function Login() {
-
+
People smiling looking at a tablet
@@ -79,9 +79,7 @@ export default function Login() {
Remember my password
- - Forgot Password? - + Forgot Password?