Started creating the page to generate exams
This commit is contained in:
@@ -3,6 +3,7 @@ import {useState} from "react";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type: "email" | "text" | "password" | "tel" | "number";
|
type: "email" | "text" | "password" | "tel" | "number";
|
||||||
|
roundness?: "full" | "xl";
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -13,7 +14,18 @@ interface Props {
|
|||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Input({type, label, placeholder, name, required = false, defaultValue, className, disabled = false, onChange}: Props) {
|
export default function Input({
|
||||||
|
type,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
name,
|
||||||
|
required = false,
|
||||||
|
defaultValue,
|
||||||
|
className,
|
||||||
|
roundness = "full",
|
||||||
|
disabled = false,
|
||||||
|
onChange,
|
||||||
|
}: Props) {
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
if (type === "password") {
|
if (type === "password") {
|
||||||
@@ -59,7 +71,11 @@ export default function Input({type, label, placeholder, name, required = false,
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className="px-8 py-6 text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
|
className={clsx(
|
||||||
|
"px-8 py-6 text-sm font-normal bg-white border border-mti-gray-platinum focus:outline-none",
|
||||||
|
"placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed",
|
||||||
|
roundness === "full" ? "rounded-full" : "rounded-xl",
|
||||||
|
)}
|
||||||
required={required}
|
required={required}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {IconType} from "react-icons";
|
import {IconType} from "react-icons";
|
||||||
import {MdSpaceDashboard} from "react-icons/md";
|
import {MdSpaceDashboard} from "react-icons/md";
|
||||||
import {BsFileEarmarkText, BsClockHistory, BsPencil, BsGraphUp, BsChevronBarRight, BsChevronBarLeft, BsShieldFill} from "react-icons/bs";
|
import {BsFileEarmarkText, BsClockHistory, BsPencil, BsGraphUp, BsChevronBarRight, BsChevronBarLeft, BsShieldFill, BsCloudFill} from "react-icons/bs";
|
||||||
import {RiLogoutBoxFill} from "react-icons/ri";
|
import {RiLogoutBoxFill} from "react-icons/ri";
|
||||||
import {SlPencil} from "react-icons/sl";
|
import {SlPencil} from "react-icons/sl";
|
||||||
import {FaAward} from "react-icons/fa";
|
import {FaAward} from "react-icons/fa";
|
||||||
@@ -92,6 +92,16 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
|
|||||||
{userType !== "student" && (
|
{userType !== "student" && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsShieldFill} label="Admin" path={path} keyPath="/admin" isMinimized={isMinimized} />
|
<Nav disabled={disableNavigation} Icon={BsShieldFill} label="Admin" path={path} keyPath="/admin" isMinimized={isMinimized} />
|
||||||
)}
|
)}
|
||||||
|
{userType === "developer" && (
|
||||||
|
<Nav
|
||||||
|
disabled={disableNavigation}
|
||||||
|
Icon={BsCloudFill}
|
||||||
|
label="Generation"
|
||||||
|
path={path}
|
||||||
|
keyPath="/generation"
|
||||||
|
isMinimized={isMinimized}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="xl:hidden -xl:flex flex-col gap-3">
|
<div className="xl:hidden -xl:flex flex-col gap-3">
|
||||||
<Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized={true} />
|
<Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized={true} />
|
||||||
@@ -102,6 +112,9 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
|
|||||||
{userType !== "student" && (
|
{userType !== "student" && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsShieldFill} label="Admin" path={path} keyPath="/admin" isMinimized={true} />
|
<Nav disabled={disableNavigation} Icon={BsShieldFill} label="Admin" path={path} keyPath="/admin" isMinimized={true} />
|
||||||
)}
|
)}
|
||||||
|
{userType === "developer" && (
|
||||||
|
<Nav disabled={disableNavigation} Icon={BsCloudFill} label="Generation" path={path} keyPath="/generation" isMinimized={true} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-0 absolute bottom-12">
|
<div className="flex flex-col gap-0 absolute bottom-12">
|
||||||
|
|||||||
@@ -3,13 +3,7 @@ import {Module} from ".";
|
|||||||
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam;
|
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam;
|
||||||
|
|
||||||
export interface ReadingExam {
|
export interface ReadingExam {
|
||||||
parts: {
|
parts: ReadingPart[];
|
||||||
text: {
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
};
|
|
||||||
exercises: Exercise[];
|
|
||||||
}[];
|
|
||||||
id: string;
|
id: string;
|
||||||
module: "reading";
|
module: "reading";
|
||||||
minTimer: number;
|
minTimer: number;
|
||||||
@@ -17,6 +11,14 @@ export interface ReadingExam {
|
|||||||
isDiagnostic: boolean;
|
isDiagnostic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReadingPart {
|
||||||
|
text: {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
exercises: Exercise[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface LevelExam {
|
export interface LevelExam {
|
||||||
module: "level";
|
module: "level";
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
286
src/pages/generation.tsx
Normal file
286
src/pages/generation.tsx
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import Head from "next/head";
|
||||||
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
|
import {sessionOptions} from "@/lib/session";
|
||||||
|
import useUser from "@/hooks/useUser";
|
||||||
|
import {ToastContainer} from "react-toastify";
|
||||||
|
import Layout from "@/components/High/Layout";
|
||||||
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {Module} from "@/interfaces";
|
||||||
|
import {RadioGroup, Tab} from "@headlessui/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import {MODULE_ARRAY} from "@/utils/moduleUtils";
|
||||||
|
import {capitalize} from "lodash";
|
||||||
|
import Button from "@/components/Low/Button";
|
||||||
|
import {Exercise, ReadingPart} from "@/interfaces/exam";
|
||||||
|
import Input from "@/components/Low/Input";
|
||||||
|
|
||||||
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
|
||||||
|
if (!user || !user.isVerified) {
|
||||||
|
res.setHeader("location", "/login");
|
||||||
|
res.statusCode = 302;
|
||||||
|
res.end();
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
user: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRedirectHome(user) || user.type !== "developer") {
|
||||||
|
res.setHeader("location", "/");
|
||||||
|
res.statusCode = 302;
|
||||||
|
res.end();
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
user: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {user: req.session.user},
|
||||||
|
};
|
||||||
|
}, sessionOptions);
|
||||||
|
|
||||||
|
export default function Generation() {
|
||||||
|
const [module, setModule] = useState<Module>("reading");
|
||||||
|
|
||||||
|
const {user} = useUser({redirectTo: "/login"});
|
||||||
|
|
||||||
|
const ReadingGeneration = () => {
|
||||||
|
const [part1, setPart1] = useState<ReadingPart>();
|
||||||
|
const [part2, setPart2] = useState<ReadingPart>();
|
||||||
|
const [part3, setPart3] = useState<ReadingPart>();
|
||||||
|
const [types, setTypes] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const availableTypes = [
|
||||||
|
{type: "fillBlanks", label: "Fill the Blanks"},
|
||||||
|
{type: "multipleChoice", label: "Multiple Choice"},
|
||||||
|
{type: "trueFalse", label: "True or False"},
|
||||||
|
{type: "writeBlanks", label: "Write the Blanks"},
|
||||||
|
];
|
||||||
|
|
||||||
|
const toggleType = (type: string) => setTypes((prev) => (prev.includes(type) ? [...prev.filter((x) => x !== type)] : [...prev, type]));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Exercises</label>
|
||||||
|
<div className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between">
|
||||||
|
{availableTypes.map((x) => (
|
||||||
|
<span
|
||||||
|
onClick={() => toggleType(x.type)}
|
||||||
|
key={x.type}
|
||||||
|
className={clsx(
|
||||||
|
"px-6 py-4 w-64 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
!types.includes(x.type)
|
||||||
|
? "bg-white border-mti-gray-platinum"
|
||||||
|
: "bg-ielts-reading/70 border-ielts-reading text-white",
|
||||||
|
)}>
|
||||||
|
{x.label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tab.Group>
|
||||||
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-reading/20 p-1">
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-reading/70",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-reading focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-reading",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Passage 1
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-reading/70",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-reading focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-reading",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Passage 2
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-reading/70",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-reading focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-reading",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Passage 3
|
||||||
|
</Tab>
|
||||||
|
</Tab.List>
|
||||||
|
<Tab.Panels>
|
||||||
|
{[part1, part2, part3].map((part, index) => (
|
||||||
|
<Tab.Panel className="w-full bg-ielts-reading/20 min-h-[600px] h-full rounded-xl p-4 flex flex-col gap-4" key={index}>
|
||||||
|
<div className="flex gap-4 items-end">
|
||||||
|
<Input type="text" placeholder="Grand Canyon..." name="topic" label="Topic" onChange={() => {}} roundness="xl" />
|
||||||
|
<button
|
||||||
|
disabled={types.length === 0}
|
||||||
|
className={clsx(
|
||||||
|
"bg-ielts-reading/70 border border-ielts-reading text-white w-full max-w-[200px] px-6 py-6 rounded-xl h-[70px]",
|
||||||
|
"hover:bg-ielts-reading disabled:bg-ielts-reading/40 disabled:cursor-not-allowed",
|
||||||
|
"transition ease-in-out duration-300",
|
||||||
|
)}>
|
||||||
|
{!part ? "Generate" : "Regenerate"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Tab.Panel>
|
||||||
|
))}
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListeningGeneration = () => {
|
||||||
|
const [types, setTypes] = useState<string[]>([]);
|
||||||
|
const availableTypes = [
|
||||||
|
{type: "fillBlanks", label: "Fill the Blanks"},
|
||||||
|
{type: "multipleChoice", label: "Multiple Choice"},
|
||||||
|
{type: "trueFalse", label: "True or False"},
|
||||||
|
{type: "writeBlanks", label: "Write the Blanks"},
|
||||||
|
];
|
||||||
|
|
||||||
|
const toggleType = (type: string) => setTypes((prev) => (prev.includes(type) ? [...prev.filter((x) => x !== type)] : [...prev, type]));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Exercises</label>
|
||||||
|
<div className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between">
|
||||||
|
{availableTypes.map((x) => (
|
||||||
|
<span
|
||||||
|
onClick={() => toggleType(x.type)}
|
||||||
|
key={x.type}
|
||||||
|
className={clsx(
|
||||||
|
"px-6 py-4 w-64 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
!types.includes(x.type)
|
||||||
|
? "bg-white border-mti-gray-platinum"
|
||||||
|
: "bg-ielts-listening/70 border-ielts-listening text-white",
|
||||||
|
)}>
|
||||||
|
{x.label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tab.Group>
|
||||||
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-listening/20 p-1">
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Passage 1
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Passage 2
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Passage 3
|
||||||
|
</Tab>
|
||||||
|
</Tab.List>
|
||||||
|
</Tab.Group>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Exam Generation | EnCoach</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
|
/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
<ToastContainer />
|
||||||
|
{user && (
|
||||||
|
<Layout user={user} className="gap-6">
|
||||||
|
<h1 className="text-2xl font-semibold">Exam Generation</h1>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Module</label>
|
||||||
|
<RadioGroup
|
||||||
|
value={module}
|
||||||
|
onChange={setModule}
|
||||||
|
className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between">
|
||||||
|
{[...MODULE_ARRAY, "level"].map((x) => (
|
||||||
|
<RadioGroup.Option value={x} key={x}>
|
||||||
|
{({checked}) => (
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
"px-6 py-4 w-64 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
x === "reading" &&
|
||||||
|
(!checked
|
||||||
|
? "bg-white border-mti-gray-platinum"
|
||||||
|
: "bg-ielts-reading/70 border-ielts-reading text-white"),
|
||||||
|
x === "listening" &&
|
||||||
|
(!checked
|
||||||
|
? "bg-white border-mti-gray-platinum"
|
||||||
|
: "bg-ielts-listening/70 border-ielts-listening text-white"),
|
||||||
|
x === "writing" &&
|
||||||
|
(!checked
|
||||||
|
? "bg-white border-mti-gray-platinum"
|
||||||
|
: "bg-ielts-writing/70 border-ielts-writing text-white"),
|
||||||
|
x === "speaking" &&
|
||||||
|
(!checked
|
||||||
|
? "bg-white border-mti-gray-platinum"
|
||||||
|
: "bg-ielts-speaking/70 border-ielts-speaking text-white"),
|
||||||
|
x === "level" &&
|
||||||
|
(!checked
|
||||||
|
? "bg-white border-mti-gray-platinum"
|
||||||
|
: "bg-ielts-level/70 border-ielts-level text-white"),
|
||||||
|
)}>
|
||||||
|
{capitalize(x)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</RadioGroup.Option>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
{module === "reading" && <ReadingGeneration />}
|
||||||
|
{module === "listening" && <ListeningGeneration />}
|
||||||
|
</Layout>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user