● Zion Boggan
repos/Sightline/frontend/src/app/states/page.tsx
zionboggan.com ↗
84 lines · tsx
History for this file →
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
}