diff --git a/app/app.css b/app/app.css index c4fe699..0885b4f 100644 --- a/app/app.css +++ b/app/app.css @@ -153,3 +153,26 @@ body { [data-sonner-toaster][data-sonner-theme='dark'] [data-description] { color: var(--primary) !important; } + +:root { + --color-red-800: theme('colors.red.800'); + --color-orange-800: theme('colors.orange.800'); + --color-amber-800: theme('colors.amber.800'); + --color-yellow-800: theme('colors.yellow.800'); + --color-lime-800: theme('colors.lime.800'); + --color-green-800: theme('colors.green.800'); + --color-emerald-800: theme('colors.emerald.800'); + --color-teal-800: theme('colors.teal.800'); + --color-cyan-800: theme('colors.cyan.800'); + --color-sky-800: theme('colors.sky.800'); + --color-blue-800: theme('colors.blue.800'); + --color-indigo-800: theme('colors.indigo.800'); + --color-violet-800: theme('colors.violet.800'); + --color-purple-800: theme('colors.purple.800'); + --color-fuchsia-800: theme('colors.fuchsia.800'); + --color-pink-800: theme('colors.pink.800'); + --color-rose-800: theme('colors.rose.800'); + --color-slate-800: theme('colors.slate.800'); + --color-gray-800: theme('colors.gray.800'); + --color-zinc-800: theme('colors.zinc.800'); +} diff --git a/app/components/details/attachment.tsx b/app/components/details/attachment.tsx index de4823e..bae3ba0 100644 --- a/app/components/details/attachment.tsx +++ b/app/components/details/attachment.tsx @@ -8,7 +8,7 @@ export default function AttachmentItem({ attachment }: { attachment: Attachment target="_blank" className="flex items-center gap-2 p-3 border rounded-md hover:bg-muted transition-colors cursor-pointer" > - {getIcon(attachment.name)} + {getIcon(attachment.path)} {attachment.name} diff --git a/app/components/grades/inde.tsx b/app/components/grades/inde.tsx index a98cc32..3c08c91 100644 --- a/app/components/grades/inde.tsx +++ b/app/components/grades/inde.tsx @@ -320,9 +320,6 @@ export default function SchoolStatsPage() {
{currentGPA}
- - Dean's List -
diff --git a/app/components/grades/index.tsx b/app/components/grades/index.tsx index dcef0d9..f9e1004 100644 --- a/app/components/grades/index.tsx +++ b/app/components/grades/index.tsx @@ -7,50 +7,131 @@ import { XAxis, YAxis, } from "recharts"; -import { useGrades, type User } from "~/lib/api"; +import { useAverages, useGrades, type User } from "~/lib/api"; import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../ui/chart"; -import { chartConfig, gradesTrendData } from "./inde"; +import { Award } from "lucide-react"; +import { getSubjectColor, getSubjectEmoji } from "~/lib/utils"; +import { useEffect, useState } from "react"; export default function GradesPage({ user }: { user: User }) { // TODO: Error handling, loading state - const { grades, subjects, isLoading, isError } = useGrades("YEAR") + const { grades, subjects, isLoading, isError } = useGrades("YEAR"); + const { + subjectAverages, + globalAverage, + isLoading: loading, + isError: error, + } = useAverages("YEAR"); + + 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 */} + {/* Global average */} +
+
+ + Moyenne générale + + +
+
{globalAverage} / 20
+
+ {/* Charts - Mobile First Layout */}
{/* Grade Trends */}
-

Grade Trends

+

+ Votre progression +

- performance across subjects + Suivez vos notes au fil du temps

-
- - - - - - {/* TODO: Custom domain */} - - } /> - - {subjects.map((subject, index) => ( - - ))} - - +
+ + + + + {/* TODO: Custom domain */} + + } /> + { + if (e.dataKey) { + toggleSubjectVisibility(e.dataKey as string); + } + }} + /> + {subjects.map((subject, index) => ( + + ))} + +
@@ -58,3 +139,21 @@ 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 70eaf8b..082b643 100644 --- a/app/lib/api.ts +++ b/app/lib/api.ts @@ -404,22 +404,47 @@ export const useGrades = (period: string) => { days: 3, // 3 days }).toMillis(), }); - const grades = data?.grades as PeriodResult[] || []; - const subjects = data?.subjects as string[] || []; + const grades = (data?.grades as PeriodResult[]) || []; + const subjects = (data?.subjects as string[]) || []; return { grades, subjects, ...props, }; -} +}; interface SubjectPerformance { - subject: string - average: number + subject: string; + average: number; } interface PeriodResult { - period: string - average: number - subjectAverages: SubjectPerformance[] + period: string; + average: number; } + +export const getAverages = async (period: string) => { + return makeRequest( + `/averages?period=${encodeURIComponent(period)}`, + "Échec de la récupération des moyennes" + ); +}; +export const useAverages = (period: string) => { + const { data, ...props } = useQuery({ + queryKey: ["averages", period], + queryFn: () => getAverages(period), + staleTime: Duration.fromObject({ + hours: 0, // 1 hour + }).toMillis(), + gcTime: Duration.fromObject({ + days: 3, // 3 days + }).toMillis(), + }); + const subjectAverages = (data?.subjectAverages as SubjectPerformance[]) || []; + const globalAverage = data?.globalAverage || 0; + return { + subjectAverages, + globalAverage, + ...props, + }; +};