frontend/app/components/grades/index.tsx
2025-08-20 18:02:22 +02:00

159 lines
4.9 KiB
TypeScript

import {
CartesianGrid,
Legend,
Line,
LineChart,
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";
export default function GradesPage({ user }: { user: User }) {
// TODO: Error handling, loading state
const { grades, subjects, isLoading, isError } = useGrades("YEAR");
const {
subjectAverages,
globalAverage,
isLoading: loading,
isError: error,
} = useAverages("YEAR");
const [hiddenSubjects, setHiddenSubjects] = useState<string[]>([]);
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 (
<div className="space-y-6 pb-20 md:pb-0">
{/* Tabs */}
{/* Global average */}
<div className="bg-muted/80 dark:bg-muted/30 rounded-lg p-4 space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs md:text-sm font-medium text-muted-foreground">
Moyenne générale
</span>
<Award className="h-4 w-4 text-muted-foreground" />
</div>
<div className="text-xl md:text-2xl font-bold">{globalAverage} / 20</div>
</div>
{/* Charts - Mobile First Layout */}
<div className="space-y-8">
{/* Grade Trends */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg md:text-xl font-semibold">
Votre progression
</h2>
<p className="text-sm text-muted-foreground">
Suivez vos notes au fil du temps
</p>
</div>
</div>
<div className="bg-muted/80 dark:bg-muted/30 rounded-lg py-4">
<ChartContainer
config={{}}
className="h-full w-full -ml-6 max-w-2xl"
>
<LineChart data={grades}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="period" />
{/* TODO: Custom domain */}
<YAxis domain={[min, max]} />
<ChartTooltip content={<ChartTooltipContent />} />
<Legend
onClick={(e) => {
if (e.dataKey) {
toggleSubjectVisibility(e.dataKey as string);
}
}}
/>
{subjects.map((subject, index) => (
<Line
key={index}
type="monotone"
dataKey={subject}
name={
subject + " " + getSubjectEmoji(subject, user.preferences)
}
hide={hiddenSubjects.includes(subject)}
stroke={`var(--color-${getSubjectColor(
subject,
user.preferences
)}-800`}
dot={{
fill: `var(--color-${getSubjectColor(
subject,
user.preferences
)}-800`,
stroke: `var(--color-${getSubjectColor(
subject,
user.preferences
)}-800)`,
}}
/>
))}
<Line
type="monotone"
dataKey="average"
name="Moyenne"
stroke="var(--primary)"
strokeWidth={2}
dot={false}
strokeDasharray="5 5"
hide={hiddenSubjects.includes("average")}
/>
</LineChart>
</ChartContainer>
</div>
</div>
</div>
</div>
);
}
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
);
}