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