Finished updating the stats page according to the client's requests

This commit is contained in:
Tiago Ribeiro
2023-12-27 09:14:13 +00:00
parent 438778a03c
commit fddc3ff2f3
2 changed files with 538 additions and 228 deletions

View File

@@ -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 (
<div
key={module}
className={clsx(
"flex gap-2 items-center w-fit text-white -md:px-4 xl:px-4 md:px-2 py-2 rounded-xl",
module === "reading" && "bg-ielts-reading",
module === "listening" && "bg-ielts-listening",
module === "writing" && "bg-ielts-writing",
module === "speaking" && "bg-ielts-speaking",
module === "level" && "bg-ielts-level",
)}>
{module === "reading" && <BsBook className="w-4 h-4" />}
{module === "listening" && <BsHeadphones className="w-4 h-4" />}
{module === "writing" && <BsPen className="w-4 h-4" />}
{module === "speaking" && <BsMegaphone className="w-4 h-4" />}
{module === "level" && <BsClipboard className="w-4 h-4" />}
<span className="text-sm">{children}</span>
</div>
);
}

View File

@@ -1,6 +1,6 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import Head from "next/head"; 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 {LinearScale, Chart as ChartJS, CategoryScale, PointElement, LineElement, Legend, Tooltip, LineController} from "chart.js";
import {withIronSessionSsr} from "iron-session/next"; import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session"; import {sessionOptions} from "@/lib/session";
@@ -25,10 +25,11 @@ import ProfileSummary from "@/components/ProfileSummary";
import moment from "moment"; import moment from "moment";
import {Stat} from "@/interfaces/user"; import {Stat} from "@/interfaces/user";
import {Divider} from "primereact/divider"; import {Divider} from "primereact/divider";
import Badge from "@/components/Low/Badge";
ChartJS.register(LinearScale, CategoryScale, PointElement, LineElement, LineController, Legend, Tooltip); 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}) => { export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
@@ -62,14 +63,15 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
export default function Stats() { export default function Stats() {
const [statsUserId, setStatsUserId] = useState<string>(); const [statsUserId, setStatsUserId] = useState<string>();
const [startDate, setStartDate] = useState<Date | null>(moment("01/01/2023").toDate()); const [startDate, setStartDate] = useState<Date | null>(moment(new Date()).subtract(1, "weeks").toDate());
const [endDate, setEndDate] = useState<Date | null>(new Date()); const [endDate, setEndDate] = useState<Date | null>(new Date());
const [displayStats, setDisplayStats] = useState<Stat[]>([]);
const [initialStatDate, setInitialStatDate] = useState<Date>(); const [initialStatDate, setInitialStatDate] = useState<Date>();
const [monthlyOverallScoreDate, setMonthlyOverallScoreDate] = useState<Date | null>(new Date()); const [monthlyOverallScoreDate, setMonthlyOverallScoreDate] = useState<Date | null>(new Date());
const [monthlyModuleScoreDate, setMonthlyModuleScoreDate] = useState<Date | null>(new Date()); const [monthlyModuleScoreDate, setMonthlyModuleScoreDate] = useState<Date | null>(new Date());
const [monthlyOverallGraphScoreDate, setMonthlyOverallGraphScoreDate] = useState<Date | null>(new Date());
const [dailyScoreDate, setDailyScoreDate] = useState<Date | null>(new Date());
const [intervalDates, setIntervalDates] = useState<Date[]>([]);
const {user} = useUser({redirectTo: "/login"}); const {user} = useUser({redirectTo: "/login"});
const {users} = useUsers(); const {users} = useUsers();
@@ -81,17 +83,6 @@ export default function Stats() {
if (user) setStatsUserId(user.id); if (user) setStatsUserId(user.id);
}, [user]); }, [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(() => { useEffect(() => {
setInitialStatDate( setInitialStatDate(
stats stats
@@ -103,10 +94,6 @@ export default function Stats() {
); );
}, [stats]); }, [stats]);
useEffect(() => {
setStartDate(initialStatDate || moment("01/01/2023").toDate());
}, [initialStatDate]);
const calculateModuleScore = (stats: Stat[]) => { const calculateModuleScore = (stats: Stat[]) => {
const moduleStats = groupByModule(stats); const moduleStats = groupByModule(stats);
return Object.keys(moduleStats).map((y) => { return Object.keys(moduleStats).map((y) => {
@@ -120,35 +107,6 @@ export default function Stats() {
}); });
}; };
const calculateTotalScorePerKey = (stats: Stat[], keyFunction: (stats: Stat[]) => Dictionary<Stat[]>) => {
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 calculateModularScorePerSession = (stats: Stat[], module: Module) => {
const groupedBySession = groupBySession(stats); const groupedBySession = groupBySession(stats);
const sessionAverage = Object.keys(groupedBySession).map((x: string) => { const sessionAverage = Object.keys(groupedBySession).map((x: string) => {
@@ -164,6 +122,33 @@ export default function Stats() {
return sessionAverage; 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 ( return (
<> <>
<Head> <Head>
@@ -238,11 +223,21 @@ export default function Stats() {
</div> </div>
{stats.length > 0 && ( {stats.length > 0 && (
<>
<div className="flex -md:flex-col -md:items-center gap-4 flex-wrap"> <div className="flex -md:flex-col -md:items-center gap-4 flex-wrap">
{/* Overall Level per Month */} {/* Overall Level per Month */}
<div className="flex flex-col items-center gap-4 border w-full h-[420px] overflow-y-scroll scrollbar-hide md:max-w-sm border-mti-gray-platinum p-4 pb-12 rounded-xl"> <div className="flex flex-col items-center gap-4 border w-full h-[420px] overflow-y-scroll scrollbar-hide md:max-w-sm border-mti-gray-platinum p-4 pb-12 rounded-xl">
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
<span className="text-sm font-bold">Overall Level per Month</span> <span className="text-sm font-bold">Overall Level per Month</span>
<div className="flex gap-2 items-center">
{monthlyOverallScoreDate && (
<button
onClick={() =>
setMonthlyOverallScoreDate((prev) => moment(prev).subtract(1, "months").toDate())
}>
<BsChevronLeft />
</button>
)}
<DatePicker <DatePicker
dateFormat="MMMM yyyy" dateFormat="MMMM yyyy"
className="border border-mti-gray-dim/40 px-2 py-1.5 rounded-lg text-center w-[200px]" className="border border-mti-gray-dim/40 px-2 py-1.5 rounded-lg text-center w-[200px]"
@@ -252,6 +247,16 @@ export default function Stats() {
showMonthYearPicker showMonthYearPicker
onChange={setMonthlyOverallScoreDate} onChange={setMonthlyOverallScoreDate}
/> />
{monthlyOverallScoreDate && (
<button
onClick={() => setMonthlyOverallScoreDate((prev) => moment(prev).add(1, "months").toDate())}>
<BsChevronRight />
</button>
)}
<button onClick={() => setMonthlyOverallScoreDate(new Date())}>
<BsArrowClockwise />
</button>
</div>
</div> </div>
<div className="w-full grid grid-cols-3 gap-4 items-center"> <div className="w-full grid grid-cols-3 gap-4 items-center">
{[...Array(31).keys()].map((day) => { {[...Array(31).keys()].map((day) => {
@@ -283,6 +288,15 @@ export default function Stats() {
<div className="w-full md:max-w-2xl border border-mti-gray-platinum p-4 pb-12 rounded-xl h-fit md:h-[420px]"> <div className="w-full md:max-w-2xl border border-mti-gray-platinum p-4 pb-12 rounded-xl h-fit md:h-[420px]">
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
<span className="text-sm font-bold">Overall Level per Month</span> <span className="text-sm font-bold">Overall Level per Month</span>
<div className="flex gap-2 items-center">
{monthlyOverallScoreDate && (
<button
onClick={() =>
setMonthlyOverallScoreDate((prev) => moment(prev).subtract(1, "months").toDate())
}>
<BsChevronLeft />
</button>
)}
<DatePicker <DatePicker
dateFormat="MMMM yyyy" dateFormat="MMMM yyyy"
className="border border-mti-gray-dim/40 px-2 py-1.5 rounded-lg text-center w-[200px]" className="border border-mti-gray-dim/40 px-2 py-1.5 rounded-lg text-center w-[200px]"
@@ -292,6 +306,16 @@ export default function Stats() {
showMonthYearPicker showMonthYearPicker
onChange={setMonthlyOverallScoreDate} onChange={setMonthlyOverallScoreDate}
/> />
{monthlyOverallScoreDate && (
<button
onClick={() => setMonthlyOverallScoreDate((prev) => moment(prev).add(1, "months").toDate())}>
<BsChevronRight />
</button>
)}
<button onClick={() => setMonthlyOverallScoreDate(new Date())}>
<BsArrowClockwise />
</button>
</div>
</div> </div>
<Chart <Chart
type="line" type="line"
@@ -342,6 +366,15 @@ export default function Stats() {
<div className="flex flex-col gap-8 border w-full h-fit md:h-[420px] md:max-w-xs border-mti-gray-platinum p-4 pb-12 rounded-xl"> <div className="flex flex-col gap-8 border w-full h-fit md:h-[420px] md:max-w-xs border-mti-gray-platinum p-4 pb-12 rounded-xl">
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
<span className="text-sm font-bold">Module Level per Day</span> <span className="text-sm font-bold">Module Level per Day</span>
<div className="flex gap-2 items-center">
{monthlyModuleScoreDate && (
<button
onClick={() =>
setMonthlyModuleScoreDate((prev) => moment(prev).subtract(1, "days").toDate())
}>
<BsChevronLeft />
</button>
)}
<DatePicker <DatePicker
dateFormat="dd MMMM yyyy" dateFormat="dd MMMM yyyy"
className="border border-mti-gray-dim/40 px-2 py-1.5 rounded-lg text-center w-[200px]" className="border border-mti-gray-dim/40 px-2 py-1.5 rounded-lg text-center w-[200px]"
@@ -350,6 +383,15 @@ export default function Stats() {
selected={monthlyModuleScoreDate} selected={monthlyModuleScoreDate}
onChange={setMonthlyModuleScoreDate} onChange={setMonthlyModuleScoreDate}
/> />
{monthlyModuleScoreDate && (
<button onClick={() => setMonthlyModuleScoreDate((prev) => moment(prev).add(1, "days").toDate())}>
<BsChevronRight />
</button>
)}
<button onClick={() => setMonthlyModuleScoreDate(new Date())}>
<BsArrowClockwise />
</button>
</div>
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{calculateModuleScore(stats.filter((s) => timestampToMoment(s).isBefore(moment(monthlyModuleScoreDate)))) {calculateModuleScore(stats.filter((s) => timestampToMoment(s).isBefore(moment(monthlyModuleScoreDate))))
@@ -362,17 +404,157 @@ export default function Stats() {
</span> </span>
<span className="text-xs">{capitalize(module)}</span> <span className="text-xs">{capitalize(module)}</span>
</div> </div>
<ProgressBar color={module as Module} percentage={(score * 100) / 9} label="" className="h-3" /> <ProgressBar
color={module as Module}
percentage={(score * 100) / 9}
label=""
className="h-3"
/>
</div> </div>
))} ))}
</div> </div>
</div> </div>
</div> </div>
)}
<Divider /> <Divider />
{displayStats.length > 0 && ( <div className="flex -md:flex-col -md:items-center gap-4 flex-wrap">
{/* Module Level per Exam */}
<div className="flex flex-col items-center gap-4 border w-full h-[420px] overflow-y-scroll scrollbar-hide md:max-w-sm border-mti-gray-platinum p-4 pb-12 rounded-xl">
<div className="flex flex-col gap-2 w-full">
<span className="text-sm font-bold">Module Level per Exam</span>
<div className="flex gap-2 items-center">
{dailyScoreDate && (
<button onClick={() => setDailyScoreDate((prev) => moment(prev).subtract(1, "days").toDate())}>
<BsChevronLeft />
</button>
)}
<DatePicker
dateFormat="dd MMMM yyyy"
className="border border-mti-gray-dim/40 px-2 py-1.5 rounded-lg text-center w-[200px]"
minDate={initialStatDate}
maxDate={new Date()}
selected={dailyScoreDate}
onChange={setDailyScoreDate}
/>
{dailyScoreDate && (
<button onClick={() => setDailyScoreDate((prev) => moment(prev).add(1, "days").toDate())}>
<BsChevronRight />
</button>
)}
<button onClick={() => setDailyScoreDate(new Date())}>
<BsArrowClockwise />
</button>
</div>
</div>
<div className="w-full grid grid-cols-1 gap-6 items-center">
{Object.keys(
groupBySession(
stats.filter(
(s) =>
Math.abs(timestampToMoment(s).diff(moment(dailyScoreDate), "days")) === 0 &&
timestampToMoment(s).day() === moment(dailyScoreDate).day(),
),
),
).length === 0 && <span className="font-semibold ml-1">No exams performed this day...</span>}
{Object.keys(
groupBySession(
stats.filter(
(s) =>
Math.abs(timestampToMoment(s).diff(moment(dailyScoreDate), "days")) === 0 &&
timestampToMoment(s).day() === moment(dailyScoreDate).day(),
),
),
).map((session, index) => (
<div key={index} className="flex flex-col gap-2 items-start rounded-lg overflow-hidden">
<span className="bg-mti-purple-ultralight w-full px-2 py-1 font-semibold">
Exam {(index + 1).toString().padStart(2, "0")}
</span>
<div className="flex justify-between w-full">
{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 : <Badge module={module}>{score.toFixed(1)}</Badge>;
}).filter((m) => !!m)}
</div>
</div>
))}
</div>
</div>
<div className="w-full md:max-w-2xl border border-mti-gray-platinum p-4 pb-12 rounded-xl h-[420px]">
<div className="flex flex-col gap-2 w-full mb-2">
<span className="text-sm font-bold">Module Level per Exam</span>
<div className="flex gap-2 items-center">
{dailyScoreDate && (
<button onClick={() => setDailyScoreDate((prev) => moment(prev).subtract(1, "days").toDate())}>
<BsChevronLeft />
</button>
)}
<DatePicker
dateFormat="dd MMMM yyyy"
className="border border-mti-gray-dim/40 px-2 py-1.5 rounded-lg text-center w-[200px]"
minDate={initialStatDate}
maxDate={new Date()}
selected={dailyScoreDate}
onChange={setDailyScoreDate}
/>
{dailyScoreDate && (
<button onClick={() => setDailyScoreDate((prev) => moment(prev).add(1, "days").toDate())}>
<BsChevronRight />
</button>
)}
<button onClick={() => setDailyScoreDate(new Date())}>
<BsArrowClockwise />
</button>
</div>
</div>
<Chart
type="line"
data={{
labels: Object.keys(
groupBySession(
stats.filter(
(s) =>
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,
label: capitalize(module),
borderColor: COLORS[index],
backgroundColor: COLORS[index],
borderWidth: 2,
data: calculateModularScorePerSession(
stats.filter(
(s) =>
Math.abs(timestampToMoment(s).diff(moment(dailyScoreDate), "days")) === 0 &&
timestampToMoment(s).day() === moment(dailyScoreDate).day(),
),
module,
),
})),
],
}}
/>
</div>
</div>
<Divider />
<div className="w-full flex flex-col gap-4"> <div className="w-full flex flex-col gap-4">
<DatePicker <DatePicker
dateFormat="dd/MM/yyyy" dateFormat="dd/MM/yyyy"
@@ -383,51 +565,148 @@ export default function Stats() {
showMonthDropdown showMonthDropdown
filterDate={(date) => moment(date).isSameOrBefore(moment(new Date()))} filterDate={(date) => moment(date).isSameOrBefore(moment(new Date()))}
onChange={([initialDate, finalDate]) => { onChange={([initialDate, finalDate]) => {
setStartDate(initialDate ?? moment("01/01/2023").toDate()); setStartDate(initialDate);
setEndDate(finalDate); setEndDate(finalDate);
}} }}
/> />
<div className="flex -md:flex-col -md:items-center gap-4 flex-wrap"> <div className="flex -md:flex-col -md:items-center gap-4 flex-wrap">
{/* Module Score Band per Session */} {/* Reading Score Band in Interval */}
<div className="w-full md:max-w-2xl border border-mti-gray-platinum p-4 pb-12 rounded-xl h-fit md:h-96"> <div className="w-full md:max-w-2xl border border-mti-gray-platinum p-4 pb-12 rounded-xl h-fit md:h-96">
<span className="text-sm font-bold">Module Score Band per Session</span> <span className="text-sm font-bold">Reading Score Band in Interval</span>
<Chart <Chart
type="line" type="line"
data={{ data={{
labels: Object.keys(groupBySession(displayStats)).map((_, index) => index), labels: intervalDates.map((date) => moment(date).format("DD/MM/YYYY")),
datasets: [ datasets: [
...MODULE_ARRAY.map((module, index) => ({ {
type: "line" as const, type: "line",
label: capitalize(module), label: "Reading",
borderColor: COLORS[index], fill: false,
backgroundColor: COLORS[index], borderColor: COLORS[0],
backgroundColor: COLORS[0],
borderWidth: 2, borderWidth: 2,
data: calculateModularScorePerSession(displayStats, module), spanGaps: true,
})), data: intervalDates.map((date) => {
return calculateTotalScore(
stats.filter(
(s) => timestampToMoment(s).isBefore(date) && s.module === "reading",
),
).toFixed(1);
}),
},
], ],
}} }}
/> />
</div> </div>
{/* Average Time per Module */} {/* Listening Score Band in Interval */}
<div className="w-full md:max-w-2xl border border-mti-gray-platinum p-4 pb-12 rounded-xl h-fit md:h-96"> <div className="w-full md:max-w-2xl border border-mti-gray-platinum p-4 pb-12 rounded-xl h-fit md:h-96">
<span className="text-sm font-bold">Average Time per Module (in Minutes)</span> <span className="text-sm font-bold">Listening Score Band in Interval</span>
<Chart <Chart
type="line" type="line"
data={{ data={{
labels: Object.keys(groupBySession(displayStats.filter((s) => !!s.timeSpent))).map( labels: intervalDates.map((date) => moment(date).format("DD/MM/YYYY")),
(_, index) => index,
),
datasets: [ datasets: [
{ {
type: "line", type: "line",
label: "Average (in minutes)", label: "Listening",
fill: false, fill: false,
borderColor: "#6A5FB1", borderColor: COLORS[1],
backgroundColor: "#7872BF", backgroundColor: COLORS[1],
borderWidth: 2, borderWidth: 2,
spanGaps: true, spanGaps: true,
data: calculateAverageTimePerModule(displayStats), data: intervalDates.map((date) => {
return calculateTotalScore(
stats.filter(
(s) => timestampToMoment(s).isBefore(date) && s.module === "listening",
),
).toFixed(1);
}),
},
],
}}
/>
</div>
{/* Writing Score Band in Interval */}
<div className="w-full md:max-w-2xl border border-mti-gray-platinum p-4 pb-12 rounded-xl h-fit md:h-96">
<span className="text-sm font-bold">Writing Score Band in Interval</span>
<Chart
type="line"
data={{
labels: intervalDates.map((date) => 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);
}),
},
],
}}
/>
</div>
{/* Speaking Score Band in Interval */}
<div className="w-full md:max-w-2xl border border-mti-gray-platinum p-4 pb-12 rounded-xl h-fit md:h-96">
<span className="text-sm font-bold">Speaking Score Band in Interval</span>
<Chart
type="line"
data={{
labels: intervalDates.map((date) => 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);
}),
},
],
}}
/>
</div>
{/* Level Score Band in Interval */}
<div className="w-full md:max-w-2xl border border-mti-gray-platinum p-4 pb-12 rounded-xl h-fit md:h-96">
<span className="text-sm font-bold">Level Score Band in Interval</span>
<Chart
type="line"
data={{
labels: intervalDates.map((date) => 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);
}),
}, },
], ],
}} }}
@@ -435,9 +714,10 @@ export default function Stats() {
</div> </div>
</div> </div>
</div> </div>
</>
)} )}
</section> </section>
{displayStats.length === 0 && ( {stats.length === 0 && (
<section className="flex flex-col gap-3"> <section className="flex flex-col gap-3">
<span className="font-semibold ml-1">No stats to display...</span> <span className="font-semibold ml-1">No stats to display...</span>
</section> </section>