Added a new module called Level for level testing

This commit is contained in:
Tiago Ribeiro
2023-11-17 15:32:45 +00:00
parent 4a51bd7dfa
commit 44a89c6645
22 changed files with 290 additions and 99 deletions

View File

@@ -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 (

View File

@@ -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 (

View File

@@ -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}}}} />;
}

View File

@@ -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 (
<>

View File

@@ -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>
)}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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
View 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>
</>
);
}

View File

@@ -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&apos;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&apos;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

View File

@@ -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: {

View File

@@ -1 +1 @@
export type Module = "reading" | "listening" | "writing" | "speaking";
export type Module = "reading" | "listening" | "writing" | "speaking" | "level";

View File

@@ -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>();

View File

@@ -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...</>;
};

View File

@@ -49,6 +49,10 @@ async function update(req: NextApiRequest, res: NextApiResponse) {
correct: 0,
total: 0,
},
level: {
correct: 0,
total: 0,
},
};
MODULES.forEach((module: Module) => {

View File

@@ -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>
))}

View File

@@ -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)",
};

View File

@@ -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,
};

View File

@@ -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;
}
};

View File

@@ -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}) => {

View File

@@ -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) => {