diff --git a/package-lock.json b/package-lock.json index 0ac35a0..3004adc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", "@shadcn/ui": "^0.0.4", "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.1", @@ -40,6 +41,7 @@ "react-icons": "^5.5.0", "react-router-dom": "^7.9.6", "recharts": "^2.15.4", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss-animate": "^1.0.7", "vercel": "^25.2.0", @@ -1851,6 +1853,37 @@ } } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", @@ -1930,6 +1963,36 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -13885,6 +13948,16 @@ "node": ">=0.10.0" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", diff --git a/package.json b/package.json index 57b2258..8e23178 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", "@shadcn/ui": "^0.0.4", "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.1", @@ -44,6 +45,7 @@ "react-icons": "^5.5.0", "react-router-dom": "^7.9.6", "recharts": "^2.15.4", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss-animate": "^1.0.7", "vercel": "^25.2.0", diff --git a/src/app/api/years/route.ts b/src/app/api/years/route.ts new file mode 100644 index 0000000..b42499c --- /dev/null +++ b/src/app/api/years/route.ts @@ -0,0 +1,23 @@ +import { sql } from "drizzle-orm"; +import { NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { yearlySchoolParticipation } from "@/lib/schema"; + +export async function GET() { + try { + // Get distinct years that have data in yearlySchoolParticipation + const yearsWithData = await db + .selectDistinct({ year: yearlySchoolParticipation.year }) + .from(yearlySchoolParticipation); + + // Convert to a Set for O(1) lookup + const yearsSet = new Set(yearsWithData.map((row) => row.year)); + + return NextResponse.json({ yearsWithData: Array.from(yearsSet) }); + } catch (error) { + return NextResponse.json( + { error: "Failed to fetch years with data" }, + { status: 500 }, + ); + } +} diff --git a/src/app/globals.css b/src/app/globals.css index af5d1c3..f5dd97b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -71,7 +71,7 @@ --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); + --primary: oklch(0.4878 0.2432 264.4); --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); diff --git a/src/app/graphs/loading.tsx b/src/app/graphs/loading.tsx new file mode 100644 index 0000000..9f044c0 --- /dev/null +++ b/src/app/graphs/loading.tsx @@ -0,0 +1,5 @@ +import { GraphsPageSkeleton } from "@/components/skeletons/GraphsPageSkeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/graphs/page.tsx b/src/app/graphs/page.tsx index f466709..09e71e3 100644 --- a/src/app/graphs/page.tsx +++ b/src/app/graphs/page.tsx @@ -10,17 +10,27 @@ **************************************************************/ "use client"; -import { useState, useEffect, useMemo } from "react"; -import BarGraph, { BarDataset } from "@/components/BarGraph"; +import { + BarChart, + Calendar, + CalendarDays, + ChartColumn, + ChevronDown, + LineChart, +} from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; +import BarGraph, { type BarDataset } from "@/components/BarGraph"; +import { Breadcrumbs } from "@/components/Breadcrumbs"; +import GraphFilters, { type Filters } from "@/components/GraphFilters"; import LineGraph from "@/components/LineGraph"; -import GraphFilters, { Filters } from "@/components/GraphFilters"; +import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { Button } from "@/components/ui/button"; -import { Breadcrumbs } from "@/components/Breadcrumbs"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; // define Project type type Project = { @@ -73,9 +83,8 @@ const groupByLabels: Record = { export default function GraphsPage() { const [allProjects, setAllProjects] = useState([]); - const [loading, setLoading] = useState(true); const [filters, setFilters] = useState(defaultFilters); - const [chartType, setChartType] = useState<"line" | "bar">("bar"); + const [chartType, setChartType] = useState<"line" | "bar">("line"); const [timePeriod, setTimePeriod] = useState< "all" | "3y" | "5y" | "custom" >("all"); @@ -97,16 +106,22 @@ export default function GraphsPage() { if (!response.ok) throw new Error("Failed to fetch"); const data = await response.json(); setAllProjects(data); - } catch (error) { - // eslint-disable-next-line no-console - console.error("Error:", error); - } finally { - setLoading(false); + } catch { + toast.error( + "Failed to load project data. Please refresh the page.", + ); } }; fetchProjects(); }, []); + // Sync tempYearRange with yearRange only when popover opens in custom mode + useEffect(() => { + if (yearRangeOpen && timePeriod === "custom") { + setTempYearRange(yearRange); + } + }, [yearRangeOpen, timePeriod, yearRange]); + // Calculate the current year range based on time period selection const currentYearRange = useMemo(() => { if (timePeriod === "custom") { @@ -310,9 +325,9 @@ export default function GraphsPage() { ).sort(); return ( -
+
{/* Left Sidebar - Filter Panel */} -
+
- {loading ? ( -

- Loading project data... -

- ) : ( + {allProjects.length > 0 ? (
{/* Header */} -
-

+
+

Projects by {groupByLabels[filters.groupBy]}

- - + +
{/* Chart Controls */} -
+
- - + + + + Line + + + + Bar + + +
-
- - + + - + +
@@ -491,7 +477,7 @@ export default function GraphsPage() {

-
-