Added a new module called Level for level testing
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -13,7 +13,7 @@ import clsx from "clsx";
|
||||
import {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>
|
||||
))}
|
||||
@@ -239,11 +246,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" />}
|
||||
{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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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...</>;
|
||||
};
|
||||
|
||||
|
||||
@@ -49,6 +49,10 @@ async function update(req: NextApiRequest, res: NextApiResponse) {
|
||||
correct: 0,
|
||||
total: 0,
|
||||
},
|
||||
level: {
|
||||
correct: 0,
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
|
||||
MODULES.forEach((module: Module) => {
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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