diff --git a/package-lock.json b/package-lock.json index d4695fc..bb54bd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "lucide": "^0.544.0", "lucide-react": "^0.545.0", "next": "^16.0.7", + "nuqs": "^2.8.6", "pg": "^8.16.3", "react": "^19.2.2", "react-dom": "^19.2.2", @@ -220,6 +221,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -236,6 +238,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -252,6 +255,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -268,6 +272,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -284,6 +289,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -300,6 +306,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -316,6 +323,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -332,6 +340,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -348,6 +357,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -364,6 +374,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -380,6 +391,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -396,6 +408,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -412,6 +425,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -428,6 +442,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -444,6 +459,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -460,6 +476,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -476,6 +493,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -492,6 +510,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -508,6 +527,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -524,6 +544,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -540,6 +561,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -556,6 +578,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -622,6 +645,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -638,6 +662,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -654,6 +679,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -670,6 +696,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -686,6 +713,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -702,6 +730,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -718,6 +747,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -734,6 +764,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -750,6 +781,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -766,6 +798,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -782,6 +815,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -798,6 +832,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -814,6 +849,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -830,6 +866,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -846,6 +883,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -862,6 +900,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -878,6 +917,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -894,6 +934,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -910,6 +951,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -926,6 +968,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -942,6 +985,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -958,6 +1002,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -974,6 +1019,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -990,6 +1036,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1006,6 +1053,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1022,6 +1070,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12398,6 +12447,49 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nuqs": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.8.6.tgz", + "integrity": "sha512-aRxeX68b4ULmhio8AADL2be1FWDy0EPqaByPvIYWrA7Pm07UjlrICp/VPlSnXJNAG0+3MQwv3OporO2sOXMVGA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/franky47" + }, + "peerDependencies": { + "@remix-run/react": ">=2", + "@tanstack/react-router": "^1", + "next": ">=14.2.0", + "react": ">=18.2.0 || ^19.0.0-0", + "react-router": "^5 || ^6 || ^7", + "react-router-dom": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "@tanstack/react-router": { + "optional": true + }, + "next": { + "optional": true + }, + "react-router": { + "optional": true + }, + "react-router-dom": { + "optional": true + } + } + }, + "node_modules/nuqs/node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", diff --git a/package.json b/package.json index 42dd7ec..ce4e02b 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "lucide": "^0.544.0", "lucide-react": "^0.545.0", "next": "^16.0.7", + "nuqs": "^2.8.6", "pg": "^8.16.3", "react": "^19.2.2", "react-dom": "^19.2.2", diff --git a/src/app/graphs/page.tsx b/src/app/graphs/page.tsx index f2d84b0..bdd7986 100644 --- a/src/app/graphs/page.tsx +++ b/src/app/graphs/page.tsx @@ -2,8 +2,8 @@ * * graphs/page.tsx * - * Author: Elki, Zander, Chiara, and Steven - * Date: 12/6/2025 + * Author: Jack, Anne, Elki, Zander, Chiara, and Steven + * Date: 1/30/2026 * * Summary: display bar/line graph of project data with toggle * @@ -11,8 +11,6 @@ "use client"; import { - BarChart, - Calendar, CalendarDays, ChartColumn, ChevronDown, @@ -35,8 +33,13 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + useQueryState, + parseAsInteger, + parseAsString, + parseAsArrayOf, +} from "nuqs"; -// define Project type type Project = { id: number; title: string; @@ -53,7 +56,6 @@ type Project = { numStudents: number; }; -// define default filters type const defaultFilters: Filters = { individualProjects: true, groupProjects: true, @@ -88,21 +90,101 @@ const groupByLabels: Record = { export default function GraphsPage() { const [allProjects, setAllProjects] = useState([]); - const [filters, setFilters] = useState(defaultFilters); const [gatewayCities, setGatewayCities] = useState([]); - const [chartType, setChartType] = useState<"line" | "bar">("line"); - const [timePeriod, setTimePeriod] = useState< - "all" | "3y" | "5y" | "custom" - >("all"); - const [yearRange, setYearRange] = useState<{ start: number; end: number }>({ - start: 2020, - end: 2025, - }); - const [yearRangeOpen, setYearRangeOpen] = useState(false); + + // Setting hooks + const [timePeriod, setTimePeriod] = useQueryState( + "period", + parseAsString.withDefault("all"), + ); + const [startYear, setStartYear] = useQueryState( + "startYear", + parseAsInteger.withDefault(2020), + ); + const [endYear, setEndYear] = useQueryState( + "endYear", + parseAsInteger.withDefault(2025), + ); + const yearRange = useMemo( + () => ({ + start: startYear, + end: endYear, + }), + [startYear, endYear], + ); const [tempYearRange, setTempYearRange] = useState({ - start: 2020, - end: 2025, + start: startYear, + end: endYear, }); + const [yearRangeOpen, setYearRangeOpen] = useState(false); + + const [chartType, setChartType] = useQueryState( + "type", + parseAsString.withDefault("line"), + ); + + // Filter hooks + const [groupBy, setGroupBy] = useQueryState( + "groupBy", + parseAsString.withDefault("region"), + ); + const [measuredAs, setMeasuredAs] = useQueryState( + "measuredAs", + parseAsString.withDefault("total-school-count"), + ); + const [selectedSchools, setSelectedSchools] = useQueryState( + "schools", + parseAsArrayOf(parseAsString).withDefault([]), + ); + const [selectedCities, setSelectedCities] = useQueryState( + "cities", + parseAsArrayOf(parseAsString).withDefault([]), + ); + const [selectedProjectTypes, setSelectedProjectTypes] = useQueryState( + "projectTypes", + parseAsArrayOf(parseAsString).withDefault([]), + ); + const [teacherYearsValue, setTeacherYearsValue] = useQueryState( + "teacherYearsValue", + parseAsString.withDefault(""), + ); + const [teacherYearsOperator, setTeacherYearsOperator] = useQueryState( + "teacherYearsOperator", + parseAsString.withDefault("="), + ); + const [teacherYearsValue2, setTeacherYearsValue2] = useQueryState( + "teacherYearsValue2", + parseAsString.withDefault(""), + ); + + const filters: Filters = useMemo( + () => ({ + individualProjects: true, + groupProjects: true, + selectedSchools, + selectedCities, + selectedProjectTypes, + teacherYearsValue, + teacherYearsOperator: teacherYearsOperator as + | "=" + | ">" + | "<" + | "between", + teacherYearsValue2: teacherYearsValue2 || undefined, + groupBy: groupBy as any, + measuredAs: measuredAs as any, + }), + [ + selectedSchools, + selectedCities, + selectedProjectTypes, + teacherYearsValue, + teacherYearsOperator, + teacherYearsValue2, + groupBy, + measuredAs, + ], + ); // Fetch all project data useEffect(() => { @@ -144,25 +226,43 @@ export default function GraphsPage() { } }, [yearRangeOpen, timePeriod, yearRange]); + // Could break when loggin in using liveshare? (network url issue w/ "npm run dev") + const copyURLtoClipboard = async () => { + try { + const url = window.location.href; + await navigator.clipboard.writeText(url); + toast.success("URL copied to clipboard!"); + } catch (error) { + console.log(error); + } + }; + // Calculate the current year range based on time period selection - const currentYearRange = useMemo(() => { + useMemo(() => { if (timePeriod === "custom") { - return yearRange; + return; } const allYears = allProjects.map((p) => p.year); - const maxYear = Math.max(...allYears, new Date().getFullYear()); + const maxYear = Math.max(...allYears, new Date().getFullYear()) - 1; if (timePeriod === "3y") { - return { start: maxYear - 2, end: maxYear }; + //return { start: maxYear - 2, end: maxYear }; + setStartYear(maxYear - 2); + setEndYear(maxYear); + return; } else if (timePeriod === "5y") { - return { start: maxYear - 4, end: maxYear }; + //return { start: maxYear - 4, end: maxYear }; + setStartYear(maxYear - 4); + setEndYear(maxYear); + return; } // "all" - use full range const minYear = Math.min(...allYears); - return { start: minYear, end: maxYear }; - }, [timePeriod, yearRange, allProjects]); + setStartYear(minYear); + setEndYear(maxYear); + }, [timePeriod, allProjects]); // Memoize graph dataset calculation to run only when data or filters change const graphDataset: BarDataset[] = useMemo(() => { @@ -186,10 +286,7 @@ export default function GraphsPage() { if (!filters) return true; // Year range filter - if ( - p.year < currentYearRange.start || - p.year > currentYearRange.end - ) { + if (p.year < yearRange.start || p.year > yearRange.end) { return false; } @@ -257,16 +354,21 @@ export default function GraphsPage() { // set groupKey based on filter selection if (filters?.groupBy === "division") { + setGroupBy("division"); groupKey = "division"; } else if (filters?.groupBy === "project-type") { + setGroupBy("project-type"); groupKey = "category"; } else if (filters?.groupBy === "region") { + setGroupBy("region"); // TO DO: Add proper 'region' field to Project type and database. Currently using schoolTown (city) as a temporary substitute groupKey = "schoolTown"; } else if (filters?.groupBy === "school-type") { + setGroupBy("school-type"); // TO DO: Add 'schoolType' field to Project type and database, then map it here groupKey = "category"; // Temporary fallback } else if (filters?.groupBy === "implementation-type") { + setGroupBy("implementation-type"); // TO DO: Add 'implementationType' field to Project type and database, then map it here groupKey = "category"; // Temporary fallback } @@ -334,6 +436,7 @@ export default function GraphsPage() { projectsByYear[p.year].push(p); }); + setMeasuredAs(filters?.measuredAs || "total-school-count"); const metric = filters?.measuredAs || "total-school-count"; const dataPoints = Object.entries(projectsByYear) @@ -348,7 +451,18 @@ export default function GraphsPage() { data: dataPoints, }; }); - }, [allProjects, filters, currentYearRange]); + }, [ + allProjects, + selectedSchools, + selectedCities, + selectedProjectTypes, + teacherYearsValue, + teacherYearsOperator, + teacherYearsValue2, + groupBy, + measuredAs, + yearRange, + ]); // Calculate filtered count (based on selected 'measured by' category) const filteredProjectCount = useMemo(() => { @@ -380,7 +494,23 @@ export default function GraphsPage() { cities={cities} projectTypes={projectTypes} gatewayCities={gatewayCities} - onFiltersChange={setFilters} + filters={filters} + onFiltersChange={(newFilters) => { + setSelectedSchools(newFilters.selectedSchools); + setSelectedCities(newFilters.selectedCities); + setSelectedProjectTypes( + newFilters.selectedProjectTypes, + ); + setGroupBy(newFilters.groupBy); + setMeasuredAs(newFilters.measuredAs); + setTeacherYearsValue(newFilters.teacherYearsValue); + setTeacherYearsOperator( + newFilters.teacherYearsOperator, + ); + setTeacherYearsValue2( + newFilters.teacherYearsValue2 ?? "", + ); + }} /> @@ -406,6 +536,7 @@ export default function GraphsPage() { variant="outline" size="sm" className="flex items-center gap-2" + onClick={copyURLtoClipboard} > Share @@ -489,8 +620,7 @@ export default function GraphsPage() { className="rounded-r-md rounded-l-none border-l-0 -ml-px flex items-center gap-2" > - {currentYearRange.start} -{" "} - {currentYearRange.end} + {yearRange.start} - {yearRange.end} @@ -564,8 +694,11 @@ export default function GraphsPage() {