- Updated the code to remove the exam pages to components;
- Added some state logic to make it so it keeps track of the current exam the user is in;
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
"transcript": "",
|
"transcript": "",
|
||||||
"repeatableTimes": 3
|
"repeatableTimes": 3
|
||||||
},
|
},
|
||||||
|
"module": "listening",
|
||||||
"exercises": [
|
"exercises": [
|
||||||
{
|
{
|
||||||
"type": "multipleChoice",
|
"type": "multipleChoice",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"title": "A spark, a flint: How fire leapt to life",
|
"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."
|
"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."
|
||||||
},
|
},
|
||||||
|
"module": "reading",
|
||||||
"exercises": [
|
"exercises": [
|
||||||
{
|
{
|
||||||
"type": "fillBlanks",
|
"type": "fillBlanks",
|
||||||
|
|||||||
73
src/exams/Listening.tsx
Normal file
73
src/exams/Listening.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import {ListeningExam} from "@/interfaces/exam";
|
||||||
|
import {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";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
exam: ListeningExam;
|
||||||
|
onFinish: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Listening({exam, onFinish}: Props) {
|
||||||
|
const [exerciseIndex, setExerciseIndex] = useState(-1);
|
||||||
|
const [timesListened, setTimesListened] = useState(0);
|
||||||
|
|
||||||
|
const nextExercise = () => {
|
||||||
|
if (exerciseIndex + 1 < exam.exercises.length) {
|
||||||
|
setExerciseIndex((prev) => prev + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFinish();
|
||||||
|
};
|
||||||
|
|
||||||
|
const previousExercise = () => {
|
||||||
|
setExerciseIndex((prev) => prev - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAudioPlayer = () => (
|
||||||
|
<>
|
||||||
|
{exerciseIndex === -1 && (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-lg font-semibold">Please listen to the following audio attentively.</span>
|
||||||
|
{exam.audio.repeatableTimes > 0 ? (
|
||||||
|
<span className="self-center text-sm">
|
||||||
|
You will only be allowed to listen to the audio {exam.audio.repeatableTimes} time(s).
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="self-center text-sm">You may listen to the audio as many times as you would like.</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.audio.title}</span>
|
||||||
|
{exam.audio.repeatableTimes > 0 && (
|
||||||
|
<>{exam.audio.repeatableTimes <= timesListened && <span>You are no longer allowed to listen to the audio again.</span>}</>
|
||||||
|
)}
|
||||||
|
<span>AUDIO WILL GO HERE</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full h-full relative flex flex-col gap-8 items-center justify-center p-8 px-16 overflow-hidden">
|
||||||
|
{renderAudioPlayer()}
|
||||||
|
{exerciseIndex > -1 &&
|
||||||
|
exerciseIndex < exam.exercises.length &&
|
||||||
|
renderExercise(exam.exercises[exerciseIndex], nextExercise, previousExercise)}
|
||||||
|
{exerciseIndex === -1 && (
|
||||||
|
<button className={clsx("btn btn-wide gap-4 relative text-white self-end", infoButtonStyle)} onClick={nextExercise}>
|
||||||
|
Next
|
||||||
|
<div className="absolute right-4">
|
||||||
|
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
145
src/exams/Reading.tsx
Normal file
145
src/exams/Reading.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import {ReadingExam} from "@/interfaces/exam";
|
||||||
|
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 {Dialog, Transition} from "@headlessui/react";
|
||||||
|
import {renderExercise} from "@/components/Exercises";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
exam: ReadingExam;
|
||||||
|
onFinish: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: string; content: string; 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-4xl transform 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">
|
||||||
|
{title}
|
||||||
|
</Dialog.Title>
|
||||||
|
<div className="mt-2 overflow-auto">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{content.split("\n").map((line, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</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, onFinish}: Props) {
|
||||||
|
const [exerciseIndex, setExerciseIndex] = useState(-1);
|
||||||
|
const [showTextModal, setShowTextModal] = useState(false);
|
||||||
|
|
||||||
|
const nextExercise = () => {
|
||||||
|
if (exerciseIndex + 1 < exam.exercises.length) {
|
||||||
|
setExerciseIndex((prev) => prev + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFinish();
|
||||||
|
};
|
||||||
|
|
||||||
|
const previousExercise = () => {
|
||||||
|
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, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
<span>{line}</span>
|
||||||
|
<br />
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextModal {...exam.text} 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 &&
|
||||||
|
renderExercise(exam.exercises[exerciseIndex], nextExercise, previousExercise)}
|
||||||
|
<div className={clsx("flex gap-8", exerciseIndex > -1 ? "w-full justify-between" : "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>
|
||||||
|
)}
|
||||||
|
{exerciseIndex === -1 && (
|
||||||
|
<button className={clsx("btn btn-wide gap-4 relative text-white self-end", infoButtonStyle)} onClick={nextExercise}>
|
||||||
|
Next
|
||||||
|
<div className="absolute right-4">
|
||||||
|
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
107
src/exams/Selection.tsx
Normal file
107
src/exams/Selection.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import Icon from "@mdi/react";
|
||||||
|
import {mdiAccountVoice, mdiArrowLeft, mdiArrowRight, mdiBookOpen, mdiHeadphones, mdiPen} from "@mdi/js";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {Module} from "@/interfaces";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import {useRouter} from "next/router";
|
||||||
|
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
||||||
|
import ProfileLevel from "@/components/ProfileLevel";
|
||||||
|
import {User} from "@/interfaces/user";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User;
|
||||||
|
onStart: (modules: Module[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Selection({user, onStart}: Props) {
|
||||||
|
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const toggleModule = (module: Module) => {
|
||||||
|
const modules = selectedModules.filter((x) => x !== module);
|
||||||
|
setSelectedModules((prev) => (prev.includes(module) ? modules : [...modules, module]));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full h-full relative">
|
||||||
|
<section className="h-full w-full flex flex-col items-center justify-center">
|
||||||
|
<ProfileLevel user={user} className="h-1/2" />
|
||||||
|
<div className="h-1/2 flex flex-col">
|
||||||
|
<div className="h-1/2 flex gap-8">
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => toggleModule("reading")}
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col gap-2 items-center justify-center",
|
||||||
|
"border-ielts-reading hover:bg-ielts-reading text-white",
|
||||||
|
"border-2 rounded-xl p-4 h-fit w-48 cursor-pointer",
|
||||||
|
selectedModules.includes("reading") ? "bg-ielts-reading " : "bg-ielts-reading-transparent ",
|
||||||
|
)}>
|
||||||
|
<Icon path={mdiBookOpen} color="white" size={3} />
|
||||||
|
<span>Reading</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => toggleModule("listening")}
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col gap-2 items-center justify-center",
|
||||||
|
"border-ielts-listening hover:bg-ielts-listening text-white",
|
||||||
|
"border-2 rounded-xl p-4 h-fit w-48 cursor-pointer",
|
||||||
|
selectedModules.includes("listening") ? "bg-ielts-listening " : "bg-ielts-listening-transparent ",
|
||||||
|
)}>
|
||||||
|
<Icon path={mdiHeadphones} color="white" size={3} />
|
||||||
|
<span>Listening</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => toggleModule("speaking")}
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col gap-2 items-center justify-center",
|
||||||
|
"border-ielts-speaking hover:bg-ielts-speaking text-white",
|
||||||
|
"border-2 rounded-xl p-4 h-fit w-48 cursor-pointer",
|
||||||
|
selectedModules.includes("speaking") ? "bg-ielts-speaking " : "bg-ielts-speaking-transparent ",
|
||||||
|
)}>
|
||||||
|
<Icon path={mdiAccountVoice} color="white" size={3} />
|
||||||
|
<span>Speaking</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => toggleModule("writing")}
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col gap-2 items-center justify-center",
|
||||||
|
"border-ielts-writing hover:bg-ielts-writing text-white",
|
||||||
|
"border-2 rounded-xl p-4 h-fit w-48 cursor-pointer",
|
||||||
|
selectedModules.includes("writing") ? "bg-ielts-writing " : "bg-ielts-writing-transparent ",
|
||||||
|
)}>
|
||||||
|
<Icon path={mdiPen} color="white" size={3} />
|
||||||
|
<span>Writing</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex justify-between">
|
||||||
|
<button onClick={() => router.push("/")} className={clsx("btn btn-wide gap-4 relative text-white", errorButtonStyle)}>
|
||||||
|
<div className="absolute left-4">
|
||||||
|
<Icon path={mdiArrowLeft} color="white" size={1} />
|
||||||
|
</div>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)}
|
||||||
|
onClick={() => onStart(selectedModules)}>
|
||||||
|
Start
|
||||||
|
<div className="absolute right-4">
|
||||||
|
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
|
export type Exam = ReadingExam | ListeningExam;
|
||||||
|
|
||||||
export interface ReadingExam {
|
export interface ReadingExam {
|
||||||
text: {
|
text: {
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
};
|
};
|
||||||
exercises: Exercise[];
|
exercises: Exercise[];
|
||||||
|
module: "reading";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListeningExam {
|
export interface ListeningExam {
|
||||||
@@ -14,6 +17,7 @@ export interface ListeningExam {
|
|||||||
repeatableTimes: number; // *The amount of times the user is allowed to repeat the audio, 0 for unlimited
|
repeatableTimes: number; // *The amount of times the user is allowed to repeat the audio, 0 for unlimited
|
||||||
};
|
};
|
||||||
exercises: Exercise[];
|
exercises: Exercise[];
|
||||||
|
module: "listening";
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Exercise = FillBlanksExercise | MatchSentencesExercise | MultipleChoiceExercise | WriteBlanksExercise;
|
export type Exercise = FillBlanksExercise | MatchSentencesExercise | MultipleChoiceExercise | WriteBlanksExercise;
|
||||||
|
|||||||
@@ -1,25 +1,59 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Navbar from "@/components/Navbar";
|
import Navbar from "@/components/Navbar";
|
||||||
import Icon from "@mdi/react";
|
import {useEffect, useState} from "react";
|
||||||
import {mdiAccountVoice, mdiArrowLeft, mdiArrowRight, mdiBookOpen, mdiHeadphones, mdiPen} from "@mdi/js";
|
|
||||||
import {useState} from "react";
|
|
||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import clsx from "clsx";
|
|
||||||
import {useRouter} from "next/router";
|
|
||||||
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
|
||||||
import ProfileLevel from "@/components/ProfileLevel";
|
|
||||||
|
|
||||||
// TODO: Remove this import
|
// TODO: Remove this import
|
||||||
import JSON_USER from "@/demo/user.json";
|
import JSON_USER from "@/demo/user.json";
|
||||||
|
import JSON_READING from "@/demo/reading.json";
|
||||||
|
import JSON_LISTENING from "@/demo/listening.json";
|
||||||
|
|
||||||
|
import Selection from "@/exams/Selection";
|
||||||
|
import Reading from "@/exams/Reading";
|
||||||
|
import {Exam, ListeningExam, ReadingExam} from "@/interfaces/exam";
|
||||||
|
import Listening from "@/exams/Listening";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
||||||
const router = useRouter();
|
const [moduleIndex, setModuleIndex] = useState(0);
|
||||||
|
const [exam, setExam] = useState<Exam>();
|
||||||
|
|
||||||
const toggleModule = (module: Module) => {
|
useEffect(() => {
|
||||||
const modules = selectedModules.filter((x) => x !== module);
|
if (selectedModules.length > 0 && moduleIndex < selectedModules.length) {
|
||||||
setSelectedModules((prev) => (prev.includes(module) ? modules : [...modules, module]));
|
setExam(getExam(selectedModules[moduleIndex]));
|
||||||
|
}
|
||||||
|
}, [selectedModules, moduleIndex]);
|
||||||
|
|
||||||
|
const getExam = (module: Module): Exam | undefined => {
|
||||||
|
switch (module) {
|
||||||
|
case "reading":
|
||||||
|
return JSON_READING as ReadingExam;
|
||||||
|
case "listening":
|
||||||
|
return JSON_LISTENING as ListeningExam;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderScreen = () => {
|
||||||
|
if (selectedModules.length === 0) {
|
||||||
|
return <Selection user={JSON_USER} onStart={setSelectedModules} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moduleIndex >= selectedModules.length) {
|
||||||
|
return <>Finished!</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exam && exam.module === "reading") {
|
||||||
|
return <Reading exam={exam} onFinish={() => setModuleIndex((prev) => prev + 1)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exam && exam.module === "listening") {
|
||||||
|
return <Listening exam={exam} onFinish={() => setModuleIndex((prev) => prev + 1)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>Loading...</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -32,84 +66,7 @@ export default function Home() {
|
|||||||
</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">
|
||||||
<Navbar profilePicture={JSON_USER.profilePicture} />
|
<Navbar profilePicture={JSON_USER.profilePicture} />
|
||||||
<div className="w-full h-full relative">
|
{renderScreen()}
|
||||||
<section className="h-full w-full flex flex-col items-center justify-center">
|
|
||||||
{/* //TODO: Change this section to work with the user account */}
|
|
||||||
<ProfileLevel user={JSON_USER} className="h-1/2" />
|
|
||||||
<div className="h-1/2 flex flex-col">
|
|
||||||
<div className="h-1/2 flex gap-8">
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => toggleModule("reading")}
|
|
||||||
className={clsx(
|
|
||||||
"flex flex-col gap-2 items-center justify-center",
|
|
||||||
"border-ielts-reading hover:bg-ielts-reading text-white",
|
|
||||||
"border-2 rounded-xl p-4 h-fit w-48 cursor-pointer",
|
|
||||||
selectedModules.includes("reading") ? "bg-ielts-reading " : "bg-ielts-reading-transparent ",
|
|
||||||
)}>
|
|
||||||
<Icon path={mdiBookOpen} color="white" size={3} />
|
|
||||||
<span>Reading</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => toggleModule("listening")}
|
|
||||||
className={clsx(
|
|
||||||
"flex flex-col gap-2 items-center justify-center",
|
|
||||||
"border-ielts-listening hover:bg-ielts-listening text-white",
|
|
||||||
"border-2 rounded-xl p-4 h-fit w-48 cursor-pointer",
|
|
||||||
selectedModules.includes("listening") ? "bg-ielts-listening " : "bg-ielts-listening-transparent ",
|
|
||||||
)}>
|
|
||||||
<Icon path={mdiHeadphones} color="white" size={3} />
|
|
||||||
<span>Listening</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => toggleModule("speaking")}
|
|
||||||
className={clsx(
|
|
||||||
"flex flex-col gap-2 items-center justify-center",
|
|
||||||
"border-ielts-speaking hover:bg-ielts-speaking text-white",
|
|
||||||
"border-2 rounded-xl p-4 h-fit w-48 cursor-pointer",
|
|
||||||
selectedModules.includes("speaking") ? "bg-ielts-speaking " : "bg-ielts-speaking-transparent ",
|
|
||||||
)}>
|
|
||||||
<Icon path={mdiAccountVoice} color="white" size={3} />
|
|
||||||
<span>Speaking</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => toggleModule("writing")}
|
|
||||||
className={clsx(
|
|
||||||
"flex flex-col gap-2 items-center justify-center",
|
|
||||||
"border-ielts-writing hover:bg-ielts-writing text-white",
|
|
||||||
"border-2 rounded-xl p-4 h-fit w-48 cursor-pointer",
|
|
||||||
selectedModules.includes("writing") ? "bg-ielts-writing " : "bg-ielts-writing-transparent ",
|
|
||||||
)}>
|
|
||||||
<Icon path={mdiPen} color="white" size={3} />
|
|
||||||
<span>Writing</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex justify-between">
|
|
||||||
<button onClick={() => router.push("/")} className={clsx("btn btn-wide gap-4 relative text-white", errorButtonStyle)}>
|
|
||||||
<div className="absolute left-4">
|
|
||||||
<Icon path={mdiArrowLeft} color="white" size={1} />
|
|
||||||
</div>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<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} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user