diff --git a/frontend/app/overview/page.tsx b/frontend/app/overview/page.tsx index dc83d793..131dafbc 100644 --- a/frontend/app/overview/page.tsx +++ b/frontend/app/overview/page.tsx @@ -1,17 +1,40 @@ "use client" -import { useEffect, useState } from "react" +import { useEffect, useMemo, useState } from "react" import { LineChart } from "@mui/x-charts/LineChart" +import Slider from "@mui/material/Slider" +import ToggleButton from "@mui/material/ToggleButton" +import ToggleButtonGroup from "@mui/material/ToggleButtonGroup" import { apiFetch } from "@/utils/apiFetch" +import { useAuth } from "@/providers/AuthProvider" import { apiBaseUrl } from "@/utils/apiRoutes" import API_ROUTES from "@/utils/apiRoutes" import { ComplaintsOverTimePoint } from "@/utils/api" +const WINDOW_OPTIONS = [5, 10, 20] as const +type WindowSize = (typeof WINDOW_OPTIONS)[number] | "all" +const MIN_YEAR = 1990 +const MIN_VISIBLE_POINTS = 2 + +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max) +} + export default function OverviewPage() { + const { accessToken, hasHydrated } = useAuth() const [data, setData] = useState(null) const [error, setError] = useState(null) + const [windowSize, setWindowSize] = useState("all") + const [range, setRange] = useState<[number, number]>([0, 0]) + const loginRequired = hasHydrated && !accessToken + useEffect(() => { + if (loginRequired) { + window.dispatchEvent(new Event("npdc:login-required-search")) + } + }, [loginRequired]) useEffect(() => { + if (!hasHydrated || !accessToken) return let active = true const loadData = async (): Promise => { @@ -21,8 +44,10 @@ export default function OverviewPage() { throw new Error(`Request failed with status ${res.status}`) } const json: ComplaintsOverTimePoint[] = await res.json() + const points = json.filter((point) => Number(point.year) >= MIN_YEAR) if (active) { - setData(json) + setData(points) + setRange([0, Math.max(0, points.length - 1)]) } } catch (err) { if (active) { @@ -35,7 +60,68 @@ export default function OverviewPage() { return () => { active = false } - }, []) + }, [hasHydrated, accessToken]) + + const pointCount = data?.length ?? 0 + const lastIndex = Math.max(0, pointCount - 1) + const selectedWindow: WindowSize = + windowSize !== "all" && windowSize < pointCount ? windowSize : "all" + const isAll = selectedWindow === "all" + const presetWidth = isAll ? pointCount : selectedWindow + const maxStartIndex = Math.max(0, pointCount - presetWidth) + const startIndex = clamp(range[0], 0, isAll ? lastIndex : maxStartIndex) + const endIndex = isAll + ? clamp(range[1], startIndex, lastIndex) + : Math.min(startIndex + presetWidth - 1, lastIndex) + + const visibleData = useMemo( + () => (data ?? []).slice(startIndex, endIndex + 1), + [data, startIndex, endIndex] + ) + + const handleWindowSizeChange = (_event: unknown, next: WindowSize | null): void => { + if (next === null) return + if (next === "all") { + setRange([0, lastIndex]) + setWindowSize(next) + return + } + + const width = Math.min(next, pointCount) + const nextStart = clamp(endIndex - width + 1, 0, Math.max(0, pointCount - width)) + setRange([nextStart, nextStart + width - 1]) + setWindowSize(next) + } + + const handleWindowSlide = (_event: unknown, value: number | number[]): void => { + const nextStart = value as number + setRange([nextStart, nextStart + presetWidth - 1]) + } + + const handleRangeChange = (_event: unknown, value: number | number[], thumb: number): void => { + const [nextStart, nextEnd] = value as [number, number] + if (nextEnd - nextStart >= MIN_VISIBLE_POINTS - 1) { + setRange([nextStart, nextEnd]) + } else if (thumb === 0) { + const start = Math.min(nextStart, lastIndex - (MIN_VISIBLE_POINTS - 1)) + setRange([start, start + MIN_VISIBLE_POINTS - 1]) + } else { + const end = Math.max(nextEnd, MIN_VISIBLE_POINTS - 1) + setRange([end - (MIN_VISIBLE_POINTS - 1), end]) + } + } + + if (!hasHydrated) { + return
Loading…
+ } + + if (loginRequired) { + return ( +
+ Please log in before searching. +
+ ) + } if (error) { return
Error: {error}
@@ -45,25 +131,96 @@ export default function OverviewPage() { return
Loading…
} + if (pointCount === 0) { + return
No complaint data available.
+ } + + const firstVisibleYear = visibleData[0]?.year + const lastVisibleYear = visibleData[visibleData.length - 1]?.year + return (

Complaints Over Time

+
+ + {WINDOW_OPTIONS.filter((option) => option < pointCount).map((option) => ( + + {option} yrs + + ))} + All + + + + {firstVisibleYear === lastVisibleYear + ? firstVisibleYear + : `${firstVisibleYear}–${lastVisibleYear}`} + +
+ point.year), + data: visibleData.map((point) => point.year), scaleType: "point", label: "Year" } ]} series={[ { - data: data.map((point) => point.complaint_count), + data: visibleData.map((point) => point.complaint_count), label: "Complaints" } ]} height={400} /> + +
+ {isAll ? ( + (index === 0 ? "First year shown" : "Last year shown")} + valueLabelDisplay="auto" + valueLabelFormat={(value) => data[value].year} + /> + ) : ( + + `${data[value].year}–${data[Math.min(value + presetWidth - 1, lastIndex)].year}` + } + /> + )} +
) }