| 1 | "use client"; |
| 2 | import { useEffect, useState } from "react"; |
| 3 | import Link from "next/link"; |
| 4 | import { api, StateRow } from "@/lib/api"; |
| 5 | |
| 6 | const STATE_NAMES: Record<string, string> = { |
| 7 | AL: "Alabama", AK: "Alaska", AZ: "Arizona", AR: "Arkansas", CA: "California", CO: "Colorado", |
| 8 | CT: "Connecticut", DE: "Delaware", FL: "Florida", GA: "Georgia", HI: "Hawaii", ID: "Idaho", |
| 9 | IL: "Illinois", IN: "Indiana", IA: "Iowa", KS: "Kansas", KY: "Kentucky", LA: "Louisiana", |
| 10 | ME: "Maine", MD: "Maryland", MA: "Massachusetts", MI: "Michigan", MN: "Minnesota", MS: "Mississippi", |
| 11 | MO: "Missouri", MT: "Montana", NE: "Nebraska", NV: "Nevada", NH: "New Hampshire", NJ: "New Jersey", |
| 12 | NM: "New Mexico", NY: "New York", NC: "North Carolina", ND: "North Dakota", OH: "Ohio", |
| 13 | OK: "Oklahoma", OR: "Oregon", PA: "Pennsylvania", RI: "Rhode Island", SC: "South Carolina", |
| 14 | SD: "South Dakota", TN: "Tennessee", TX: "Texas", UT: "Utah", VT: "Vermont", VA: "Virginia", |
| 15 | WA: "Washington", WV: "West Virginia", WI: "Wisconsin", WY: "Wyoming", |
| 16 | }; |
| 17 | |
| 18 | export default function States() { |
| 19 | const [rows, setRows] = useState<StateRow[]>([]); |
| 20 | const [sel, setSel] = useState<string | null>(null); |
| 21 | const [plates, setPlates] = useState<{ plate_norm: string; reads: number }[]>([]); |
| 22 | const [err, setErr] = useState<string | null>(null); |
| 23 | |
| 24 | useEffect(() => { |
| 25 | api.states().then(setRows) |
| 26 | .catch((e) => setErr(e.status === 401 ? "Sign in to view state coverage." : String(e))); |
| 27 | }, []); |
| 28 | |
| 29 | function open(state: string) { |
| 30 | setSel(state); |
| 31 | api.statePlates(state).then((r) => setPlates(r.plates)).catch(() => setPlates([])); |
| 32 | } |
| 33 | |
| 34 | const maxReads = Math.max(1, ...rows.map((r) => r.reads)); |
| 35 | |
| 36 | return ( |
| 37 | <div> |
| 38 | <h1>Nationwide Coverage</h1> |
| 39 | <p className="muted">Plate-read volume by registration state across the network. Click any |
| 40 | state to drill into its vehicles.</p> |
| 41 | {err && <div className="badge danger">{err}</div>} |
| 42 | |
| 43 | <div style={{ display: "grid", gridTemplateColumns: sel ? "1fr 360px" : "1fr", gap: 16 }}> |
| 44 | <div className="card" style={{ padding: 8 }}> |
| 45 | <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(150px,1fr))", gap: 6 }}> |
| 46 | {rows.map((r) => ( |
| 47 | <div key={r.state} onClick={() => open(r.state)} |
| 48 | style={{ padding: "8px 10px", borderRadius: 6, cursor: "pointer", |
| 49 | border: "1px solid var(--border)", |
| 50 | background: sel === r.state ? "var(--panel-2)" : "transparent" }}> |
| 51 | <div className="row" style={{ justifyContent: "space-between" }}> |
| 52 | <strong>{r.state}</strong><span className="muted">{r.vehicles} veh</span> |
| 53 | </div> |
| 54 | <div className="muted" style={{ fontSize: 11 }}>{STATE_NAMES[r.state] || ""}</div> |
| 55 | <div style={{ height: 4, marginTop: 4, borderRadius: 2, |
| 56 | background: "var(--accent)", width: `${(r.reads / maxReads) * 100}%` }} /> |
| 57 | <div className="muted" style={{ fontSize: 11, marginTop: 2 }}>{r.reads} reads</div> |
| 58 | </div> |
| 59 | ))} |
| 60 | {!rows.length && !err && <div className="muted">Loading states…</div>} |
| 61 | </div> |
| 62 | </div> |
| 63 | |
| 64 | {sel && ( |
| 65 | <div className="card"> |
| 66 | <h2 style={{ marginTop: 0 }}>{STATE_NAMES[sel] || sel} ({sel})</h2> |
| 67 | <table> |
| 68 | <thead><tr><th>Plate</th><th>Reads</th></tr></thead> |
| 69 | <tbody> |
| 70 | {plates.map((p) => ( |
| 71 | <tr key={p.plate_norm}> |
| 72 | <td><Link href={`/vehicles/${p.plate_norm}`}>{p.plate_norm}</Link></td> |
| 73 | <td><span className="badge">{p.reads}</span></td> |
| 74 | </tr> |
| 75 | ))} |
| 76 | {!plates.length && <tr><td colSpan={2} className="muted">No vehicles.</td></tr>} |
| 77 | </tbody> |
| 78 | </table> |
| 79 | </div> |
| 80 | )} |
| 81 | </div> |
| 82 | </div> |
| 83 | ); |
| 84 | } |