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