Updated the Finish screen along with other tweaks
This commit is contained in:
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"arcanis.vscode-zipfs",
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
"axios": "^1.3.5",
|
||||
"chart.js": "^4.2.1",
|
||||
"clsx": "^1.2.1",
|
||||
"daisyui": "^2.50.0",
|
||||
"daisyui": "^3.1.5",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-config-next": "13.1.6",
|
||||
"firebase": "9.19.1",
|
||||
|
||||
@@ -88,8 +88,9 @@ export default function FillBlanks({
|
||||
const calculateScore = () => {
|
||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||
const correct = answers.filter((x) => solutions.find((y) => x.id === y.id)?.solution === x.solution.toLowerCase() || false).length;
|
||||
const missing = total - answers.filter((x) => solutions.find((y) => x.id === y.id)).length;
|
||||
|
||||
return {total, correct};
|
||||
return {total, correct, missing};
|
||||
};
|
||||
|
||||
const renderLines = (line: string) => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import clsx from "clsx";
|
||||
import {Fragment, useState} from "react";
|
||||
import LineTo from "react-lineto";
|
||||
import {CommonProps} from ".";
|
||||
import Button from "../Low/Button";
|
||||
|
||||
export default function MatchSentences({id, options, type, prompt, sentences, userSolutions, onNext, onBack}: MatchSentencesExercise & CommonProps) {
|
||||
const [selectedQuestion, setSelectedQuestion] = useState<string>();
|
||||
@@ -14,8 +15,9 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
|
||||
const calculateScore = () => {
|
||||
const total = sentences.length;
|
||||
const correct = answers.filter((x) => sentences.find((y) => y.id === x.question)?.solution === x.option || false).length;
|
||||
const missing = total - answers.filter((x) => sentences.find((y) => y.id === x.question)).length;
|
||||
|
||||
return {total, correct};
|
||||
return {total, correct, missing};
|
||||
};
|
||||
|
||||
const selectOption = (option: string) => {
|
||||
@@ -92,23 +94,21 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="self-end flex flex-col-reverse items-center w-full md:justify-between md:items-start md:flex-row gap-8">
|
||||
<button
|
||||
className={clsx("btn btn-wide gap-4 relative text-white", errorButtonStyle)}
|
||||
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}>
|
||||
<div className="absolute left-4">
|
||||
<Icon path={mdiArrowLeft} color="white" size={1} />
|
||||
</div>
|
||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||
<Button
|
||||
color="green"
|
||||
variant="outline"
|
||||
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}
|
||||
className="max-w-[200px] w-full">
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)}
|
||||
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="green"
|
||||
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
<div className="absolute right-4">
|
||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||
</div>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -59,8 +59,9 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
|
||||
const calculateScore = () => {
|
||||
const total = questions.length;
|
||||
const correct = answers.filter((x) => questions.find((y) => y.id === x.question)?.solution === x.option || false).length;
|
||||
const missing = total - answers.filter((x) => questions.find((y) => y.id === x.question)).length;
|
||||
|
||||
return {total, correct};
|
||||
return {total, correct, missing};
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
|
||||
@@ -180,14 +180,14 @@ export default function Speaking({id, title, text, type, prompts, onNext, onBack
|
||||
<Button
|
||||
color="green"
|
||||
variant="outline"
|
||||
onClick={() => onBack({exercise: id, solutions: [], score: {correct: 1, total: 1}, type})}
|
||||
onClick={() => onBack({exercise: id, solutions: [], score: {correct: 1, total: 1, missing: 0}, type})}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
color="green"
|
||||
disabled={!mediaBlob}
|
||||
onClick={() => onNext({exercise: id, solutions: [], score: {correct: 1, total: 1}, type})}
|
||||
onClick={() => onNext({exercise: id, solutions: [], score: {correct: 1, total: 1, missing: 0}, type})}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
</Button>
|
||||
|
||||
@@ -57,8 +57,9 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user
|
||||
?.solution.map((y) => y.toLowerCase())
|
||||
.includes(x.solution.toLowerCase()) || false,
|
||||
).length;
|
||||
const missing = total - answers.filter((x) => solutions.find((y) => x.id === y.id)).length;
|
||||
|
||||
return {total, correct};
|
||||
return {total, correct, missing};
|
||||
};
|
||||
|
||||
const renderLines = (line: string) => {
|
||||
|
||||
@@ -58,14 +58,14 @@ export default function Writing({id, prompt, info, type, wordCounter, attachment
|
||||
<Button
|
||||
color="green"
|
||||
variant="outline"
|
||||
onClick={() => onBack({exercise: id, solutions: [inputText], score: {correct: 1, total: 1}, type})}
|
||||
onClick={() => onBack({exercise: id, solutions: [inputText], score: {correct: 1, total: 1, missing: 0}, type})}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
color="green"
|
||||
disabled={!isSubmitEnabled}
|
||||
onClick={() => onNext({exercise: id, solutions: [inputText], score: {correct: 1, total: 1}, type})}
|
||||
onClick={() => onNext({exercise: id, solutions: [inputText], score: {correct: 1, total: 1, missing: 0}, type})}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
</Button>
|
||||
|
||||
@@ -9,18 +9,21 @@ interface Props {
|
||||
module: Module;
|
||||
exerciseIndex: number;
|
||||
totalExercises: number;
|
||||
disableTimer?: boolean;
|
||||
}
|
||||
|
||||
export default function ModuleTitle({minTimer, module, exerciseIndex, totalExercises}: Props) {
|
||||
export default function ModuleTitle({minTimer, module, exerciseIndex, totalExercises, disableTimer = false}: Props) {
|
||||
const [timer, setTimer] = useState(minTimer * 60);
|
||||
|
||||
useEffect(() => {
|
||||
const timerInterval = setInterval(() => setTimer((prev) => prev - 1), 1000);
|
||||
if (!disableTimer) {
|
||||
const timerInterval = setInterval(() => setTimer((prev) => prev - 1), 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(timerInterval);
|
||||
};
|
||||
}, [minTimer]);
|
||||
return () => {
|
||||
clearInterval(timerInterval);
|
||||
};
|
||||
}
|
||||
}, [disableTimer, minTimer]);
|
||||
|
||||
const moduleIcon: {[key in Module]: ReactNode} = {
|
||||
reading: <BsBook className="text-ielts-reading w-6 h-6" />,
|
||||
@@ -57,7 +60,7 @@ export default function ModuleTitle({minTimer, module, exerciseIndex, totalExerc
|
||||
Question {exerciseIndex}/{totalExercises}
|
||||
</span>
|
||||
</div>
|
||||
<ProgressBar color={module} label="" percentage={((exerciseIndex - 1) * 100) / totalExercises} className="h-2 w-full" />
|
||||
<ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
||||
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import {Fragment} from "react";
|
||||
import Button from "../Low/Button";
|
||||
|
||||
export default function MatchSentencesSolutions({options, prompt, sentences, userSolutions, onNext, onBack}: MatchSentencesExercise & CommonProps) {
|
||||
return (
|
||||
@@ -63,19 +64,14 @@ export default function MatchSentencesSolutions({options, prompt, sentences, use
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="self-end flex flex-col-reverse items-center w-full md:justify-between md:items-start md:flex-row gap-8">
|
||||
<button className={clsx("btn btn-wide gap-4 relative text-white", errorButtonStyle)} onClick={onBack}>
|
||||
<div className="absolute left-4">
|
||||
<Icon path={mdiArrowLeft} color="white" size={1} />
|
||||
</div>
|
||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||
<Button color="green" variant="outline" onClick={() => onBack()} className="max-w-[200px] w-full">
|
||||
Back
|
||||
</button>
|
||||
<button className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)} onClick={onNext}>
|
||||
</Button>
|
||||
|
||||
<Button color="green" onClick={() => onNext()} className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
<div className="absolute right-4">
|
||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||
</div>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,92 +1,199 @@
|
||||
import ProfileLevel from "@/components/ProfileLevel";
|
||||
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
||||
import Button from "@/components/Low/Button";
|
||||
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
||||
import {Module} from "@/interfaces";
|
||||
import {User} from "@/interfaces/user";
|
||||
import {ICONS} from "@/resources/modules";
|
||||
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import {calculateBandScore} from "@/utils/score";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import router from "next/router";
|
||||
import {useRouter} from "next/router";
|
||||
import {useEffect, useState} from "react";
|
||||
import {BsArrowCounterclockwise, BsBook, BsEyeFill, BsHeadphones, BsMegaphone, BsPen, BsShareFill} from "react-icons/bs";
|
||||
|
||||
interface Score {
|
||||
module: Module;
|
||||
correct: number;
|
||||
total: number;
|
||||
missing: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
modules: Module[];
|
||||
scores: {
|
||||
module?: Module;
|
||||
correct: number;
|
||||
total: number;
|
||||
}[];
|
||||
scores: Score[];
|
||||
onViewResults: () => void;
|
||||
}
|
||||
|
||||
export default function Finish({user, scores, modules, onViewResults}: Props) {
|
||||
const renderModuleScore = (module: Module) => {
|
||||
const moduleScores = scores.filter((x) => x.module === module);
|
||||
if (moduleScores.length === 0) {
|
||||
return <>0</>;
|
||||
}
|
||||
const [selectedModule, setSelectedModule] = useState(modules[0]);
|
||||
const [selectedScore, setSelectedScore] = useState<Score>(scores.find((x) => x.module === modules[0])!);
|
||||
|
||||
return moduleScores.map((x, index) => (
|
||||
<span key={index}>
|
||||
{x.correct} / {x.total}
|
||||
</span>
|
||||
));
|
||||
};
|
||||
const exams = useExamStore((state) => state.exams);
|
||||
|
||||
const renderModuleTotal = (module: Module) => {
|
||||
const moduleScores = scores.filter((x) => x.module === module);
|
||||
if (moduleScores.length === 0) {
|
||||
return <>0</>;
|
||||
}
|
||||
useEffect(() => setSelectedScore(scores.find((x) => x.module === selectedModule)!), [scores, selectedModule]);
|
||||
|
||||
const total = moduleScores.reduce((accumulator, current) => accumulator + current.total, 0);
|
||||
const correct = moduleScores.reduce((accumulator, current) => accumulator + current.correct, 0);
|
||||
|
||||
return (
|
||||
<span>
|
||||
{correct} / {total} | {Math.floor((correct / total) * 100)}%
|
||||
</span>
|
||||
);
|
||||
const moduleColors: {[key in Module]: {progress: string; inner: string}} = {
|
||||
reading: {
|
||||
progress: "text-ielts-reading",
|
||||
inner: "bg-ielts-reading-light",
|
||||
},
|
||||
listening: {
|
||||
progress: "text-ielts-listening",
|
||||
inner: "bg-ielts-listening-light",
|
||||
},
|
||||
writing: {
|
||||
progress: "text-ielts-writing",
|
||||
inner: "bg-ielts-writing-light",
|
||||
},
|
||||
speaking: {
|
||||
progress: "text-ielts-speaking",
|
||||
inner: "bg-ielts-speaking-light",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full relative">
|
||||
<section className="h-full w-full flex flex-col items-center justify-center gap-4">
|
||||
<ProfileLevel user={user} className="h-1/2" />
|
||||
<div className="h-2/3 w-2/3 flex flex-col items-center gap-4">
|
||||
<div className="rounded-xl p-2 md:p-4 items-center flex lg:justify-center gap-2 md:gap-4 max-w-[100%] overflow-auto">
|
||||
{modules.map((module) => (
|
||||
<>
|
||||
<div className="w-full min-h-full h-fit flex flex-col items-center justify-between gap-8">
|
||||
<ModuleTitle
|
||||
module={selectedModule}
|
||||
totalExercises={exams.find((x) => x.module === selectedModule)!.exercises.length}
|
||||
exerciseIndex={exams.find((x) => x.module === selectedModule)!.exercises.length}
|
||||
minTimer={exams.find((x) => x.module === selectedModule)!.minTimer}
|
||||
disableTimer
|
||||
/>
|
||||
<div className="flex gap-4 self-start">
|
||||
{modules.includes("reading") && (
|
||||
<div
|
||||
onClick={() => setSelectedModule("reading")}
|
||||
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-reading hover:text-white",
|
||||
selectedModule === "reading" ? "bg-ielts-reading text-white" : "bg-mti-gray-smoke text-ielts-reading",
|
||||
)}>
|
||||
<BsBook className="w-6 h-6" />
|
||||
<span className="font-semibold">Reading</span>
|
||||
</div>
|
||||
)}
|
||||
{modules.includes("listening") && (
|
||||
<div
|
||||
onClick={() => setSelectedModule("listening")}
|
||||
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-listening hover:text-white",
|
||||
selectedModule === "listening" ? "bg-ielts-listening text-white" : "bg-mti-gray-smoke text-ielts-listening",
|
||||
)}>
|
||||
<BsHeadphones className="w-6 h-6" />
|
||||
<span className="font-semibold">Listening</span>
|
||||
</div>
|
||||
)}
|
||||
{modules.includes("writing") && (
|
||||
<div
|
||||
onClick={() => setSelectedModule("writing")}
|
||||
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-writing hover:text-white",
|
||||
selectedModule === "writing" ? "bg-ielts-writing text-white" : "bg-mti-gray-smoke text-ielts-writing",
|
||||
)}>
|
||||
<BsPen className="w-6 h-6" />
|
||||
<span className="font-semibold">Writing</span>
|
||||
</div>
|
||||
)}
|
||||
{modules.includes("speaking") && (
|
||||
<div
|
||||
onClick={() => setSelectedModule("speaking")}
|
||||
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-speaking hover:text-white",
|
||||
selectedModule === "speaking" ? "bg-ielts-speaking text-white" : "bg-mti-gray-smoke text-ielts-speaking",
|
||||
)}>
|
||||
<BsMegaphone className="w-6 h-6" />
|
||||
<span className="font-semibold">Speaking</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full flex gap-9 mt-32 items-center justify-between">
|
||||
<span className="max-w-3xl">
|
||||
Congratulations on your exam performance! You achieved an impressive{" "}
|
||||
<span className="font-bold">
|
||||
level {calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus)}
|
||||
</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. Please contact us for further information.
|
||||
<br />
|
||||
<br />
|
||||
Congratulations again on your outstanding achievement! We are here to support you on your academic journey.
|
||||
</span>
|
||||
<div className="flex gap-9 px-16">
|
||||
<div
|
||||
className={clsx("radial-progress overflow-hidden", moduleColors[selectedModule].progress)}
|
||||
style={{"--value": (selectedScore.correct / selectedScore.total) * 100, "--thickness": "12px", "--size": "13rem"} as any}>
|
||||
<div
|
||||
className={`flex flex-col gap-12 min-w-[176px] items-center justify-center border-2 border-ielts-${module} bg-ielts-${module}-transparent py-4 px-2 rounded-xl text-white font-semibold`}
|
||||
key={module}>
|
||||
<div className="flex flex-col items-center">
|
||||
<Icon path={ICONS[module]} color="white" size={2} />
|
||||
<span>{module.toUpperCase()}</span>
|
||||
</div>
|
||||
<div className="flex flex-col">{renderModuleScore(module)}</div>
|
||||
<div>TOTAL: {renderModuleTotal(module)}</div>
|
||||
className={clsx(
|
||||
"w-48 h-48 rounded-full flex flex-col items-center justify-center",
|
||||
moduleColors[selectedModule].inner,
|
||||
)}>
|
||||
<span className="text-xl">Level</span>
|
||||
<span className="text-3xl font-bold">
|
||||
{calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-full flex flex-col gap-4 lg:gap-0 md:flex-row justify-center items-center lg:justify-between">
|
||||
<Link href="/">
|
||||
<button className={clsx("btn btn-wide gap-4 relative text-white", errorButtonStyle)}>
|
||||
<div className="absolute left-4">
|
||||
<Icon path={mdiArrowLeft} color="white" size={1} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex gap-2">
|
||||
<div className="w-3 h-3 bg-mti-blue-light rounded-full mt-1" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-mti-blue-light">
|
||||
{(((selectedScore.total - selectedScore.missing) / selectedScore.total) * 100).toFixed(0)}%
|
||||
</span>
|
||||
<span className="text-lg">Completion</span>
|
||||
</div>
|
||||
Go Home
|
||||
</button>
|
||||
</Link>
|
||||
<button className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)} onClick={onViewResults}>
|
||||
View Solutions
|
||||
<div className="absolute right-4">
|
||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||
</div>
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
<div className="w-3 h-3 bg-mti-green-light rounded-full mt-1" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-mti-green-light">{selectedScore.total.toString().padStart(2, "0")}</span>
|
||||
<span className="text-lg">Correct</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="w-3 h-3 bg-mti-orange-light rounded-full mt-1" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-mti-orange-light">
|
||||
{(selectedScore.total - selectedScore.correct).toString().padStart(2, "0")}
|
||||
</span>
|
||||
<span className="text-lg">Wrong</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||
<div className="flex gap-8">
|
||||
<div className="w-fit flex flex-col items-center gap-1 cursor-pointer">
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="w-11 h-11 rounded-full bg-mti-green-light hover:bg-mti-green flex items-center justify-center transition duration-300 ease-in-out">
|
||||
<BsArrowCounterclockwise className="text-white w-7 h-7" />
|
||||
</button>
|
||||
<span>Play Again</span>
|
||||
</div>
|
||||
<div className="w-fit flex flex-col items-center gap-1 cursor-pointer">
|
||||
<button
|
||||
onClick={onViewResults}
|
||||
className="w-11 h-11 rounded-full bg-mti-green-light hover:bg-mti-green flex items-center justify-center transition duration-300 ease-in-out">
|
||||
<BsEyeFill className="text-white w-7 h-7" />
|
||||
</button>
|
||||
<span>Review Answers</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link href="/" className="max-w-[200px] w-full self-end">
|
||||
<Button color="green" className="max-w-[200px] self-end w-full">
|
||||
Dashboard
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,7 +80,13 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col h-full w-full gap-8 justify-between">
|
||||
<ModuleTitle exerciseIndex={exerciseIndex + 1} minTimer={exam.minTimer} module="listening" totalExercises={exam.exercises.length} />
|
||||
<ModuleTitle
|
||||
exerciseIndex={exerciseIndex + 1}
|
||||
minTimer={exam.minTimer}
|
||||
module="listening"
|
||||
totalExercises={exam.exercises.length}
|
||||
disableTimer={showSolutions}
|
||||
/>
|
||||
{exerciseIndex === -1 && renderAudioPlayer()}
|
||||
{exerciseIndex > -1 &&
|
||||
exerciseIndex < exam.exercises.length &&
|
||||
|
||||
@@ -142,7 +142,13 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props)
|
||||
<>
|
||||
<div className="flex flex-col h-full w-full gap-8">
|
||||
<TextModal {...exam.text} isOpen={showTextModal} onClose={() => setShowTextModal(false)} />
|
||||
<ModuleTitle minTimer={exam.minTimer} exerciseIndex={exerciseIndex + 1} module="reading" totalExercises={exam.exercises.length} />
|
||||
<ModuleTitle
|
||||
minTimer={exam.minTimer}
|
||||
exerciseIndex={exerciseIndex + 1}
|
||||
module="reading"
|
||||
totalExercises={exam.exercises.length}
|
||||
disableTimer={showSolutions}
|
||||
/>
|
||||
{exerciseIndex === -1 && renderText()}
|
||||
{exerciseIndex > -1 &&
|
||||
exerciseIndex < exam.exercises.length &&
|
||||
|
||||
@@ -59,7 +59,13 @@ export default function Speaking({exam, showSolutions = false, onFinish}: Props)
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col h-full w-full gap-8 items-center">
|
||||
<ModuleTitle minTimer={exam.minTimer} exerciseIndex={exerciseIndex + 1} module="speaking" totalExercises={exam.exercises.length} />
|
||||
<ModuleTitle
|
||||
minTimer={exam.minTimer}
|
||||
exerciseIndex={exerciseIndex + 1}
|
||||
module="speaking"
|
||||
totalExercises={exam.exercises.length}
|
||||
disableTimer={showSolutions}
|
||||
/>
|
||||
{exerciseIndex > -1 &&
|
||||
exerciseIndex < exam.exercises.length &&
|
||||
!showSolutions &&
|
||||
|
||||
@@ -59,7 +59,13 @@ export default function Writing({exam, showSolutions = false, onFinish}: Props)
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col h-full w-full gap-8 items-center">
|
||||
<ModuleTitle minTimer={exam.minTimer} exerciseIndex={exerciseIndex + 1} module="writing" totalExercises={exam.exercises.length} />
|
||||
<ModuleTitle
|
||||
minTimer={exam.minTimer}
|
||||
exerciseIndex={exerciseIndex + 1}
|
||||
module="writing"
|
||||
totalExercises={exam.exercises.length}
|
||||
disableTimer={showSolutions}
|
||||
/>
|
||||
{exerciseIndex > -1 &&
|
||||
exerciseIndex < exam.exercises.length &&
|
||||
!showSolutions &&
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface UserSolution {
|
||||
score: {
|
||||
correct: number;
|
||||
total: number;
|
||||
missing: number;
|
||||
};
|
||||
exercise: string;
|
||||
}
|
||||
|
||||
@@ -148,6 +148,43 @@ export default function Page() {
|
||||
setModuleIndex((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const aggregateScoresByModule = (answers: UserSolution[]): {module: Module; total: number; missing: number; correct: number}[] => {
|
||||
const scores: {[key in Module]: {total: number; missing: number; correct: number}} = {
|
||||
reading: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
listening: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
writing: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
speaking: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
};
|
||||
|
||||
answers.forEach((x) => {
|
||||
scores[x.module!] = {
|
||||
total: scores[x.module!].total + x.score.total,
|
||||
correct: scores[x.module!].correct + x.score.correct,
|
||||
missing: scores[x.module!].missing + x.score.missing,
|
||||
};
|
||||
});
|
||||
|
||||
return Object.keys(scores)
|
||||
.filter((x) => scores[x as Module].total > 0)
|
||||
.map((x) => ({module: x as Module, ...scores[x as Module]}));
|
||||
};
|
||||
|
||||
const renderScreen = () => {
|
||||
if (selectedModules.length === 0) {
|
||||
return <Selection user={user!} onStart={setSelectedModules} />;
|
||||
@@ -163,7 +200,7 @@ export default function Page() {
|
||||
setModuleIndex(0);
|
||||
setExam(exams[0]);
|
||||
}}
|
||||
scores={userSolutions.map((x) => ({...x.score, module: x.module}))}
|
||||
scores={aggregateScoresByModule(userSolutions)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ const academicMarking: {[key: number]: number} = {
|
||||
10: 2.5,
|
||||
};
|
||||
|
||||
const moduleMarkings: {[key in "reading" | "listening"]: {[key in Type]: {[key: number]: number}}} = {
|
||||
const moduleMarkings: {[key in Module]: {[key in Type]: {[key: number]: number}}} = {
|
||||
reading: {
|
||||
academic: academicMarking,
|
||||
general: readingGeneralMarking,
|
||||
@@ -45,9 +45,17 @@ const moduleMarkings: {[key in "reading" | "listening"]: {[key in Type]: {[key:
|
||||
academic: academicMarking,
|
||||
general: academicMarking,
|
||||
},
|
||||
writing: {
|
||||
academic: academicMarking,
|
||||
general: readingGeneralMarking,
|
||||
},
|
||||
speaking: {
|
||||
academic: academicMarking,
|
||||
general: academicMarking,
|
||||
},
|
||||
};
|
||||
|
||||
export const calculateBandScore = (correct: number, total: number, module: "reading" | "listening", type: Type) => {
|
||||
export const calculateBandScore = (correct: number, total: number, module: Module, type: Type) => {
|
||||
const marking = moduleMarkings[module][type];
|
||||
const percentage = (correct * 100) / total;
|
||||
|
||||
|
||||
@@ -22,10 +22,10 @@ module.exports = {
|
||||
black: "#353338",
|
||||
},
|
||||
ielts: {
|
||||
reading: {DEFAULT: "#1EB3FF", transparent: "rgba(255, 99, 132, 0.5)"},
|
||||
listening: {DEFAULT: "#FF790A", transparent: "rgba(54, 162, 235, 0.5)"},
|
||||
writing: {DEFAULT: "#3D9F11", transparent: "rgba(255, 206, 86, 0.5)"},
|
||||
speaking: {DEFAULT: "#EF5DA8", transparent: "rgba(75, 192, 192, 0.5)"},
|
||||
reading: {DEFAULT: "#1EB3FF", light: "#F0F9FF", transparent: "rgba(255, 99, 132, 0.5)"},
|
||||
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)"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
51
yarn.lock
51
yarn.lock
@@ -1338,26 +1338,15 @@ color-convert@^2.0.1:
|
||||
dependencies:
|
||||
color-name "~1.1.4"
|
||||
|
||||
color-name@^1.0.0, color-name@~1.1.4:
|
||||
color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
color-string@^1.9.0:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
|
||||
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
|
||||
dependencies:
|
||||
color-name "^1.0.0"
|
||||
simple-swizzle "^0.2.2"
|
||||
|
||||
color@^4.2:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a"
|
||||
integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
|
||||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
color-string "^1.9.0"
|
||||
colord@^2.9:
|
||||
version "2.9.3"
|
||||
resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43"
|
||||
integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
@@ -1390,7 +1379,7 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
css-selector-tokenizer@^0.8.0:
|
||||
css-selector-tokenizer@^0.8:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz#88267ef6238e64f2215ea2764b3e2cf498b845dd"
|
||||
integrity sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==
|
||||
@@ -1408,14 +1397,14 @@ csstype@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||
|
||||
daisyui@^2.50.0:
|
||||
version "2.52.0"
|
||||
resolved "https://registry.yarnpkg.com/daisyui/-/daisyui-2.52.0.tgz#1e22abd655bf4a5cb466e1a1f264bb597e9ec254"
|
||||
integrity sha512-LQTA5/IVXAJHBMFoeaEMfd7/akAFPPcdQPR3O9fzzcFiczneJFM73CFPnScmW2sOgn/D83cvkP854ep2T9OfTg==
|
||||
daisyui@^3.1.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/daisyui/-/daisyui-3.1.5.tgz#49cb961d03504beebd238d58f0169dbe81103c95"
|
||||
integrity sha512-3nk5L//IU31ISzsonKEFG8ris8hS/MRAc+PrOTOyRbssjv1fYmNKSdIrOhG6op2ED3RyJL4p6I81JUwd7RWqTw==
|
||||
dependencies:
|
||||
color "^4.2"
|
||||
css-selector-tokenizer "^0.8.0"
|
||||
postcss-js "^4.0.0"
|
||||
colord "^2.9"
|
||||
css-selector-tokenizer "^0.8"
|
||||
postcss-js "^4"
|
||||
tailwindcss "^3"
|
||||
|
||||
damerau-levenshtein@^1.0.8:
|
||||
@@ -2323,11 +2312,6 @@ is-array-buffer@^3.0.1, is-array-buffer@^3.0.2:
|
||||
get-intrinsic "^1.2.0"
|
||||
is-typed-array "^1.1.10"
|
||||
|
||||
is-arrayish@^0.3.1:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
|
||||
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
|
||||
|
||||
is-bigint@^1.0.1:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
|
||||
@@ -2962,7 +2946,7 @@ postcss-import@^15.1.0:
|
||||
read-cache "^1.0.0"
|
||||
resolve "^1.1.7"
|
||||
|
||||
postcss-js@^4.0.0, postcss-js@^4.0.1:
|
||||
postcss-js@^4, postcss-js@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2"
|
||||
integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==
|
||||
@@ -3360,13 +3344,6 @@ signal-exit@^3.0.3, signal-exit@^3.0.7:
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
|
||||
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
|
||||
|
||||
simple-swizzle@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
|
||||
integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==
|
||||
dependencies:
|
||||
is-arrayish "^0.3.1"
|
||||
|
||||
slash@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||
|
||||
Reference in New Issue
Block a user