Created a simple component for the writing exam
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
"react-lineto": "^3.3.0",
|
||||
"react-player": "^2.12.0",
|
||||
"react-string-replace": "^1.1.0",
|
||||
"react-toastify": "^9.1.2",
|
||||
"typescript": "4.9.5",
|
||||
"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 {
|
||||
text: {
|
||||
@@ -20,6 +20,20 @@ export interface ListeningExam {
|
||||
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 interface FillBlanksExercise {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import '@/styles/globals.css'
|
||||
import type { AppProps } from 'next/app'
|
||||
import "@/styles/globals.css";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import type {AppProps} from "next/app";
|
||||
|
||||
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_READING from "@/demo/reading.json";
|
||||
import JSON_LISTENING from "@/demo/listening.json";
|
||||
import JSON_WRITING from "@/demo/writing.json";
|
||||
|
||||
import Selection from "@/exams/Selection";
|
||||
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 Writing from "@/exams/Writing";
|
||||
import {ToastContainer} from "react-toastify";
|
||||
|
||||
export default function Home() {
|
||||
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
||||
@@ -31,6 +34,8 @@ export default function Home() {
|
||||
return JSON_READING as ReadingExam;
|
||||
case "listening":
|
||||
return JSON_LISTENING as ListeningExam;
|
||||
case "writing":
|
||||
return JSON_WRITING as WritingExam;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -53,6 +58,10 @@ export default function Home() {
|
||||
return <Listening exam={exam} onFinish={() => setModuleIndex((prev) => prev + 1)} />;
|
||||
}
|
||||
|
||||
if (exam && exam.module === "writing") {
|
||||
return <Writing exam={exam} />;
|
||||
}
|
||||
|
||||
return <>Loading...</>;
|
||||
};
|
||||
|
||||
@@ -65,6 +74,7 @@ export default function Home() {
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<main className="w-full h-screen flex flex-col items-center bg-neutral-100 text-black">
|
||||
<ToastContainer />
|
||||
<Navbar profilePicture={JSON_USER.profilePicture} />
|
||||
{renderScreen()}
|
||||
</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"
|
||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||
|
||||
clsx@^1.2.1:
|
||||
clsx@^1.1.1, clsx@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
||||
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"
|
||||
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:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||
|
||||
Reference in New Issue
Block a user