● Zion Boggan
repos/Docket/app/states/page.tsx
zionboggan.com ↗
237 lines · tsx
History for this file →
1
import type { Metadata } from "next";
2
import Link from "next/link";
3
import { Card, PageHeader, SectionHeader } from "@/components/ui";
4
import { DataTable, type Column, type Row } from "@/components/data-table";
5
import { getStates, type StateCoverage } from "@/lib/feed";
6
import { clsx } from "@/lib/cx";
7
import { formatDate } from "@/lib/format";
8
 
9
export const metadata: Metadata = {
10
  title: "States",
11
  description:
12
    "Coverage for the 50 states, DC, and U.S. territories. State-legislative coverage is rolling out; gaps are recorded, not hidden. Open a state for its delegation and sponsored bills.",
13
};
14
 
15
export const revalidate = 300;
16
 
17
 
18
function coverageMeta(c: StateCoverage["confidence"]): {
19
  label: string;
20
  color: string;
21
} {
22
  if (c === "ok") return { label: "Current", color: "var(--dk-yea)" };
23
  if (c === "stale") return { label: "Stale", color: "var(--dk-hold)" };
24
  return { label: "Coverage rolling out", color: "var(--dk-ash)" };
25
}
26
 
27
 
28
const NON_STATE_CODES = new Set(["DC", "PR", "AS", "GU", "MP", "VI"]);
29
 
30
function buildColumns(): Column[] {
31
  return [
32
    { key: "state", header: "Name", cardTitle: true },
33
    { key: "confidence", header: "Coverage" },
34
    { key: "seen", header: "Last seen", hideOnCard: true },
35
    { key: "missing", header: "Not yet in record" },
36
  ];
37
}
38
 
39
function buildRows(entries: StateCoverage[]): Row[] {
40
  return entries.map((s) => {
41
    const m = coverageMeta(s.confidence);
42
    return {
43
      key: s.code,
44
      cells: {
45
        state: (
46
          <Link
47
            href={`/states/${s.code}`}
48
            className="focus-ring rounded-sm text-paper underline underline-offset-4 hover:text-paper"
49
          >
50
            <span className="mr-2 text-ash-2">{s.code}</span>
51
            {s.name}
52
          </Link>
53
        ),
54
        confidence: (
55
          <span className="inline-flex items-center gap-1.5" style={{ color: m.color }}>
56
            <span
57
              aria-hidden="true"
58
              className="inline-block h-1.5 w-1.5 rounded-full"
59
              style={{ backgroundColor: m.color }}
60
            />
61
            {m.label}
62
          </span>
63
        ),
64
        seen: (
65
          <span className="tabular-nums text-ash-2">
66
            {s.lastScrape ? formatDate(s.lastScrape) : "-"}
67
          </span>
68
        ),
69
        missing: (
70
          <span className="uppercase text-ash-2">
71
            {s.missingTypes.length ? s.missingTypes.join(", ") : "-"}
72
          </span>
73
        ),
74
      },
75
    };
76
  });
77
}
78
 
