From c4dfdc56b900df7fe0984250f9ab073108ab2d8b Mon Sep 17 00:00:00 2001 From: woleary2 Date: Fri, 20 Feb 2026 16:09:32 -0500 Subject: [PATCH 1/4] Added charts to dashboard and added arrows to school table --- src/app/schools/[name]/page.tsx | 7 +++ src/app/schools/page.tsx | 22 +++++++++ src/components/Dashboard.tsx | 76 +++++++++++++++++++++++++++++ src/components/DataTableSchools.tsx | 54 +++++++++++++++++++- src/components/LineGraph.tsx | 6 ++- 5 files changed, 162 insertions(+), 3 deletions(-) diff --git a/src/app/schools/[name]/page.tsx b/src/app/schools/[name]/page.tsx index b6bcca2..012b04f 100644 --- a/src/app/schools/[name]/page.tsx +++ b/src/app/schools/[name]/page.tsx @@ -18,6 +18,7 @@ import { Breadcrumbs } from "@/components/Breadcrumbs"; import { SchoolProfileSkeleton } from "@/components/skeletons/SchoolProfileSkeleton"; import { MapPlacer } from "@/components/ui/mapPlacer"; import { SchoolInfoRow } from "@/components/SchoolInfoRow"; +import YearDropdown from "@/components/YearDropdown"; // interface such that data can be blank if API is loading type SchoolData = { @@ -43,6 +44,7 @@ export default function SchoolProfilePage() { const [schoolData, setSchoolData] = useState(null); const [coordinates, setCoordinates] = useState(null); + const [year, setYear] = useState(null); useEffect(() => { fetch(`/api/schools/${schoolName}`) @@ -76,6 +78,11 @@ export default function SchoolProfilePage() { {/* Header with school name */}

{schoolData.name}

