● Zion Boggan
repos/Docket/components/executive-browser.tsx
zionboggan.com ↗
158 lines · tsx
History for this file →
1
"use client";
2
 
3
import { useMemo, useState } from "react";
4
import { Card } from "@/components/ui";
5
import { DataTable, type Column, type Row } from "@/components/data-table";
6
import { SourceLink } from "@/components/source-link";
7
import { clsx } from "@/lib/cx";
8
import { formatDate } from "@/lib/format";
9
import type { ExecutiveAction } from "@/lib/feed";
10
 
11
 
12
export function ExecutiveBrowser({
13
  seed,
14
  typeLabels,
15
}: {
16
  seed: ExecutiveAction[];
17
  typeLabels: Record<string, string>;
18
}) {
19
  const types = useMemo(
20
    () =>
21
      Array.from(new Set(seed.map((e) => (e.type || "").toLowerCase()))).sort(),
22
    [seed]
23
  );
24
 
25
  const [type, setType] = useState("");
26
  const [q, setQ] = useState("");
27
 
28
  const filtered = useMemo(() => {
29
    const query = q.trim().toLowerCase();
30
    return seed.filter((e) => {
31
      if (type && (e.type || "").toLowerCase() !== type) return false;
32
      if (
33
        query &&
34
        !e.title.toLowerCase().includes(query) &&
35
        !e.agency.toLowerCase().includes(query)
36
      ) {
37
        return false;
38
      }
39
      return true;
40
    });
41
  }, [seed, type, q]);
42
 
43
  return (
44
    <>
45
      <Card className="p-4 sm:p-5">
46
        <div className="grid gap-3 sm:grid-cols-[1fr_auto] sm:items-end">
47
          <label className="block">
48
            <span className="label mb-1.5 block">Search title or agency</span>
49
            <input
50
              type="search"
51
              value={q}
52
              onChange={(e) => setQ(e.target.value)}
53
              placeholder="e.g. emissions, FAA, Treasury"
54
              aria-label="Search Federal Register actions"
55
              className="focus-ring w-full rounded-[2px] border border-line bg-ink px-3 py-2 font-mono text-[13px] text-paper placeholder:text-ash-2"
56
            />
57
          </label>
58
          <label className="block">
59
            <span className="label mb-1.5 block">Type</span>
60
            <select
61
              value={type}
62
              onChange={(e) => setType(e.target.value)}
63
              aria-label="Filter by action type"
64
              className={clsx(
65
                "focus-ring rounded-[2px] border border-line bg-ink px-3 py-2",
66
                "font-mono text-[13px] text-paper"
67
              )}
68
            >
69
              <option value="" className="bg-surface text-paper">
70
                All types
71
              </option>
72
              {types.map((t) => (
73
                <option key={t} value={t} className="bg-surface text-paper">
74
                  {typeLabels[t] || t}
75
                </option>
76
              ))}
77
            </select>
78
          </label>
79
        </div>
80
        <div className="mt-3 flex items-center justify-between">
81
          <p className="label text-ash-2">
82
            {filtered.length} of {seed.length} actions
83
          </p>
84
          {(q || type) && (
85
            <button
86
              type="button"
87
              onClick={() => {
88
                setQ("");
89
                setType("");
90
              }}
91
              className="focus-ring label rounded-sm px-2 py-1 text-ash underline underline-offset-4 hover:text-paper"
92
            >
93
              Clear
94
            </button>
95
          )}
96
        </div>
97
      </Card>
98
 
99
      <div className="mt-6">
100
        {filtered.length === 0 ? (
101
          <Card className="p-8 text-center">
102
            <p className="display text-2xl text-paper">No actions match.</p>
103
            <p className="prose-lead mx-auto mt-3 max-w-md text-ash">
104
              Adjust the filter or search. The Federal Register feed updates daily.
105
            </p>
106
          </Card>
107
        ) : (
108
          <ExecutiveTable rows={filtered} typeLabels={typeLabels} />
109
        )}
110
      </div>
111
    </>
112
  );
113
}
114
 
115
function ExecutiveTable({
116
  rows: items,
117
  typeLabels,
118
}: {
119
  rows: ExecutiveAction[];
120
  typeLabels: Record<string, string>;
121
}) {
122
  const columns: Column[] = [
123
    { key: "date", header: "Date", width: "6.5rem" },
124
    { key: "type", header: "Type", hideOnCard: true, width: "9rem" },
125
    { key: "title", header: "Title", cardTitle: true },
126
    { key: "source", header: "Source", align: "right", width: "13rem" },
127
  ];
128
  const data: Row[] = items.map((e) => ({
129
    key: e.id,
130
    cells: {
131
      date: <span className="tabular-nums text-ash">{formatDate(e.date)}</span>,
132
      type: (
133
        <span className="uppercase text-ash-2">
134
          {typeLabels[(e.type || "").toLowerCase()] || e.type || "-"}
135
        </span>
136
      ),
137
      title: (
138
        <span className="text-paper">
139
          <span className="block">{e.title}</span>
140
          <span className="block text-ash-2">
141
            {e.agency}
142
            {e.frDocNumber ? ` · Doc ${e.frDocNumber}` : ""}
143
          </span>
144
        </span>
145
      ),
146
      source: <SourceLink url={e.sourceUrl} label="FR" />,
147
    },
148
  }));
149
  return (
150
    <DataTable
151
      caption="Federal Register actions: date, type, title, agency, document number, source."
152
      cardLabel="Federal Register actions"
153
      columns={columns}
154
      rows={data}
155
      pageSize={50}
156
    />
157
  );
158
}