Created a simple component for the writing exam
This commit is contained in:
@@ -30,6 +30,7 @@
|
|||||||
"react-lineto": "^3.3.0",
|
"react-lineto": "^3.3.0",
|
||||||
"react-player": "^2.12.0",
|
"react-player": "^2.12.0",
|
||||||
"react-string-replace": "^1.1.0",
|
"react-string-replace": "^1.1.0",
|
||||||
|
"react-toastify": "^9.1.2",
|
||||||
"typescript": "4.9.5",
|
"typescript": "4.9.5",
|
||||||
"zustand": "^4.3.6"
|
"zustand": "^4.3.6"
|
||||||
},
|
},
|
||||||
|
|||||||
11
src/demo/writing.json
Normal file
11
src/demo/writing.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"module": "writing",
|
||||||
|
"text": {
|
||||||
|
"info": "You should spend about 20 minutes on this task.",
|
||||||
|
"prompt": "The charts below show the results of a survey of adult education. The first chart shows the reasons why adults decide to study. The pie chart shows how people think the costs of adult education should be shared.\nWrite a report for a university lecturer, describing the information shown below.",
|
||||||
|
"wordCounter": {
|
||||||
|
"type": "min",
|
||||||
|
"limit": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/exams/Writing.tsx
Normal file
67
src/exams/Writing.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import {infoButtonStyle} from "@/constants/buttonStyles";
|
||||||
|
import {WritingExam} from "@/interfaces/exam";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import {Fragment, useEffect, useState} from "react";
|
||||||
|
import {toast} from "react-toastify";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
exam: WritingExam;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Writing({exam}: Props) {
|
||||||
|
const [inputText, setInputText] = useState("");
|
||||||
|
const [isSubmitEnabled, setIsSubmitEnabled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const words = inputText.split(" ").filter((x) => x !== "");
|
||||||
|
const {wordCounter} = exam.text;
|
||||||
|
if (wordCounter.type === "min") {
|
||||||
|
setIsSubmitEnabled(wordCounter.limit <= words.length);
|
||||||
|
} else {
|
||||||
|
setIsSubmitEnabled(true);
|
||||||
|
if (wordCounter.limit < words.length) {
|
||||||
|
toast.warning(`You have reached your word limit of ${wordCounter.limit} words!`);
|
||||||
|
setInputText(words.slice(0, words.length - 1).join(" "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [inputText, exam]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full flex flex-col items-center justify-center gap-8">
|
||||||
|
<div className="flex flex-col max-w-2xl gap-2">
|
||||||
|
<span>{exam.text.info}</span>
|
||||||
|
<span className="font-bold ml-8">
|
||||||
|
{exam.text.prompt.split("\n").map((line, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
<span>{line}</span>
|
||||||
|
<br />
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
You should write {exam.text.wordCounter.type === "min" ? "at least" : "at most"} {exam.text.wordCounter.limit} words.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
className="w-1/2 h-1/3 cursor-text p-2 input input-bordered"
|
||||||
|
onChange={(e) => setInputText(e.target.value)}
|
||||||
|
value={inputText}
|
||||||
|
placeholder="Write your text here..."
|
||||||
|
/>
|
||||||
|
<div className="w-1/2 flex justify-end">
|
||||||
|
{!isSubmitEnabled && (
|
||||||
|
<div className="tooltip" data-tip={`You have not yet reached your minimum word count of ${exam.text.wordCounter.limit} words!`}>
|
||||||
|
<button className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)} disabled={!isSubmitEnabled}>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isSubmitEnabled && (
|
||||||
|
<button className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)} disabled={!isSubmitEnabled}>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export type Exam = ReadingExam | ListeningExam;
|
export type Exam = ReadingExam | ListeningExam | WritingExam;
|
||||||
|
|
||||||
export interface ReadingExam {
|
export interface ReadingExam {
|
||||||
text: {
|
text: {
|
||||||
@@ -20,6 +20,20 @@ export interface ListeningExam {
|
|||||||
module: "listening";
|
module: "listening";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WritingExam {
|
||||||
|
module: "writing";
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WordCounter {
|
||||||
|
type: "min" | "max";
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type Exercise = FillBlanksExercise | MatchSentencesExercise | MultipleChoiceExercise | WriteBlanksExercise;
|
export type Exercise = FillBlanksExercise | MatchSentencesExercise | MultipleChoiceExercise | WriteBlanksExercise;
|
||||||
|
|
||||||
export interface FillBlanksExercise {
|
export interface FillBlanksExercise {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import '@/styles/globals.css'
|
import "@/styles/globals.css";
|
||||||
import type { AppProps } from 'next/app'
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
import type {AppProps} from "next/app";
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
export default function App({Component, pageProps}: AppProps) {
|
||||||
return <Component {...pageProps} />
|
return <Component {...pageProps} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ import {Module} from "@/interfaces";
|
|||||||
import JSON_USER from "@/demo/user.json";
|
import JSON_USER from "@/demo/user.json";
|
||||||
import JSON_READING from "@/demo/reading.json";
|
import JSON_READING from "@/demo/reading.json";
|
||||||
import JSON_LISTENING from "@/demo/listening.json";
|
import JSON_LISTENING from "@/demo/listening.json";
|
||||||
|
import JSON_WRITING from "@/demo/writing.json";
|
||||||
|
|
||||||
import Selection from "@/exams/Selection";
|
import Selection from "@/exams/Selection";
|
||||||
import Reading from "@/exams/Reading";
|
import Reading from "@/exams/Reading";
|
||||||
import {Exam, ListeningExam, ReadingExam} from "@/interfaces/exam";
|
import {Exam, ListeningExam, ReadingExam, WritingExam} from "@/interfaces/exam";
|
||||||
import Listening from "@/exams/Listening";
|
import Listening from "@/exams/Listening";
|
||||||
|
import Writing from "@/exams/Writing";
|
||||||
|
import {ToastContainer} from "react-toastify";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
||||||
@@ -31,6 +34,8 @@ export default function Home() {
|
|||||||
return JSON_READING as ReadingExam;
|
return JSON_READING as ReadingExam;
|
||||||
case "listening":
|
case "listening":
|
||||||
return JSON_LISTENING as ListeningExam;
|
return JSON_LISTENING as ListeningExam;
|
||||||
|
case "writing":
|
||||||
|
return JSON_WRITING as WritingExam;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -53,6 +58,10 @@ export default function Home() {
|
|||||||
return <Listening exam={exam} onFinish={() => setModuleIndex((prev) => prev + 1)} />;
|
return <Listening exam={exam} onFinish={() => setModuleIndex((prev) => prev + 1)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (exam && exam.module === "writing") {
|
||||||
|
return <Writing exam={exam} />;
|
||||||
|
}
|
||||||
|
|
||||||
return <>Loading...</>;
|
return <>Loading...</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,6 +74,7 @@ export default function Home() {
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<main className="w-full h-screen flex flex-col items-center bg-neutral-100 text-black">
|
<main className="w-full h-screen flex flex-col items-center bg-neutral-100 text-black">
|
||||||
|
<ToastContainer />
|
||||||
<Navbar profilePicture={JSON_USER.profilePicture} />
|
<Navbar profilePicture={JSON_USER.profilePicture} />
|
||||||
{renderScreen()}
|
{renderScreen()}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -594,7 +594,7 @@ client-only@0.0.1, client-only@^0.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||||
|
|
||||||
clsx@^1.2.1:
|
clsx@^1.1.1, clsx@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
||||||
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
||||||
@@ -2129,6 +2129,13 @@ react-string-replace@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/react-string-replace/-/react-string-replace-1.1.0.tgz#a3f7b458e697e77d70b0ea663caf38ab38f7cc17"
|
resolved "https://registry.yarnpkg.com/react-string-replace/-/react-string-replace-1.1.0.tgz#a3f7b458e697e77d70b0ea663caf38ab38f7cc17"
|
||||||
integrity sha512-N6RalSDFGbOHs0IJi1H611WbZsvk3ZT47Jl2JEXFbiS3kTwsdCYij70Keo/tWtLy7sfhDsYm7CwNM/WmjXIaMw==
|
integrity sha512-N6RalSDFGbOHs0IJi1H611WbZsvk3ZT47Jl2JEXFbiS3kTwsdCYij70Keo/tWtLy7sfhDsYm7CwNM/WmjXIaMw==
|
||||||
|
|
||||||
|
react-toastify@^9.1.2:
|
||||||
|
version "9.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.1.2.tgz#293aa1f952240129fe485ae5cb2f8d09c652cf3f"
|
||||||
|
integrity sha512-PBfzXO5jMGEtdYR5jxrORlNZZe/EuOkwvwKijMatsZZm8IZwLj01YvobeJYNjFcA6uy6CVrx2fzL9GWbhWPTDA==
|
||||||
|
dependencies:
|
||||||
|
clsx "^1.1.1"
|
||||||
|
|
||||||
react@17.0.2:
|
react@17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||||
|
|||||||
Reference in New Issue
Block a user