From 49c515b02a7dbd7050a8989e474982edfe66b39f Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Tue, 11 Apr 2023 21:35:44 +0100 Subject: [PATCH] Finished implementing a Solutions version for each exercise --- src/components/Exercises/FillBlanks.tsx | 71 ++--------- src/components/Exercises/MatchSentences.tsx | 118 ++--------------- src/components/Exercises/MultipleChoice.tsx | 60 ++------- src/components/Exercises/WriteBlanks.tsx | 92 ++++---------- src/components/Exercises/index.tsx | 6 +- src/components/Solutions/FillBlanks.tsx | 73 +++++++++++ src/components/Solutions/MatchSentences.tsx | 73 +++++++++++ src/components/Solutions/MultipleChoice.tsx | 133 ++++++++++++++++++++ src/components/Solutions/WriteBlanks.tsx | 110 ++++++++++++++++ src/components/Solutions/index.tsx | 25 ++++ src/demo/listening.json | 7 +- src/demo/reading.json | 3 + src/demo/writing.json | 2 + src/exams/Finish.tsx | 0 src/exams/Listening.tsx | 34 +++-- src/exams/Reading.tsx | 28 ++++- src/exams/Writing.tsx | 16 ++- src/interfaces/exam.ts | 26 +++- src/pages/exam/index.tsx | 51 +++++++- src/stores/examStore.ts | 15 +-- 20 files changed, 610 insertions(+), 333 deletions(-) create mode 100644 src/components/Solutions/FillBlanks.tsx create mode 100644 src/components/Solutions/MatchSentences.tsx create mode 100644 src/components/Solutions/MultipleChoice.tsx create mode 100644 src/components/Solutions/WriteBlanks.tsx create mode 100644 src/components/Solutions/index.tsx create mode 100644 src/exams/Finish.tsx diff --git a/src/components/Exercises/FillBlanks.tsx b/src/components/Exercises/FillBlanks.tsx index 239537f6..622393c5 100644 --- a/src/components/Exercises/FillBlanks.tsx +++ b/src/components/Exercises/FillBlanks.tsx @@ -15,8 +15,6 @@ interface WordsPopoutProps { onAnswer: (answer: string) => void; } -type UserSolution = {id: string; solution: string}; - function WordsPopout({words, isOpen, onCancel, onAnswer}: WordsPopoutProps) { return ( @@ -72,10 +70,17 @@ function WordsPopout({words, isOpen, onCancel, onAnswer}: WordsPopoutProps) { ); } -export default function FillBlanks({allowRepetition, prompt, solutions, text, words, onNext, onBack}: FillBlanksExercise & CommonProps) { +export default function FillBlanks({id, allowRepetition, prompt, solutions, text, words, onNext, onBack}: FillBlanksExercise & CommonProps) { const [userSolutions, setUserSolutions] = useState<{id: string; solution: string}[]>([]); const [currentBlankId, setCurrentBlankId] = useState(); + 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; + + return {total, correct}; + }; + const renderLines = (line: string) => { return ( @@ -123,7 +128,9 @@ export default function FillBlanks({allowRepetition, prompt, solutions, text, wo Back - - - ); - } - - if (userSolution.solution === solution.solution) { - return ; - } - - if (userSolution.solution !== solution.solution) { - return ( - <> - - - - ); - } - })} - - ); - }; - - return ( -
- {prompt} - - {text.split("\n").map((line) => ( - <> - {renderLines(line)} -
- - ))} -
-
- ); -} diff --git a/src/components/Exercises/MatchSentences.tsx b/src/components/Exercises/MatchSentences.tsx index 5503a39d..8f974240 100644 --- a/src/components/Exercises/MatchSentences.tsx +++ b/src/components/Exercises/MatchSentences.tsx @@ -7,12 +7,17 @@ import {useState} from "react"; import LineTo from "react-lineto"; import {CommonProps} from "."; -const AVAILABLE_COLORS = ["#63526a", "#f7651d", "#278f04", "#ef4487", "#ca68c0", "#f5fe9b", "#b3ab01", "#af963a", "#9a85f1", "#1b1750"]; - -export default function MatchSentences({allowRepetition, options, prompt, sentences, onNext, onBack}: MatchSentencesExercise & CommonProps) { +export default function MatchSentences({id, options, prompt, sentences, onNext, onBack}: MatchSentencesExercise & CommonProps) { const [selectedQuestion, setSelectedQuestion] = useState(); const [userSolutions, setUserSolutions] = useState<{question: string; option: string}[]>([]); + const calculateScore = () => { + const total = sentences.length; + const correct = userSolutions.filter((x) => sentences.find((y) => y.id === x.question)?.solution === x.option || false).length; + + return {total, correct}; + }; + const selectOption = (option: string) => { if (!selectedQuestion) return; setUserSolutions((prev) => [...prev.filter((x) => x.question !== selectedQuestion), {question: selectedQuestion, option}]); @@ -87,7 +92,9 @@ export default function MatchSentences({allowRepetition, options, prompt, senten Back - - - - - ); -} diff --git a/src/components/Exercises/WriteBlanks.tsx b/src/components/Exercises/WriteBlanks.tsx index 5f014c40..810d41f5 100644 --- a/src/components/Exercises/WriteBlanks.tsx +++ b/src/components/Exercises/WriteBlanks.tsx @@ -11,51 +11,53 @@ import {toast} from "react-toastify"; function Blank({ id, maxWords, - solutions, - userSolution, - disabled = false, + showSolutions = false, setUserSolution, }: { id: string; solutions?: string[]; userSolution?: string; maxWords: number; - disabled?: boolean; + showSolutions?: boolean; setUserSolution?: (solution: string) => void; }) { - const [userInput, setUserInput] = useState(userSolution || ""); + const [userInput, setUserInput] = useState(""); useEffect(() => { const words = userInput.split(" ").filter((x) => x !== ""); if (words.length >= maxWords) { toast.warning(`You have reached your word limit of ${maxWords} words!`, {toastId: "word-limit"}); setUserInput(words.join(" ").trim()); - if (setUserSolution) setUserSolution(words.join(" ").trim()); } - }, [maxWords, userInput, setUserSolution]); - - const getSolutionStyling = () => { - if (solutions && userSolution) { - if (solutions.map((x) => x.trim().toLowerCase()).includes(userSolution.trim().toLowerCase())) return "text-green-500 border-green-500"; - } - - return "text-red-500 border-red-500"; - }; + }, [maxWords, userInput]); return ( setUserInput(e.target.value)} - value={!solutions ? userInput : solutions.join(" / ")} - contentEditable={disabled} + value={userInput} + contentEditable={showSolutions} /> ); } -export default function WriteBlanks({prompt, maxWords, solutions, text, onNext, onBack}: WriteBlanksExercise & CommonProps) { +export default function WriteBlanks({id, prompt, maxWords, solutions, text, onNext, onBack}: WriteBlanksExercise & CommonProps) { const [userSolutions, setUserSolutions] = useState<{id: string; solution: string}[]>([]); + 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; + + return {total, correct}; + }; + const renderLines = (line: string) => { return ( @@ -63,6 +65,7 @@ export default function WriteBlanks({prompt, maxWords, solutions, text, onNext, const id = match.replaceAll(/[\{\}]/g, ""); const userSolution = userSolutions.find((x) => x.id === id); const setUserSolution = (solution: string) => { + console.log({solution}); setUserSolutions((prev) => [...prev.filter((x) => x.id !== id), {id, solution}]); }; @@ -93,54 +96,9 @@ export default function WriteBlanks({prompt, maxWords, solutions, text, onNext, Back - - - - ); -} - -export function WriteBlanksSolutions({prompt, maxWords, solutions, userSolutions, text, onNext, onBack}: WriteBlanksExercise & CommonProps) { - const renderLines = (line: string) => { - return ( - - {reactStringReplace(line, /({{\d+}})/g, (match) => { - const id = match.replaceAll(/[\{\}]/g, ""); - const userSolution = userSolutions.find((x) => x.id === id); - const solution = solutions.find((x) => x.id === id)!; - - return ; - })} - - ); - }; - - return ( - <> -
- {prompt} - - {text.split("\n").map((line) => ( - <> - {renderLines(line)} -
- - ))} -
-
- -
- - + + ); + } + + if (userSolution.solution === solution.solution) { + return ; + } + + if (userSolution.solution !== solution.solution) { + return ( + <> + + + + ); + } + })} + + ); + }; + + return ( + <> +
+ {prompt} + + {text.split("\n").map((line) => ( + <> + {renderLines(line)} +
+ + ))} +
+
+ +
+ + +
+ + ); +} diff --git a/src/components/Solutions/MatchSentences.tsx b/src/components/Solutions/MatchSentences.tsx new file mode 100644 index 00000000..cb3d77dc --- /dev/null +++ b/src/components/Solutions/MatchSentences.tsx @@ -0,0 +1,73 @@ +import {MatchSentencesExercise} from "@/interfaces/exam"; +import clsx from "clsx"; +import LineTo from "react-lineto"; +import {CommonProps} from "."; +import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles"; +import {mdiArrowLeft, mdiArrowRight} from "@mdi/js"; +import Icon from "@mdi/react"; + +export default function MatchSentencesSolutions({options, prompt, sentences, userSolutions, onNext, onBack}: MatchSentencesExercise & CommonProps) { + return ( + <> +
+ {prompt} +
+
+ {sentences.map(({sentence, id, color, solution}) => ( +
x.question === id)?.option === solution ? "text-green-500" : "text-red-500", + )}> + + {id}. {sentence}{" "} + +
+
+ ))} +
+
+ {options.map(({sentence, id}) => ( +
+
x.solution === id) + ? { + border: `2px solid ${sentences.find((x) => x.solution === id)!.color}`, + } + : {} + } + className={clsx("border-2 border-green-500 bg-transparent w-4 h-4 rounded-full", id)} + /> + + {id}. {sentence}{" "} + +
+ ))} +
+ {sentences.map((sentence, index) => ( +
+ +
+ ))} +
+
+ +
+ + +
+ + ); +} diff --git a/src/components/Solutions/MultipleChoice.tsx b/src/components/Solutions/MultipleChoice.tsx new file mode 100644 index 00000000..564c7076 --- /dev/null +++ b/src/components/Solutions/MultipleChoice.tsx @@ -0,0 +1,133 @@ +/* eslint-disable @next/next/no-img-element */ +import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles"; +import {MultipleChoiceExercise, MultipleChoiceQuestion} from "@/interfaces/exam"; +import {mdiArrowLeft, mdiArrowRight, mdiCheck, mdiClose} from "@mdi/js"; +import Icon from "@mdi/react"; +import clsx from "clsx"; +import {useState} from "react"; +import {CommonProps} from "."; + +function Question({ + variant, + prompt, + solution, + options, + userSolution, + onSelectOption, + showSolution = false, +}: MultipleChoiceQuestion & {userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean}) { + const optionColor = (option: string) => { + if (!showSolution) { + return userSolution === option ? "border-blue-400" : ""; + } + + if (option === solution) { + return "border-green-500 text-green-500"; + } + + return userSolution === option ? "border-red-500 text-red-500" : ""; + }; + + const optionBadge = (option: string) => { + if (option === userSolution) { + if (solution === option) { + return ( +
+
+ +
+
+ ); + } + + return ( +
+
+ +
+
+ ); + } + }; + + return ( +
+ {prompt} +
+ {variant === "image" && + options.map((option) => ( +
(onSelectOption ? onSelectOption(option.id) : null)} + className={clsx( + "flex flex-col items-center border-2 p-4 rounded-xl gap-4 cursor-pointer bg-white relative", + optionColor(option.id), + )}> + {showSolution && optionBadge(option.id)} + {`Option + {option.id} +
+ ))} + {variant === "text" && + options.map((option) => ( +
(onSelectOption ? onSelectOption(option.id) : null)} + className={clsx("flex border-2 p-4 rounded-xl gap-2 cursor-pointer bg-white", optionColor(option.id))}> + {option.id}. + {option.text} +
+ ))} +
+
+ ); +} + +export default function MultipleChoice({prompt, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) { + const [questionIndex, setQuestionIndex] = useState(0); + + const next = () => { + if (questionIndex === questions.length - 1) { + onNext(); + } else { + setQuestionIndex((prev) => prev + 1); + } + }; + + const back = () => { + if (questionIndex === 0) { + onBack(); + } else { + setQuestionIndex((prev) => prev - 1); + } + }; + + return ( + <> +
+ {prompt} + {questionIndex < questions.length && ( + questions[questionIndex].id === x.question)?.option} + showSolution + /> + )} +
+
+ + +
+ + ); +} diff --git a/src/components/Solutions/WriteBlanks.tsx b/src/components/Solutions/WriteBlanks.tsx new file mode 100644 index 00000000..f8c7421f --- /dev/null +++ b/src/components/Solutions/WriteBlanks.tsx @@ -0,0 +1,110 @@ +import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles"; +import {WriteBlanksExercise} from "@/interfaces/exam"; +import {mdiArrowLeft, mdiArrowRight} from "@mdi/js"; +import Icon from "@mdi/react"; +import clsx from "clsx"; +import {useEffect, useState} from "react"; +import reactStringReplace from "react-string-replace"; +import {CommonProps} from "."; +import {toast} from "react-toastify"; + +function Blank({ + id, + maxWords, + solutions, + userSolution, + disabled = false, + setUserSolution, +}: { + id: string; + solutions?: string[]; + userSolution?: string; + maxWords: number; + disabled?: boolean; + setUserSolution?: (solution: string) => void; +}) { + const [userInput, setUserInput] = useState(userSolution || ""); + + useEffect(() => { + const words = userInput.split(" ").filter((x) => x !== ""); + if (words.length >= maxWords) { + toast.warning(`You have reached your word limit of ${maxWords} words!`, {toastId: "word-limit"}); + setUserInput(words.join(" ").trim()); + if (setUserSolution) setUserSolution(words.join(" ").trim()); + } + }, [maxWords, userInput, setUserSolution]); + + const getSolutionStyling = () => { + if (solutions && userSolution) { + if (solutions.map((x) => x.trim().toLowerCase()).includes(userSolution.trim().toLowerCase())) return "text-green-500 border-green-500"; + } + + return "text-red-500 border-red-500"; + }; + + return ( + setUserInput(e.target.value)} + value={!solutions ? userInput : solutions.join(" / ")} + contentEditable={disabled} + /> + ); +} + +export default function WriteBlanksSolutions({ + id, + prompt, + maxWords, + solutions, + userSolutions, + text, + onNext, + onBack, +}: WriteBlanksExercise & CommonProps) { + const renderLines = (line: string) => { + return ( + + {reactStringReplace(line, /({{\d+}})/g, (match) => { + const id = match.replaceAll(/[\{\}]/g, ""); + const userSolution = userSolutions.find((x) => x.id === id); + const solution = solutions.find((x) => x.id === id)!; + + return ; + })} + + ); + }; + + return ( + <> +
+ {prompt} + + {text.split("\n").map((line) => ( + <> + {renderLines(line)} +
+ + ))} +
+
+ +
+ + +
+ + ); +} diff --git a/src/components/Solutions/index.tsx b/src/components/Solutions/index.tsx new file mode 100644 index 00000000..3f503911 --- /dev/null +++ b/src/components/Solutions/index.tsx @@ -0,0 +1,25 @@ +import {Exercise, FillBlanksExercise, MatchSentencesExercise, MultipleChoiceExercise, WriteBlanksExercise} from "@/interfaces/exam"; +import dynamic from "next/dynamic"; +import FillBlanks from "./FillBlanks"; +import MultipleChoice from "./MultipleChoice"; +import WriteBlanks from "./WriteBlanks"; + +const MatchSentences = dynamic(() => import("@/components/Solutions/MatchSentences"), {ssr: false}); + +export interface CommonProps { + onNext: () => void; + onBack: () => void; +} + +export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: () => void) => { + switch (exercise.type) { + case "fillBlanks": + return ; + case "matchSentences": + return ; + case "multipleChoice": + return ; + case "writeBlanks": + return ; + } +}; diff --git a/src/demo/listening.json b/src/demo/listening.json index 30407f04..3633dc32 100644 --- a/src/demo/listening.json +++ b/src/demo/listening.json @@ -1,16 +1,16 @@ { "audio": { - "title": "", - "source": "", - "transcript": "", + "source": "https://www.sndup.net/hqvf/d", "repeatableTimes": 3 }, "module": "listening", "minTimer": 5, + "id": "1c70c6a2-8d4c-4dfc-849c-f89786fa38dd", "exercises": [ { "type": "multipleChoice", "prompt": "Select the appropriate option", + "id": "57f21739-146d-4d1f-aed9-274f351ca27f", "questions": [ { "id": "1", @@ -66,6 +66,7 @@ "type": "writeBlanks", "prompt": "Complete the notes below by writing NO MORE THAN THREE WORDS in the spaces provided.", "maxWords": 3, + "id": "81215364-b18e-4717-9a33-a6dfc1816960", "text": "The Government plans to give ${{14}} to assist the farmers. This money was to be spent on improving Sydney’s {{15}} but has now been re-allocated. Australia has experienced its worst drought in over fifty years. Farmers say that the money will not help them because it is {{16}}.", "solutions": [ { diff --git a/src/demo/reading.json b/src/demo/reading.json index 580ce6ba..8d0de134 100644 --- a/src/demo/reading.json +++ b/src/demo/reading.json @@ -5,11 +5,13 @@ }, "module": "reading", "minTimer": 5, + "id": "ffb738a5-265c-4daa-85c4-3681d3f8b48b", "exercises": [ { "type": "fillBlanks", "prompt": "Complete the summary below. Click a blank to select the corresponding word for it.\nThere are more words than spaces so you will not use them all. You may use any of the words more than once.", "allowRepetition": true, + "id": "acf930d1-3615-4b7d-b057-9f6e60b30f74", "solutions": [ { "id": "1", @@ -71,6 +73,7 @@ { "type": "matchSentences", "prompt": "Look at the following notes that have been made about the matches described in Reading Passage 1. Decide which type of match (A-H) corresponds with each description and write your answers in boxes 9 15 on your answer sheet.", + "id": "404437ff-2bc0-44b5-9e32-40ef4eedb023", "sentences": [ { "id": "9", diff --git a/src/demo/writing.json b/src/demo/writing.json index 5a847d21..c5f01241 100644 --- a/src/demo/writing.json +++ b/src/demo/writing.json @@ -8,5 +8,7 @@ "limit": 5 } }, + "id": "908286eb-2c5f-4d43-8806-0e15379143dd", + "exercises": [], "minTimer": 5 } \ No newline at end of file diff --git a/src/exams/Finish.tsx b/src/exams/Finish.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/exams/Listening.tsx b/src/exams/Listening.tsx index 9d9e43da..3e10030c 100644 --- a/src/exams/Listening.tsx +++ b/src/exams/Listening.tsx @@ -1,20 +1,23 @@ -import {ListeningExam} from "@/interfaces/exam"; +import {ListeningExam, UserSolution} from "@/interfaces/exam"; import {useEffect, useState} from "react"; import Icon from "@mdi/react"; import {mdiArrowRight} from "@mdi/js"; import clsx from "clsx"; import {infoButtonStyle} from "@/constants/buttonStyles"; import {renderExercise} from "@/components/Exercises"; +import {renderSolution} from "@/components/Solutions"; interface Props { exam: ListeningExam; - onFinish: () => void; + showSolutions?: boolean; + onFinish: (userSolutions: UserSolution[]) => void; } -export default function Listening({exam, onFinish}: Props) { +export default function Listening({exam, showSolutions = false, onFinish}: Props) { const [exerciseIndex, setExerciseIndex] = useState(-1); const [timesListened, setTimesListened] = useState(0); const [timer, setTimer] = useState(); + const [userSolutions, setUserSolutions] = useState([]); useEffect(() => { setTimer(exam.minTimer * 60); @@ -25,13 +28,21 @@ export default function Listening({exam, onFinish}: Props) { }; }, [exam.minTimer]); - const nextExercise = () => { + const nextExercise = (solution?: UserSolution) => { + if (solution) { + setUserSolutions((prev) => [...prev.filter((x) => x.id !== solution.id), solution]); + } + if (exerciseIndex + 1 < exam.exercises.length) { setExerciseIndex((prev) => prev + 1); return; } - onFinish(); + if (solution) { + onFinish([...userSolutions.filter((x) => x.id !== solution.id), solution].map((x) => ({...x, module: "listening"}))); + } else { + onFinish(userSolutions.map((x) => ({...x, module: "listening"}))); + } }; const previousExercise = () => { @@ -53,13 +64,11 @@ export default function Listening({exam, onFinish}: Props) {
)}
- {exam.audio.title} {exam.audio.repeatableTimes > 0 && ( <>{exam.audio.repeatableTimes <= timesListened && You are no longer allowed to listen to the audio again.} )} -
@@ -79,9 +88,14 @@ export default function Listening({exam, onFinish}: Props) { {renderAudioPlayer()} {exerciseIndex > -1 && exerciseIndex < exam.exercises.length && + !showSolutions && renderExercise(exam.exercises[exerciseIndex], nextExercise, previousExercise)} + {exerciseIndex > -1 && + exerciseIndex < exam.exercises.length && + showSolutions && + renderSolution(exam.exercises[exerciseIndex], nextExercise, previousExercise)} {exerciseIndex === -1 && ( -
)} {isSubmitEnabled && ( - )} diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index 5083cab2..c5b0a27d 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -1,3 +1,5 @@ +import {Module} from "."; + export type Exam = ReadingExam | ListeningExam | WritingExam; export interface ReadingExam { @@ -5,6 +7,7 @@ export interface ReadingExam { title: string; content: string; }; + id: string; exercises: Exercise[]; module: "reading"; minTimer: number; @@ -12,24 +15,35 @@ export interface ReadingExam { export interface ListeningExam { audio: { - title: string; source: string; - transcript: string; repeatableTimes: number; // *The amount of times the user is allowed to repeat the audio, 0 for unlimited }; + id: string; exercises: Exercise[]; module: "listening"; minTimer: number; } +export interface UserSolution { + solutions: any[]; + module?: Module; + score: { + correct: number; + total: number; + }; + id: string; +} + export interface WritingExam { module: "writing"; + id: string; text: { info: string; //* The information about the task, like the amount of time they should spend on it prompt: string; //* The context given to the user containing what they should write about wordCounter: WordCounter; //* The minimum or maximum amount of words that should be written }; minTimer: number; + exercises: Exercise[]; } interface WordCounter { @@ -42,6 +56,7 @@ export type Exercise = FillBlanksExercise | MatchSentencesExercise | MultipleCho export interface FillBlanksExercise { prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it." type: "fillBlanks"; + id: string; words: string[]; // *EXAMPLE: ["preserve", "unaware"] text: string; // *EXAMPLE: "They tried to {{1}} burning" allowRepetition: boolean; @@ -49,12 +64,17 @@ export interface FillBlanksExercise { id: string; // *EXAMPLE: "1" solution: string; // *EXAMPLE: "preserve" }[]; + userSolutions: { + id: string; // *EXAMPLE: "1" + solution: string; // *EXAMPLE: "preserve" + }[]; } 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 type: "writeBlanks"; + id: string; text: string; // *EXAMPLE: "The Government plans to give ${{14}}" solutions: { id: string; // *EXAMPLE: "14" @@ -68,6 +88,7 @@ export interface WriteBlanksExercise { export interface MatchSentencesExercise { type: "matchSentences"; + id: string; prompt: string; userSolutions: {question: string; option: string}[]; sentences: { @@ -85,6 +106,7 @@ export interface MatchSentencesExercise { export interface MultipleChoiceExercise { type: "multipleChoice"; + id: string; prompt: string; // *EXAMPLE: "Select the appropriate option." questions: MultipleChoiceQuestion[]; userSolutions: {question: string; option: string}[]; diff --git a/src/pages/exam/index.tsx b/src/pages/exam/index.tsx index 09d77de6..7b24c575 100644 --- a/src/pages/exam/index.tsx +++ b/src/pages/exam/index.tsx @@ -12,20 +12,25 @@ import JSON_WRITING from "@/demo/writing.json"; import Selection from "@/exams/Selection"; import Reading from "@/exams/Reading"; -import {Exam, ListeningExam, ReadingExam, WritingExam} from "@/interfaces/exam"; +import {Exam, ListeningExam, ReadingExam, UserSolution, WritingExam} from "@/interfaces/exam"; import Listening from "@/exams/Listening"; import Writing from "@/exams/Writing"; import {ToastContainer} from "react-toastify"; +import Link from "next/link"; export default function Home() { const [selectedModules, setSelectedModules] = useState([]); const [moduleIndex, setModuleIndex] = useState(0); const [exam, setExam] = useState(); + const [userSolutions, setUserSolutions] = useState([]); + const [showSolutions, setShowSolutions] = useState(false); useEffect(() => { if (selectedModules.length > 0 && moduleIndex < selectedModules.length) { - setExam(getExam(selectedModules[moduleIndex])); + const nextExam = getExam(selectedModules[moduleIndex]); + setExam(nextExam ? updateExamWithUserSolutions(nextExam) : undefined); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedModules, moduleIndex]); const getExam = (module: Module): Exam | undefined => { @@ -41,25 +46,59 @@ export default function Home() { return undefined; }; + const updateExamWithUserSolutions = (exam: Exam): Exam => { + const exercises = exam.exercises.map((x) => Object.assign(x, {userSolutions: userSolutions.find((y) => x.id === y.id)?.solutions})); + + return Object.assign(exam, exercises); + }; + + const onFinish = (solutions: UserSolution[]) => { + const solutionIds = solutions.map((x) => x.id); + console.log({solutions}); + + setUserSolutions((prev) => [...prev.filter((x) => !solutionIds.includes(x.id)), ...solutions]); + setModuleIndex((prev) => prev + 1); + }; + const renderScreen = () => { if (selectedModules.length === 0) { return ; } if (moduleIndex >= selectedModules.length) { - return <>Finished!; + return ( + <> + Finished!{" "} + + + + + + ); } if (exam && exam.module === "reading") { - return setModuleIndex((prev) => prev + 1)} />; + return ; } if (exam && exam.module === "listening") { - return setModuleIndex((prev) => prev + 1)} />; + return ; + } + + if (exam && exam.module === "writing" && showSolutions) { + setModuleIndex((prev) => prev + 1); + return <>; } if (exam && exam.module === "writing") { - return ; + return ; } return <>Loading...; diff --git a/src/stores/examStore.ts b/src/stores/examStore.ts index a0598959..8cd2ee09 100644 --- a/src/stores/examStore.ts +++ b/src/stores/examStore.ts @@ -1,19 +1,6 @@ import {Module} from "@/interfaces"; import {create} from "zustand"; -const useExamStore = create((set) => ({ - reading: undefined, - listening: undefined, - speaking: undefined, - writing: undefined, - updateModule: (module: Module, id: string) => set(() => ({[module]: id})), - clearExam: () => - set(() => ({ - reading: undefined, - listening: undefined, - speaking: undefined, - writing: undefined, - })), -})); +const useExamStore = create((set) => ({})); export default useExamStore;