From 3d74bf9bf1de19e43c8651e19edd6fd5a4abea61 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Thu, 23 Mar 2023 16:22:48 +0000 Subject: [PATCH] Implemented a simple "match the sentence" exercise --- package.json | 1 + src/components/Exercises/FillBlanks.tsx | 11 +- src/components/Exercises/MatchSentences.tsx | 150 ++++++++++++++++++++ src/demo/reading.json | 83 +++++++++++ src/interfaces/exam.ts | 18 ++- src/pages/exam/reading/[id].tsx | 64 ++++++--- yarn.lock | 27 +++- 7 files changed, 327 insertions(+), 27 deletions(-) create mode 100644 src/components/Exercises/MatchSentences.tsx diff --git a/package.json b/package.json index 472df4a8..6c95333b 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "react": "18.2.0", "react-chartjs-2": "^5.2.0", "react-dom": "18.2.0", + "react-lineto": "^3.3.0", "react-string-replace": "^1.1.0", "typescript": "4.9.5", "zustand": "^4.3.6" diff --git a/src/components/Exercises/FillBlanks.tsx b/src/components/Exercises/FillBlanks.tsx index f6aa276b..11d81c4f 100644 --- a/src/components/Exercises/FillBlanks.tsx +++ b/src/components/Exercises/FillBlanks.tsx @@ -6,7 +6,7 @@ import {Fragment, useState} from "react"; import reactStringReplace from "react-string-replace"; interface WordsPopoutProps { - words: string[]; + words: {word: string; isDisabled: boolean}[]; isOpen: boolean; onCancel: () => void; onAnswer: (answer: string) => void; @@ -44,10 +44,11 @@ function WordsPopout({words, isOpen, onCancel, onAnswer}: WordsPopoutProps) {
{words.map((word) => ( ))}
@@ -90,7 +91,7 @@ export default function FillBlanks({allowRepetition, prompt, solutions, text, wo return (
({word, isDisabled: allowRepetition ? false : userSolutions.map((x) => x.solution).includes(word)}))} isOpen={!!currentBlankId} onCancel={() => setCurrentBlankId(undefined)} onAnswer={(solution: string) => { diff --git a/src/components/Exercises/MatchSentences.tsx b/src/components/Exercises/MatchSentences.tsx new file mode 100644 index 00000000..1fc4cf29 --- /dev/null +++ b/src/components/Exercises/MatchSentences.tsx @@ -0,0 +1,150 @@ +import {MatchSentencesExercise} from "@/interfaces/exam"; +import clsx from "clsx"; +import {useState} from "react"; +import LineTo from "react-lineto"; + +const AVAILABLE_COLORS = ["#63526a", "#f7651d", "#278f04", "#ef4487", "#ca68c0", "#f5fe9b", "#b3ab01", "#af963a", "#9a85f1", "#1b1750"]; + +export default function MatchSentences({allowRepetition, options, prompt, sentences}: MatchSentencesExercise) { + const [selectedQuestion, setSelectedQuestion] = useState(); + const [userSolutions, setUserSolutions] = useState<{question: string; option: string}[]>([]); + + const selectOption = (option: string) => { + if (!selectedQuestion) return; + setUserSolutions((prev) => [...prev.filter((x) => x.question !== selectedQuestion), {question: selectedQuestion, option}]); + setSelectedQuestion(undefined); + }; + + const getSentenceColor = (id: string) => { + return sentences.find((x) => x.id === id)?.color || ""; + }; + + return ( +
+ {prompt} +
+
+ {sentences.map(({sentence, id, color}) => ( +
setSelectedQuestion((prev) => (prev === id ? undefined : id))}> + + {id}. {sentence}{" "} + +
+
+ ))} +
+
+ {options.map(({sentence, id}) => ( +
selectOption(id)}> +
x.option === id) + ? { + border: `2px solid ${getSentenceColor(userSolutions.find((x) => x.option === id)!.question)}`, + } + : {} + } + className={clsx("border-2 border-green-500 bg-transparent w-4 h-4 rounded-full", id)} + /> + + {id}. {sentence}{" "} + +
+ ))} +
+ {userSolutions.map((solution, index) => ( +
+ x.id === solution.question)!.color} + borderWidth={5} + /> +
+ ))} +
+
+ ); +} + +export function MatchSentencesSolutions({allowRepetition, options, prompt, sentences}: MatchSentencesExercise) { + const [selectedQuestion, setSelectedQuestion] = useState(); + const [userSolutions, setUserSolutions] = useState<{question: string; option: string}[]>([]); + + const selectOption = (option: string) => { + if (!selectedQuestion) return; + setUserSolutions((prev) => [...prev.filter((x) => x.question !== selectedQuestion), {question: selectedQuestion, option}]); + setSelectedQuestion(undefined); + }; + + const getSentenceColor = (id: string) => { + return sentences.find((x) => x.id === id)?.color || ""; + }; + + return ( +
+ {prompt} +
+
+ {sentences.map(({sentence, id, color}) => ( +
setSelectedQuestion((prev) => (prev === id ? undefined : id))}> + + {id}. {sentence}{" "} + +
+
+ ))} +
+
+ {options.map(({sentence, id}) => ( +
selectOption(id)}> +
x.option === id) + ? { + border: `2px solid ${getSentenceColor(userSolutions.find((x) => x.option === id)!.question)}`, + } + : {} + } + className={clsx("border-2 border-green-500 bg-transparent w-4 h-4 rounded-full", id)} + /> + + {id}. {sentence}{" "} + +
+ ))} +
+ {userSolutions.map((solution, index) => ( +
+ x.id === solution.question)!.color} + borderWidth={5} + /> +
+ ))} +
+
+ ); +} diff --git a/src/demo/reading.json b/src/demo/reading.json index c135c49f..4f3e6bb6 100644 --- a/src/demo/reading.json +++ b/src/demo/reading.json @@ -65,6 +65,89 @@ "surprise" ], "text": "They tried to {{1}} burning logs or charcoal {{2}} that they could create fire themselves. It is suspected that the first man-made flame were produced by {{3}}.\n\nThe very first fire-lighting methods involved the creating of {{4}} by, for example, rapidly {{5}} a wooden stick in a round hole. The use of {{6}} or persistent chipping was also widespread in Europe and among other peoples such as the Chinese and {{7}}. European practice of this method continued until the 1850s {{8}} the discovery of phosphorus some years earlier." + }, + { + "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.", + "sentences": [ + { + "id": "9", + "sentence": "made using a less poisonous type of phosphorus", + "solution": "F", + "color": "#76af37" + }, + { + "id": "10", + "sentence": "identical to a previous type of match", + "solution": "D", + "color": "#9b3029" + }, + { + "id": "11", + "sentence": "caused a deadly illness", + "solution": "E", + "color": "#453539" + }, + { + "id": "12", + "sentence": "first to look like modern matches", + "solution": "C", + "color": "#1888e7" + }, + { + "id": "13", + "sentence": "first matches used for advertising", + "solution": "G", + "color": "#ec049f" + }, + { + "id": "14", + "sentence": "relied on an airtight glass container", + "solution": "A", + "color": "#a4578a" + }, + { + "id": "15", + "sentence": "made with the help of an army design", + "solution": "C", + "color": "#dba996" + } + ], + "allowRepetition": true, + "options": [ + { + "id": "A", + "sentence": "the Ethereal Match" + }, + { + "id": "B", + "sentence": "the Instantaneous Lightbox" + }, + { + "id": "C", + "sentence": "Congreves" + }, + { + "id": "D", + "sentence": "Lucifers" + }, + { + "id": "E", + "sentence": "the first strike-anywhere match" + }, + { + "id": "F", + "sentence": "Lundstrom’s safety match" + }, + { + "id": "G", + "sentence": "book matches" + }, + { + "id": "H", + "sentence": "waterproof matches" + } + ] } ] } \ No newline at end of file diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index 023b4993..de0c645d 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -8,7 +8,7 @@ export interface ReadingExam { exercises: Exercise[]; } -type Exercise = FillBlanksExercise; +type Exercise = FillBlanksExercise | MatchSentencesExercise; export interface FillBlanksExercise { prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it." @@ -21,3 +21,19 @@ export interface FillBlanksExercise { solution: string; // *EXAMPLE: "preserve" }[]; } + +export interface MatchSentencesExercise { + type: string; + prompt: string; + sentences: { + id: string; + sentence: string; + solution: string; + color: string; + }[]; + allowRepetition: boolean; + options: { + id: string; + sentence: string; + }[]; +} diff --git a/src/pages/exam/reading/[id].tsx b/src/pages/exam/reading/[id].tsx index 56ff3647..c61fd1a7 100644 --- a/src/pages/exam/reading/[id].tsx +++ b/src/pages/exam/reading/[id].tsx @@ -1,5 +1,5 @@ import Navbar from "@/components/Navbar"; -import {ReadingExam} from "@/interfaces/exam"; +import {FillBlanksExercise, MatchSentencesExercise, ReadingExam} from "@/interfaces/exam"; import Head from "next/head"; // TODO: Remove this import @@ -7,11 +7,14 @@ import JSON_READING from "@/demo/reading.json"; import JSON_USER from "@/demo/user.json"; import {Fragment, useState} from "react"; import Icon from "@mdi/react"; -import {mdiArrowRight, mdiNotebook} from "@mdi/js"; +import {mdiArrowLeft, mdiArrowRight, mdiNotebook} from "@mdi/js"; import clsx from "clsx"; -import {infoButtonStyle} from "@/constants/buttonStyles"; +import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles"; import FillBlanks from "@/components/Exercises/FillBlanks"; import {Dialog, Transition} from "@headlessui/react"; +import dynamic from "next/dynamic"; + +const MatchSentences = dynamic(() => import("@/components/Exercises/MatchSentences"), {ssr: false}); interface Props { exam: ReadingExam; @@ -25,7 +28,7 @@ export const getServerSideProps = () => { }; }; -function TextModal({isOpen, onClose}: {isOpen: boolean; onClose: () => void}) { +function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: string; content: string; onClose: () => void}) { return ( @@ -50,13 +53,18 @@ function TextModal({isOpen, onClose}: {isOpen: boolean; onClose: () => void}) { leave="ease-in duration-200" leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95"> - + - Payment successful + {title} -
+

- Your payment has been successfully submitted. We’ve sent you an email with all of the details of your order. + {content.split("\n").map((line, index) => ( + + {line} +
+
+ ))}

@@ -85,6 +93,10 @@ export default function Reading({exam}: Props) { setExerciseIndex((prev) => prev + 1); }; + const previousExercise = () => { + setExerciseIndex((prev) => prev - 1); + }; + const renderText = () => ( <>
@@ -96,11 +108,11 @@ export default function Reading({exam}: Props) {
{exam.text.title} - {exam.text.content.split("\n").map((line) => ( - <> + {exam.text.content.split("\n").map((line, index) => ( + {line}
- +
))}
@@ -111,7 +123,9 @@ export default function Reading({exam}: Props) { const exercise = exam.exercises[exerciseIndex]; switch (exercise.type) { case "fillBlanks": - return ; + return ; + case "matchSentences": + return ; } }; @@ -125,11 +139,11 @@ export default function Reading({exam}: Props) {
- setShowTextModal(false)} /> + setShowTextModal(false)} />
{exerciseIndex === -1 && renderText()} {exerciseIndex > -1 && exerciseIndex < exam.exercises.length && renderQuestion()} -
+
-1 ? "w-full justify-between" : "self-end")}> {exerciseIndex > -1 && ( )} - +
+ {exerciseIndex > -1 && ( + + )} + +
diff --git a/yarn.lock b/yarn.lock index 43bc7505..8fa9348b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2034,6 +2034,15 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prop-types@15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" @@ -2071,16 +2080,32 @@ react-dom@18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" -react-is@^16.13.1: +react-is@^16.13.1, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-lineto@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/react-lineto/-/react-lineto-3.3.0.tgz#0ca2e59ecd6b8615aa1edfa515b6feac57449d02" + integrity sha512-mDs9aX2ryM7lQ9G+XYZKmDmogzpR/2j1YYVQNDrcDbdgKloWOWcKaMkRX/9Ya4PHang4N1qxBbH3GUAIByDa6w== + dependencies: + prop-types "15.7.2" + react "17.0.2" + react-string-replace@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/react-string-replace/-/react-string-replace-1.1.0.tgz#a3f7b458e697e77d70b0ea663caf38ab38f7cc17" integrity sha512-N6RalSDFGbOHs0IJi1H611WbZsvk3ZT47Jl2JEXFbiS3kTwsdCYij70Keo/tWtLy7sfhDsYm7CwNM/WmjXIaMw== +react@17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + react@18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"