Created a prototype for one exercise of the reading module
This commit is contained in:
112
src/components/Exercises/FillBlanks.tsx
Normal file
112
src/components/Exercises/FillBlanks.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
||||
import {FillBlanksExercise} from "@/interfaces/exam";
|
||||
import {Dialog, Transition} from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import {Fragment, useState} from "react";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
|
||||
interface WordsPopoutProps {
|
||||
words: string[];
|
||||
isOpen: boolean;
|
||||
onCancel: () => void;
|
||||
onAnswer: (answer: string) => void;
|
||||
}
|
||||
|
||||
function WordsPopout({words, isOpen, onCancel, onAnswer}: WordsPopoutProps) {
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={onCancel}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0">
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95">
|
||||
<Dialog.Panel className="w-fit transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all flex flex-col">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||
List of words
|
||||
</Dialog.Title>
|
||||
<div className="mt-4 grid grid-cols-3 gap-4">
|
||||
{words.map((word) => (
|
||||
<button
|
||||
key={word}
|
||||
onClick={() => onAnswer(word)}
|
||||
className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)}>
|
||||
{word}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 self-end">
|
||||
<button onClick={onCancel} className={clsx("btn btn-wide gap-4 relative text-white", errorButtonStyle)}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FillBlanks({allowRepetition, prompt, solutions, text, words}: FillBlanksExercise) {
|
||||
const [userSolutions, setUserSolutions] = useState<{id: string; solution: string}[]>([]);
|
||||
const [currentBlankId, setCurrentBlankId] = useState<string>();
|
||||
|
||||
const renderLines = (line: string) => {
|
||||
return (
|
||||
<span>
|
||||
{reactStringReplace(line, /({{\d}})/g, (match) => {
|
||||
const id = match.replaceAll(/[\{\}]/g, "");
|
||||
const userSolution = userSolutions.find((x) => x.id === id);
|
||||
|
||||
return (
|
||||
<button className="border-2 rounded-xl px-4 text-blue-400 border-blue-400" onClick={() => setCurrentBlankId(id)}>
|
||||
{userSolution ? userSolution.solution : id}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<WordsPopout
|
||||
words={words}
|
||||
isOpen={!!currentBlankId}
|
||||
onCancel={() => setCurrentBlankId(undefined)}
|
||||
onAnswer={(solution: string) => {
|
||||
setUserSolutions((prev) => [...prev.filter((x) => x.id !== currentBlankId), {id: currentBlankId!, solution}]);
|
||||
setCurrentBlankId(undefined);
|
||||
}}
|
||||
/>
|
||||
<span className="text-lg font-medium text-center px-48">{prompt}</span>
|
||||
<span>
|
||||
{text.split("\n").map((line) => (
|
||||
<>
|
||||
{renderLines(line)}
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
70
src/demo/reading.json
Normal file
70
src/demo/reading.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"text": {
|
||||
"title": "A spark, a flint: How fire leapt to life",
|
||||
"content": "The control of fire was the first and perhaps greatest of humanity’s steps towards a life-enhancing technology.\nTo early man, fire was a divine gift randomly delivered in the form of lightning, forest fire or burning lava. Unable to make flame for themselves, the earliest peoples probabh stored fire by keeping slow burning logs alight or by carrying charcoal in pots.\nHow and where man learnt how to produce flame at will is unknown. It was probably a secondary invention, accidentally made during tool-making operations with wood or stone. Studies of primitive societies suggest that the earliest method of making fire was through friction. European peasants would insert a wooden drill in a round hole and rotate it briskly between their palms This process could be speeded up by wrapping a cord around the drill and pulling on each end.\nThe Ancient Greeks used lenses or concave mirrors to concentrate the sun’s rays and burning glasses were also used by Mexican Aztecs and the Chinese.\nPercussion methods of fire-lighting date back to Paleolithic times, when some Stone Age tool-makers discovered that chipping flints produced sparks.\nThe technique became more efficient after the discovery of iron, about 5000 vears ago In Arctic North America, the Eskimos produced a slow-burning spark by striking quartz against iron pyrites, a compound that contains sulphur. The Chinese lit their fires by striking porcelain with bamboo. In Europe, the combination of steel, flint and tinder remained the main method of fire- lighting until the mid 19th century.\nFire-lighting was revolutionised by the discovery of phosphorus, isolated in 1669 by a German alchemist trying to transmute silver into gold. Impressed by the element’s combustibility, several 17th century chemists used it to manufacture fire-lighting devices, but the results were dangerously inflammable. With phosphorus costing the equivalent of several hundred pounds per ounce, the hrst matches were expensive.\nThe quest for a practical match really began after 1781 when a group of French chemists came up with the Phosphoric Candle or Ethereal Match, a sealed glass tube containing a twist of paper tipped with phosphorus. When the tube was broken, air rushed in, causing the phosphorus to self- combust. An even more hazardous device, popular in America, was the Instantaneous Light Box — a bottle filled with sulphuric acid into which splints treated with chemicals were dipped.\nThe first matches resembling those used today were made in 1827 by John Walker, an English pharmacist who borrowed the formula from a military rocket-maker called Congreve. Costing a shilling a box, Congreves were splints coated with sulphur and tipped with potassium chlorate. To light them, the user drew them quickly through folded glass paper.\nWalker never patented his invention, and three years later it was copied by a Samuel Jones, who marketed his product as Lucifers. About the same time, a French chemistry student called Charles Sauria produced the first “strike-anywhere” match by substituting white phosphorus for the potassium chlorate in the Walker formula. However, since white phosphorus is a deadly poison, from 1845 match-makers exposed to its fumes succumbed to necrosis, a disease that eats away jaw-bones. It wasn’t until 1906 that the substance was eventually banned.\nThat was 62 years after a Swedish chemist called Pasch had discovered non-toxic red or amorphous phosphorus, a development exploited commercially by Pasch’s compatriot J E Lundstrom in 1885. Lundstrom’s safety matches were safe because the red phosphorus was non-toxic; it was painted on to the striking surface instead of the match tip, which contained potassium chlorate with a relatively high ignition temperature of 182 degrees centigrade.\nAmerica lagged behind Europe in match technology and safety standards. It wasn’t until 1900 that the Diamond Match Company bought a French patent for safety matches — but the formula did not work properly in the different climatic conditions prevailing in America and it was another 11 years before scientists finally adapted the French patent for the US.\nThe Americans, however, can claim several “firsts” in match technology and marketing. In 1892 the Diamond Match Company pioneered book matches. The innovation didn’t catch on until after 1896, when a brewery had the novel idea of advertising its product in match books. Today book matches are the most widely used type in the US, with 90 percent handed out free by hotels, restaurants and others.\nOther American innovations include an anti- afterglow solution to prevent the match from smouldering after it has been blown out; and the waterproof match, which lights after eight hours in water."
|
||||
},
|
||||
"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,
|
||||
"solutions": [
|
||||
{
|
||||
"id": "1",
|
||||
"solution": "preserve"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"solution": "unaware"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"solution": "chance"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"solution": "friction"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"solution": "rotating"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"solution": "percussion"
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"solution": "Eskimos"
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"solution": "despite"
|
||||
}
|
||||
],
|
||||
"words": [
|
||||
"Mexicans",
|
||||
"despite",
|
||||
"sunlight",
|
||||
"percussion",
|
||||
"unaware",
|
||||
"heating",
|
||||
"until",
|
||||
"random",
|
||||
"preserve",
|
||||
"lacking",
|
||||
"chance",
|
||||
"without",
|
||||
"Eskimos",
|
||||
"smoke",
|
||||
"rotating",
|
||||
"realising",
|
||||
"heavenly",
|
||||
"friction",
|
||||
"make",
|
||||
"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."
|
||||
}
|
||||
]
|
||||
}
|
||||
23
src/interfaces/exam.ts
Normal file
23
src/interfaces/exam.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export type Type = "fillBlanks" | "matchingSentences";
|
||||
|
||||
export interface ReadingExam {
|
||||
text: {
|
||||
title: string;
|
||||
content: string;
|
||||
};
|
||||
exercises: Exercise[];
|
||||
}
|
||||
|
||||
type Exercise = FillBlanksExercise;
|
||||
|
||||
export interface FillBlanksExercise {
|
||||
prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it."
|
||||
type: "fillBlanks";
|
||||
words: string[]; // *EXAMPLE: ["preserve", "unaware"]
|
||||
text: string; // *EXAMPLE: "They tried to {{1}} burning"
|
||||
allowRepetition: boolean;
|
||||
solutions: {
|
||||
id: string; // *EXAMPLE: "1"
|
||||
solution: string; // *EXAMPLE: "preserve"
|
||||
}[];
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export default function Home() {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<main className="w-full h-screen flex flex-col items-center bg-white text-black">
|
||||
<main className="w-full h-screen flex flex-col items-center bg-neutral-100 text-black">
|
||||
<Navbar profilePicture={JSON_USER.profilePicture} />
|
||||
<div className="w-full h-full relative">
|
||||
<section className="h-full w-full flex flex-col items-center justify-center">
|
||||
@@ -98,7 +98,9 @@ export default function Home() {
|
||||
</div>
|
||||
Back
|
||||
</button>
|
||||
<button className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)}>
|
||||
<button
|
||||
className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)}
|
||||
onClick={() => router.push("/exam/reading/demo")}>
|
||||
Start
|
||||
<div className="absolute right-4">
|
||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||
157
src/pages/exam/reading/[id].tsx
Normal file
157
src/pages/exam/reading/[id].tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import Navbar from "@/components/Navbar";
|
||||
import {ReadingExam} from "@/interfaces/exam";
|
||||
import Head from "next/head";
|
||||
|
||||
// TODO: Remove this import
|
||||
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 clsx from "clsx";
|
||||
import {infoButtonStyle} from "@/constants/buttonStyles";
|
||||
import FillBlanks from "@/components/Exercises/FillBlanks";
|
||||
import {Dialog, Transition} from "@headlessui/react";
|
||||
|
||||
interface Props {
|
||||
exam: ReadingExam;
|
||||
}
|
||||
|
||||
export const getServerSideProps = () => {
|
||||
return {
|
||||
props: {
|
||||
exam: JSON_READING,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function TextModal({isOpen, onClose}: {isOpen: boolean; onClose: () => void}) {
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0">
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95">
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||
Payment successful
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Your payment has been successfully submitted. We’ve sent you an email with all of the details of your order.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={onClose}>
|
||||
Got it, thanks!
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Reading({exam}: Props) {
|
||||
const [exerciseIndex, setExerciseIndex] = useState(-1);
|
||||
const [showTextModal, setShowTextModal] = useState(false);
|
||||
|
||||
const nextExercise = () => {
|
||||
setExerciseIndex((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const renderText = () => (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-lg font-semibold">
|
||||
Please read the following excerpt attentively, you will then be asked questions about the text you've read.
|
||||
</span>
|
||||
<span className="self-end text-sm">You will be allowed to read the text while doing the exercises</span>
|
||||
</div>
|
||||
<div className="bg-gray-300 rounded-xl p-4 flex flex-col gap-4 items-center w-full overflow-auto">
|
||||
<span className="text-xl font-semibold">{exam.text.title}</span>
|
||||
<span>
|
||||
{exam.text.content.split("\n").map((line) => (
|
||||
<>
|
||||
<span>{line}</span>
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderQuestion = () => {
|
||||
const exercise = exam.exercises[exerciseIndex];
|
||||
switch (exercise.type) {
|
||||
case "fillBlanks":
|
||||
return <FillBlanks {...exercise} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<meta name="description" content="Generated by create next app" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<main className="w-full h-screen flex flex-col items-center bg-neutral-100 text-black">
|
||||
<Navbar profilePicture={JSON_USER.profilePicture} />
|
||||
<TextModal isOpen={showTextModal} onClose={() => setShowTextModal(false)} />
|
||||
<div className="w-full h-full relative flex flex-col gap-8 items-center justify-center p-8 px-16 overflow-hidden">
|
||||
{exerciseIndex === -1 && renderText()}
|
||||
{exerciseIndex > -1 && exerciseIndex < exam.exercises.length && renderQuestion()}
|
||||
<div className="flex gap-8 self-end">
|
||||
{exerciseIndex > -1 && (
|
||||
<button
|
||||
className={clsx(
|
||||
"btn btn-wide gap-4 relative text-white",
|
||||
"border-2 border-ielts-reading hover:bg-ielts-reading hover:border-ielts-reading bg-ielts-reading-transparent",
|
||||
)}
|
||||
onClick={() => setShowTextModal(true)}>
|
||||
Read Text
|
||||
<div className="absolute right-4">
|
||||
<Icon path={mdiNotebook} color="white" size={1} />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)} onClick={nextExercise}>
|
||||
Next
|
||||
<div className="absolute right-4">
|
||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
19
src/stores/examStore.ts
Normal file
19
src/stores/examStore.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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,
|
||||
})),
|
||||
}));
|
||||
|
||||
export default useExamStore;
|
||||
Reference in New Issue
Block a user