+ {/* Stats cards */}
diff --git a/src/app/schools/page.tsx b/src/app/schools/page.tsx index f67f6a7..1e34096 100644 --- a/src/app/schools/page.tsx +++ b/src/app/schools/page.tsx @@ -20,6 +20,7 @@ import YearDropdown from "@/components/YearDropdown"; export default function SchoolsPage() { const [schoolInfo, setSchoolInfo] = useState([]); + const [prevYearSchoolInfo, setPrevYearSchoolInfo] = useState([]); const [year, setYear] = useState(2025); const [search, setSearch] = useState(""); const [error, setError] = useState(null); @@ -44,6 +45,26 @@ export default function SchoolsPage() { }); }, [year]); + useEffect(() => { + if (!year) return; + + setError(null); + + fetch(`/api/schools?year=${year - 1}`) + .then((response) => { + if (!response.ok) { + throw new Error(`Failed to fetch school data`); + } + return response.json(); + }) + .then((data) => { + setPrevYearSchoolInfo(data); + }) + .catch((error) => { + setError(error.message || "Failed to load school data"); + }); + }, [year]); + return (
@@ -68,6 +89,7 @@ export default function SchoolsPage() { diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 90d7fad..c426952 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -14,6 +14,8 @@ import { useEffect, useState } from "react"; import { toast } from "sonner"; import YearDropdown from "@/components/YearDropdown"; +import MultiLineGraph from "./LineGraph"; +import { GraphDataset } from "./LineGraph"; type Stats = { totals: { @@ -46,6 +48,70 @@ export default function Dashboard() { fetchStats(year); }, [year]); + const [projectsyearData, setprojectsYearData] = useState< + { x: string | number; y: number }[] + >([]); + const [schoolyearData, setschoolYearData] = useState< + { x: string | number; y: number }[] + >([]); + + useEffect(() => { + const fetchData = async () => { + for (let i = 5; i >= 0; i--) { + try { + const res = await fetch( + `/api/yearly-totals?year=${year - i}`, + ); + const yearInfo = await res.json(); + + const thisYear: { x: string | number; y: number } = { + x: year - i, + y: yearInfo.yearlyStats.totals.total_projects, + }; + setprojectsYearData((prev) => [thisYear, ...prev]); + } catch { + toast.error( + "Failed to load dashboard data. Please try again.", + ); + } + } + }; + fetchData(); + }, [year]); + + const projectsData: GraphDataset = { + label: "Projects by Year", + data: projectsyearData, + }; + + useEffect(() => { + const fetchData = async () => { + for (let i = 5; i >= 0; i--) { + try { + const res = await fetch( + `/api/yearly-totals?year=${year - i}`, + ); + const yearInfo = await res.json(); + + const thisYear: { x: string | number; y: number } = { + x: year - i, + y: yearInfo.yearlyStats.totals.total_schools, + }; + setschoolYearData((prev) => [thisYear, ...prev]); + } catch { + toast.error( + "Failed to load dashboard data. Please try again.", + ); + } + } + }; + fetchData(); + }, [year]); + + const schoolData: GraphDataset = { + label: "Schools by Year", + data: schoolyearData, + }; return (
@@ -83,6 +149,16 @@ export default function Dashboard() { /> {/* TODO: Once we store type of school, make this correct */} + +
) : null} diff --git a/src/components/DataTableSchools.tsx b/src/components/DataTableSchools.tsx index d1d43ff..e3f6af7 100644 --- a/src/components/DataTableSchools.tsx +++ b/src/components/DataTableSchools.tsx @@ -10,7 +10,9 @@ **************************************************************/ "use client"; -import React from "react"; +import React, { ReactNode } from "react"; +import { ArrowUp, ArrowDown, Minus } from "lucide-react"; + import { ColumnDef, flexRender, @@ -19,6 +21,8 @@ import { useReactTable, SortingState, getSortedRowModel, + Cell, + Row, } from "@tanstack/react-table"; import { @@ -31,10 +35,12 @@ import { } from "@/components/ui/table"; import Link from "next/link"; +import { Arrow } from "@radix-ui/react-popover"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; + prevData: TData[]; globalFilter: string; setGlobalFilter: (value: string) => void; } @@ -42,6 +48,7 @@ interface DataTableProps { export function SchoolsDataTable({ columns, data, + prevData, globalFilter, setGlobalFilter, }: DataTableProps) { @@ -62,6 +69,50 @@ export function SchoolsDataTable({ }, }); + function yoyChange(cell: Cell, row: Row): ReactNode { + // Check if it is in students/teachers/projects column + if ( + cell.column.getIndex() !== 5 && + cell.column.getIndex() !== 6 && + cell.column.getIndex() !== 7 + ) { + return <>; + } + const rowIndex: number = row.index; + const prevRow = prevData[rowIndex] as Record; + const colIndex: number = cell.column.getIndex(); + const prevYearValue: number = prevRow[cell.column.id]; + const diff = cell.getValue() - prevYearValue; + + const percentChange = Math.abs(diff / prevYearValue) * 100; + + if (diff > 0) { + return ( +
+ + {percentChange} +
+ ); + } else if (diff < 0) { + return ( +
+ + {percentChange} +
+ ); + } else { + return ( +
+ + {percentChange} +
+ ); + } + + // If so, calc year over year change + // Render icon/number based on that + } + return ( //Example code should be changed //border for school name column disappears when scrolling right @@ -127,6 +178,7 @@ export function SchoolsDataTable({ cell.column.columnDef.cell, cell.getContext(), )} + {yoyChange(cell, row)}
)} diff --git a/src/components/LineGraph.tsx b/src/components/LineGraph.tsx index 8f4c213..d4123d5 100644 --- a/src/components/LineGraph.tsx +++ b/src/components/LineGraph.tsx @@ -14,7 +14,7 @@ type MultiLineGraphProps = { yAxisLabel: string; xAxisLabel: string; legendTitle?: string; - svgRefCopy: React.RefObject; + svgRefCopy?: React.RefObject; }; export default function MultiLineGraph({ @@ -416,7 +416,9 @@ export default function MultiLineGraph({ }); }); - svgRefCopy.current = svgRef.current; + if (svgRefCopy != null) { + svgRefCopy.current = svgRef.current; + } }, [datasets, xAxisLabel, yAxisLabel, colorScale]); return ( From d18b654374ff1f0c02c6a41af1bcc0f1e1999ba1 Mon Sep 17 00:00:00 2001 From: woleary2 Date: Tue, 24 Feb 2026 16:47:07 -0500 Subject: [PATCH 2/4] Fixed school up and down arrows, added data table/year selector to schools page --- src/app/api/schools/[name]/route.ts | 27 ++++++++---- src/app/schools/[name]/page.tsx | 46 ++++++++++++++++---- src/components/BarGraph.tsx | 6 ++- src/components/Dashboard.tsx | 67 ++++++++++++++++++++++++----- src/components/DataTableSchools.tsx | 32 ++++++++------ 5 files changed, 135 insertions(+), 43 deletions(-) diff --git a/src/app/api/schools/[name]/route.ts b/src/app/api/schools/[name]/route.ts index 14827b1..5424197 100644 --- a/src/app/api/schools/[name]/route.ts +++ b/src/app/api/schools/[name]/route.ts @@ -74,6 +74,8 @@ export async function GET( { params }: { params: Promise<{ name: string }> }, ) { try { + const { searchParams } = new URL(req.url); + const year = Number(searchParams.get("year")); const { name } = await params; const searchName = name.replace(/-/g, " "); @@ -100,10 +102,7 @@ export async function GET( .select({ total: sum(projects.numStudents) }) .from(projects) .where( - and( - eq(projects.schoolId, school.id), - eq(projects.year, pastYear), - ), + and(eq(projects.schoolId, school.id), eq(projects.year, year)), ); const teacherCount = await db @@ -112,7 +111,7 @@ export async function GET( .where( and( eq(yearlyTeacherParticipation.schoolId, school.id), - eq(yearlyTeacherParticipation.year, pastYear), + eq(yearlyTeacherParticipation.year, year), ), ); @@ -120,10 +119,19 @@ export async function GET( .select({ count: sql`count(*)` }) .from(projects) .where( - and( - eq(projects.schoolId, school.id), - eq(projects.year, pastYear), - ), + and(eq(projects.schoolId, school.id), eq(projects.year, year)), + ); + + const projectRows = await db + .select({ + id: projects.id, + title: projects.title, + numStudents: projects.numStudents, + year: projects.year, + }) + .from(projects) + .where( + and(eq(projects.schoolId, school.id), eq(projects.year, year)), ); // First year would be minimum year found in a school's projects @@ -143,6 +151,7 @@ export async function GET( teacherCount: teacherCount[0]?.count ?? 0, projectCount: projectCount[0]?.count ?? 0, firstYear: firstYearData[0]?.year ?? null, + projects: projectRows, // TO DO: Instructional model not in database yet instructionalModel: "normal", }); diff --git a/src/app/schools/[name]/page.tsx b/src/app/schools/[name]/page.tsx index 012b04f..62ecec1 100644 --- a/src/app/schools/[name]/page.tsx +++ b/src/app/schools/[name]/page.tsx @@ -19,6 +19,10 @@ import { SchoolProfileSkeleton } from "@/components/skeletons/SchoolProfileSkele import { MapPlacer } from "@/components/ui/mapPlacer"; import { SchoolInfoRow } from "@/components/SchoolInfoRow"; import YearDropdown from "@/components/YearDropdown"; +import MultiLineGraph from "@/components/LineGraph"; +import BarGraph from "@/components/BarGraph"; +import { DataTable } from "@/components/DataTable"; +import { ColumnDef } from "@tanstack/react-table"; // interface such that data can be blank if API is loading type SchoolData = { @@ -28,6 +32,7 @@ type SchoolData = { teacherCount: string; projectCount: string; firstYear: string; + projects: ProjectRow[]; instructionalModel: string; }; @@ -36,6 +41,13 @@ type MapCoordinates = { longitude: number | null; }; +type ProjectRow = { + id: string; + title: string; + numStudents: number; + year: number; +}; + export default function SchoolProfilePage() { const params = useParams(); const schoolName = params.name as string; @@ -44,10 +56,28 @@ export default function SchoolProfilePage() { const [schoolData, setSchoolData] = useState(null); const [coordinates, setCoordinates] = useState(null); - const [year, setYear] = useState(null); + const [year, setYear] = useState(2025); + const [projects, setProjects] = useState([]); + + const projectColumns: ColumnDef[] = [ + { + accessorKey: "title", + header: "Title", + }, + { + accessorKey: "numStudents", + header: "Students", + }, + { + accessorKey: "year", + header: "Year", + }, + ]; useEffect(() => { - fetch(`/api/schools/${schoolName}`) + if (!year) return; + + fetch(`/api/schools/${schoolName}?year=${year}`) .then((response) => { if (!response.ok) { throw new Error(`Failed to fetch school data`); @@ -56,6 +86,7 @@ export default function SchoolProfilePage() { }) .then((data) => { setSchoolData(data); + setProjects(data.projects); }) .catch(() => { toast.error( @@ -66,7 +97,7 @@ export default function SchoolProfilePage() { router.push("/schools"); }, 2000); }); - }, [schoolName, router]); + }, [schoolName, router, year]); if (!schoolData) { return ; @@ -159,11 +190,10 @@ export default function SchoolProfilePage() {

View and edit data

-
-

- Data table placeholder -

-
+
diff --git a/src/components/BarGraph.tsx b/src/components/BarGraph.tsx index 0c62cbc..b818eca 100644 --- a/src/components/BarGraph.tsx +++ b/src/components/BarGraph.tsx @@ -24,7 +24,7 @@ type BarGraphProps = { yAxisLabel: string; xAxisLabel: string; legendTitle?: string; - svgRefCopy: React.RefObject; + svgRefCopy?: React.RefObject; }; export default function BarGraph({ @@ -281,7 +281,9 @@ export default function BarGraph({ return transform; }); - svgRefCopy.current = svgRef.current; + if (svgRefCopy !== undefined) { + svgRefCopy.current = svgRef.current; + } }, [dataset]); return ( diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index c426952..91da49a 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -16,6 +16,8 @@ import { toast } from "sonner"; import YearDropdown from "@/components/YearDropdown"; import MultiLineGraph from "./LineGraph"; import { GraphDataset } from "./LineGraph"; +import { Button } from "./ui/button"; +import { useRouter } from "next/navigation"; type Stats = { totals: { @@ -28,6 +30,12 @@ type Stats = { year: number; }; +type chartFilters = { + yearStart: number; + yearEnd: number; + isProjects: boolean; +}; + export default function Dashboard() { const [year, setYear] = useState(() => new Date().getFullYear()); const [stats, setStats] = useState(null); @@ -57,6 +65,7 @@ export default function Dashboard() { useEffect(() => { const fetchData = async () => { + setschoolYearData([]); for (let i = 5; i >= 0; i--) { try { const res = await fetch( @@ -86,6 +95,7 @@ export default function Dashboard() { useEffect(() => { const fetchData = async () => { + setprojectsYearData([]); for (let i = 5; i >= 0; i--) { try { const res = await fetch( @@ -108,6 +118,33 @@ export default function Dashboard() { fetchData(); }, [year]); + const router = useRouter(); + + const projectChartFilters = { + yearStart: year - 5, + yearEnd: year, + isProjects: true, + }; + + const schoolChartFilters = { + yearStart: year - 5, + yearEnd: year, + isProjects: false, + }; + + function linkToGraph(filters: chartFilters) { + if (filters.isProjects) { + router.push( + `/chart?type=line&startYear=${filters.yearStart}&endYear=${filters.yearEnd}&measuredAs=total-project-count`, + ); + return; + } + + router.push( + `/chart?type=line&startYear=${filters.yearStart}&endYear=${filters.yearEnd}`, + ); + } + const schoolData: GraphDataset = { label: "Schools by Year", data: schoolyearData, @@ -149,16 +186,26 @@ export default function Dashboard() { /> {/* TODO: Once we store type of school, make this correct */} - - + +
+ Total # Projects + + Total # Schools +
) : null} diff --git a/src/components/DataTableSchools.tsx b/src/components/DataTableSchools.tsx index e3f6af7..a16f784 100644 --- a/src/components/DataTableSchools.tsx +++ b/src/components/DataTableSchools.tsx @@ -80,31 +80,35 @@ export function SchoolsDataTable({ } const rowIndex: number = row.index; const prevRow = prevData[rowIndex] as Record; + + if (!prevRow) return <>; const colIndex: number = cell.column.getIndex(); - const prevYearValue: number = prevRow[cell.column.id]; + const prevYearValue: number = prevRow[cell.column.id] ?? 0; const diff = cell.getValue() - prevYearValue; - const percentChange = Math.abs(diff / prevYearValue) * 100; + const percentChange = + prevYearValue !== 0 ? Math.abs(diff / prevYearValue) * 100 : 0; - if (diff > 0) { + const formattedPercent = percentChange.toFixed(0); + if (percentChange < 0.5) { return ( -
- - {percentChange} +
+ + {formattedPercent}%
); - } else if (diff < 0) { + } else if (diff > 0) { return ( -
- - {percentChange} +
+ + {formattedPercent}%
); - } else { + } else if (diff < 0) { return ( -
+
- {percentChange} + {formattedPercent}%
); } @@ -173,7 +177,7 @@ export function SchoolsDataTable({ )} ) : ( -
+
{flexRender( cell.column.columnDef.cell, cell.getContext(), From ab6a220af90ac037c9f80bce635d78718bc20c7d Mon Sep 17 00:00:00 2001 From: woleary2 Date: Thu, 26 Feb 2026 21:16:28 -0500 Subject: [PATCH 3/4] Added charts to invidual schools page, changed year dropdown to display years with data for specific schools --- src/app/schools/[name]/page.tsx | 84 +++++++++++++++++++++++++++-- src/components/Dashboard.tsx | 58 ++++++++++++-------- src/components/DataTableSchools.tsx | 8 ++- src/components/YearDropdown.tsx | 49 ++++++++++++----- 4 files changed, 159 insertions(+), 40 deletions(-) diff --git a/src/app/schools/[name]/page.tsx b/src/app/schools/[name]/page.tsx index 62ecec1..526408b 100644 --- a/src/app/schools/[name]/page.tsx +++ b/src/app/schools/[name]/page.tsx @@ -19,10 +19,11 @@ import { SchoolProfileSkeleton } from "@/components/skeletons/SchoolProfileSkele import { MapPlacer } from "@/components/ui/mapPlacer"; import { SchoolInfoRow } from "@/components/SchoolInfoRow"; import YearDropdown from "@/components/YearDropdown"; -import MultiLineGraph from "@/components/LineGraph"; -import BarGraph from "@/components/BarGraph"; +import MultiLineGraph, { GraphDataset } from "@/components/LineGraph"; import { DataTable } from "@/components/DataTable"; import { ColumnDef } from "@tanstack/react-table"; +import { ArrowRight } from "lucide-react"; +import { Button } from "@/components/ui/button"; // interface such that data can be blank if API is loading type SchoolData = { @@ -48,6 +49,12 @@ type ProjectRow = { year: number; }; +type chartFilters = { + yearStart: number; + yearEnd: number; + isProjects: boolean; +}; + export default function SchoolProfilePage() { const params = useParams(); const schoolName = params.name as string; @@ -56,8 +63,11 @@ export default function SchoolProfilePage() { const [schoolData, setSchoolData] = useState(null); const [coordinates, setCoordinates] = useState(null); - const [year, setYear] = useState(2025); + const [year, setYear] = useState(2025); const [projects, setProjects] = useState([]); + const [studentYearData, setstudentYearData] = useState< + { x: string | number; y: number }[] + >([]); const projectColumns: ColumnDef[] = [ { @@ -99,6 +109,43 @@ export default function SchoolProfilePage() { }); }, [schoolName, router, year]); + // Fetches student data for the last 5 years + useEffect(() => { + const fetchData = async () => { + setstudentYearData([]); + for (let i = 5; i >= 0; i--) { + try { + const res = await fetch( + `/api/schools/${schoolName}?year=${year - i}`, + ); + const yearInfo = await res.json(); + + const thisYear: { x: string | number; y: number } = { + x: year - i, + y: yearInfo.studentCount, + }; + setstudentYearData((prev) => [thisYear, ...prev]); + } catch { + toast.error( + "Failed to load dashboard data. Please try again.", + ); + } + } + }; + fetchData(); + }, [year]); + + const studentData: GraphDataset = { + label: "Students by Year", + data: studentYearData, + }; + + const studentChartFilters: chartFilters = { + yearStart: year - 5, + yearEnd: year, + isProjects: false, + }; + if (!schoolData) { return ; } @@ -112,7 +159,12 @@ export default function SchoolProfilePage() { { + if (selectedYear !== null) { + setYear(selectedYear); + } + }} + school={schoolData.name} /> {/* Stats cards */} @@ -137,6 +189,28 @@ export default function SchoolProfilePage() { instructionalModel={schoolData.instructionalModel} firstYear={schoolData.firstYear} /> +
+
+ Total # Students + +
+ +
{/* Placeholders for charts */}
@@ -188,7 +262,7 @@ export default function SchoolProfilePage() { {/* Data table placeholder */}

- View and edit data + Project Data

([]); + /* + * Fetches data for the chart with total # of projects + */ useEffect(() => { const fetchData = async () => { - setschoolYearData([]); + setprojectsYearData([]); for (let i = 5; i >= 0; i--) { try { const res = await fetch( @@ -93,9 +95,17 @@ export default function Dashboard() { data: projectsyearData, }; + const schoolData: GraphDataset = { + label: "Schools by Year", + data: schoolyearData, + }; + + /* + * Fetches data for the chart with total # of schools + */ useEffect(() => { const fetchData = async () => { - setprojectsYearData([]); + setschoolYearData([]); for (let i = 5; i >= 0; i--) { try { const res = await fetch( @@ -132,6 +142,14 @@ export default function Dashboard() { isProjects: false, }; + /** + * linkToGraph + * Routes the user to the page corresponding to a chart with the filters + * passed in + * @param filters The chart filters that are used to produce a link to a + * a chart on the actual charts page + * returns: none + */ function linkToGraph(filters: chartFilters) { if (filters.isProjects) { router.push( @@ -145,29 +163,26 @@ export default function Dashboard() { ); } - const schoolData: GraphDataset = { - label: "Schools by Year", - data: schoolyearData, - }; - return (
-

Overview Dashboard

-
- { - if (selectedYear !== null) { - setYear(selectedYear); - } - }} - /> +
+

Overview Dashboard

+
+ { + if (selectedYear !== null) { + setYear(selectedYear); + } + }} + /> +
{stats ? (
-
+
{/* TODO: Once we store type of school, make this correct */} -
-
+
Total # Projects
-
+
Total # Projects