255 lines
8 KiB
TypeScript
255 lines
8 KiB
TypeScript
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 { Tabs, TabsList, tabsStyle, TabsTrigger } from "../ui/tabs";
|
|
|
|
const periods = [
|
|
{
|
|
id: "YEAR",
|
|
label: "Cette année",
|
|
},
|
|
{
|
|
id: "TRIMESTER",
|
|
label: "Ce trimestre",
|
|
},
|
|
{
|
|
id: "MONTH",
|
|
label: "Ce mois",
|
|
},
|
|
];
|
|
|
|
export default function GradesPage({ user }: { user: User }) {
|
|
const [period, setPeriod] = useState(periods[0].id);
|
|
|
|
const { grades, subjects, isLoading, isError } = useGrades(period);
|
|
const {
|
|
subjectAverages,
|
|
globalAverage,
|
|
isLoading: loading,
|
|
isError: error,
|
|
} = useAverages(period);
|
|
|
|
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 */}
|
|
<Tabs
|
|
defaultValue={periods[0].id}
|
|
value={period}
|
|
onValueChange={setPeriod}
|
|
className="max-w-md w-full"
|
|
>
|
|
<TabsList className="w-full p-0 bg-background justify-start border-b rounded-none">
|
|
{periods.map((period) => (
|
|
<TabsTrigger
|
|
key={period.id}
|
|
value={period.id}
|
|
className={tabsStyle}
|
|
>
|
|
{period.label}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
</Tabs>
|
|
|
|
{isNaN(globalAverage) ? (
|
|
<div className="text-center text-muted-foreground">
|
|
Aucune note disponible pour cette période.
|
|
</div>
|
|
) : isLoading || loading ? (
|
|
<div className="flex items-center justify-center h-64">
|
|
<span className="text-muted-foreground">Chargement...</span>
|
|
</div>
|
|
) : isError || error ? (
|
|
<div className="text-center text-red-500">
|
|
Une erreur est survenue lors de la récupération des données.
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Global average */}
|
|
<div className="bg-muted/50 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/50 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" />
|
|
<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>
|
|
|
|
{/* Radar chart */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-lg md:text-xl font-semibold">
|
|
Vos points forts
|
|
</h2>
|
|
<p className="text-sm text-muted-foreground">
|
|
Visualisez vos performances par matière
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="bg-muted/50 rounded-lg py-4">
|
|
<ChartContainer
|
|
config={{}}
|
|
className="h-full w-full -ml-6 max-w-2xl"
|
|
>
|
|
<RadarChart
|
|
data={subjectAverages}
|
|
margin={{ top: 10, right: 10, bottom: 10, left: 10 }}
|
|
>
|
|
<PolarGrid />
|
|
<PolarAngleAxis dataKey="subject" />
|
|
<PolarRadiusAxis angle={90} domain={[0, 20]} />
|
|
<Radar
|
|
name="Moyenne"
|
|
dataKey="average"
|
|
stroke="var(--chart-2)"
|
|
fill="var(--chart-2)"
|
|
fillOpacity={0.3}
|
|
/>
|
|
<ChartTooltip content={<ChartTooltipContent />} />
|
|
</RadarChart>
|
|
</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
|
|
);
|
|
}
|