● Zion Boggan
repos/Docket/app/register/page.tsx
zionboggan.com ↗
250 lines · tsx
History for this file →
1
import type { Metadata } from "next";
2
import Link from "next/link";
3
import { RegisterHero } from "@/components/register-hero";
4
import { Card, SectionHeader } from "@/components/ui";
5
import { DataTable, type Column, type Row } from "@/components/data-table";
6
import { SourceLink } from "@/components/source-link";
7
import { ChangesFeed } from "@/components/changes-feed";
8
import {
9
  getFeedItems,
10
  getAwardsSummary,
11
  getChangeEvents,
12
  getCategories,
13
  type FeedItem,
14
} from "@/lib/feed";
15
import { formatUsd, formatDate, formatEt } from "@/lib/format";
16
 
17
export const metadata: Metadata = {
18
  title: "The Register",
19
  description:
20
    "A live, stamped ledger of official US government actions - bills, votes, and dollars. Every fact sourced.",
21
};
22
 
23
export const revalidate = 300;
24
 
25
const ACTION_LABEL: Record<FeedItem["kind"], string> = {
26
  bill: "Bills",
27
  vote: "Votes",
28
  award: "Awards",
29
  amendment: "Amendments",
30
  rider: "Riders",
31
  procedural: "Procedural",
32
  executive: "Federal Register",
33
  apportionment: "Apportionments",
34
};
35
 
36
const FEED_RENDER_LIMIT = 150;
37
 
38
export default async function RegisterPage() {
39
  const items = getFeedItems();
40
  const shown = items.slice(0, FEED_RENDER_LIMIT);
41
  const awardStats = getAwardsSummary();
42
  const changes = getChangeEvents(12);
43
  const topTopics = getCategories()
44
    .filter((c) => c.total > 0)
45
    .sort((a, b) => b.total - a.total)
46
    .slice(0, 8);
47
  const kindBreakdown = byKind(items);
48
 
49
  return (
50
    <>
51
      <RegisterHero seed={shown.slice(0, 12)} />
52
 
53
      <div className="mx-auto w-full max-w-register px-4 py-12 sm:px-6">
54
        {
55
}
56
        <p className="prose-lead mb-8 max-w-2xl text-ash">
57
          Every bill, vote, and federal award is listed below, drawn from
58
          Congress.gov, the Federal Register, and USAspending.gov. Ranked by
59
          date - never by clicks or editorial judgment. Every entry links to
60
          its official source.
61
        </p>
62
 
63
        <SectionHeader
64
          eyebrow="Full register · ranked"
65
          title="Every official action, in order"
66
        />
67
 
68
        {items.length === 0 ? (
69
          <Card className="p-8 text-center">
70
            <p className="display text-2xl text-paper">No actions on record.</p>
71
            <p className="prose-lead mx-auto mt-3 max-w-md text-ash">
72
              The register fills as the government acts. Each line will carry a
73
              link to its official source.
74
            </p>
75
          </Card>
76
        ) : (
77
          <RegisterTable items={shown} />
78
        )}
79
 
80
        {
81
}
82
        <details className="group mt-12">
83
          <summary className="focus-ring label inline-flex cursor-pointer list-none items-center gap-2 rounded-[3px] border border-line bg-surface px-4 py-3 text-ash hover:text-paper">
84
            <span aria-hidden="true" className="transition-transform group-open:rotate-90">▶</span>
85
            More: composition, awards &amp; recently changed
86
          </summary>
87
 
88
          <div className="mt-6 grid gap-6 sm:grid-cols-2">
89
            {}
90
            <Card className="p-5">
91
              <p className="label mb-4">Composition by action type</p>
92
              <ul className="space-y-2.5">
93
                {kindBreakdown.map((a) => {
94
                  const max = Math.max(1, ...kindBreakdown.map((x) => x.count));
95
                  return (
96
                    <li key={a.kind} className="font-mono text-[12px]">
97
                      <div className="flex items-center justify-between">
98
                        <span className="text-ash">{ACTION_LABEL[a.kind]}</span>
99
                        <span className="tabular-nums text-paper">{a.count}</span>
100
                      </div>
101
                      <div className="mt-1 h-1 w-full overflow-hidden rounded-full bg-ink">
102
                        <span
103
                          className="block h-full rounded-full"
104
                          style={{
105
                            width: `${(a.count / max) * 100}%`,
106
                            backgroundColor: "var(--dk-ash)",
107
                          }}
108
                        />
109
                      </div>
110
                    </li>
111
                  );
112
                })}
113
              </ul>
114
              <p className="label mt-4 text-ash-2">
115
                Proportional to action volume. No editorial weighting.
116
              </p>
117
            </Card>
118
 
119
            <Card className="p-5">
120
              <p className="label mb-4">Awards, on record</p>
121
              <p className="display text-3xl text-paper">
122
                {formatUsd(awardStats.total, { compact: true })}
123
              </p>
124
              <p className="prose-lead mt-2 text-ash">
125
                {awardStats.count} federal awards from USAspending.gov, summed across
126
                the record. Figures only - no characterization.
127
              </p>
128
              <Link
129
                href="/spending"
130
                className="focus-ring tap label mt-5 gap-1 text-ash underline underline-offset-4 hover:text-paper"
131
              >
132
                Spending register →
133
              </Link>
134
            </Card>
135
          </div>
136
 
137
          <div className="mt-6 grid gap-6 sm:grid-cols-2">
138
            <Card className="p-5">
139
              <div className="mb-3 flex items-baseline justify-between">
140
                <p className="label">Recently changed</p>
141
                <Link
142
                  href="/calendar"
143
                  className="focus-ring tap label gap-1 text-ash underline underline-offset-4 hover:text-paper"
144
                >
145
                  Calendar →
146
                </Link>
147
              </div>
148
              <ChangesFeed items={changes} />
149
              <p className="label mt-4 text-ash-2">
150
                What the ingest added, updated, or removed. Each entry links to its
151
                source.
152
              </p>
153
            </Card>
154
 
155
            <Card className="p-5">
156
              <div className="mb-3 flex items-baseline justify-between">
157
                <p className="label">Browse by topic</p>
158
                <Link
159
                  href="/categories"
160
                  className="focus-ring tap label gap-1 text-ash underline underline-offset-4 hover:text-paper"
161
                >
162
                  All topics →
163
                </Link>
164
              </div>
165
              <ul className="flex flex-wrap gap-1.5">
166
                {topTopics.map((c) => (
167
                  <li key={c.slug}>
168
                    <Link
169
                      href={`/bills?policyArea=${encodeURIComponent(c.label)}`}
170
                      className="focus-ring label inline-flex min-h-[44px] items-center gap-2 rounded-[3px] border border-line px-2.5 text-ash hover:text-paper"
171
                    >
172
                      <span className="text-paper">{c.label}</span>
173
                      <span className="text-ash-2">{c.total}</span>
174
                    </Link>
175
                  </li>
176
                ))}
177
              </ul>
178
              <p className="label mt-4 text-ash-2">
179
                Bills and awards grouped by policy area. Counts only - no weighting.
180
              </p>
181
            </Card>
182
          </div>
183
        </details>
184
      </div>
185
    </>
186
  );
187
}
188
 
