feat: add radar chart
This commit is contained in:
parent
e6ba7e0d9a
commit
3e01fa47cd
2 changed files with 178 additions and 811 deletions
|
|
@ -1,729 +0,0 @@
|
||||||
import { useState } from "react";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { Badge } from "~/components/ui/badge";
|
|
||||||
import {
|
|
||||||
ChartContainer,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
} from "~/components/ui/chart";
|
|
||||||
import {
|
|
||||||
LineChart,
|
|
||||||
Line,
|
|
||||||
BarChart,
|
|
||||||
Bar,
|
|
||||||
PieChart,
|
|
||||||
Pie,
|
|
||||||
Cell,
|
|
||||||
AreaChart,
|
|
||||||
Area,
|
|
||||||
RadarChart,
|
|
||||||
PolarGrid,
|
|
||||||
PolarAngleAxis,
|
|
||||||
PolarRadiusAxis,
|
|
||||||
Radar,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
CartesianGrid,
|
|
||||||
ResponsiveContainer,
|
|
||||||
Legend,
|
|
||||||
Tooltip,
|
|
||||||
ReferenceLine,
|
|
||||||
} from "recharts";
|
|
||||||
import {
|
|
||||||
Maximize2,
|
|
||||||
X,
|
|
||||||
TrendingUp,
|
|
||||||
Award,
|
|
||||||
BookOpen,
|
|
||||||
Target,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
export const gradesTrendData = {
|
|
||||||
"This Month": [
|
|
||||||
{
|
|
||||||
period: "Week 1",
|
|
||||||
math: 89,
|
|
||||||
science: 88,
|
|
||||||
english: 96,
|
|
||||||
history: 92,
|
|
||||||
average: 91.25,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
period: "Week 2",
|
|
||||||
math: 91,
|
|
||||||
science: 89,
|
|
||||||
english: 97,
|
|
||||||
history: 93,
|
|
||||||
average: 92.5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
period: "Week 3",
|
|
||||||
math: 94,
|
|
||||||
science: 91,
|
|
||||||
english: 93,
|
|
||||||
history: 95,
|
|
||||||
average: 93.25,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
period: "Week 4",
|
|
||||||
math: 92,
|
|
||||||
science: 90,
|
|
||||||
english: 98,
|
|
||||||
history: 94,
|
|
||||||
average: 93.5,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"3 Months": [
|
|
||||||
{
|
|
||||||
period: "Dec",
|
|
||||||
math: 89,
|
|
||||||
science: 88,
|
|
||||||
english: 96,
|
|
||||||
history: 92,
|
|
||||||
average: 91.25,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
period: "Jan",
|
|
||||||
math: 94,
|
|
||||||
science: 91,
|
|
||||||
english: 93,
|
|
||||||
history: 95,
|
|
||||||
average: 93.25,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
period: "Feb",
|
|
||||||
math: 91,
|
|
||||||
science: 89,
|
|
||||||
english: 97,
|
|
||||||
history: 93,
|
|
||||||
average: 92.5,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"Whole Year": [
|
|
||||||
{
|
|
||||||
period: "Sep",
|
|
||||||
math: 85,
|
|
||||||
science: 78,
|
|
||||||
english: 92,
|
|
||||||
history: 88,
|
|
||||||
average: 85.75,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
period: "Oct",
|
|
||||||
math: 88,
|
|
||||||
science: 82,
|
|
||||||
english: 89,
|
|
||||||
history: 91,
|
|
||||||
average: 87.5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
period: "Nov",
|
|
||||||
math: 92,
|
|
||||||
science: 85,
|
|
||||||
english: 94,
|
|
||||||
history: 89,
|
|
||||||
average: 90,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
period: "Dec",
|
|
||||||
math: 89,
|
|
||||||
science: 88,
|
|
||||||
english: 96,
|
|
||||||
history: 92,
|
|
||||||
average: 91.25,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
period: "Jan",
|
|
||||||
math: 94,
|
|
||||||
science: 91,
|
|
||||||
english: 93,
|
|
||||||
history: 95,
|
|
||||||
average: 93.25,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
period: "Feb",
|
|
||||||
math: 91,
|
|
||||||
science: 89,
|
|
||||||
english: 97,
|
|
||||||
history: 93,
|
|
||||||
average: 92.5,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const subjectPerformanceData = {
|
|
||||||
"This Month": [
|
|
||||||
{ subject: "Mathematics", grade: 94, target: 90, credits: 4 },
|
|
||||||
{ subject: "Science", grade: 91, target: 85, credits: 4 },
|
|
||||||
{ subject: "English", grade: 98, target: 92, credits: 3 },
|
|
||||||
{ subject: "History", grade: 95, target: 88, credits: 3 },
|
|
||||||
{ subject: "Art", grade: 96, target: 90, credits: 2 },
|
|
||||||
{ subject: "PE", grade: 90, target: 85, credits: 1 },
|
|
||||||
],
|
|
||||||
"3 Months": [
|
|
||||||
{ subject: "Mathematics", grade: 92, target: 90, credits: 4 },
|
|
||||||
{ subject: "Science", grade: 90, target: 85, credits: 4 },
|
|
||||||
{ subject: "English", grade: 96, target: 92, credits: 3 },
|
|
||||||
{ subject: "History", grade: 93, target: 88, credits: 3 },
|
|
||||||
{ subject: "Art", grade: 95, target: 90, credits: 2 },
|
|
||||||
{ subject: "PE", grade: 89, target: 85, credits: 1 },
|
|
||||||
],
|
|
||||||
"Whole Year": [
|
|
||||||
{ subject: "Mathematics", grade: 91, target: 90, credits: 4 },
|
|
||||||
{ subject: "Science", grade: 89, target: 85, credits: 4 },
|
|
||||||
{ subject: "English", grade: 97, target: 92, credits: 3 },
|
|
||||||
{ subject: "History", grade: 93, target: 88, credits: 3 },
|
|
||||||
{ subject: "Art", grade: 95, target: 90, credits: 2 },
|
|
||||||
{ subject: "PE", grade: 88, target: 85, credits: 1 },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const gradeDistributionData = {
|
|
||||||
"This Month": [
|
|
||||||
{ grade: "A+", count: 12, percentage: 35 },
|
|
||||||
{ grade: "A", count: 15, percentage: 44 },
|
|
||||||
{ grade: "A-", count: 5, percentage: 15 },
|
|
||||||
{ grade: "B+", count: 2, percentage: 6 },
|
|
||||||
{ grade: "B", count: 0, percentage: 0 },
|
|
||||||
],
|
|
||||||
"3 Months": [
|
|
||||||
{ grade: "A+", count: 10, percentage: 30 },
|
|
||||||
{ grade: "A", count: 14, percentage: 42 },
|
|
||||||
{ grade: "A-", count: 6, percentage: 18 },
|
|
||||||
{ grade: "B+", count: 3, percentage: 9 },
|
|
||||||
{ grade: "B", count: 1, percentage: 3 },
|
|
||||||
],
|
|
||||||
"Whole Year": [
|
|
||||||
{ grade: "A+", count: 8, percentage: 25 },
|
|
||||||
{ grade: "A", count: 12, percentage: 37.5 },
|
|
||||||
{ grade: "A-", count: 6, percentage: 18.75 },
|
|
||||||
{ grade: "B+", count: 4, percentage: 12.5 },
|
|
||||||
{ grade: "B", count: 2, percentage: 6.25 },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const skillsRadar = [
|
|
||||||
{ skill: "Problem Solving", score: 92 },
|
|
||||||
{ skill: "Critical Thinking", score: 88 },
|
|
||||||
{ skill: "Communication", score: 95 },
|
|
||||||
{ skill: "Creativity", score: 87 },
|
|
||||||
{ skill: "Collaboration", score: 91 },
|
|
||||||
{ skill: "Leadership", score: 85 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const COLORS = [
|
|
||||||
"#8884d8",
|
|
||||||
"#82ca9d",
|
|
||||||
"#ffc658",
|
|
||||||
"#ff7300",
|
|
||||||
"#00ff00",
|
|
||||||
"#ff00ff",
|
|
||||||
];
|
|
||||||
export const chartConfig = {
|
|
||||||
math: { label: "Mathematics", color: "hsl(var(--chart-1))" },
|
|
||||||
science: { label: "Science", color: "hsl(var(--chart-2))" },
|
|
||||||
english: { label: "English", color: "hsl(var(--chart-3))" },
|
|
||||||
history: { label: "History", color: "hsl(var(--chart-4))" },
|
|
||||||
average: { label: "Average", color: "hsl(var(--chart-5))" },
|
|
||||||
};
|
|
||||||
export default function SchoolStatsPage() {
|
|
||||||
const [fullscreenChart, setFullscreenChart] = useState<string | null>(null);
|
|
||||||
const [selectedPeriod, setSelectedPeriod] = useState<
|
|
||||||
"This Month" | "3 Months" | "Whole Year"
|
|
||||||
>("3 Months");
|
|
||||||
|
|
||||||
const gradesTrend = gradesTrendData[selectedPeriod];
|
|
||||||
const subjectPerformance = subjectPerformanceData[selectedPeriod];
|
|
||||||
const gradeDistribution = gradeDistributionData[selectedPeriod];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const FullscreenModal = ({
|
|
||||||
chartId,
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
chartId: string;
|
|
||||||
title: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) => {
|
|
||||||
if (fullscreenChart !== chartId) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center p-2">
|
|
||||||
<div
|
|
||||||
className="bg-background rounded-lg w-full h-full flex flex-col
|
|
||||||
w-[100vh] h-[100vw] rotate-90 origin-center"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between p-3 border-b shrink-0">
|
|
||||||
<h2 className="text-lg md:text-xl lg:text-2xl font-bold rotate-0">
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setFullscreenChart(null)}
|
|
||||||
className="rotate-0"
|
|
||||||
>
|
|
||||||
<X className="h-5 w-5 md:h-6 md:w-6" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 md:p-4">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentGPA = 3.7;
|
|
||||||
const totalCredits = 17;
|
|
||||||
const completedAssignments = 156;
|
|
||||||
const upcomingTests = 3;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-background">
|
|
||||||
<div className="w-full max-w-7xl mx-auto px-4 py-6 space-y-8">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="text-center space-y-3">
|
|
||||||
<h1 className="text-2xl md:text-4xl font-bold">Academic Dashboard</h1>
|
|
||||||
<p className="text-sm md:text-base text-muted-foreground">
|
|
||||||
Track your grades and progress
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="bg-muted/50 rounded-lg p-1 flex gap-1">
|
|
||||||
{(["This Month", "3 Months", "Whole Year"] as const).map(
|
|
||||||
(period) => (
|
|
||||||
<Button
|
|
||||||
key={period}
|
|
||||||
variant={selectedPeriod === period ? "default" : "ghost"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setSelectedPeriod(period)}
|
|
||||||
className="text-xs md:text-sm"
|
|
||||||
>
|
|
||||||
{period}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Key Stats - Mobile First Grid */}
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
<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">
|
|
||||||
Current GPA
|
|
||||||
</span>
|
|
||||||
<Award className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<div className="text-xl md:text-2xl font-bold">{currentGPA}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
Credits
|
|
||||||
</span>
|
|
||||||
<BookOpen className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<div className="text-xl md:text-2xl font-bold">{totalCredits}</div>
|
|
||||||
<p className="text-xs text-muted-foreground">This semester</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
Assignments
|
|
||||||
</span>
|
|
||||||
<Target className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<div className="text-xl md:text-2xl font-bold">
|
|
||||||
{completedAssignments}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">Completed</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
Tests
|
|
||||||
</span>
|
|
||||||
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<div className="text-xl md:text-2xl font-bold">{upcomingTests}</div>
|
|
||||||
<p className="text-xs text-muted-foreground">Upcoming</p>
|
|
||||||
</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">
|
|
||||||
Grade Trends
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{selectedPeriod === "This Month"
|
|
||||||
? "Weekly"
|
|
||||||
: selectedPeriod === "3 Months"
|
|
||||||
? "Monthly"
|
|
||||||
: "Monthly"}{" "}
|
|
||||||
performance across subjects
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setFullscreenChart("trends")}
|
|
||||||
>
|
|
||||||
<Maximize2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="bg-muted/30 rounded-lg">
|
|
||||||
<ChartContainer
|
|
||||||
config={chartConfig}
|
|
||||||
className="h-full w-full -ml-6"
|
|
||||||
>
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<LineChart data={gradesTrend}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="period" />
|
|
||||||
<YAxis domain={[70, 100]} />
|
|
||||||
<ChartTooltip content={<ChartTooltipContent />} />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="math" stroke="blue" />
|
|
||||||
<Area
|
|
||||||
type="monotone"
|
|
||||||
dataKey="science"
|
|
||||||
stackId="2"
|
|
||||||
stroke="var(--color-science)"
|
|
||||||
fill="var(--color-science)"
|
|
||||||
fillOpacity={0.3}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
type="monotone"
|
|
||||||
dataKey="english"
|
|
||||||
stackId="3"
|
|
||||||
stroke="var(--color-english)"
|
|
||||||
fill="var(--color-english)"
|
|
||||||
fillOpacity={0.3}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
type="monotone"
|
|
||||||
dataKey="history"
|
|
||||||
stackId="4"
|
|
||||||
stroke="var(--color-history)"
|
|
||||||
fill="var(--color-history)"
|
|
||||||
fillOpacity={0.3}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
type="monotone"
|
|
||||||
dataKey="average"
|
|
||||||
stroke="var(--color-average)"
|
|
||||||
strokeWidth={4}
|
|
||||||
/>
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</ChartContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Subject Performance */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg md:text-xl font-semibold">
|
|
||||||
Subject Performance
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Current grades vs targets
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setFullscreenChart("performance")}
|
|
||||||
>
|
|
||||||
<Maximize2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="bg-muted/30 rounded-lg p-4">
|
|
||||||
<ChartContainer
|
|
||||||
config={chartConfig}
|
|
||||||
className="h-[250px] md:h-[300px] w-full"
|
|
||||||
>
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<BarChart
|
|
||||||
data={subjectPerformance}
|
|
||||||
layout="horizontal"
|
|
||||||
margin={{ top: 5, right: 10, left: 60, bottom: 5 }}
|
|
||||||
>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis type="number" domain={[0, 100]} />
|
|
||||||
<YAxis dataKey="subject" type="category" width={50} />
|
|
||||||
<ChartTooltip content={<ChartTooltipContent />} />
|
|
||||||
<Legend />
|
|
||||||
<Bar
|
|
||||||
dataKey="grade"
|
|
||||||
fill="hsl(var(--chart-1))"
|
|
||||||
name="Current"
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="target"
|
|
||||||
fill="hsl(var(--chart-2))"
|
|
||||||
name="Target"
|
|
||||||
/>
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</ChartContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Grade Distribution & Skills - Side by Side on Desktop */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
||||||
{/* Grade Distribution */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg md:text-xl font-semibold">
|
|
||||||
Grade Distribution
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Breakdown of all grades
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setFullscreenChart("distribution")}
|
|
||||||
>
|
|
||||||
<Maximize2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="bg-muted/30 rounded-lg p-4">
|
|
||||||
<ChartContainer
|
|
||||||
config={chartConfig}
|
|
||||||
className="h-[250px] md:h-[300px] w-full"
|
|
||||||
>
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<PieChart margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
|
|
||||||
<Pie
|
|
||||||
data={gradeDistribution}
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
labelLine={false}
|
|
||||||
label={({ grade, percentage }) =>
|
|
||||||
`${grade} (${percentage}%)`
|
|
||||||
}
|
|
||||||
outerRadius={80}
|
|
||||||
fill="#8884d8"
|
|
||||||
dataKey="count"
|
|
||||||
>
|
|
||||||
{gradeDistribution.map((entry, index) => (
|
|
||||||
<Cell
|
|
||||||
key={`cell-${index}`}
|
|
||||||
fill={COLORS[index % COLORS.length]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
<ChartTooltip content={<ChartTooltipContent />} />
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</ChartContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Skills Assessment */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg md:text-xl font-semibold">
|
|
||||||
Skills Assessment
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Competency across key areas
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setFullscreenChart("skills")}
|
|
||||||
>
|
|
||||||
<Maximize2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="bg-muted/30 rounded-lg p-4">
|
|
||||||
<ChartContainer
|
|
||||||
config={chartConfig}
|
|
||||||
className="h-[250px] md:h-[300px] w-full"
|
|
||||||
>
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<RadarChart
|
|
||||||
data={skillsRadar}
|
|
||||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
|
||||||
>
|
|
||||||
<PolarGrid />
|
|
||||||
<PolarAngleAxis dataKey="skill" />
|
|
||||||
<PolarRadiusAxis angle={90} domain={[0, 100]} />
|
|
||||||
<Radar
|
|
||||||
name="Skills"
|
|
||||||
dataKey="score"
|
|
||||||
stroke="hsl(var(--chart-1))"
|
|
||||||
fill="hsl(var(--chart-1))"
|
|
||||||
fillOpacity={0.3}
|
|
||||||
/>
|
|
||||||
<ChartTooltip content={<ChartTooltipContent />} />
|
|
||||||
</RadarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</ChartContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Fullscreen Modals */}
|
|
||||||
<FullscreenModal
|
|
||||||
chartId="trends"
|
|
||||||
title={`Grade Trends - ${selectedPeriod}`}
|
|
||||||
>
|
|
||||||
<ChartContainer config={chartConfig} className="h-full w-full z-100 ">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<AreaChart data={gradesTrend}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="period" />
|
|
||||||
<YAxis domain={[70, 100]} />
|
|
||||||
<ChartTooltip content={<ChartTooltipContent />} />
|
|
||||||
<Legend />
|
|
||||||
<Area
|
|
||||||
type="monotone"
|
|
||||||
dataKey="math"
|
|
||||||
stackId="1"
|
|
||||||
stroke="blue"
|
|
||||||
fill="blue"
|
|
||||||
fillOpacity={0.3}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
type="monotone"
|
|
||||||
dataKey="science"
|
|
||||||
stackId="2"
|
|
||||||
stroke="var(--color-science)"
|
|
||||||
fill="var(--color-science)"
|
|
||||||
fillOpacity={0.3}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
type="monotone"
|
|
||||||
dataKey="english"
|
|
||||||
stackId="3"
|
|
||||||
stroke="var(--color-english)"
|
|
||||||
fill="var(--color-english)"
|
|
||||||
fillOpacity={0.3}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
type="monotone"
|
|
||||||
dataKey="history"
|
|
||||||
stackId="4"
|
|
||||||
stroke="var(--color-history)"
|
|
||||||
fill="var(--color-history)"
|
|
||||||
fillOpacity={0.3}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
type="monotone"
|
|
||||||
dataKey="average"
|
|
||||||
stroke="var(--color-average)"
|
|
||||||
strokeWidth={4}
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</ChartContainer>
|
|
||||||
</FullscreenModal>
|
|
||||||
|
|
||||||
<FullscreenModal
|
|
||||||
chartId="performance"
|
|
||||||
title={`Subject Performance - ${selectedPeriod}`}
|
|
||||||
>
|
|
||||||
<ChartContainer config={chartConfig} className="h-full">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<BarChart data={subjectPerformance}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="subject" />
|
|
||||||
<YAxis domain={[0, 100]} />
|
|
||||||
<ChartTooltip content={<ChartTooltipContent />} />
|
|
||||||
<Legend />
|
|
||||||
<Bar
|
|
||||||
dataKey="grade"
|
|
||||||
fill="hsl(var(--chart-1))"
|
|
||||||
name="Current Grade"
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="target"
|
|
||||||
fill="hsl(var(--chart-2))"
|
|
||||||
name="Target Grade"
|
|
||||||
/>
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</ChartContainer>
|
|
||||||
</FullscreenModal>
|
|
||||||
|
|
||||||
<FullscreenModal
|
|
||||||
chartId="distribution"
|
|
||||||
title={`Grade Distribution - ${selectedPeriod}`}
|
|
||||||
>
|
|
||||||
<ChartContainer config={chartConfig} className="h-full">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<PieChart>
|
|
||||||
<Pie
|
|
||||||
data={gradeDistribution}
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
labelLine={false}
|
|
||||||
label={({ grade, percentage }) => `${grade} (${percentage}%)`}
|
|
||||||
outerRadius={200}
|
|
||||||
fill="#8884d8"
|
|
||||||
dataKey="count"
|
|
||||||
>
|
|
||||||
{gradeDistribution.map((entry, index) => (
|
|
||||||
<Cell
|
|
||||||
key={`cell-${index}`}
|
|
||||||
fill={COLORS[index % COLORS.length]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
<ChartTooltip content={<ChartTooltipContent />} />
|
|
||||||
<Legend />
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</ChartContainer>
|
|
||||||
</FullscreenModal>
|
|
||||||
|
|
||||||
<FullscreenModal
|
|
||||||
chartId="skills"
|
|
||||||
title="Skills Assessment - Fullscreen"
|
|
||||||
>
|
|
||||||
<ChartContainer config={chartConfig} className="h-full">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<RadarChart data={skillsRadar}>
|
|
||||||
<PolarGrid />
|
|
||||||
<PolarAngleAxis dataKey="skill" />
|
|
||||||
<PolarRadiusAxis angle={90} domain={[0, 100]} />
|
|
||||||
<Radar
|
|
||||||
name="Skills"
|
|
||||||
dataKey="score"
|
|
||||||
stroke="hsl(var(--chart-1))"
|
|
||||||
fill="hsl(var(--chart-1))"
|
|
||||||
fillOpacity={0.3}
|
|
||||||
strokeWidth={3}
|
|
||||||
/>
|
|
||||||
<ChartTooltip content={<ChartTooltipContent />} />
|
|
||||||
</RadarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</ChartContainer>
|
|
||||||
</FullscreenModal>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -3,6 +3,11 @@ import {
|
||||||
Legend,
|
Legend,
|
||||||
Line,
|
Line,
|
||||||
LineChart,
|
LineChart,
|
||||||
|
PolarAngleAxis,
|
||||||
|
PolarGrid,
|
||||||
|
PolarRadiusAxis,
|
||||||
|
Radar,
|
||||||
|
RadarChart,
|
||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
|
|
@ -12,16 +17,33 @@ import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../ui/chart";
|
||||||
import { Award } from "lucide-react";
|
import { Award } from "lucide-react";
|
||||||
import { getSubjectColor, getSubjectEmoji } from "~/lib/utils";
|
import { getSubjectColor, getSubjectEmoji } from "~/lib/utils";
|
||||||
import { useEffect, useState } from "react";
|
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 }) {
|
export default function GradesPage({ user }: { user: User }) {
|
||||||
// TODO: Error handling, loading state
|
const [period, setPeriod] = useState(periods[0].id);
|
||||||
const { grades, subjects, isLoading, isError } = useGrades("YEAR");
|
|
||||||
|
const { grades, subjects, isLoading, isError } = useGrades(period);
|
||||||
const {
|
const {
|
||||||
subjectAverages,
|
subjectAverages,
|
||||||
globalAverage,
|
globalAverage,
|
||||||
isLoading: loading,
|
isLoading: loading,
|
||||||
isError: error,
|
isError: error,
|
||||||
} = useAverages("YEAR");
|
} = useAverages(period);
|
||||||
|
|
||||||
const [hiddenSubjects, setHiddenSubjects] = useState<string[]>([]);
|
const [hiddenSubjects, setHiddenSubjects] = useState<string[]>([]);
|
||||||
const toggleSubjectVisibility = (subject: string) => {
|
const toggleSubjectVisibility = (subject: string) => {
|
||||||
|
|
@ -52,90 +74,164 @@ export default function GradesPage({ user }: { user: User }) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 pb-20 md:pb-0">
|
<div className="space-y-6 pb-20 md:pb-0">
|
||||||
{/* Tabs */}
|
{/* 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>
|
||||||
|
|
||||||
{/* Global average */}
|
{isNaN(globalAverage) ? (
|
||||||
<div className="bg-muted/80 dark:bg-muted/30 rounded-lg p-4 space-y-2">
|
<div className="text-center text-muted-foreground">
|
||||||
<div className="flex items-center justify-between">
|
Aucune note disponible pour cette période.
|
||||||
<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>
|
||||||
<div className="text-xl md:text-2xl font-bold">{globalAverage} / 20</div>
|
) : isLoading || loading ? (
|
||||||
</div>
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<span className="text-muted-foreground">Chargement...</span>
|
||||||
{/* Charts - Mobile First Layout */}
|
</div>
|
||||||
<div className="space-y-8">
|
) : isError || error ? (
|
||||||
{/* Grade Trends */}
|
<div className="text-center text-red-500">
|
||||||
<div className="space-y-4">
|
Une erreur est survenue lors de la récupération des données.
|
||||||
<div className="flex items-center justify-between">
|
</div>
|
||||||
<div>
|
) : (
|
||||||
<h2 className="text-lg md:text-xl font-semibold">
|
<>
|
||||||
Votre progression
|
{/* Global average */}
|
||||||
</h2>
|
<div className="bg-muted/50 rounded-lg p-4 space-y-2">
|
||||||
<p className="text-sm text-muted-foreground">
|
<div className="flex items-center justify-between">
|
||||||
Suivez vos notes au fil du temps
|
<span className="text-xs md:text-sm font-medium text-muted-foreground">
|
||||||
</p>
|
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>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-muted/80 dark:bg-muted/30 rounded-lg py-4">
|
|
||||||
<ChartContainer
|
{/* Charts - Mobile First Layout */}
|
||||||
config={{}}
|
<div className="space-y-8">
|
||||||
className="h-full w-full -ml-6 max-w-2xl"
|
{/* Grade Trends */}
|
||||||
>
|
<div className="space-y-4">
|
||||||
<LineChart data={grades}>
|
<div className="flex items-center justify-between">
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<div>
|
||||||
<XAxis dataKey="period" />
|
<h2 className="text-lg md:text-xl font-semibold">
|
||||||
{/* TODO: Custom domain */}
|
Votre progression
|
||||||
<YAxis domain={[min, max]} />
|
</h2>
|
||||||
<ChartTooltip content={<ChartTooltipContent />} />
|
<p className="text-sm text-muted-foreground">
|
||||||
<Legend
|
Suivez vos notes au fil du temps
|
||||||
onClick={(e) => {
|
</p>
|
||||||
if (e.dataKey) {
|
</div>
|
||||||
toggleSubjectVisibility(e.dataKey as string);
|
</div>
|
||||||
}
|
<div className="bg-muted/50 rounded-lg py-4">
|
||||||
}}
|
<ChartContainer
|
||||||
/>
|
config={{}}
|
||||||
{subjects.map((subject, index) => (
|
className="h-full w-full -ml-6 max-w-2xl"
|
||||||
<Line
|
>
|
||||||
key={index}
|
<LineChart data={grades}>
|
||||||
type="monotone"
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
dataKey={subject}
|
<XAxis dataKey="period" />
|
||||||
name={
|
<YAxis domain={[min, max]} />
|
||||||
subject + " " + getSubjectEmoji(subject, user.preferences)
|
<ChartTooltip content={<ChartTooltipContent />} />
|
||||||
}
|
<Legend
|
||||||
hide={hiddenSubjects.includes(subject)}
|
onClick={(e) => {
|
||||||
stroke={`var(--color-${getSubjectColor(
|
if (e.dataKey) {
|
||||||
subject,
|
toggleSubjectVisibility(e.dataKey as string);
|
||||||
user.preferences
|
}
|
||||||
)}-800`}
|
}}
|
||||||
dot={{
|
/>
|
||||||
fill: `var(--color-${getSubjectColor(
|
{subjects.map((subject, index) => (
|
||||||
subject,
|
<Line
|
||||||
user.preferences
|
key={index}
|
||||||
)}-800`,
|
type="monotone"
|
||||||
stroke: `var(--color-${getSubjectColor(
|
dataKey={subject}
|
||||||
subject,
|
name={
|
||||||
user.preferences
|
subject +
|
||||||
)}-800)`,
|
" " +
|
||||||
}}
|
getSubjectEmoji(subject, user.preferences)
|
||||||
/>
|
}
|
||||||
))}
|
hide={hiddenSubjects.includes(subject)}
|
||||||
<Line
|
stroke={`var(--color-${getSubjectColor(
|
||||||
type="monotone"
|
subject,
|
||||||
dataKey="average"
|
user.preferences
|
||||||
name="Moyenne"
|
)}-800`}
|
||||||
stroke="var(--primary)"
|
dot={{
|
||||||
strokeWidth={2}
|
fill: `var(--color-${getSubjectColor(
|
||||||
dot={false}
|
subject,
|
||||||
strokeDasharray="5 5"
|
user.preferences
|
||||||
hide={hiddenSubjects.includes("average")}
|
)}-800`,
|
||||||
/>
|
stroke: `var(--color-${getSubjectColor(
|
||||||
</LineChart>
|
subject,
|
||||||
</ChartContainer>
|
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>
|
||||||
</div>
|
</>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue