From dc5e10efd108d85edbe6f41489d9b4d5930fe2d9 Mon Sep 17 00:00:00 2001 From: Nathan Lamy Date: Wed, 20 Aug 2025 16:09:06 +0200 Subject: [PATCH] feat: add grades & averages --- app/app.css | 18 + app/components/grades/inde.tsx | 732 ++++++++++++++++++++++++++++++++ app/components/grades/index.tsx | 60 +++ app/components/ui/chart.tsx | 353 +++++++++++++++ app/lib/api.ts | 40 ++ app/routes.ts | 1 + app/routes/grades.tsx | 35 ++ app/routes/settings.tsx | 2 +- package.json | 1 + pnpm-lock.yaml | 298 +++++++++++++ 10 files changed, 1539 insertions(+), 1 deletion(-) create mode 100644 app/components/grades/inde.tsx create mode 100644 app/components/grades/index.tsx create mode 100644 app/components/ui/chart.tsx create mode 100644 app/routes/grades.tsx diff --git a/app/app.css b/app/app.css index 04ce1f3..c4fe699 100644 --- a/app/app.css +++ b/app/app.css @@ -123,6 +123,24 @@ } } +@layer base { + :root { + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + } + + .dark { + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + } +} + html, body { @apply w-full h-full; diff --git a/app/components/grades/inde.tsx b/app/components/grades/inde.tsx new file mode 100644 index 0000000..a98cc32 --- /dev/null +++ b/app/components/grades/inde.tsx @@ -0,0 +1,732 @@ +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(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 ( +
+
+
+

+ {title} +

+ +
+
+ {children} +
+
+
+ ); + }; + + const currentGPA = 3.7; + const totalCredits = 17; + const completedAssignments = 156; + const upcomingTests = 3; + + return ( +
+
+ {/* Header */} +
+

Academic Dashboard

+

+ Track your grades and progress +

+
+ +
+
+ {(["This Month", "3 Months", "Whole Year"] as const).map( + (period) => ( + + ) + )} +
+
+ + {/* Key Stats - Mobile First Grid */} +
+
+
+ + Current GPA + + +
+
{currentGPA}
+ + Dean's List + +
+ +
+
+ + Credits + + +
+
{totalCredits}
+

This semester

+
+ +
+
+ + Assignments + + +
+
+ {completedAssignments} +
+

Completed

+
+ +
+
+ + Tests + + +
+
{upcomingTests}
+

Upcoming

+
+
+ + {/* Charts - Mobile First Layout */} +
+ {/* Grade Trends */} +
+
+
+

+ Grade Trends +

+

+ {selectedPeriod === "This Month" + ? "Weekly" + : selectedPeriod === "3 Months" + ? "Monthly" + : "Monthly"}{" "} + performance across subjects +

+
+ +
+
+ + + + + + + } /> + + + + + + + + + +
+
+ + {/* Subject Performance */} +
+
+
+

+ Subject Performance +

+

+ Current grades vs targets +

+
+ +
+
+ + + + + + + } /> + + + + + + +
+
+ + {/* Grade Distribution & Skills - Side by Side on Desktop */} +
+ {/* Grade Distribution */} +
+
+
+

+ Grade Distribution +

+

+ Breakdown of all grades +

+
+ +
+
+ + + + + `${grade} (${percentage}%)` + } + outerRadius={80} + fill="#8884d8" + dataKey="count" + > + {gradeDistribution.map((entry, index) => ( + + ))} + + } /> + + + +
+
+ + {/* Skills Assessment */} +
+
+
+

+ Skills Assessment +

+

+ Competency across key areas +

+
+ +
+
+ + + + + + + + } /> + + + +
+
+
+
+ + {/* Fullscreen Modals */} + + + + + + + + } /> + + + + + + + + + + + + + + + + + + + } /> + + + + + + + + + + + + + `${grade} (${percentage}%)`} + outerRadius={200} + fill="#8884d8" + dataKey="count" + > + {gradeDistribution.map((entry, index) => ( + + ))} + + } /> + + + + + + + + + + + + + + + } /> + + + + +
+
+ ); +} diff --git a/app/components/grades/index.tsx b/app/components/grades/index.tsx new file mode 100644 index 0000000..dcef0d9 --- /dev/null +++ b/app/components/grades/index.tsx @@ -0,0 +1,60 @@ +import { + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + XAxis, + YAxis, +} from "recharts"; +import { useGrades, type User } from "~/lib/api"; +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "../ui/chart"; +import { chartConfig, gradesTrendData } from "./inde"; + +export default function GradesPage({ user }: { user: User }) { + // TODO: Error handling, loading state + const { grades, subjects, isLoading, isError } = useGrades("YEAR") + + return ( +
+ {/* Tabs */} + + {/* Charts - Mobile First Layout */} +
+ {/* Grade Trends */} +
+
+
+

Grade Trends

+

+ performance across subjects +

+
+
+
+ + + + + + {/* TODO: Custom domain */} + + } /> + + {subjects.map((subject, index) => ( + + ))} + + + +
+
+
+
+ ); +} diff --git a/app/components/ui/chart.tsx b/app/components/ui/chart.tsx new file mode 100644 index 0000000..6c3c5d1 --- /dev/null +++ b/app/components/ui/chart.tsx @@ -0,0 +1,353 @@ +"use client"; + +import * as React from "react"; +import * as RechartsPrimitive from "recharts"; + +import { cn } from "~/lib/utils"; + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const; + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ); +}; + +type ChartContextProps = { + config: ChartConfig; +}; + +const ChartContext = React.createContext(null); + +function useChart() { + const context = React.useContext(ChartContext); + + if (!context) { + throw new Error("useChart must be used within a "); + } + + return context; +} + +function ChartContainer({ + id, + className, + children, + config, + ...props +}: React.ComponentProps<"div"> & { + config: ChartConfig; + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"]; +}) { + const uniqueId = React.useId(); + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; + + return ( + +
+ + + {children} + +
+
+ ); +} + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme || config.color + ); + + if (!colorConfig.length) { + return null; + } + + return ( +