189
function byKind(items: FeedItem[]) {
190
  const map = new Map<FeedItem["kind"], number>();
191
  for (const i of items) map.set(i.kind, (map.get(i.kind) ?? 0) + 1);
192
  return Array.from(map, ([kind, count]) => ({ kind, count })).sort(
193
    (a, b) => b.count - a.count
194
  );
195
}
196
 
197
function RegisterTable({ items }: { items: FeedItem[] }) {
198
  const columns: Column[] = [
199
    { key: "date", header: "Date", width: "8rem" },
200
    { key: "body", header: "Body", hideOnCard: true, width: "6rem" },
201
    { key: "item", header: "Item", cardTitle: true },
202
    { key: "action", header: "Action", width: "8rem" },
203
    { key: "result", header: "Result", align: "right", width: "7rem" },
204
    { key: "source", header: "Source", align: "right", width: "11rem" },
205
  ];
206
  const rows: Row[] = items.map((r) => ({
207
    key: r.id,
208
    cells: {
209
      date: (
210
        <span className="tabular-nums text-ash">
211
          {formatEt(r.actionDate) || formatDate(r.actionDate)}
212
        </span>
213
      ),
214
      body: <span className="uppercase text-ash-2">{r.chamber}</span>,
215
      item: (
216
        <span className="text-paper">
217
          {r.href ? (
218
            <Link
219
              href={r.href}
220
              className="focus-ring rounded-sm underline underline-offset-4"
221
            >
222
              {r.title}
223
            </Link>
224
          ) : (
225
            r.title
226
          )}
227
          {r.summary ? (
228
            <span className="block text-ash-2">{r.summary}</span>
229
          ) : null}
230
        </span>
231
      ),
232
      action: r.actionType ? (
233
        <span className="uppercase text-ash">{r.actionType}</span>
234
      ) : null,
235
      result: r.amountText ? (
236
        <span className="tabular-nums text-paper">{r.amountText}</span>
237
      ) : null,
238
      source: <SourceLink url={r.sourceUrl} />,
239
    },
240
  }));
241
  return (
242
    <DataTable
243
      caption="Official actions in ranked order: date, body, item, action, result, and source."
244
      cardLabel="Official actions in ranked order"
245
      columns={columns}
246
      rows={rows}
247
      pageSize={50}
248
    />
249
  );
250
}