diff --git a/app/components/grades/average-chart.tsx b/app/components/grades/average-chart.tsx new file mode 100644 index 0000000..3f5a7b9 --- /dev/null +++ b/app/components/grades/average-chart.tsx @@ -0,0 +1,110 @@ +import { CartesianGrid, Legend, Line, LineChart, XAxis, YAxis } from "recharts"; +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../ui/chart"; +import { getSubjectEmoji, getSubjectColor } from "~/lib/utils"; +import type { PeriodResult, User } from "~/lib/api"; +import { useEffect, useState } from "react"; + +export default function AverageChart({ + averages, + subjects, + user, +}: { + averages: PeriodResult[]; + subjects: string[]; + user: User; +}) { + const [hiddenSubjects, setHiddenSubjects] = useState([]); + const toggleSubjectVisibility = (subject: string) => { + setHiddenSubjects((prev) => + prev.includes(subject) + ? prev.filter((s) => s !== subject) + : [...prev, subject] + ); + }; + + const [min, setMin] = useState(0); + const [max, setMax] = useState(20); + useEffect(() => { + if (averages.length > 0) { + const filteredGrades = averages.map((o) => + Object.entries(o) + .filter(([key, value]) => { + return !hiddenSubjects.includes(key) && !isNaN(parseFloat(value)); + }) + .map(([_, v]) => parseFloat(v)) + .filter(Boolean) + ); + setMin(getMin(filteredGrades)); + setMax(getMax(filteredGrades)); + } + }, [averages, hiddenSubjects]); + + return ( + + + + + + } /> + { + if (e.dataKey) { + toggleSubjectVisibility(e.dataKey as string); + } + }} + /> + {subjects.map((subject, index) => ( + + ))} + + + + ); +} + +function getMin(grades: number[][]) { + return Math.max( + Math.round( + Math.min(...grades.map((g) => Math.min(...g)).filter(Boolean)) - 1 + ), + 0 + ); +} + +function getMax(grades: number[][]) { + return Math.min( + Math.round( + Math.max(...grades.map((g) => Math.max(...g)).filter(Boolean)) + 1 + ), + 20 + ); +} diff --git a/app/components/grades/grades-chart.tsx b/app/components/grades/grades-chart.tsx new file mode 100644 index 0000000..4d708b3 --- /dev/null +++ b/app/components/grades/grades-chart.tsx @@ -0,0 +1,97 @@ +import { CartesianGrid, Legend, Line, LineChart, XAxis, YAxis } from "recharts"; +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../ui/chart"; +import { getSubjectEmoji, getSubjectColor } from "~/lib/utils"; +import type { Grade, User } from "~/lib/api"; +import { useEffect, useState } from "react"; +import { DateTime } from "luxon"; + +export default function GradesChart({ + grades, + subjects, + user, +}: { + grades: Grade[]; + subjects: string[]; + user: User; +}) { + const [hiddenSubjects, setHiddenSubjects] = useState([]); + const toggleSubjectVisibility = (subject: string) => { + setHiddenSubjects((prev) => + prev.includes(subject) + ? prev.filter((s) => s !== subject) + : [...prev, subject] + ); + }; + + const [min, setMin] = useState(0); + const [max, setMax] = useState(20); + useEffect(() => { + if (grades.length > 0) { + const filteredGrades = grades + .filter( + (g) => !hiddenSubjects.includes(g.subject) && !isNaN(Number(g.grade)) + ) + .map((g) => Number(g.grade)); + setMin(Math.max(0, Math.min(...filteredGrades) - 1)); + setMax(Math.min(20, Math.max(...filteredGrades) + 1)); + } + }, [grades, hiddenSubjects]); + + return ( + + + + + + } /> + { + if (e.dataKey) { + toggleSubjectVisibility(e.dataKey as string); + } + }} + /> + {subjects.map((subject, index) => ( + + ))} + + + ); +} + +function transformGrades(data: Grade[], subjects: string[]) { + const grouped: Record = {}; + + data.forEach((g) => { + const d = DateTime.fromMillis(g.date).toISODate()!; + if (!grouped[d]) { + grouped[d] = { date: d }; + // initialize all subjects with null + subjects.forEach((s) => (grouped[d][s] = null)); + } + grouped[d][g.subject] = Number(g.grade); // or keep as string + }); + + return Object.values(grouped); +} diff --git a/app/components/grades/index.tsx b/app/components/grades/index.tsx index 3d038d8..25ff1bb 100644 --- a/app/components/grades/index.tsx +++ b/app/components/grades/index.tsx @@ -1,23 +1,17 @@ import { - CartesianGrid, - Legend, - Line, - LineChart, PolarAngleAxis, PolarGrid, PolarRadiusAxis, Radar, RadarChart, - ResponsiveContainer, - XAxis, - YAxis, } from "recharts"; import { useAverages, useGrades, type User } from "~/lib/api"; import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../ui/chart"; import { Award } from "lucide-react"; -import { getSubjectColor, getSubjectEmoji } from "~/lib/utils"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Tabs, TabsList, tabsStyle, TabsTrigger } from "../ui/tabs"; +import AverageChart from "./average-chart"; +import GradesChart from "./grades-chart"; const periods = [ { @@ -37,7 +31,7 @@ const periods = [ export default function GradesPage({ user }: { user: User }) { const [period, setPeriod] = useState(periods[0].id); - const { grades, subjects, isLoading, isError } = useGrades(period); + const { grades, averages, subjects, isLoading, isError } = useGrades(period); const { subjectAverages, globalAverage, @@ -45,32 +39,6 @@ export default function GradesPage({ user }: { user: User }) { isError: error, } = useAverages(period); - const [hiddenSubjects, setHiddenSubjects] = useState([]); - const toggleSubjectVisibility = (subject: string) => { - setHiddenSubjects((prev) => - prev.includes(subject) - ? prev.filter((s) => s !== subject) - : [...prev, subject] - ); - }; - - const [min, setMin] = useState(0); - const [max, setMax] = useState(20); - useEffect(() => { - if (grades.length > 0) { - const filteredGrades = grades.map((o) => - Object.entries(o) - .filter(([key, value]) => { - return !hiddenSubjects.includes(key) && !isNaN(parseFloat(value)); - }) - .map(([_, v]) => parseFloat(v)) - .filter(Boolean) - ); - setMin(getMin(filteredGrades)); - setMax(getMax(filteredGrades)); - } - }, [grades, hiddenSubjects]); - return (
{/* Tabs */} @@ -135,61 +103,28 @@ export default function GradesPage({ user }: { user: User }) {
- - - - - - } /> - { - if (e.dataKey) { - toggleSubjectVisibility(e.dataKey as string); - } - }} - /> - {subjects.map((subject, index) => ( - - ))} - - - + +
+ + + {/* Grade Trends */} +
+
+
+

+ Vos moyennes par matière +

+

+ Comparez vos moyennes par matière +

+
+
+
+
@@ -235,21 +170,3 @@ export default function GradesPage({ user }: { user: User }) { ); } - -function getMin(grades: number[][]) { - return Math.max( - Math.round( - Math.min(...grades.map((g) => Math.min(...g)).filter(Boolean)) - 1 - ), - 0 - ); -} - -function getMax(grades: number[][]) { - return Math.min( - Math.round( - Math.max(...grades.map((g) => Math.max(...g)).filter(Boolean)) + 1 - ), - 20 - ); -} diff --git a/app/lib/api.ts b/app/lib/api.ts index 9f2f2e0..5c15d64 100644 --- a/app/lib/api.ts +++ b/app/lib/api.ts @@ -405,11 +405,13 @@ export const useGrades = (period: string) => { days: 3, // 3 days }).toMillis(), }); - const grades = (data?.grades as PeriodResult[]) || []; + const averages = (data?.averages as PeriodResult[]) || []; const subjects = (data?.subjects as string[]) || []; + const grades = (data?.grades as Grade[]) || []; return { - grades, + averages, subjects, + grades, ...props, }; }; @@ -419,11 +421,17 @@ interface SubjectPerformance { average: number; } -interface PeriodResult { +export interface PeriodResult { period: string; average: number; } +export interface Grade { + subject: string; + date: number; // timestamp + grade: string; +} + export const getAverages = async (period: string) => { return makeRequest( `/averages?period=${encodeURIComponent(period)}`,