Updated the Finish screen along with other tweaks

This commit is contained in:
Tiago Ribeiro
2023-06-21 14:54:22 +01:00
parent d2cf50be68
commit 808ec6315b
20 changed files with 313 additions and 158 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,7 @@ export interface UserSolution {
score: {
correct: number;
total: number;
missing: number;
};
exercise: string;
}

View File

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

View File

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