Compare commits
12 Commits
feature/co
...
feature/le
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13c8459d4b | ||
|
|
19b3bbe139 | ||
|
|
44a89c6645 | ||
|
|
4a51bd7dfa | ||
|
|
dc759a368e | ||
|
|
c28f7bb024 | ||
|
|
d412c1616f | ||
|
|
c2a807efc7 | ||
|
|
6056735c72 | ||
|
|
261ba74105 | ||
|
|
4328a1d72d | ||
|
|
82643b51d3 |
@@ -33,7 +33,7 @@ export default function Layout({user, children, className, navDisabled = false,
|
||||
focusMode={focusMode}
|
||||
onFocusLayerMouseEnter={onFocusLayerMouseEnter}
|
||||
className="-md:hidden"
|
||||
showAdmin={user.type !== "student"}
|
||||
userType={user.type}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
|
||||
@@ -19,6 +19,7 @@ export default function ProgressBar({label, percentage, color, useColor = false,
|
||||
listening: "bg-ielts-listening",
|
||||
writing: "bg-ielts-writing",
|
||||
speaking: "bg-ielts-speaking",
|
||||
level: "bg-ielts-level",
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import {moduleLabels} from "@/utils/moduleUtils";
|
||||
import clsx from "clsx";
|
||||
import {motion} from "framer-motion";
|
||||
import {ReactNode, useEffect, useState} from "react";
|
||||
import {BsBook, BsHeadphones, BsMegaphone, BsPen, BsStopwatch} from "react-icons/bs";
|
||||
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch} from "react-icons/bs";
|
||||
import ProgressBar from "../Low/ProgressBar";
|
||||
import TimerEndedModal from "../TimerEndedModal";
|
||||
|
||||
@@ -46,6 +46,7 @@ export default function ModuleTitle({minTimer, module, label, exerciseIndex, tot
|
||||
listening: <BsHeadphones className="text-ielts-listening w-6 h-6" />,
|
||||
writing: <BsPen className="text-ielts-writing w-6 h-6" />,
|
||||
speaking: <BsMegaphone className="text-ielts-speaking w-6 h-6" />,
|
||||
level: <BsClipboard className="text-ielts-level w-6 h-6" />,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -66,22 +66,27 @@ export default function MobileMenu({isOpen, onClose, path, user}: Props) {
|
||||
)}>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="/exam"
|
||||
className={clsx(
|
||||
"transition ease-in-out duration-300 w-fit",
|
||||
path === "/exam" && "text-mti-purple-light font-semibold border-b-2 border-b-mti-purple-light ",
|
||||
)}>
|
||||
Exams
|
||||
</Link>
|
||||
<Link
|
||||
href="/exercises"
|
||||
className={clsx(
|
||||
"transition ease-in-out duration-300 w-fit",
|
||||
path === "/exercises" && "text-mti-purple-light font-semibold border-b-2 border-b-mti-purple-light ",
|
||||
)}>
|
||||
Exercises
|
||||
</Link>
|
||||
{(user.type === "student" || user.type === "teacher" || user.type === "developer") && (
|
||||
<>
|
||||
<Link
|
||||
href="/exam"
|
||||
className={clsx(
|
||||
"transition ease-in-out duration-300 w-fit",
|
||||
path === "/exam" && "text-mti-purple-light font-semibold border-b-2 border-b-mti-purple-light ",
|
||||
)}>
|
||||
Exams
|
||||
</Link>
|
||||
<Link
|
||||
href="/exercises"
|
||||
className={clsx(
|
||||
"transition ease-in-out duration-300 w-fit",
|
||||
path === "/exercises" &&
|
||||
"text-mti-purple-light font-semibold border-b-2 border-b-mti-purple-light ",
|
||||
)}>
|
||||
Exercises
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
href="/stats"
|
||||
className={clsx(
|
||||
|
||||
@@ -12,13 +12,14 @@ import FocusLayer from "@/components/FocusLayer";
|
||||
import {preventNavigation} from "@/utils/navigation.disabled";
|
||||
import {useState} from "react";
|
||||
import usePreferencesStore from "@/stores/preferencesStore";
|
||||
import {Type} from "@/interfaces/user";
|
||||
interface Props {
|
||||
path: string;
|
||||
navDisabled?: boolean;
|
||||
focusMode?: boolean;
|
||||
onFocusLayerMouseEnter?: () => void;
|
||||
className?: string;
|
||||
showAdmin?: boolean;
|
||||
userType?: Type;
|
||||
}
|
||||
|
||||
interface NavProps {
|
||||
@@ -44,7 +45,7 @@ const Nav = ({Icon, label, path, keyPath, disabled = false, isMinimized = false}
|
||||
</Link>
|
||||
);
|
||||
|
||||
export default function Sidebar({path, navDisabled = false, focusMode = false, showAdmin = false, onFocusLayerMouseEnter, className}: Props) {
|
||||
export default function Sidebar({path, navDisabled = false, focusMode = false, userType, onFocusLayerMouseEnter, className}: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [state.isSidebarMinimized, state.toggleSidebarMinimized]);
|
||||
@@ -66,11 +67,29 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, s
|
||||
)}>
|
||||
<div className="xl:flex -xl:hidden flex-col gap-3">
|
||||
<Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized={isMinimized} />
|
||||
<Nav disabled={disableNavigation} Icon={BsFileEarmarkText} label="Exams" path={path} keyPath="/exam" isMinimized={isMinimized} />
|
||||
<Nav disabled={disableNavigation} Icon={BsPencil} label="Exercises" path={path} keyPath="/exercises" isMinimized={isMinimized} />
|
||||
{(userType === "student" || userType === "teacher" || userType === "developer") && (
|
||||
<>
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
Icon={BsFileEarmarkText}
|
||||
label="Exams"
|
||||
path={path}
|
||||
keyPath="/exam"
|
||||
isMinimized={isMinimized}
|
||||
/>
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
Icon={BsPencil}
|
||||
label="Exercises"
|
||||
path={path}
|
||||
keyPath="/exercises"
|
||||
isMinimized={isMinimized}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={isMinimized} />
|
||||
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
|
||||
{showAdmin && (
|
||||
{userType !== "student" && (
|
||||
<Nav disabled={disableNavigation} Icon={BsShieldFill} label="Admin" path={path} keyPath="/admin" isMinimized={isMinimized} />
|
||||
)}
|
||||
</div>
|
||||
@@ -80,7 +99,9 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, s
|
||||
<Nav disabled={disableNavigation} Icon={BsPencil} label="Exercises" path={path} keyPath="/exercises" isMinimized={true} />
|
||||
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={true} />
|
||||
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={true} />
|
||||
{showAdmin && <Nav disabled={disableNavigation} Icon={BsShieldFill} label="Admin" path={path} keyPath="/admin" isMinimized={true} />}
|
||||
{userType !== "student" && (
|
||||
<Nav disabled={disableNavigation} Icon={BsShieldFill} label="Admin" path={path} keyPath="/admin" isMinimized={true} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-0 absolute bottom-12">
|
||||
|
||||
@@ -15,6 +15,8 @@ import Checkbox from "./Low/Checkbox";
|
||||
import CountrySelect from "./Low/CountrySelect";
|
||||
import Input from "./Low/Input";
|
||||
import ProfileSummary from "./ProfileSummary";
|
||||
import Select from "react-select";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
|
||||
const expirationDateColor = (date: Date) => {
|
||||
const momentDate = moment(date);
|
||||
@@ -27,20 +29,26 @@ const expirationDateColor = (date: Date) => {
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
loggedInUser: User;
|
||||
onClose: (reload?: boolean) => void;
|
||||
onViewStudents?: () => void;
|
||||
onViewTeachers?: () => void;
|
||||
}
|
||||
|
||||
const UserCard = ({user, onClose, onViewStudents, onViewTeachers}: Props) => {
|
||||
const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers}: Props) => {
|
||||
const [expiryDate, setExpiryDate] = useState<Date | null | undefined>(user.subscriptionExpirationDate);
|
||||
const [referralAgent, setReferralAgent] = useState(user.corporateInformation?.referralAgent);
|
||||
const [type, setType] = useState(user.type);
|
||||
const [status, setStatus] = useState(user.status);
|
||||
|
||||
const {stats} = useStats(user.id);
|
||||
const {users} = useUsers();
|
||||
|
||||
const updateUser = () => {
|
||||
if (!confirm(`Are you sure you want to update ${user.name}'s account?`)) return;
|
||||
|
||||
axios
|
||||
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {...user, subscriptionExpirationDate: expiryDate})
|
||||
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {...user, subscriptionExpirationDate: expiryDate, type, status})
|
||||
.then(() => {
|
||||
toast.success("User updated successfully!");
|
||||
onClose(true);
|
||||
@@ -212,7 +220,12 @@ const UserCard = ({user, onClose, onViewStudents, onViewTeachers}: Props) => {
|
||||
expirationDateColor(expiryDate),
|
||||
"transition duration-300 ease-in-out",
|
||||
)}
|
||||
filterDate={(date) => moment(date).isAfter(new Date())}
|
||||
filterDate={(date) =>
|
||||
moment(date).isAfter(new Date()) &&
|
||||
(loggedInUser.subscriptionExpirationDate
|
||||
? moment(date).isBefore(moment(loggedInUser.subscriptionExpirationDate))
|
||||
: true)
|
||||
}
|
||||
dateFormat="dd/MM/yyyy"
|
||||
selected={moment(expiryDate).toDate()}
|
||||
onChange={(date) => setExpiryDate(date)}
|
||||
@@ -221,7 +234,39 @@ const UserCard = ({user, onClose, onViewStudents, onViewTeachers}: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{user.corporateInformation && (
|
||||
{(loggedInUser.type === "developer" || loggedInUser.type === "owner") && (
|
||||
<>
|
||||
<Divider className="w-full" />
|
||||
<div className="flex flex-col md:flex-row gap-8 w-full">
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Status</label>
|
||||
<select
|
||||
defaultValue={user.status}
|
||||
onChange={(e) => setStatus(e.target.value as typeof user.status)}
|
||||
className="p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white">
|
||||
<option value="active">Active</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="paymentDue">Payment Due</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Type</label>
|
||||
<select
|
||||
defaultValue={user.type}
|
||||
onChange={(e) => setType(e.target.value as typeof user.type)}
|
||||
className="p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white">
|
||||
<option value="student">Student</option>
|
||||
<option value="teacher">Teacher</option>
|
||||
<option value="corporate">Corporate</option>
|
||||
<option value="agent">Country Agent</option>
|
||||
<option value="owner">Owner</option>
|
||||
<option value="developer">Developer</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{user.type === "corporate" && (
|
||||
<>
|
||||
<Divider className="w-full" />
|
||||
<div className="flex flex-col md:flex-row gap-8 w-full">
|
||||
@@ -231,8 +276,7 @@ const UserCard = ({user, onClose, onViewStudents, onViewTeachers}: Props) => {
|
||||
name="companyName"
|
||||
onChange={() => null}
|
||||
placeholder="Enter company name"
|
||||
defaultValue={user.corporateInformation.companyInformation.name}
|
||||
disabled
|
||||
defaultValue={user.corporateInformation?.companyInformation.name}
|
||||
/>
|
||||
<Input
|
||||
label="Amount of Users"
|
||||
@@ -240,9 +284,40 @@ const UserCard = ({user, onClose, onViewStudents, onViewTeachers}: Props) => {
|
||||
name="userAmount"
|
||||
onChange={() => null}
|
||||
placeholder="Enter amount of users"
|
||||
defaultValue={user.corporateInformation.companyInformation.userAmount}
|
||||
disabled
|
||||
defaultValue={user.corporateInformation?.companyInformation.userAmount}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Country Agent</label>
|
||||
<Select
|
||||
className="px-4 py-4 w-full 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"
|
||||
options={[
|
||||
{value: "", label: "No referral"},
|
||||
...users.filter((u) => u.type === "agent").map((x) => ({value: x.id, label: `${x.name} - ${x.email}`})),
|
||||
]}
|
||||
defaultValue={{
|
||||
value: referralAgent,
|
||||
label: referralAgent ? users.find((u) => u.id === referralAgent)?.name || "" : "No agent",
|
||||
}}
|
||||
onChange={(value) => setReferralAgent(value?.value)}
|
||||
styles={{
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: "4px",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
":focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
color: state.isFocused ? "black" : styles.color,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import {SEMI_TRANSPARENT} from "@/resources/colors";
|
||||
import {Chart as ChartJS, RadialLinearScale, ArcElement, Tooltip, Legend} from "chart.js";
|
||||
import clsx from "clsx";
|
||||
import {PolarArea} from "react-chartjs-2";
|
||||
import {Chart} from "primereact/chart";
|
||||
|
||||
interface Props {
|
||||
data: {label: string; value: number}[];
|
||||
label?: string;
|
||||
title: string;
|
||||
type: string;
|
||||
colors?: string[];
|
||||
}
|
||||
|
||||
ChartJS.register(RadialLinearScale, ArcElement, Tooltip, Legend);
|
||||
|
||||
export default function SingleDatasetChart({data, type, label, title, colors = Object.values(SEMI_TRANSPARENT)}: Props) {
|
||||
const labels = data.map((x) => x.label);
|
||||
const chartData = {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label,
|
||||
data: data.map((x) => x.value),
|
||||
backgroundColor: colors,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return <Chart type={type} data={chartData} options={{plugins: {title: {text: title, display: true}}}} />;
|
||||
}
|
||||
@@ -7,17 +7,78 @@ export const BAND_SCORES: {[key in Module]: number[]} = {
|
||||
listening: [0, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9],
|
||||
writing: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
speaking: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
level: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
};
|
||||
|
||||
export const LEVEL_TEXT = {
|
||||
excellent:
|
||||
"Congratulations on your exam performance! You achieved an impressive {{level}}, demonstrating excellent mastery of the assessed knowledge.\n\nIf you disagree with the result, you can request a review by a qualified teacher. We are committed to the accuracy and transparency of the results.\n\nPlease contact us for further information. Congratulations again on your outstanding achievement! We are here to support you on your academic journey.",
|
||||
high: "Congratulations on your exam performance! You achieved a commendable {{level}}, demonstrating a good understanding of the assessed knowledge.\n\nIf you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and transparency of the results.\n\nPlease contact us for further information. Congratulations again on your achievement! We are here to support you on your academic journey.",
|
||||
medium: "Congratulations on your exam performance! You achieved a {{level}}, demonstrating a satisfactory understanding of the assessed knowledge.\n\nIf you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and transparency of the results.\n\nPlease contact us for further information. Congratulations again on your achievement! We are here to support you on your academic journey.",
|
||||
low: "Thank you for taking the exam. You achieved a {{level}}, but unfortunately, it did not meet the required standards.\n\nIf you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and transparency of the results.\n\nPlease contact us for further information. We encourage you to continue your studies and wish you the best of luck in your future endeavors.",
|
||||
};
|
||||
|
||||
export const levelText = (level: number) => {
|
||||
export const moduleResultText = (level: number) => {
|
||||
if (level === 9) {
|
||||
return (
|
||||
<>
|
||||
Congratulations on your exam performance! You achieved an impressive <span className="font-bold">level {level}</span>, demonstrating
|
||||
excellent mastery of the assessed knowledge.
|
||||
<br />
|
||||
<br />
|
||||
If you disagree with the result, you can request a review by a qualified teacher. We are committed to the accuracy and transparency of
|
||||
the results.
|
||||
<br />
|
||||
<br />
|
||||
Please contact us for further information. Congratulations again on your outstanding achievement! We are here to support you on your
|
||||
academic journey.
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (level >= 6) {
|
||||
return (
|
||||
<>
|
||||
Congratulations on your exam performance! You achieved a commendable <span className="font-bold">level {level}</span>, demonstrating a
|
||||
good understanding of the assessed knowledge.
|
||||
<br />
|
||||
<br />
|
||||
If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and
|
||||
transparency of the results.
|
||||
<br />
|
||||
<br />
|
||||
Please contact us for further information. Congratulations again on your achievement! We are here to support you on your academic
|
||||
journey.
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (level >= 3) {
|
||||
return (
|
||||
<>
|
||||
Congratulations on your exam performance! You achieved a <span className="font-bold">level of {level}</span>, demonstrating a
|
||||
satisfactory understanding of the assessed knowledge.
|
||||
<br />
|
||||
<br />
|
||||
If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and
|
||||
transparency of the results.
|
||||
<br />
|
||||
<br />
|
||||
Please contact us for further information. Congratulations again on your achievement! We are here to support you on your academic
|
||||
journey.
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
Thank you for taking the exam. You achieved a <span className="font-bold">level {level}</span>, but unfortunately, it did not meet the
|
||||
required standards.
|
||||
<br />
|
||||
<br />
|
||||
If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and
|
||||
transparency of the results.
|
||||
<br />
|
||||
<br />
|
||||
Please contact us for further information. We encourage you to continue your studies and wish you the best of luck in your future
|
||||
endeavors.
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const levelResultText = (level: number) => {
|
||||
if (level === 9) {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -2,7 +2,7 @@ import {Type} from "@/interfaces/user";
|
||||
|
||||
export const PERMISSIONS = {
|
||||
generateCode: {
|
||||
student: ["teacher", "corporate", "developer", "owner"],
|
||||
student: ["corporate", "developer", "owner"],
|
||||
teacher: ["corporate", "developer", "owner"],
|
||||
corporate: ["owner", "developer"],
|
||||
owner: ["developer", "owner"],
|
||||
|
||||
@@ -7,7 +7,7 @@ import {calculateBandScore} from "@/utils/score";
|
||||
import clsx from "clsx";
|
||||
import moment from "moment";
|
||||
import {useState} from "react";
|
||||
import {BsBook, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
|
||||
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
|
||||
|
||||
interface Props {
|
||||
onClick?: () => void;
|
||||
@@ -57,11 +57,13 @@ export default function AssignmentCard({id, name, assigner, startDate, endDate,
|
||||
module === "listening" && "bg-ielts-listening",
|
||||
module === "writing" && "bg-ielts-writing",
|
||||
module === "speaking" && "bg-ielts-speaking",
|
||||
module === "level" && "bg-ielts-level",
|
||||
)}>
|
||||
{module === "reading" && <BsBook className="w-4 h-4" />}
|
||||
{module === "listening" && <BsHeadphones className="w-4 h-4" />}
|
||||
{module === "writing" && <BsPen className="w-4 h-4" />}
|
||||
{module === "speaking" && <BsMegaphone className="w-4 h-4" />}
|
||||
{module === "level" && <BsClipboard className="w-4 h-4" />}
|
||||
{calculateAverageModuleScore(module) > -1 && (
|
||||
<span className="text-sm">{calculateAverageModuleScore(module).toFixed(1)}</span>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Modal from "@/components/Modal";
|
||||
import {Module} from "@/interfaces";
|
||||
import clsx from "clsx";
|
||||
import {useState} from "react";
|
||||
import {BsBook, BsCheckCircle, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
|
||||
import {BsBook, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs";
|
||||
import {generate} from "random-words";
|
||||
import {capitalize} from "lodash";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
@@ -99,59 +99,96 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
||||
return (
|
||||
<Modal isOpen={isCreating} onClose={cancelCreation} title="New Assignment">
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<section className="w-full grid -md:grid-cols-1 md:grid-cols-2 place-items-center lg:grid-cols-4 -md:flex-col -md:items-center -md:gap-12 justify-between gap-8 mt-8 px-8">
|
||||
<section className="w-full grid -md:grid-cols-1 md:grid-cols-2 place-items-center lg:grid-cols-6 -md:flex-col -md:items-center -md:gap-12 justify-between gap-8 mt-8 px-8">
|
||||
<div
|
||||
onClick={() => toggleModule("reading")}
|
||||
onClick={!selectedModules.includes("level") ? () => toggleModule("reading") : undefined}
|
||||
className={clsx(
|
||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||
"lg:col-span-2",
|
||||
selectedModules.includes("reading") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||
)}>
|
||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-reading top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||
<BsBook className="text-white w-7 h-7" />
|
||||
</div>
|
||||
<span className="ml-8 font-semibold">Reading</span>
|
||||
{!selectedModules.includes("reading") && <div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />}
|
||||
{!selectedModules.includes("reading") && !selectedModules.includes("level") && (
|
||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||
)}
|
||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
||||
{selectedModules.includes("reading") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => toggleModule("listening")}
|
||||
onClick={!selectedModules.includes("level") ? () => toggleModule("listening") : undefined}
|
||||
className={clsx(
|
||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||
"lg:col-span-2",
|
||||
selectedModules.includes("listening") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||
)}>
|
||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-listening top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||
<BsHeadphones className="text-white w-7 h-7" />
|
||||
</div>
|
||||
<span className="ml-8 font-semibold">Listening</span>
|
||||
{!selectedModules.includes("listening") && <div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />}
|
||||
{!selectedModules.includes("listening") && !selectedModules.includes("level") && (
|
||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||
)}
|
||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
||||
{selectedModules.includes("listening") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => toggleModule("writing")}
|
||||
onClick={!selectedModules.includes("level") ? () => toggleModule("writing") : undefined}
|
||||
className={clsx(
|
||||
"w-52 relative max-w-xs flex lg:flex-row-reverse items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||
"lg:col-span-2",
|
||||
selectedModules.includes("writing") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||
)}>
|
||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-writing top-1/2 -translate-y-1/2 -lg:left-0 -lg:-translate-x-1/2 lg:right-0 lg:translate-x-1/2">
|
||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-writing top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||
<BsPen className="text-white w-7 h-7" />
|
||||
</div>
|
||||
<span className="lg:mr-8 -lg:ml-8 font-semibold">Writing</span>
|
||||
{!selectedModules.includes("writing") && <div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />}
|
||||
<span className="ml-8 font-semibold">Writing</span>
|
||||
{!selectedModules.includes("writing") && !selectedModules.includes("level") && (
|
||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||
)}
|
||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
||||
{selectedModules.includes("writing") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => toggleModule("speaking")}
|
||||
onClick={!selectedModules.includes("level") ? () => toggleModule("speaking") : undefined}
|
||||
className={clsx(
|
||||
"w-52 relative max-w-xs flex lg:flex-row-reverse items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||
"lg:col-span-3",
|
||||
selectedModules.includes("speaking") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||
)}>
|
||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-speaking top-1/2 -translate-y-1/2 -lg:left-0 -lg:-translate-x-1/2 lg:right-0 lg:translate-x-1/2">
|
||||
<BsMegaphone className="text-white w-7 h-7 lg:-scale-x-100" />
|
||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-speaking top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||
<BsMegaphone className="text-white w-7 h-7" />
|
||||
</div>
|
||||
<span className="lg:mr-8 -lg:ml-8 font-semibold">Speaking</span>
|
||||
{!selectedModules.includes("speaking") && <div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />}
|
||||
<span className="ml-8 font-semibold">Speaking</span>
|
||||
{!selectedModules.includes("speaking") && !selectedModules.includes("level") && (
|
||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||
)}
|
||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
||||
{selectedModules.includes("speaking") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
||||
</div>
|
||||
<div
|
||||
onClick={
|
||||
(!selectedModules.includes("level") && selectedModules.length === 0) || selectedModules.includes("level")
|
||||
? () => toggleModule("level")
|
||||
: undefined
|
||||
}
|
||||
className={clsx(
|
||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||
"lg:col-span-3",
|
||||
selectedModules.includes("level") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||
)}>
|
||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-level top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||
<BsClipboard className="text-white w-7 h-7" />
|
||||
</div>
|
||||
<span className="ml-8 font-semibold">Level</span>
|
||||
{!selectedModules.includes("level") && selectedModules.length === 0 && (
|
||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||
)}
|
||||
{!selectedModules.includes("level") && selectedModules.length > 0 && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
||||
{selectedModules.includes("level") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Input type="text" name="name" onChange={(e) => setName(e)} defaultValue={name} label="Assignment Name" required />
|
||||
|
||||
@@ -10,10 +10,10 @@ import {sortByModule} from "@/utils/moduleUtils";
|
||||
import {calculateBandScore} from "@/utils/score";
|
||||
import {convertToUserSolutions} from "@/utils/stats";
|
||||
import clsx from "clsx";
|
||||
import {uniqBy} from "lodash";
|
||||
import {capitalize, uniqBy} from "lodash";
|
||||
import moment from "moment";
|
||||
import {useRouter} from "next/router";
|
||||
import {BsBook, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
|
||||
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
@@ -73,6 +73,11 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
level: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
};
|
||||
|
||||
stats.forEach((x) => {
|
||||
@@ -153,11 +158,13 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
|
||||
module === "listening" && "bg-ielts-listening",
|
||||
module === "writing" && "bg-ielts-writing",
|
||||
module === "speaking" && "bg-ielts-speaking",
|
||||
module === "level" && "bg-ielts-level",
|
||||
)}>
|
||||
{module === "reading" && <BsBook className="w-4 h-4" />}
|
||||
{module === "listening" && <BsHeadphones className="w-4 h-4" />}
|
||||
{module === "writing" && <BsPen className="w-4 h-4" />}
|
||||
{module === "speaking" && <BsMegaphone className="w-4 h-4" />}
|
||||
{module === "level" && <BsClipboard className="w-4 h-4" />}
|
||||
<span className="text-sm">{level.toFixed(1)}</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -232,18 +239,21 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
|
||||
<div className="grid grid-cols-4 gap-2 place-items-start w-full -md:mt-2">
|
||||
{assignment?.exams.map(({module}) => (
|
||||
<div
|
||||
data-tip={capitalize(module)}
|
||||
key={module}
|
||||
className={clsx(
|
||||
"flex gap-2 items-center w-fit text-white -md:px-4 xl:px-4 md:px-2 py-2 rounded-xl",
|
||||
"flex gap-2 items-center w-fit text-white -md:px-4 xl:px-4 md:px-2 py-2 rounded-xl tooltip",
|
||||
module === "reading" && "bg-ielts-reading",
|
||||
module === "listening" && "bg-ielts-listening",
|
||||
module === "writing" && "bg-ielts-writing",
|
||||
module === "speaking" && "bg-ielts-speaking",
|
||||
module === "level" && "bg-ielts-level",
|
||||
)}>
|
||||
{module === "reading" && <BsBook className="w-4 h-4" />}
|
||||
{module === "listening" && <BsHeadphones className="w-4 h-4" />}
|
||||
{module === "writing" && <BsPen className="w-4 h-4" />}
|
||||
{module === "speaking" && <BsMegaphone className="w-4 h-4" />}
|
||||
{module === "level" && <BsClipboard className="w-4 h-4" />}
|
||||
{calculateAverageModuleScore(module) > -1 && (
|
||||
<span className="text-sm">{calculateAverageModuleScore(module).toFixed(1)}</span>
|
||||
)}
|
||||
|
||||
@@ -147,7 +147,7 @@ export default function CorporateDashboard({user}: Props) {
|
||||
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
||||
}));
|
||||
|
||||
const levels: {[key in Module]: number} = {reading: 0, listening: 0, writing: 0, speaking: 0};
|
||||
const levels: {[key in Module]: number} = {reading: 0, listening: 0, writing: 0, speaking: 0, level: 0};
|
||||
bandScores.forEach((b) => (levels[b.module] += b.level));
|
||||
|
||||
return calculateAverageLevel(levels);
|
||||
@@ -250,6 +250,7 @@ export default function CorporateDashboard({user}: Props) {
|
||||
{selectedUser && (
|
||||
<div className="w-full flex flex-col gap-8">
|
||||
<UserCard
|
||||
loggedInUser={user}
|
||||
onClose={(shouldReload) => {
|
||||
setSelectedUser(undefined);
|
||||
if (shouldReload) reload();
|
||||
|
||||
@@ -244,7 +244,8 @@ export default function OwnerDashboard({user}: Props) {
|
||||
(x) =>
|
||||
x.type === "student" &&
|
||||
x.subscriptionExpirationDate &&
|
||||
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")),
|
||||
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
|
||||
moment().isBefore(moment(x.subscriptionExpirationDate)),
|
||||
)
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
@@ -259,7 +260,8 @@ export default function OwnerDashboard({user}: Props) {
|
||||
(x) =>
|
||||
x.type === "teacher" &&
|
||||
x.subscriptionExpirationDate &&
|
||||
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")),
|
||||
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
|
||||
moment().isBefore(moment(x.subscriptionExpirationDate)),
|
||||
)
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
@@ -274,7 +276,45 @@ export default function OwnerDashboard({user}: Props) {
|
||||
(x) =>
|
||||
x.type === "corporate" &&
|
||||
x.subscriptionExpirationDate &&
|
||||
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")),
|
||||
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
|
||||
moment().isBefore(moment(x.subscriptionExpirationDate)),
|
||||
)
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Expired Students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{users
|
||||
.filter(
|
||||
(x) => x.type === "student" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
|
||||
)
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Expired Teachers</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{users
|
||||
.filter(
|
||||
(x) => x.type === "teacher" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
|
||||
)
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Expired Corporate</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{users
|
||||
.filter(
|
||||
(x) =>
|
||||
x.type === "corporate" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
|
||||
)
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
@@ -292,6 +332,7 @@ export default function OwnerDashboard({user}: Props) {
|
||||
{selectedUser && (
|
||||
<div className="w-full flex flex-col gap-8">
|
||||
<UserCard
|
||||
loggedInUser={user}
|
||||
onClose={(shouldReload) => {
|
||||
setSelectedUser(undefined);
|
||||
if (shouldReload) reload();
|
||||
|
||||
@@ -7,14 +7,14 @@ import {Assignment} from "@/interfaces/results";
|
||||
import {User} from "@/interfaces/user";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import {getExamById} from "@/utils/exams";
|
||||
import {MODULE_ARRAY, sortByModule} from "@/utils/moduleUtils";
|
||||
import {MODULE_ARRAY, sortByModule, sortByModuleName} from "@/utils/moduleUtils";
|
||||
import {averageScore, groupBySession} from "@/utils/stats";
|
||||
import clsx from "clsx";
|
||||
import {capitalize} from "lodash";
|
||||
import moment from "moment";
|
||||
import Link from "next/link";
|
||||
import {useRouter} from "next/router";
|
||||
import {BsArrowRepeat, BsBook, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs";
|
||||
import {BsArrowRepeat, BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs";
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
@@ -115,34 +115,28 @@ export default function StudentDashboard({user}: Props) {
|
||||
</div>
|
||||
<div className="flex justify-between w-full items-center">
|
||||
<div className="grid grid-cols-2 gap-2 place-items-center justify-center w-fit min-w-[104px] -md:mt-2">
|
||||
{MODULE_ARRAY.map((module) => (
|
||||
<div
|
||||
key={module}
|
||||
className={clsx(
|
||||
"flex gap-2 items-center w-fit text-white -md:px-4 xl:px-4 md:px-2 py-2 rounded-xl",
|
||||
module === "reading" &&
|
||||
(assignment.exams.map((e) => e.module).includes("reading")
|
||||
? "bg-ielts-reading"
|
||||
: "bg-mti-black/40"),
|
||||
module === "listening" &&
|
||||
(assignment.exams.map((e) => e.module).includes("listening")
|
||||
? "bg-ielts-listening"
|
||||
: "bg-mti-black/40"),
|
||||
module === "writing" &&
|
||||
(assignment.exams.map((e) => e.module).includes("writing")
|
||||
? "bg-ielts-writing"
|
||||
: "bg-mti-black/40"),
|
||||
module === "speaking" &&
|
||||
(assignment.exams.map((e) => e.module).includes("speaking")
|
||||
? "bg-ielts-speaking"
|
||||
: "bg-mti-black/40"),
|
||||
)}>
|
||||
{module === "reading" && <BsBook className="w-4 h-4" />}
|
||||
{module === "listening" && <BsHeadphones className="w-4 h-4" />}
|
||||
{module === "writing" && <BsPen className="w-4 h-4" />}
|
||||
{module === "speaking" && <BsMegaphone className="w-4 h-4" />}
|
||||
</div>
|
||||
))}
|
||||
{assignment.exams
|
||||
.map((e) => e.module)
|
||||
.sort(sortByModuleName)
|
||||
.map((module) => (
|
||||
<div
|
||||
key={module}
|
||||
data-tip={capitalize(module)}
|
||||
className={clsx(
|
||||
"flex gap-2 items-center w-fit text-white -md:px-4 xl:px-4 md:px-2 py-2 rounded-xl tooltip",
|
||||
module === "reading" && "bg-ielts-reading",
|
||||
module === "listening" && "bg-ielts-listening",
|
||||
module === "writing" && "bg-ielts-writing",
|
||||
module === "speaking" && "bg-ielts-speaking",
|
||||
module === "level" && "bg-ielts-level",
|
||||
)}>
|
||||
{module === "reading" && <BsBook className="w-4 h-4" />}
|
||||
{module === "listening" && <BsHeadphones className="w-4 h-4" />}
|
||||
{module === "writing" && <BsPen className="w-4 h-4" />}
|
||||
{module === "speaking" && <BsMegaphone className="w-4 h-4" />}
|
||||
{module === "level" && <BsClipboard className="w-4 h-4" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{!assignment.results.map((r) => r.user).includes(user.id) && (
|
||||
<>
|
||||
|
||||
@@ -138,7 +138,7 @@ export default function TeacherDashboard({user}: Props) {
|
||||
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
||||
}));
|
||||
|
||||
const levels: {[key in Module]: number} = {reading: 0, listening: 0, writing: 0, speaking: 0};
|
||||
const levels: {[key in Module]: number} = {reading: 0, listening: 0, writing: 0, speaking: 0, level: 0};
|
||||
bandScores.forEach((b) => (levels[b.module] += b.level));
|
||||
|
||||
return calculateAverageLevel(levels);
|
||||
@@ -317,6 +317,7 @@ export default function TeacherDashboard({user}: Props) {
|
||||
{selectedUser && (
|
||||
<div className="w-full flex flex-col gap-8">
|
||||
<UserCard
|
||||
loggedInUser={user}
|
||||
onClose={(shouldReload) => {
|
||||
setSelectedUser(undefined);
|
||||
if (shouldReload) reload();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Button from "@/components/Low/Button";
|
||||
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
||||
import {levelText, LEVEL_TEXT} from "@/constants/ielts";
|
||||
import {moduleResultText} from "@/constants/ielts";
|
||||
import {Module} from "@/interfaces";
|
||||
import {User} from "@/interfaces/user";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
@@ -9,7 +9,7 @@ import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import {useRouter} from "next/router";
|
||||
import {Fragment, useEffect, useState} from "react";
|
||||
import {BsArrowCounterclockwise, BsBook, BsEyeFill, BsHeadphones, BsMegaphone, BsPen, BsShareFill} from "react-icons/bs";
|
||||
import {BsArrowCounterclockwise, BsBook, BsClipboard, BsEyeFill, BsHeadphones, BsMegaphone, BsPen, BsShareFill} from "react-icons/bs";
|
||||
|
||||
interface Score {
|
||||
module: Module;
|
||||
@@ -51,6 +51,10 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
|
||||
progress: "text-ielts-speaking",
|
||||
inner: "bg-ielts-speaking-light",
|
||||
},
|
||||
level: {
|
||||
progress: "text-ielts-level",
|
||||
inner: "bg-ielts-level-light",
|
||||
},
|
||||
};
|
||||
|
||||
const getTotalExercises = () => {
|
||||
@@ -117,6 +121,17 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
|
||||
<span className="font-semibold">Speaking</span>
|
||||
</div>
|
||||
)}
|
||||
{modules.includes("level") && (
|
||||
<div
|
||||
onClick={() => setSelectedModule("level")}
|
||||
className={clsx(
|
||||
"flex gap-2 items-center rounded-xl p-4 cursor-pointer hover:shadow-lg transition duration-300 ease-in-out hover:bg-ielts-level hover:text-white",
|
||||
selectedModule === "level" ? "bg-ielts-level text-white" : "bg-mti-gray-smoke text-ielts-level",
|
||||
)}>
|
||||
<BsClipboard className="w-6 h-6" />
|
||||
<span className="font-semibold">Level</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="w-fit h-fit absolute top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 animate-pulse flex flex-col gap-12 items-center">
|
||||
@@ -127,7 +142,7 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
|
||||
{!isLoading && (
|
||||
<div className="w-full flex gap-9 mt-32 items-center justify-between mb-20">
|
||||
<span className="max-w-3xl">
|
||||
{levelText(calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus))}
|
||||
{moduleResultText(calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus))}
|
||||
</span>
|
||||
<div className="flex gap-9 px-16">
|
||||
<div
|
||||
|
||||
89
src/exams/Level.tsx
Normal file
89
src/exams/Level.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import {renderExercise} from "@/components/Exercises";
|
||||
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
||||
import {renderSolution} from "@/components/Solutions";
|
||||
import {infoButtonStyle} from "@/constants/buttonStyles";
|
||||
import {LevelExam, UserSolution, WritingExam} from "@/interfaces/exam";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import {defaultUserSolutions} from "@/utils/exams";
|
||||
import {countExercises} from "@/utils/moduleUtils";
|
||||
import {mdiArrowRight} from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import clsx from "clsx";
|
||||
import {Fragment, useEffect, useState} from "react";
|
||||
import {toast} from "react-toastify";
|
||||
|
||||
interface Props {
|
||||
exam: LevelExam;
|
||||
showSolutions?: boolean;
|
||||
onFinish: (userSolutions: UserSolution[]) => void;
|
||||
}
|
||||
|
||||
export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
||||
const [exerciseIndex, setExerciseIndex] = useState(0);
|
||||
const [userSolutions, setUserSolutions] = useState<UserSolution[]>(exam.exercises.map((x) => defaultUserSolutions(x, exam)));
|
||||
|
||||
const [hasExamEnded, setHasExamEnded] = useExamStore((state) => [state.hasExamEnded, state.setHasExamEnded]);
|
||||
|
||||
const nextExercise = (solution?: UserSolution) => {
|
||||
if (solution) {
|
||||
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]);
|
||||
}
|
||||
|
||||
if (exerciseIndex + 1 < exam.exercises.length) {
|
||||
setExerciseIndex((prev) => prev + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (exerciseIndex >= exam.exercises.length) return;
|
||||
|
||||
setHasExamEnded(false);
|
||||
|
||||
if (solution) {
|
||||
onFinish(
|
||||
[...userSolutions.filter((x) => x.exercise !== solution.exercise), solution].map((x) => ({...x, module: "level", exam: exam.id})),
|
||||
);
|
||||
} else {
|
||||
onFinish(userSolutions.map((x) => ({...x, module: "level", exam: exam.id})));
|
||||
}
|
||||
};
|
||||
|
||||
const previousExercise = (solution?: UserSolution) => {
|
||||
if (solution) {
|
||||
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]);
|
||||
}
|
||||
|
||||
if (exerciseIndex > 0) {
|
||||
setExerciseIndex((prev) => prev - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const getExercise = () => {
|
||||
const exercise = exam.exercises[exerciseIndex];
|
||||
return {
|
||||
...exercise,
|
||||
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col h-full w-full gap-8 items-center">
|
||||
<ModuleTitle
|
||||
minTimer={exam.minTimer}
|
||||
exerciseIndex={exerciseIndex + 1}
|
||||
module="level"
|
||||
totalExercises={countExercises(exam.exercises)}
|
||||
disableTimer={showSolutions}
|
||||
/>
|
||||
{exerciseIndex > -1 &&
|
||||
exerciseIndex < exam.exercises.length &&
|
||||
!showSolutions &&
|
||||
renderExercise(getExercise(), nextExercise, previousExercise)}
|
||||
{exerciseIndex > -1 &&
|
||||
exerciseIndex < exam.exercises.length &&
|
||||
showSolutions &&
|
||||
renderSolution(exam.exercises[exerciseIndex], nextExercise, previousExercise)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import {Module} from "@/interfaces";
|
||||
import clsx from "clsx";
|
||||
import {User} from "@/interfaces/user";
|
||||
import ProgressBar from "@/components/Low/ProgressBar";
|
||||
import {BsBook, BsCheck, BsCheckCircle, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
|
||||
import {BsBook, BsCheck, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs";
|
||||
import {totalExamsByModule} from "@/utils/stats";
|
||||
import useStats from "@/hooks/useStats";
|
||||
import Button from "@/components/Low/Button";
|
||||
@@ -57,6 +57,11 @@ export default function Selection({user, page, onStart, disableSelection = false
|
||||
label: "Speaking",
|
||||
value: totalExamsByModule(stats, "speaking"),
|
||||
},
|
||||
{
|
||||
icon: <BsClipboard className="text-ielts-level w-6 h-6 md:w-8 md:h-8" />,
|
||||
label: "Level",
|
||||
value: totalExamsByModule(stats, "level"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
@@ -87,11 +92,11 @@ export default function Selection({user, page, onStart, disableSelection = false
|
||||
)}
|
||||
</span>
|
||||
</section>
|
||||
<section className="w-full flex -md:flex-col -md:items-center -md:gap-12 justify-between gap-8 mt-8">
|
||||
<section className="w-full flex -lg:flex-col -lg:items-center -lg:gap-12 justify-between gap-8 mt-8">
|
||||
<div
|
||||
onClick={!disableSelection ? () => toggleModule("reading") : undefined}
|
||||
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("reading") : undefined}
|
||||
className={clsx(
|
||||
"relative w-fit max-w-xs flex flex-col items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-2 pt-12 cursor-pointer",
|
||||
"relative w-64 max-w-xs flex flex-col items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-2 pt-12 cursor-pointer",
|
||||
selectedModules.includes("reading") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||
)}>
|
||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-reading top-0 -translate-y-1/2">
|
||||
@@ -101,17 +106,18 @@ export default function Selection({user, page, onStart, disableSelection = false
|
||||
<p className="text-center text-xs">
|
||||
Expand your vocabulary, improve your reading comprehension and improve your ability to interpret texts in English.
|
||||
</p>
|
||||
{!selectedModules.includes("reading") && !disableSelection && (
|
||||
{!selectedModules.includes("reading") && !selectedModules.includes("level") && !disableSelection && (
|
||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full mt-4" />
|
||||
)}
|
||||
{(selectedModules.includes("reading") || disableSelection) && (
|
||||
<BsCheckCircle className="mt-4 text-mti-purple-light w-8 h-8" />
|
||||
)}
|
||||
{selectedModules.includes("level") && <BsXCircle className="mt-4 text-mti-red-light w-8 h-8" />}
|
||||
</div>
|
||||
<div
|
||||
onClick={!disableSelection ? () => toggleModule("listening") : undefined}
|
||||
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("listening") : undefined}
|
||||
className={clsx(
|
||||
"relative w-fit max-w-xs flex flex-col items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-2 pt-12 cursor-pointer",
|
||||
"relative w-64 max-w-xs flex flex-col items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-2 pt-12 cursor-pointer",
|
||||
selectedModules.includes("listening") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||
)}>
|
||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-listening top-0 -translate-y-1/2">
|
||||
@@ -121,17 +127,18 @@ export default function Selection({user, page, onStart, disableSelection = false
|
||||
<p className="text-center text-xs">
|
||||
Improve your ability to follow conversations in English and your ability to understand different accents and intonations.
|
||||
</p>
|
||||
{!selectedModules.includes("listening") && !disableSelection && (
|
||||
{!selectedModules.includes("listening") && !selectedModules.includes("level") && !disableSelection && (
|
||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full mt-4" />
|
||||
)}
|
||||
{(selectedModules.includes("listening") || disableSelection) && (
|
||||
<BsCheckCircle className="mt-4 text-mti-purple-light w-8 h-8" />
|
||||
)}
|
||||
{selectedModules.includes("level") && <BsXCircle className="mt-4 text-mti-red-light w-8 h-8" />}
|
||||
</div>
|
||||
<div
|
||||
onClick={!disableSelection ? () => toggleModule("writing") : undefined}
|
||||
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("writing") : undefined}
|
||||
className={clsx(
|
||||
"relative w-fit max-w-xs flex flex-col items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-2 pt-12 cursor-pointer",
|
||||
"relative w-64 max-w-xs flex flex-col items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-2 pt-12 cursor-pointer",
|
||||
selectedModules.includes("writing") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||
)}>
|
||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-writing top-0 -translate-y-1/2">
|
||||
@@ -141,17 +148,18 @@ export default function Selection({user, page, onStart, disableSelection = false
|
||||
<p className="text-center text-xs">
|
||||
Allow you to practice writing in a variety of formats, from simple paragraphs to complex essays.
|
||||
</p>
|
||||
{!selectedModules.includes("writing") && !disableSelection && (
|
||||
{!selectedModules.includes("writing") && !selectedModules.includes("level") && !disableSelection && (
|
||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full mt-4" />
|
||||
)}
|
||||
{(selectedModules.includes("writing") || disableSelection) && (
|
||||
<BsCheckCircle className="mt-4 text-mti-purple-light w-8 h-8" />
|
||||
)}
|
||||
{selectedModules.includes("level") && <BsXCircle className="mt-4 text-mti-red-light w-8 h-8" />}
|
||||
</div>
|
||||
<div
|
||||
onClick={!disableSelection ? () => toggleModule("speaking") : undefined}
|
||||
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("speaking") : undefined}
|
||||
className={clsx(
|
||||
"relative w-fit max-w-xs flex flex-col items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-2 pt-12 cursor-pointer",
|
||||
"relative w-64 max-w-xs flex flex-col items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-2 pt-12 cursor-pointer",
|
||||
selectedModules.includes("speaking") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||
)}>
|
||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-speaking top-0 -translate-y-1/2">
|
||||
@@ -161,13 +169,37 @@ export default function Selection({user, page, onStart, disableSelection = false
|
||||
<p className="text-center text-xs">
|
||||
You'll have access to interactive dialogs, pronunciation exercises and speech recordings.
|
||||
</p>
|
||||
{!selectedModules.includes("speaking") && !disableSelection && (
|
||||
{!selectedModules.includes("speaking") && !selectedModules.includes("level") && !disableSelection && (
|
||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full mt-4" />
|
||||
)}
|
||||
{(selectedModules.includes("speaking") || disableSelection) && (
|
||||
<BsCheckCircle className="mt-4 text-mti-purple-light w-8 h-8" />
|
||||
)}
|
||||
{selectedModules.includes("level") && <BsXCircle className="mt-4 text-mti-red-light w-8 h-8" />}
|
||||
</div>
|
||||
{!disableSelection && (
|
||||
<div
|
||||
onClick={selectedModules.length === 0 || selectedModules.includes("level") ? () => toggleModule("level") : undefined}
|
||||
className={clsx(
|
||||
"relative w-64 max-w-xs flex flex-col items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-2 pt-12 cursor-pointer",
|
||||
selectedModules.includes("level") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||
)}>
|
||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-level top-0 -translate-y-1/2">
|
||||
<BsClipboard className="text-white w-7 h-7" />
|
||||
</div>
|
||||
<span className="font-semibold">Level:</span>
|
||||
<p className="text-center text-xs">You'll be able to test your english level with multiple choice questions.</p>
|
||||
{!selectedModules.includes("level") && selectedModules.length === 0 && !disableSelection && (
|
||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full mt-4" />
|
||||
)}
|
||||
{(selectedModules.includes("level") || disableSelection) && (
|
||||
<BsCheckCircle className="mt-4 text-mti-purple-light w-8 h-8" />
|
||||
)}
|
||||
{!selectedModules.includes("level") && selectedModules.length > 0 && (
|
||||
<BsXCircle className="mt-4 text-mti-red-light w-8 h-8" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<div className="flex w-full -md:flex-col -md:gap-4 -md:justify-center md:justify-between items-center">
|
||||
<div
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function useUsers() {
|
||||
const getData = () => {
|
||||
setIsLoading(true);
|
||||
axios
|
||||
.get<User[]>("/api/users/list")
|
||||
.get<User[]>("/api/users/list", {headers: {page: "register"}})
|
||||
.then((response) => setUsers(response.data))
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Module} from ".";
|
||||
|
||||
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam;
|
||||
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam;
|
||||
|
||||
export interface ReadingExam {
|
||||
parts: {
|
||||
@@ -17,6 +17,14 @@ export interface ReadingExam {
|
||||
isDiagnostic: boolean;
|
||||
}
|
||||
|
||||
export interface LevelExam {
|
||||
module: "level";
|
||||
id: string;
|
||||
exercises: Exercise[];
|
||||
minTimer: number;
|
||||
isDiagnostic: boolean;
|
||||
}
|
||||
|
||||
export interface ListeningExam {
|
||||
parts: {
|
||||
audio: {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export type Module = "reading" | "listening" | "writing" | "speaking";
|
||||
export type Module = "reading" | "listening" | "writing" | "speaking" | "level";
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface CorporateInformation {
|
||||
value: number;
|
||||
currency: string;
|
||||
};
|
||||
monthlyDuration: number;
|
||||
referralAgent?: string;
|
||||
allowedUserAmount?: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,12 +63,12 @@ export default function BatchCodeGenerator({user}: {user: User}) {
|
||||
}
|
||||
|
||||
if (status === 403) {
|
||||
toast.error(`You do not have permission to generate ${capitalize(type)} codes!`, {toastId: "forbidden"});
|
||||
toast.error(data.reason, {toastId: "forbidden"});
|
||||
}
|
||||
})
|
||||
.catch(({response: {status}}) => {
|
||||
.catch(({response: {status, data}}) => {
|
||||
if (status === 403) {
|
||||
toast.error(`You do not have permission to generate ${capitalize(type)} codes!`, {toastId: "forbidden"});
|
||||
toast.error(data.reason, {toastId: "forbidden"});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ export default function BatchCodeGenerator({user}: {user: User}) {
|
||||
variant="outline"
|
||||
onClick={() => generateCode("corporate")}
|
||||
disabled={emails.length === 0 || isLoading || !PERMISSIONS.generateCode.corporate.includes(user.type)}>
|
||||
Admin
|
||||
Corporate
|
||||
</Button>
|
||||
<Button
|
||||
className="w-44 2xl:w-48"
|
||||
|
||||
@@ -40,12 +40,12 @@ export default function CodeGenerator({user}: {user: User}) {
|
||||
}
|
||||
|
||||
if (status === 403) {
|
||||
toast.error(`You do not have permission to generate a ${capitalize(type)} code!`, {toastId: "forbidden"});
|
||||
toast.error(data.reason, {toastId: "forbidden"});
|
||||
}
|
||||
})
|
||||
.catch(({response: {status}}) => {
|
||||
.catch(({response: {status, data}}) => {
|
||||
if (status === 403) {
|
||||
toast.error(`You do not have permission to generate a ${capitalize(type)} code!`, {toastId: "forbidden"});
|
||||
toast.error(data.reason, {toastId: "forbidden"});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,28 +59,28 @@ export default function CodeGenerator({user}: {user: User}) {
|
||||
{user && (
|
||||
<div className="grid -md:grid-cols-2 md:grid-cols-1 place-items-center 2xl:grid-cols-2 gap-4">
|
||||
<Button
|
||||
className="w-44 2xl:w-48"
|
||||
className="w-44 md:w-48"
|
||||
variant="outline"
|
||||
onClick={() => generateCode("student")}
|
||||
disabled={!PERMISSIONS.generateCode.student.includes(user.type) || (isExpiryDateEnabled && expiryDate === null)}>
|
||||
Student
|
||||
</Button>
|
||||
<Button
|
||||
className="w-44 2xl:w-48"
|
||||
className="w-44 md:w-48"
|
||||
variant="outline"
|
||||
onClick={() => generateCode("teacher")}
|
||||
disabled={!PERMISSIONS.generateCode.teacher.includes(user.type) || (isExpiryDateEnabled && expiryDate === null)}>
|
||||
Teacher
|
||||
</Button>
|
||||
<Button
|
||||
className="w-44 2xl:w-48"
|
||||
className="w-44 md:w-48"
|
||||
variant="outline"
|
||||
onClick={() => generateCode("corporate")}
|
||||
disabled={!PERMISSIONS.generateCode.corporate.includes(user.type) || (isExpiryDateEnabled && expiryDate === null)}>
|
||||
Admin
|
||||
Corporate
|
||||
</Button>
|
||||
<Button
|
||||
className="w-44 2xl:w-48"
|
||||
className="w-44 md:w-48"
|
||||
variant="outline"
|
||||
onClick={() => generateCode("owner")}
|
||||
disabled={!PERMISSIONS.generateCode.owner.includes(user.type) || (isExpiryDateEnabled && expiryDate === null)}>
|
||||
|
||||
50
src/pages/(admin)/ExamGenerator.tsx
Normal file
50
src/pages/(admin)/ExamGenerator.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import Button from "@/components/Low/Button";
|
||||
import Input from "@/components/Low/Input";
|
||||
import {Module} from "@/interfaces";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import {getExamById} from "@/utils/exams";
|
||||
import {MODULE_ARRAY} from "@/utils/moduleUtils";
|
||||
import {RadioGroup} from "@headlessui/react";
|
||||
import axios from "axios";
|
||||
import clsx from "clsx";
|
||||
import {capitalize} from "lodash";
|
||||
import {useRouter} from "next/router";
|
||||
import {FormEvent, useState} from "react";
|
||||
import {toast} from "react-toastify";
|
||||
|
||||
export default function ExamGenerator() {
|
||||
const [selectedModule, setSelectedModule] = useState<Module>();
|
||||
const [examId, setExamId] = useState<string>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const setExams = useExamStore((state) => state.setExams);
|
||||
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const generateExam = (module: Module) => {
|
||||
axios.get(`/api/exam/${module}/generate`).then((result) => console.log(result.data));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 border p-4 border-mti-gray-platinum rounded-xl">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Exam Generator</label>
|
||||
<div className="w-full grid grid-cols-2 gap-2">
|
||||
{MODULE_ARRAY.map((module) => (
|
||||
<Button
|
||||
onClick={() => generateExam(module)}
|
||||
key={module}
|
||||
className={clsx(
|
||||
"w-full min-w-[200px]",
|
||||
module === "reading" && "!bg-ielts-reading/80 !border-ielts-reading hover:!bg-ielts-reading",
|
||||
module === "listening" && "!bg-ielts-listening/80 !border-ielts-listening hover:!bg-ielts-listening",
|
||||
module === "writing" && "!bg-ielts-writing/80 !border-ielts-writing hover:!bg-ielts-writing",
|
||||
module === "speaking" && "!bg-ielts-speaking/80 !border-ielts-speaking hover:!bg-ielts-speaking",
|
||||
)}>
|
||||
{capitalize(module)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -69,8 +69,8 @@ export default function ExamLoader() {
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</RadioGroup>
|
||||
<Input type="text" name="examId" onChange={setExamId} placeholder="Exam ID" className="-md:!w-full md:!w-44 2xl:!w-full" />
|
||||
<Button disabled={!selectedModule || !examId} isLoading={isLoading} className="-md:!w-full md:!w-44 2xl:!w-full">
|
||||
<Input type="text" name="examId" onChange={setExamId} placeholder="Exam ID" className="w-full" />
|
||||
<Button disabled={!selectedModule || !examId} isLoading={isLoading} className="w-full">
|
||||
Load Exam
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -20,6 +20,7 @@ const CLASSES: {[key in Module]: string} = {
|
||||
listening: "text-ielts-listening",
|
||||
speaking: "text-ielts-speaking",
|
||||
writing: "text-ielts-writing",
|
||||
level: "text-ielts-level",
|
||||
};
|
||||
|
||||
const columnHelper = createColumnHelper<Exam>();
|
||||
|
||||
@@ -28,6 +28,16 @@ export default function UserList({user, filter}: {user: User; filter?: (user: Us
|
||||
const {users, reload} = useUsers();
|
||||
const {groups} = useGroups(user ? user.id : undefined);
|
||||
|
||||
const expirationDateColor = (date: Date) => {
|
||||
const momentDate = moment(date);
|
||||
const today = moment(new Date());
|
||||
|
||||
if (today.isAfter(momentDate)) return "!text-mti-red-light font-bold line-through";
|
||||
if (today.add(1, "weeks").isAfter(momentDate)) return "!text-mti-red-light";
|
||||
if (today.add(2, "weeks").isAfter(momentDate)) return "!text-mti-rose-light";
|
||||
if (today.add(1, "months").isAfter(momentDate)) return "!text-mti-orange-light";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user && users) {
|
||||
const filterUsers =
|
||||
@@ -315,7 +325,11 @@ export default function UserList({user, filter}: {user: User; filter?: (user: Us
|
||||
<SorterArrow name="expiryDate" />
|
||||
</button>
|
||||
) as any,
|
||||
cell: (info) => (!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")),
|
||||
cell: (info) => (
|
||||
<span className={clsx(info.getValue() ? expirationDateColor(moment(info.getValue()).toDate()) : "")}>
|
||||
{!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")}
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("isVerified", {
|
||||
header: (
|
||||
@@ -439,6 +453,7 @@ export default function UserList({user, filter}: {user: User; filter?: (user: Us
|
||||
{selectedUser && (
|
||||
<div className="w-full flex flex-col gap-8">
|
||||
<UserCard
|
||||
loggedInUser={user}
|
||||
onClose={(shouldReload) => {
|
||||
setSelectedUser(undefined);
|
||||
if (shouldReload) reload();
|
||||
|
||||
@@ -30,7 +30,6 @@ export default function EmailVerification({user, isLoading, setIsLoading}: Props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider className="max-w-xs lg:max-w-md" />
|
||||
<div className="flex flex-col items-center gap-6 w-full -lg:px-8 lg:w-1/2 relative">
|
||||
<h4 className="font-semibold text-2xl text-mti-purple-light">Please confirm your account!</h4>
|
||||
<span className="text-center">
|
||||
|
||||
@@ -22,6 +22,7 @@ import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation"
|
||||
import {useRouter} from "next/router";
|
||||
import {getExam} from "@/utils/exams";
|
||||
import {capitalize} from "lodash";
|
||||
import Level from "@/exams/Level";
|
||||
|
||||
interface Props {
|
||||
page: "exams" | "exercises";
|
||||
@@ -182,6 +183,11 @@ export default function ExamPage({page}: Props) {
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
level: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
};
|
||||
|
||||
answers.forEach((x) => {
|
||||
@@ -244,6 +250,10 @@ export default function ExamPage({page}: Props) {
|
||||
return <Speaking exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
|
||||
}
|
||||
|
||||
if (exam && exam.module === "level") {
|
||||
return <Level exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
|
||||
}
|
||||
|
||||
return <>Loading...</>;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Button from "@/components/Low/Button";
|
||||
import Input from "@/components/Low/Input";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
import {User} from "@/interfaces/user";
|
||||
import {sendEmailVerification} from "@/utils/email";
|
||||
import axios from "axios";
|
||||
@@ -7,6 +8,8 @@ import {Divider} from "primereact/divider";
|
||||
import {useState} from "react";
|
||||
import {toast} from "react-toastify";
|
||||
import {KeyedMutator} from "swr";
|
||||
import Select from "react-select";
|
||||
import moment from "moment";
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean;
|
||||
@@ -15,14 +18,25 @@ interface Props {
|
||||
sendEmailVerification: typeof sendEmailVerification;
|
||||
}
|
||||
|
||||
const availableDurations = {
|
||||
"1_month": {label: "1 Month", number: 1},
|
||||
"3_months": {label: "3 Months", number: 3},
|
||||
"6_months": {label: "6 Months", number: 6},
|
||||
"12_months": {label: "12 Months", number: 12},
|
||||
};
|
||||
|
||||
export default function RegisterCorporate({isLoading, setIsLoading, mutateUser, sendEmailVerification}: Props) {
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [referralAgent, setReferralAgent] = useState<string | undefined>();
|
||||
|
||||
const [companyName, setCompanyName] = useState("");
|
||||
const [companyUsers, setCompanyUsers] = useState(0);
|
||||
const [subscriptionDuration, setSubscriptionDuration] = useState(1);
|
||||
|
||||
const {users} = useUsers();
|
||||
|
||||
const onSuccess = () => toast.success("An e-mail has been sent, please make sure to check your spam folder!");
|
||||
|
||||
@@ -47,12 +61,15 @@ export default function RegisterCorporate({isLoading, setIsLoading, mutateUser,
|
||||
password,
|
||||
type: "corporate",
|
||||
profilePicture: "/defaultAvatar.png",
|
||||
subscriptionExpirationDate: moment().add(1, "days").add(subscriptionDuration, "months").toISOString(),
|
||||
corporateInformation: {
|
||||
companyInformation: {
|
||||
name: companyName,
|
||||
userAmount: companyUsers,
|
||||
},
|
||||
referralAgent,
|
||||
allowedUserAmount: companyUsers,
|
||||
monthlyDuration: subscriptionDuration,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
@@ -78,8 +95,11 @@ export default function RegisterCorporate({isLoading, setIsLoading, mutateUser,
|
||||
|
||||
return (
|
||||
<form className="flex flex-col items-center gap-4 w-full" onSubmit={register}>
|
||||
<Input type="text" name="name" onChange={(e) => setName(e)} placeholder="Enter your name" defaultValue={name} required />
|
||||
<Input type="email" name="email" onChange={(e) => setEmail(e)} placeholder="Enter email address" defaultValue={email} required />
|
||||
<div className="w-full flex gap-4">
|
||||
<Input type="text" name="name" onChange={(e) => setName(e)} placeholder="Enter your name" defaultValue={name} required />
|
||||
<Input type="email" name="email" onChange={(e) => setEmail(e)} placeholder="Enter email address" defaultValue={email} required />
|
||||
</div>
|
||||
|
||||
<div className="w-full flex gap-4">
|
||||
<Input
|
||||
type="password"
|
||||
@@ -101,22 +121,87 @@ export default function RegisterCorporate({isLoading, setIsLoading, mutateUser,
|
||||
|
||||
<Divider className="w-full !my-2" />
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
name="companyName"
|
||||
onChange={(e) => setCompanyName(e)}
|
||||
placeholder="Institution name"
|
||||
defaultValue={companyName}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
name="companyUsers"
|
||||
onChange={(e) => setCompanyUsers(parseInt(e))}
|
||||
placeholder="Institution name"
|
||||
defaultValue={companyUsers}
|
||||
required
|
||||
/>
|
||||
<div className="w-full flex gap-4">
|
||||
<Input
|
||||
type="text"
|
||||
name="companyName"
|
||||
onChange={(e) => setCompanyName(e)}
|
||||
placeholder="Institution name"
|
||||
label="Institution name"
|
||||
defaultValue={companyName}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
name="companyUsers"
|
||||
onChange={(e) => setCompanyUsers(parseInt(e))}
|
||||
label="Amount of users"
|
||||
defaultValue={companyUsers}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex gap-4">
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Referral</label>
|
||||
<Select
|
||||
className="px-4 py-4 w-full 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"
|
||||
options={[
|
||||
{value: "", label: "No referral"},
|
||||
...users.filter((u) => u.type === "agent").map((x) => ({value: x.id, label: `${x.name} - ${x.email}`})),
|
||||
]}
|
||||
defaultValue={{value: "", label: "No referral"}}
|
||||
onChange={(value) => setReferralAgent(value?.value)}
|
||||
styles={{
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: "4px",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
":focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
color: state.isFocused ? "black" : styles.color,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Subscription Duration</label>
|
||||
<Select
|
||||
className="px-4 py-4 w-full 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"
|
||||
options={Object.keys(availableDurations).map((value) => ({
|
||||
value,
|
||||
label: availableDurations[value as keyof typeof availableDurations].label,
|
||||
}))}
|
||||
defaultValue={{value: "1_month", label: availableDurations["1_month"].label}}
|
||||
onChange={(value) =>
|
||||
setSubscriptionDuration(value ? availableDurations[value.value as keyof typeof availableDurations].number : 1)
|
||||
}
|
||||
styles={{
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: "4px",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
":focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
color: state.isFocused ? "black" : styles.color,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="lg:mt-8 w-full"
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function Reset({code, mode, apiKey, continueUrl}: {code: string;
|
||||
if (response.data.ok) {
|
||||
toast.success("Your account has been verified!", {toastId: "verify-successful"});
|
||||
setTimeout(() => {
|
||||
router.push("/");
|
||||
router.reload();
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import clsx from "clsx";
|
||||
import Lists from "./(admin)/Lists";
|
||||
import BatchCodeGenerator from "./(admin)/BatchCodeGenerator";
|
||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||
import ExamGenerator from "./(admin)/ExamGenerator";
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||
const user = req.session.user;
|
||||
@@ -61,7 +62,7 @@ export default function Admin() {
|
||||
{user && (
|
||||
<Layout user={user} className="gap-6">
|
||||
<section className="w-full flex -md:flex-col -xl:gap-2 gap-8 justify-between">
|
||||
<ExamLoader />
|
||||
{user.email === "tiago.ribeiro@ecrop.dev" ? <ExamGenerator /> : <ExamLoader />}
|
||||
<CodeGenerator user={user} />
|
||||
<BatchCodeGenerator user={user} />
|
||||
</section>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import {app} from "@/firebase";
|
||||
import {getFirestore, setDoc, doc} from "firebase/firestore";
|
||||
import {getFirestore, setDoc, doc, query, collection, where, getDocs} from "firebase/firestore";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {Type} from "@/interfaces/user";
|
||||
@@ -15,7 +15,7 @@ export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ok: false});
|
||||
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -23,10 +23,26 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const permission = PERMISSIONS.generateCode[type];
|
||||
|
||||
if (!permission.includes(req.session.user.type)) {
|
||||
res.status(403).json({ok: false});
|
||||
res.status(403).json({ok: false, reason: "Your account type does not have permissions to generate a code for that type of user!"});
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.session.user.type === "corporate") {
|
||||
const codesGeneratedByUserSnapshot = await getDocs(query(collection(db, "codes"), where("creator", "==", req.session.user.id)));
|
||||
const totalCodes = codesGeneratedByUserSnapshot.docs.length + codes.length;
|
||||
const allowedCodes = req.session.user.corporateInformation?.companyInformation.userAmount || 0;
|
||||
|
||||
if (totalCodes > allowedCodes) {
|
||||
res.status(403).json({
|
||||
ok: false,
|
||||
reason: `You have or would have exceeded your amount of allowed codes, you currently are allowed to generate ${
|
||||
allowedCodes - codesGeneratedByUserSnapshot.docs.length
|
||||
} codes.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const codePromises = codes.map(async (code, index) => {
|
||||
const codeRef = doc(db, "codes", code);
|
||||
await setDoc(codeRef, {type, code, creator: req.session.user!.id, expiryDate});
|
||||
|
||||
41
src/pages/api/exam/[module]/generate.tsx
Normal file
41
src/pages/api/exam/[module]/generate.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import {app} from "@/firebase";
|
||||
import {getFirestore, collection, getDocs, query, where} from "firebase/firestore";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {shuffle} from "lodash";
|
||||
import {Exam} from "@/interfaces/exam";
|
||||
import {Stat} from "@/interfaces/user";
|
||||
import {Module} from "@/interfaces";
|
||||
import axios from "axios";
|
||||
|
||||
const db = getFirestore(app);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.session.user.type !== "developer") {
|
||||
res.status(403).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
const {module} = req.query as {module: Module};
|
||||
|
||||
switch (module) {
|
||||
case "reading":
|
||||
const result = await axios.get(
|
||||
`${process.env.BACKEND_URL}/reading_passage_1?topic=football manager video game&exercises=multipleChoice&exercises=trueFalse&exercises=fillBlanks&exercises=writeBlanks`,
|
||||
{headers: {Authorization: `Bearer ${process.env.BACKEND_JWT}`}},
|
||||
);
|
||||
res.status(200).json(result.data);
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({ok: true});
|
||||
}
|
||||
@@ -18,7 +18,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleExamsPromises = MODULE_ARRAY.map(async (module) => {
|
||||
const moduleExamsPromises = [...MODULE_ARRAY, "level"].map(async (module) => {
|
||||
const moduleRef = collection(db, module);
|
||||
|
||||
const q = query(moduleRef, where("isDiagnostic", "==", false));
|
||||
|
||||
@@ -104,7 +104,7 @@ async function registerCorporate(req: NextApiRequest, res: NextApiResponse) {
|
||||
isFirstLogin: false,
|
||||
focus: "academic",
|
||||
type: "corporate",
|
||||
subscriptionExpirationDate: null,
|
||||
subscriptionExpirationDate: req.body.subscriptionExpirationDate || null,
|
||||
status: "paymentDue",
|
||||
registrationDate: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -49,6 +49,10 @@ async function update(req: NextApiRequest, res: NextApiResponse) {
|
||||
correct: 0,
|
||||
total: 0,
|
||||
},
|
||||
level: {
|
||||
correct: 0,
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
|
||||
MODULES.forEach((module: Module) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ const db = getFirestore(app);
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
if (!req.session.user && !req.headers["page"] && req.headers["page"] !== "register") {
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,8 +84,8 @@ export default function Login() {
|
||||
<img src="/people-talking-tablet.png" alt="People smiling looking at a tablet" className="h-full aspect-auto" />
|
||||
</section>
|
||||
<section className="h-full w-full flex flex-col items-center justify-center gap-2">
|
||||
<div className="flex flex-col gap-2 items-center relative">
|
||||
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-36 lg:w-64 absolute -top-36 lg:-top-64" />
|
||||
<div className={clsx("flex flex-col items-center", !user && "mb-4")}>
|
||||
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-36 lg:w-56" />
|
||||
<h1 className="font-bold text-2xl lg:text-4xl">Login to your account</h1>
|
||||
<p className="self-start text-sm lg:text-base font-normal text-mti-gray-cool">with your registered Email Address</p>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {sortByModule} from "@/utils/moduleUtils";
|
||||
import Layout from "@/components/High/Layout";
|
||||
import clsx from "clsx";
|
||||
import {calculateBandScore} from "@/utils/score";
|
||||
import {BsBook, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
|
||||
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
|
||||
import Select from "react-select";
|
||||
import useGroups from "@/hooks/useGroups";
|
||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||
@@ -139,6 +139,11 @@ export default function History({user}: {user: User}) {
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
level: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
};
|
||||
|
||||
stats.forEach((x) => {
|
||||
@@ -224,11 +229,13 @@ export default function History({user}: {user: User}) {
|
||||
module === "listening" && "bg-ielts-listening",
|
||||
module === "writing" && "bg-ielts-writing",
|
||||
module === "speaking" && "bg-ielts-speaking",
|
||||
module === "level" && "bg-ielts-level",
|
||||
)}>
|
||||
{module === "reading" && <BsBook className="w-4 h-4" />}
|
||||
{module === "listening" && <BsHeadphones className="w-4 h-4" />}
|
||||
{module === "writing" && <BsPen className="w-4 h-4" />}
|
||||
{module === "speaking" && <BsMegaphone className="w-4 h-4" />}
|
||||
{module === "level" && <BsClipboard className="w-4 h-4" />}
|
||||
<span className="text-sm">{level.toFixed(1)}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -10,6 +10,7 @@ import RegisterIndividual from "./(register)/RegisterIndividual";
|
||||
import RegisterCorporate from "./(register)/RegisterCorporate";
|
||||
import EmailVerification from "./(auth)/EmailVerification";
|
||||
import {sendEmailVerification} from "@/utils/email";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
|
||||
export const getServerSideProps = (context: any) => {
|
||||
const {code} = context.query;
|
||||
@@ -35,7 +36,7 @@ export default function Register({code: queryCode}: {code: string}) {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<main className="w-full h-[100vh] flex bg-white text-black">
|
||||
<main className="w-full min-h-[100vh] h-full flex bg-white text-black">
|
||||
<ToastContainer />
|
||||
<section className="h-full w-fit min-w-fit relative hidden lg:flex">
|
||||
<div className="absolute h-full w-full bg-mti-rose-light z-10 bg-opacity-50" />
|
||||
@@ -48,7 +49,7 @@ export default function Register({code: queryCode}: {code: string}) {
|
||||
</div>
|
||||
{!user && (
|
||||
<>
|
||||
<div className="flex flex-col gap-6 w-full -lg:px-8 lg:w-2/3">
|
||||
<div className="flex flex-col gap-6 w-full -lg:px-8 lg:w-3/4">
|
||||
<Tab.Group>
|
||||
<Tab.List className="flex space-x-1 rounded-xl bg-mti-purple-ultralight/40 p-1">
|
||||
<Tab
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import {Module} from "@/interfaces";
|
||||
|
||||
export const OPAQUE: {[key in Module]: string} = {
|
||||
reading: "#FF6384",
|
||||
listening: "#36A2EB",
|
||||
writing: "#FFCE56",
|
||||
speaking: "#4bc0c0",
|
||||
};
|
||||
|
||||
export const SEMI_TRANSPARENT: {[key in Module]: string} = {
|
||||
reading: "rgba(255, 99, 132, 0.5)",
|
||||
listening: "rgba(54, 162, 235, 0.5)",
|
||||
writing: "rgba(255, 206, 86, 0.5)",
|
||||
speaking: "rgba(75, 192, 192, 0.5)",
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import {Module} from "@/interfaces";
|
||||
import {mdiAccountVoice, mdiBookOpen, mdiHeadphones, mdiPen} from "@mdi/js";
|
||||
|
||||
export const ICONS: {[key in Module]: string} = {
|
||||
listening: mdiHeadphones,
|
||||
reading: mdiBookOpen,
|
||||
speaking: mdiAccountVoice,
|
||||
writing: mdiPen,
|
||||
};
|
||||
@@ -1,15 +1,5 @@
|
||||
import {Module} from "@/interfaces";
|
||||
import {
|
||||
Exam,
|
||||
ReadingExam,
|
||||
ListeningExam,
|
||||
WritingExam,
|
||||
SpeakingExam,
|
||||
Exercise,
|
||||
UserSolution,
|
||||
FillBlanksExercise,
|
||||
MatchSentencesExercise,
|
||||
} from "@/interfaces/exam";
|
||||
import {Exam, ReadingExam, ListeningExam, WritingExam, SpeakingExam, Exercise, UserSolution, LevelExam} from "@/interfaces/exam";
|
||||
import axios from "axios";
|
||||
|
||||
export const getExam = async (module: Module, avoidRepeated: boolean): Promise<Exam | undefined> => {
|
||||
@@ -29,6 +19,8 @@ export const getExam = async (module: Module, avoidRepeated: boolean): Promise<E
|
||||
return newExam.shift() as WritingExam;
|
||||
case "speaking":
|
||||
return newExam.shift() as SpeakingExam;
|
||||
case "level":
|
||||
return newExam.shift() as LevelExam;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -49,6 +41,8 @@ export const getExamById = async (module: Module, id: string): Promise<Exam | un
|
||||
return newExam as WritingExam;
|
||||
case "speaking":
|
||||
return newExam as SpeakingExam;
|
||||
case "level":
|
||||
return newExam as LevelExam;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export const moduleLabels: {[key in Module]: string} = {
|
||||
reading: "Reading",
|
||||
speaking: "Speaking",
|
||||
writing: "Writing",
|
||||
level: "Level",
|
||||
};
|
||||
|
||||
export const sortByModule = (a: {module: Module}, b: {module: Module}) => {
|
||||
|
||||
@@ -93,6 +93,14 @@ const academicMarking: {[key: number]: number} = {
|
||||
10: 2.5,
|
||||
};
|
||||
|
||||
const levelMarking: {[key: number]: number} = {
|
||||
88: 9,
|
||||
64: 8,
|
||||
52: 6,
|
||||
32: 4,
|
||||
16: 2,
|
||||
};
|
||||
|
||||
const moduleMarkings: {[key in Module]: {[key in Type]: {[key: number]: number}}} = {
|
||||
reading: {
|
||||
academic: academicMarking,
|
||||
@@ -110,6 +118,10 @@ const moduleMarkings: {[key in Module]: {[key in Type]: {[key: number]: number}}
|
||||
academic: writingMarking,
|
||||
general: writingMarking,
|
||||
},
|
||||
level: {
|
||||
academic: levelMarking,
|
||||
general: levelMarking,
|
||||
},
|
||||
};
|
||||
|
||||
export const calculateBandScore = (correct: number, total: number, module: Module, type: Type) => {
|
||||
|
||||
@@ -29,6 +29,7 @@ module.exports = {
|
||||
listening: {DEFAULT: "#FF790A", light: "#FFF1E5", transparent: "rgba(54, 162, 235, 0.5)"},
|
||||
writing: {DEFAULT: "#3D9F11", light: "#E8FCDF", transparent: "rgba(255, 206, 86, 0.5)"},
|
||||
speaking: {DEFAULT: "#EF5DA8", light: "#FEF6FA", transparent: "rgba(75, 192, 192, 0.5)"},
|
||||
level: {DEFAULT: "#414288", light: "#C8C8E4", transparent: "rgba(65, 66, 136, 0.5)"},
|
||||
},
|
||||
},
|
||||
screens: {
|
||||
|
||||
Reference in New Issue
Block a user