79
export default function StatesPage() {
80
  const allJurisdictions = getStates();
81
 
82
  const states50 = allJurisdictions.filter((s) => !NON_STATE_CODES.has(s.code));
83
  const dcAndTerritories = allJurisdictions.filter((s) => NON_STATE_CODES.has(s.code));
84
 
85
  const ok      = states50.filter((s) => s.confidence === "ok").length;
86
  const stale   = states50.filter((s) => s.confidence === "stale").length;
87
  const rolling = states50.filter((s) => s.confidence === "missing").length;
88
 
89
  const columns = buildColumns();
90
 
91
  return (
92
    <div
93
      className="mx-auto w-full max-w-[860px] px-6 sm:px-8"
94
      style={{
95
        paddingTop: "clamp(32px, 6vw, 56px)",
96
        paddingBottom: "clamp(48px, 8vw, 80px)",
97
      }}
98
    >
99
      {}
100
      <nav aria-label="Breadcrumb" className="mb-10">
101
        <ol className="flex flex-wrap items-center gap-2" style={{ marginBottom: "12px" }}>
102
          <li>
103
            <Link
104
              href="/"
105
              className="focus-ring label no-underline hover:underline"
106
              style={{ color: "var(--dk-signal)", fontSize: "12px", letterSpacing: "0.12em" }}
107
            >
108
              Home
109
            </Link>
110
          </li>
111
          <li aria-hidden="true" className="label" style={{ color: "var(--dk-ash-2)", fontSize: "12px" }}>/</li>
112
          <li
113
            className="label"
114
            style={{ color: "var(--dk-ash)", fontSize: "12px", letterSpacing: "0.12em" }}
115
            aria-current="page"
116
          >
117
            States
118
          </li>
119
        </ol>
120
        <Link
121
          href="/"
122
          className="focus-ring tap no-underline hover:underline"
123
          style={{
124
            display: "inline-flex",
125
            alignItems: "center",
126
            gap: "8px",
127
            fontFamily: "var(--font-mono), ui-monospace, monospace",
128
            fontSize: "13px",
129
            letterSpacing: "0.08em",
130
            color: "var(--dk-signal)",
131
          }}
132
        >
133
          ← Back to home
134
        </Link>
135
      </nav>
136
 
137
      <PageHeader
138
        eyebrow="State register"
139
        title="50 states + DC"
140
        lead="Federal delegation and sponsored bills are on record for every state, DC, and U.S. territory. State-level legislative coverage is rolling out; gaps are recorded here, not hidden."
141
      />
142
 
143
      {}
144
      <div className="grid gap-4 sm:grid-cols-3">
145
        <CoverageCard label="Current" count={ok} level="ok" />
146
        <CoverageCard label="Stale" count={stale} level="stale" />
147
        <CoverageCard label="Coverage rolling out" count={rolling} level="missing" />
148
      </div>
149
      <p className="label mt-3 text-ash-2">
150
        Coverage counts above apply to the 50 states. DC and U.S. territories are listed
151
        in their own section below.
152
      </p>
153
 
154
      {}
155
      <div className="mt-12">
156
        <div className="mb-6">
157
          <p className="label mb-2">50 states · federal delegation on record</p>
158
          <h2
159
            className="display text-paper"
160
            style={{ fontSize: "clamp(1.25rem, 4vw, 1.875rem)" }}
161
          >
162
            Coverage by state
163
          </h2>
164
          <p className="prose-lead mt-3 max-w-2xl text-ash">
165
            How much of each state&apos;s legislature is on record here - across bills,
166
            votes, and sponsors. Federal delegation and bill sponsorships (from
167
            congress.gov) are on record regardless. Open a state for its congressional
168
            delegation.
169
          </p>
170
        </div>
171
 
172
        <DataTable
173
          caption="Per-state coverage: name, coverage level, last scrape, data not yet in record."
174
          cardLabel="Per-state coverage"
175
          columns={columns}
176
          rows={buildRows(states50)}
177
        />
178
      </div>
179
 
180
      {}
181
      <div className="mt-14">
182
        <div className="mb-6">
183
          <p className="label mb-2">DC &amp; U.S. territories · not states</p>
184
          <h2
185
            className="display text-paper"
186
            style={{ fontSize: "clamp(1.25rem, 4vw, 1.875rem)" }}
187
          >
188
            District of Columbia &amp; territories
189
          </h2>
190
          <p className="prose-lead mt-3 max-w-2xl text-ash">
191
            DC is a federal district. Puerto Rico, American Samoa, Guam, the Northern
192
            Mariana Islands, and the U.S. Virgin Islands are U.S. territories. None are
193
            states. Congressional delegation and federal data are included in the record.
194
          </p>
195
        </div>
196
 
197
        <DataTable
198
          caption="DC and U.S. territory coverage: name, coverage level, last scrape, data not yet in record."
199
          cardLabel="DC and territory coverage"
200
          columns={columns}
201
          rows={buildRows(dcAndTerritories)}
202
        />
203
      </div>
204
    </div>
205
  );
206
}
207
 
208
function CoverageCard({
209
  label,
210
  count,
211
  level,
212
}: {
213
  label: string;
214
  count: number;
215
  level: StateCoverage["confidence"];
216
}) {
217
  const m = coverageMeta(level);
218
  return (
219
    <Card className="p-5">
220
      <p className="label mb-2">{label}</p>
221
      <div className="flex items-baseline gap-2">
222
        <span
223
          className="display text-paper"
224
          style={{ fontSize: "clamp(1.75rem, 6vw, 1.875rem)" }}
225
        >
226
          {count}
227
        </span>
228
        <span className="label text-ash-2">states</span>
229
      </div>
230
      <span
231
        className={clsx("mt-3 inline-block h-0.5 w-12 rounded-full")}
232
        style={{ backgroundColor: m.color }}
233
        aria-hidden="true"
234
      />
235
    </Card>
236
  );
237
}