From 438778a03cafe8c43c670411fbb96ed7369e91a0 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Mon, 18 Dec 2023 22:42:14 +0000 Subject: [PATCH 1/4] Added more control over the stats appearing in the stats page --- src/pages/stats.tsx | 344 +++++++++++++++++++++++++-------------- src/utils/moduleUtils.ts | 4 +- src/utils/stats.ts | 5 + 3 files changed, 225 insertions(+), 128 deletions(-) diff --git a/src/pages/stats.tsx b/src/pages/stats.tsx index 5be1f2cb..ef75cb04 100644 --- a/src/pages/stats.tsx +++ b/src/pages/stats.tsx @@ -6,15 +6,15 @@ import {withIronSessionSsr} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import {useEffect, useState} from "react"; import useStats from "@/hooks/useStats"; -import {averageScore, totalExamsByModule, groupBySession, groupByModule} from "@/utils/stats"; +import {averageScore, totalExamsByModule, groupBySession, groupByModule, timestampToMoment, groupByDate} from "@/utils/stats"; import useUser from "@/hooks/useUser"; import {ToastContainer} from "react-toastify"; -import {capitalize} from "lodash"; +import {capitalize, Dictionary} from "lodash"; import {Module} from "@/interfaces"; import ProgressBar from "@/components/Low/ProgressBar"; import Layout from "@/components/High/Layout"; import {calculateAverageLevel, calculateBandScore} from "@/utils/score"; -import {MODULE_ARRAY} from "@/utils/moduleUtils"; +import {MODULE_ARRAY, sortByModule} from "@/utils/moduleUtils"; import {Chart} from "react-chartjs-2"; import useUsers from "@/hooks/useUsers"; import Select from "react-select"; @@ -24,6 +24,7 @@ import {shouldRedirectHome} from "@/utils/navigation.disabled"; import ProfileSummary from "@/components/ProfileSummary"; import moment from "moment"; import {Stat} from "@/interfaces/user"; +import {Divider} from "primereact/divider"; ChartJS.register(LinearScale, CategoryScale, PointElement, LineElement, LineController, Legend, Tooltip); @@ -64,6 +65,11 @@ export default function Stats() { const [startDate, setStartDate] = useState(moment("01/01/2023").toDate()); const [endDate, setEndDate] = useState(new Date()); const [displayStats, setDisplayStats] = useState([]); + const [initialStatDate, setInitialStatDate] = useState(); + + const [monthlyOverallScoreDate, setMonthlyOverallScoreDate] = useState(new Date()); + const [monthlyModuleScoreDate, setMonthlyModuleScoreDate] = useState(new Date()); + const [monthlyOverallGraphScoreDate, setMonthlyOverallGraphScoreDate] = useState(new Date()); const {user} = useUser({redirectTo: "/login"}); const {users} = useUsers(); @@ -76,11 +82,8 @@ export default function Stats() { }, [user]); useEffect(() => { - const startDateFilter = (s: Stat) => moment.unix(s.date / 1000).isAfter(moment(startDate)); - const endDateFilter = (s: Stat) => { - console.log(moment.unix(s.date / 1000), moment(endDate).isAfter(moment.unix(s.date))); - return moment(endDate).isAfter(moment.unix(s.date / 1000)); - }; + const startDateFilter = (s: Stat) => timestampToMoment(s).isAfter(moment(startDate)); + const endDateFilter = (s: Stat) => moment(endDate).isAfter(timestampToMoment(s)); const filters = []; if (startDate) filters.push(startDateFilter); @@ -89,20 +92,39 @@ export default function Stats() { setDisplayStats(filters.reduce((d, f) => d.filter(f), stats)); }, [endDate, startDate, stats]); - const calculateTotalScorePerSession = () => { - const groupedBySession = groupBySession(stats); + useEffect(() => { + setInitialStatDate( + stats + .filter((s) => s.date) + .sort((a, b) => timestampToMoment(a).diff(timestampToMoment(b))) + .map(timestampToMoment) + .shift() + ?.toDate(), + ); + }, [stats]); + + useEffect(() => { + setStartDate(initialStatDate || moment("01/01/2023").toDate()); + }, [initialStatDate]); + + const calculateModuleScore = (stats: Stat[]) => { + const moduleStats = groupByModule(stats); + return Object.keys(moduleStats).map((y) => { + const correct = moduleStats[y].reduce((accumulator, current) => accumulator + current.score.correct, 0); + const total = moduleStats[y].reduce((accumulator, current) => accumulator + current.score.total, 0); + + return { + module: y as Module, + score: calculateBandScore(correct, total, y as Module, user?.focus || "academic"), + }; + }); + }; + + const calculateTotalScorePerKey = (stats: Stat[], keyFunction: (stats: Stat[]) => Dictionary) => { + const groupedBySession = keyFunction(stats); const sessionAverage = Object.keys(groupedBySession).map((x: string) => { const session = groupedBySession[x]; - const moduleStats = groupByModule(session); - const moduleScores = Object.keys(moduleStats).map((y) => { - const correct = moduleStats[y].reduce((accumulator, current) => accumulator + current.score.correct, 0); - const total = moduleStats[y].reduce((accumulator, current) => accumulator + current.score.total, 0); - - return { - module: y, - score: calculateBandScore(correct, total, y as Module, user?.focus || "academic"), - }; - }); + const moduleScores = calculateModuleScore(session); return moduleScores.reduce((acc, curr) => acc + curr.score, 0) / 4; }); @@ -110,7 +132,12 @@ export default function Stats() { return sessionAverage; }; - const calculateAverageTimePerModule = () => { + const calculateTotalScore = (stats: Stat[]) => { + const moduleScores = calculateModuleScore(stats); + return moduleScores.reduce((acc, curr) => acc + curr.score, 0) / 4; + }; + + const calculateAverageTimePerModule = (stats: Stat[]) => { const groupedBySession = groupBySession(stats.filter((x) => !!x.timeSpent)); const sessionAverage = Object.keys(groupedBySession).map((x: string) => { const session = groupedBySession[x]; @@ -122,7 +149,7 @@ export default function Stats() { return sessionAverage; }; - const calculateModularScorePerSession = (module: Module) => { + const calculateModularScorePerSession = (stats: Stat[], module: Module) => { const groupedBySession = groupBySession(stats); const sessionAverage = Object.keys(groupedBySession).map((x: string) => { const session = groupedBySession[x]; @@ -208,81 +235,78 @@ export default function Stats() { /> )} - moment(date).isSameOrBefore(moment(new Date()))} - onChange={([initialDate, finalDate]) => { - setStartDate(initialDate ?? moment("01/01/2023").toDate()); - setEndDate(finalDate); - }} - /> - {displayStats.length > 0 && ( + {stats.length > 0 && (
- {/* Exams per module */} -
- Exams per Module -
- {MODULE_ARRAY.map((module) => ( -
-
- - {totalExamsByModule(displayStats, module)} of{" "} - {Object.keys(groupBySession(displayStats)).length} + {/* Overall Level per Month */} +
+
+ Overall Level per Month + +
+
+ {[...Array(31).keys()].map((day) => { + const date = moment( + `${(day + 1).toString().padStart(2, "0")}/${ + moment(monthlyOverallScoreDate).get("month") + 1 + }/${moment(monthlyOverallScoreDate).get("year")}`, + "DD/MM/yyyy", + ); + + return date.isValid() ? ( +
+ + Day {(day + 1).toString().padStart(2, "0")} + + + Level{" "} + {calculateTotalScore(stats.filter((s) => timestampToMoment(s).isBefore(date))).toFixed(1)} - {capitalize(module)}
- -
- ))} + ) : null; + })}
- {/* Module Score */} -
- Module Score Bands -
- {MODULE_ARRAY.map((module) => ( -
-
- - {user.levels[module]} of{" "} - {user.desiredLevels[module]} - - {capitalize(module)} -
- -
- ))} + {/* Overall Level per Month Graph */} +
+
+ Overall Level per Month +
-
- - {/* Total Score Band per Session */} -
- Total Score Band per Session index), + labels: [...Array(31).keys()] + .map((day) => { + const date = moment( + `${(day + 1).toString().padStart(2, "0")}/${ + moment(monthlyOverallScoreDate).get("month") + 1 + }/${moment(monthlyOverallScoreDate).get("year")}`, + "DD/MM/yyyy", + ); + return date.isValid() ? (day + 1).toString().padStart(2, "0") : undefined; + }) + .filter((x) => !!x), datasets: [ { type: "line", @@ -292,55 +316,123 @@ export default function Stats() { backgroundColor: "#7872BF", borderWidth: 2, spanGaps: true, - data: calculateTotalScorePerSession(), + data: [...Array(31).keys()] + .map((day) => { + const date = moment( + `${(day + 1).toString().padStart(2, "0")}/${ + moment(monthlyOverallScoreDate).get("month") + 1 + }/${moment(monthlyOverallScoreDate).get("year")}`, + "DD/MM/yyyy", + ); + + return date.isValid() + ? calculateTotalScore( + stats.filter((s) => timestampToMoment(s).isBefore(date)), + ).toFixed(1) + : undefined; + }) + .filter((x) => !!x), }, ], }} />
- {/* Module Score Band per Session */} -
- Module Score Band per Session - index), - datasets: [ - ...MODULE_ARRAY.map((module, index) => ({ - type: "line" as const, - label: capitalize(module), - borderColor: COLORS[index], - backgroundColor: COLORS[index], - borderWidth: 2, - data: calculateModularScorePerSession(module), - })), - ], - }} - /> + {/* Module Level per Day */} +
+
+ Module Level per Day + +
+
+ {calculateModuleScore(stats.filter((s) => timestampToMoment(s).isBefore(moment(monthlyModuleScoreDate)))) + .sort(sortByModule) + .map(({module, score}) => ( +
+
+ + {score} of 9 + + {capitalize(module)} +
+ +
+ ))} +
+
+ )} - {/* Average Time per Module */} -
- Average Time per Module (in Minutes) - !!s.timeSpent))).map((_, index) => index), - datasets: [ - { - type: "line", - label: "Average (in minutes)", - fill: false, - borderColor: "#6A5FB1", - backgroundColor: "#7872BF", - borderWidth: 2, - spanGaps: true, - data: calculateAverageTimePerModule(), - }, - ], - }} - /> + + + {displayStats.length > 0 && ( +
+ moment(date).isSameOrBefore(moment(new Date()))} + onChange={([initialDate, finalDate]) => { + setStartDate(initialDate ?? moment("01/01/2023").toDate()); + setEndDate(finalDate); + }} + /> +
+ {/* Module Score Band per Session */} +
+ Module Score Band per Session + index), + datasets: [ + ...MODULE_ARRAY.map((module, index) => ({ + type: "line" as const, + label: capitalize(module), + borderColor: COLORS[index], + backgroundColor: COLORS[index], + borderWidth: 2, + data: calculateModularScorePerSession(displayStats, module), + })), + ], + }} + /> +
+ + {/* Average Time per Module */} +
+ Average Time per Module (in Minutes) + !!s.timeSpent))).map( + (_, index) => index, + ), + datasets: [ + { + type: "line", + label: "Average (in minutes)", + fill: false, + borderColor: "#6A5FB1", + backgroundColor: "#7872BF", + borderWidth: 2, + spanGaps: true, + data: calculateAverageTimePerModule(displayStats), + }, + ], + }} + /> +
)} diff --git a/src/utils/moduleUtils.ts b/src/utils/moduleUtils.ts index 37783463..8abafbd8 100644 --- a/src/utils/moduleUtils.ts +++ b/src/utils/moduleUtils.ts @@ -1,7 +1,7 @@ import {Module} from "@/interfaces"; import {Exercise} from "@/interfaces/exam"; -export const MODULE_ARRAY: Module[] = ["reading", "listening", "writing", "speaking"]; +export const MODULE_ARRAY: Module[] = ["reading", "listening", "writing", "speaking", "level"]; export const moduleLabels: {[key in Module]: string} = { listening: "Listening", @@ -11,7 +11,7 @@ export const moduleLabels: {[key in Module]: string} = { level: "Level", }; -export const sortByModule = (a: {module: Module}, b: {module: Module}) => { +export const sortByModule = (a: {module: Module; [key: string]: any}, b: {module: Module; [key: string]: any}) => { return MODULE_ARRAY.findIndex((x) => a.module === x) - MODULE_ARRAY.findIndex((x) => b.module === x); }; diff --git a/src/utils/stats.ts b/src/utils/stats.ts index 8f8e7e97..ebd2f074 100644 --- a/src/utils/stats.ts +++ b/src/utils/stats.ts @@ -4,6 +4,11 @@ import {convertCamelCaseToReadable} from "@/utils/string"; import {UserSolution} from "@/interfaces/exam"; import {Module} from "@/interfaces"; import {MODULES} from "@/constants/ielts"; +import moment from "moment"; + +export const timestampToMoment = (stat: Stat): moment.Moment => { + return moment.unix(stat.date > Math.pow(10, 11) ? stat.date / 1000 : stat.date); +}; export const totalExams = (stats: Stat[]): number => { const moduleStats = formatModuleTotalStats(stats); From fddc3ff2f3e44d815aa79c937baf9e4c2bc95fd2 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 27 Dec 2023 09:14:13 +0000 Subject: [PATCH 2/4] Finished updating the stats page according to the client's requests --- src/components/Low/Badge.tsx | 30 ++ src/pages/stats.tsx | 736 ++++++++++++++++++++++++----------- 2 files changed, 538 insertions(+), 228 deletions(-) create mode 100644 src/components/Low/Badge.tsx diff --git a/src/components/Low/Badge.tsx b/src/components/Low/Badge.tsx new file mode 100644 index 00000000..76c421af --- /dev/null +++ b/src/components/Low/Badge.tsx @@ -0,0 +1,30 @@ +import {Module} from "@/interfaces"; +import clsx from "clsx"; +import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs"; + +interface Props { + module: Module; + children: string; +} + +export default function Badge({module, children}: Props) { + return ( +
+ {module === "reading" && } + {module === "listening" && } + {module === "writing" && } + {module === "speaking" && } + {module === "level" && } + {children} +
+ ); +} diff --git a/src/pages/stats.tsx b/src/pages/stats.tsx index ef75cb04..f5dd2136 100644 --- a/src/pages/stats.tsx +++ b/src/pages/stats.tsx @@ -1,6 +1,6 @@ /* eslint-disable @next/next/no-img-element */ import Head from "next/head"; -import {BsFileEarmarkText, BsPencil, BsStar} from "react-icons/bs"; +import {BsArrowClockwise, BsChevronLeft, BsChevronRight, BsFileEarmarkText, BsPencil, BsStar} from "react-icons/bs"; import {LinearScale, Chart as ChartJS, CategoryScale, PointElement, LineElement, Legend, Tooltip, LineController} from "chart.js"; import {withIronSessionSsr} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; @@ -25,10 +25,11 @@ import ProfileSummary from "@/components/ProfileSummary"; import moment from "moment"; import {Stat} from "@/interfaces/user"; import {Divider} from "primereact/divider"; +import Badge from "@/components/Low/Badge"; ChartJS.register(LinearScale, CategoryScale, PointElement, LineElement, LineController, Legend, Tooltip); -const COLORS = ["#1EB3FF", "#FF790A", "#3D9F11", "#EF5DA8"]; +const COLORS = ["#1EB3FF", "#FF790A", "#3D9F11", "#EF5DA8", "#414288"]; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; @@ -62,14 +63,15 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => { export default function Stats() { const [statsUserId, setStatsUserId] = useState(); - const [startDate, setStartDate] = useState(moment("01/01/2023").toDate()); + const [startDate, setStartDate] = useState(moment(new Date()).subtract(1, "weeks").toDate()); const [endDate, setEndDate] = useState(new Date()); - const [displayStats, setDisplayStats] = useState([]); const [initialStatDate, setInitialStatDate] = useState(); const [monthlyOverallScoreDate, setMonthlyOverallScoreDate] = useState(new Date()); const [monthlyModuleScoreDate, setMonthlyModuleScoreDate] = useState(new Date()); - const [monthlyOverallGraphScoreDate, setMonthlyOverallGraphScoreDate] = useState(new Date()); + + const [dailyScoreDate, setDailyScoreDate] = useState(new Date()); + const [intervalDates, setIntervalDates] = useState([]); const {user} = useUser({redirectTo: "/login"}); const {users} = useUsers(); @@ -81,17 +83,6 @@ export default function Stats() { if (user) setStatsUserId(user.id); }, [user]); - useEffect(() => { - const startDateFilter = (s: Stat) => timestampToMoment(s).isAfter(moment(startDate)); - const endDateFilter = (s: Stat) => moment(endDate).isAfter(timestampToMoment(s)); - - const filters = []; - if (startDate) filters.push(startDateFilter); - if (endDate) filters.push(endDateFilter); - - setDisplayStats(filters.reduce((d, f) => d.filter(f), stats)); - }, [endDate, startDate, stats]); - useEffect(() => { setInitialStatDate( stats @@ -103,10 +94,6 @@ export default function Stats() { ); }, [stats]); - useEffect(() => { - setStartDate(initialStatDate || moment("01/01/2023").toDate()); - }, [initialStatDate]); - const calculateModuleScore = (stats: Stat[]) => { const moduleStats = groupByModule(stats); return Object.keys(moduleStats).map((y) => { @@ -120,35 +107,6 @@ export default function Stats() { }); }; - const calculateTotalScorePerKey = (stats: Stat[], keyFunction: (stats: Stat[]) => Dictionary) => { - const groupedBySession = keyFunction(stats); - const sessionAverage = Object.keys(groupedBySession).map((x: string) => { - const session = groupedBySession[x]; - const moduleScores = calculateModuleScore(session); - - return moduleScores.reduce((acc, curr) => acc + curr.score, 0) / 4; - }); - - return sessionAverage; - }; - - const calculateTotalScore = (stats: Stat[]) => { - const moduleScores = calculateModuleScore(stats); - return moduleScores.reduce((acc, curr) => acc + curr.score, 0) / 4; - }; - - const calculateAverageTimePerModule = (stats: Stat[]) => { - const groupedBySession = groupBySession(stats.filter((x) => !!x.timeSpent)); - const sessionAverage = Object.keys(groupedBySession).map((x: string) => { - const session = groupedBySession[x]; - const timeSpent = session[0].timeSpent!; - - return Math.floor(timeSpent / session.length / 60); - }); - - return sessionAverage; - }; - const calculateModularScorePerSession = (stats: Stat[], module: Module) => { const groupedBySession = groupBySession(stats); const sessionAverage = Object.keys(groupedBySession).map((x: string) => { @@ -164,6 +122,33 @@ export default function Stats() { return sessionAverage; }; + const getListOfDateInInterval = (start: Date, end: Date) => { + let currentDate = moment(start); + const dates = [currentDate.toDate()]; + while (moment(end).diff(currentDate, "days") > 0) { + currentDate = currentDate.add(1, "days"); + dates.push(currentDate.toDate()); + } + + return dates; + }; + + useEffect(() => { + if (startDate && endDate) { + setIntervalDates(getListOfDateInInterval(startDate, endDate)); + } + }, [startDate, endDate]); + + const calculateTotalScore = (stats: Stat[]) => { + const moduleScores = calculateModuleScore(stats); + return moduleScores.reduce((acc, curr) => acc + curr.score, 0) / 4; + }; + + const calculateScorePerModule = (stats: Stat[], module: Module) => { + const moduleScores = calculateModuleScore(stats); + return moduleScores.find((x) => x.module === module)?.score || -1; + }; + return ( <> @@ -238,163 +223,314 @@ export default function Stats() {
{stats.length > 0 && ( -
- {/* Overall Level per Month */} -
-
- Overall Level per Month - -
-
- {[...Array(31).keys()].map((day) => { - const date = moment( - `${(day + 1).toString().padStart(2, "0")}/${ - moment(monthlyOverallScoreDate).get("month") + 1 - }/${moment(monthlyOverallScoreDate).get("year")}`, - "DD/MM/yyyy", - ); - - return date.isValid() ? ( -
- - Day {(day + 1).toString().padStart(2, "0")} - - - Level{" "} - {calculateTotalScore(stats.filter((s) => timestampToMoment(s).isBefore(date))).toFixed(1)} - -
- ) : null; - })} -
-
- - {/* Overall Level per Month Graph */} -
-
- Overall Level per Month - -
- { - const date = moment( - `${(day + 1).toString().padStart(2, "0")}/${ - moment(monthlyOverallScoreDate).get("month") + 1 - }/${moment(monthlyOverallScoreDate).get("year")}`, - "DD/MM/yyyy", - ); - return date.isValid() ? (day + 1).toString().padStart(2, "0") : undefined; - }) - .filter((x) => !!x), - datasets: [ - { - type: "line", - label: "Total", - fill: false, - borderColor: "#6A5FB1", - backgroundColor: "#7872BF", - borderWidth: 2, - spanGaps: true, - data: [...Array(31).keys()] - .map((day) => { - const date = moment( - `${(day + 1).toString().padStart(2, "0")}/${ - moment(monthlyOverallScoreDate).get("month") + 1 - }/${moment(monthlyOverallScoreDate).get("year")}`, - "DD/MM/yyyy", - ); - - return date.isValid() - ? calculateTotalScore( - stats.filter((s) => timestampToMoment(s).isBefore(date)), - ).toFixed(1) - : undefined; - }) - .filter((x) => !!x), - }, - ], - }} - /> -
- - {/* Module Level per Day */} -
-
- Module Level per Day - -
-
- {calculateModuleScore(stats.filter((s) => timestampToMoment(s).isBefore(moment(monthlyModuleScoreDate)))) - .sort(sortByModule) - .map(({module, score}) => ( -
-
- - {score} of 9 - - {capitalize(module)} -
- -
- ))} -
-
-
- )} - - - - {displayStats.length > 0 && ( -
- moment(date).isSameOrBefore(moment(new Date()))} - onChange={([initialDate, finalDate]) => { - setStartDate(initialDate ?? moment("01/01/2023").toDate()); - setEndDate(finalDate); - }} - /> + <>
- {/* Module Score Band per Session */} -
- Module Score Band per Session + {/* Overall Level per Month */} +
+
+ Overall Level per Month +
+ {monthlyOverallScoreDate && ( + + )} + + {monthlyOverallScoreDate && ( + + )} + +
+
+
+ {[...Array(31).keys()].map((day) => { + const date = moment( + `${(day + 1).toString().padStart(2, "0")}/${ + moment(monthlyOverallScoreDate).get("month") + 1 + }/${moment(monthlyOverallScoreDate).get("year")}`, + "DD/MM/yyyy", + ); + + return date.isValid() ? ( +
+ + Day {(day + 1).toString().padStart(2, "0")} + + + Level{" "} + {calculateTotalScore(stats.filter((s) => timestampToMoment(s).isBefore(date))).toFixed(1)} + +
+ ) : null; + })} +
+
+ + {/* Overall Level per Month Graph */} +
+
+ Overall Level per Month +
+ {monthlyOverallScoreDate && ( + + )} + + {monthlyOverallScoreDate && ( + + )} + +
+
index), + labels: [...Array(31).keys()] + .map((day) => { + const date = moment( + `${(day + 1).toString().padStart(2, "0")}/${ + moment(monthlyOverallScoreDate).get("month") + 1 + }/${moment(monthlyOverallScoreDate).get("year")}`, + "DD/MM/yyyy", + ); + return date.isValid() ? (day + 1).toString().padStart(2, "0") : undefined; + }) + .filter((x) => !!x), + datasets: [ + { + type: "line", + label: "Total", + fill: false, + borderColor: "#6A5FB1", + backgroundColor: "#7872BF", + borderWidth: 2, + spanGaps: true, + data: [...Array(31).keys()] + .map((day) => { + const date = moment( + `${(day + 1).toString().padStart(2, "0")}/${ + moment(monthlyOverallScoreDate).get("month") + 1 + }/${moment(monthlyOverallScoreDate).get("year")}`, + "DD/MM/yyyy", + ); + + return date.isValid() + ? calculateTotalScore( + stats.filter((s) => timestampToMoment(s).isBefore(date)), + ).toFixed(1) + : undefined; + }) + .filter((x) => !!x), + }, + ], + }} + /> +
+ + {/* Module Level per Day */} +
+
+ Module Level per Day +
+ {monthlyModuleScoreDate && ( + + )} + + {monthlyModuleScoreDate && ( + + )} + +
+
+
+ {calculateModuleScore(stats.filter((s) => timestampToMoment(s).isBefore(moment(monthlyModuleScoreDate)))) + .sort(sortByModule) + .map(({module, score}) => ( +
+
+ + {score} of 9 + + {capitalize(module)} +
+ +
+ ))} +
+
+
+ + + +
+ {/* Module Level per Exam */} +
+
+ Module Level per Exam +
+ {dailyScoreDate && ( + + )} + + {dailyScoreDate && ( + + )} + +
+
+ +
+ {Object.keys( + groupBySession( + stats.filter( + (s) => + Math.abs(timestampToMoment(s).diff(moment(dailyScoreDate), "days")) === 0 && + timestampToMoment(s).day() === moment(dailyScoreDate).day(), + ), + ), + ).length === 0 && No exams performed this day...} + + {Object.keys( + groupBySession( + stats.filter( + (s) => + Math.abs(timestampToMoment(s).diff(moment(dailyScoreDate), "days")) === 0 && + timestampToMoment(s).day() === moment(dailyScoreDate).day(), + ), + ), + ).map((session, index) => ( +
+ + Exam {(index + 1).toString().padStart(2, "0")} + +
+ {MODULE_ARRAY.map((module) => { + const score = calculateScorePerModule( + groupBySession( + stats.filter( + (s) => + Math.abs(timestampToMoment(s).diff(moment(dailyScoreDate), "days")) === 0, + ), + )[session], + module, + ); + + return score === -1 ? null : {score.toFixed(1)}; + }).filter((m) => !!m)} +
+
+ ))} +
+
+ +
+
+ Module Level per Exam +
+ {dailyScoreDate && ( + + )} + + {dailyScoreDate && ( + + )} + +
+
+ + Math.abs(timestampToMoment(s).diff(moment(dailyScoreDate), "days")) === 0 && + timestampToMoment(s).day() === moment(dailyScoreDate).day(), + ), + ), + ).map((_, index) => `Exam ${(index + 1).toString().padStart(2, "0")}`), datasets: [ ...MODULE_ARRAY.map((module, index) => ({ type: "line" as const, @@ -402,42 +538,186 @@ export default function Stats() { borderColor: COLORS[index], backgroundColor: COLORS[index], borderWidth: 2, - data: calculateModularScorePerSession(displayStats, module), + data: calculateModularScorePerSession( + stats.filter( + (s) => + Math.abs(timestampToMoment(s).diff(moment(dailyScoreDate), "days")) === 0 && + timestampToMoment(s).day() === moment(dailyScoreDate).day(), + ), + module, + ), })), ], }} />
+
- {/* Average Time per Module */} -
- Average Time per Module (in Minutes) - !!s.timeSpent))).map( - (_, index) => index, - ), - datasets: [ - { - type: "line", - label: "Average (in minutes)", - fill: false, - borderColor: "#6A5FB1", - backgroundColor: "#7872BF", - borderWidth: 2, - spanGaps: true, - data: calculateAverageTimePerModule(displayStats), - }, - ], - }} - /> + + +
+ moment(date).isSameOrBefore(moment(new Date()))} + onChange={([initialDate, finalDate]) => { + setStartDate(initialDate); + setEndDate(finalDate); + }} + /> +
+ {/* Reading Score Band in Interval */} +
+ Reading Score Band in Interval + moment(date).format("DD/MM/YYYY")), + datasets: [ + { + type: "line", + label: "Reading", + fill: false, + borderColor: COLORS[0], + backgroundColor: COLORS[0], + borderWidth: 2, + spanGaps: true, + data: intervalDates.map((date) => { + return calculateTotalScore( + stats.filter( + (s) => timestampToMoment(s).isBefore(date) && s.module === "reading", + ), + ).toFixed(1); + }), + }, + ], + }} + /> +
+ + {/* Listening Score Band in Interval */} +
+ Listening Score Band in Interval + moment(date).format("DD/MM/YYYY")), + datasets: [ + { + type: "line", + label: "Listening", + fill: false, + borderColor: COLORS[1], + backgroundColor: COLORS[1], + borderWidth: 2, + spanGaps: true, + data: intervalDates.map((date) => { + return calculateTotalScore( + stats.filter( + (s) => timestampToMoment(s).isBefore(date) && s.module === "listening", + ), + ).toFixed(1); + }), + }, + ], + }} + /> +
+ + {/* Writing Score Band in Interval */} +
+ Writing Score Band in Interval + moment(date).format("DD/MM/YYYY")), + datasets: [ + { + type: "line", + label: "Writing", + fill: false, + borderColor: COLORS[2], + backgroundColor: COLORS[2], + borderWidth: 2, + spanGaps: true, + data: intervalDates.map((date) => { + return calculateTotalScore( + stats.filter( + (s) => timestampToMoment(s).isBefore(date) && s.module === "writing", + ), + ).toFixed(1); + }), + }, + ], + }} + /> +
+ + {/* Speaking Score Band in Interval */} +
+ Speaking Score Band in Interval + moment(date).format("DD/MM/YYYY")), + datasets: [ + { + type: "line", + label: "Speaking", + fill: false, + borderColor: COLORS[3], + backgroundColor: COLORS[3], + borderWidth: 2, + spanGaps: true, + data: intervalDates.map((date) => { + return calculateTotalScore( + stats.filter( + (s) => timestampToMoment(s).isBefore(date) && s.module === "speaking", + ), + ).toFixed(1); + }), + }, + ], + }} + /> +
+ + {/* Level Score Band in Interval */} +
+ Level Score Band in Interval + moment(date).format("DD/MM/YYYY")), + datasets: [ + { + type: "line", + label: "Level", + fill: false, + borderColor: COLORS[4], + backgroundColor: COLORS[4], + borderWidth: 2, + spanGaps: true, + data: intervalDates.map((date) => { + return calculateTotalScore( + stats.filter((s) => timestampToMoment(s).isBefore(date) && s.module === "level"), + ).toFixed(1); + }), + }, + ], + }} + /> +
-
+ )} - {displayStats.length === 0 && ( + {stats.length === 0 && (
No stats to display...
From c5fe405389716f2466c24de1fbd7afd554799ed0 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 27 Dec 2023 09:53:45 +0000 Subject: [PATCH 3/4] Updated the scale to be between 0 and 9 --- src/pages/stats.tsx | 56 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/pages/stats.tsx b/src/pages/stats.tsx index f5dd2136..f919d9bb 100644 --- a/src/pages/stats.tsx +++ b/src/pages/stats.tsx @@ -319,6 +319,14 @@ export default function Stats() {
{ @@ -521,6 +529,14 @@ export default function Stats() {
Reading Score Band in Interval moment(date).format("DD/MM/YYYY")), @@ -603,6 +627,14 @@ export default function Stats() {
Listening Score Band in Interval moment(date).format("DD/MM/YYYY")), @@ -632,6 +664,14 @@ export default function Stats() {
Writing Score Band in Interval moment(date).format("DD/MM/YYYY")), @@ -661,6 +701,14 @@ export default function Stats() {
Speaking Score Band in Interval moment(date).format("DD/MM/YYYY")), @@ -690,6 +738,14 @@ export default function Stats() {
Level Score Band in Interval moment(date).format("DD/MM/YYYY")), From cb73196503b981a818dc55080db47d3adfa1d0a4 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 27 Dec 2023 14:47:42 +0000 Subject: [PATCH 4/4] Limited the chevron to only work if it does not go after today --- src/pages/stats.tsx | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/pages/stats.tsx b/src/pages/stats.tsx index f919d9bb..a6abe9ef 100644 --- a/src/pages/stats.tsx +++ b/src/pages/stats.tsx @@ -249,7 +249,9 @@ export default function Stats() { /> {monthlyOverallScoreDate && ( )} @@ -267,7 +269,7 @@ export default function Stats() { "DD/MM/yyyy", ); - return date.isValid() ? ( + return date.isValid() && date.isSameOrBefore(moment()) ? (
@@ -308,7 +310,9 @@ export default function Stats() { /> {monthlyOverallScoreDate && ( )} @@ -392,7 +396,10 @@ export default function Stats() { onChange={setMonthlyModuleScoreDate} /> {monthlyModuleScoreDate && ( - )} @@ -446,7 +453,10 @@ export default function Stats() { onChange={setDailyScoreDate} /> {dailyScoreDate && ( - )} @@ -518,7 +528,10 @@ export default function Stats() { onChange={setDailyScoreDate} /> {dailyScoreDate && ( - )}