● Zion Boggan
repos/Docket/components/register-hero.tsx
zionboggan.com ↗
190 lines · tsx
History for this file →
1
"use client";
2
 
3
import { useCallback, useEffect, useRef, useState } from "react";
4
import Link from "next/link";
5
import { SourceLink } from "@/components/source-link";
6
import { clsx } from "@/lib/cx";
7
import { formatEt, formatDateShort } from "@/lib/format";
8
import type { FeedItem } from "@/lib/feed";
9
 
10
 
11
export function RegisterHero({ seed }: { seed: FeedItem[] }) {
12
  const [rows, setRows] = useState<FeedItem[]>(seed);
13
  const [flashId, setFlashId] = useState<string | null>(null);
14
  const prevIds = useRef<Set<string>>(new Set(seed.map((r) => r.id)));
15
 
16
  const refresh = useCallback(async () => {
17
    try {
18
      const res = await fetch("/api/feed?limit=12", { cache: "no-store" });
19
      if (!res.ok) return;
20
      const data = (await res.json()) as { items: FeedItem[] };
21
      if (!data.items?.length) return;
22
      setRows((prev) => {
23
        const next = data.items.slice(0, 12);
24
        const newTop = next.find((it) => !prevIds.current.has(it.id));
25
        next.forEach((it) => prevIds.current.add(it.id));
26
        if (newTop) setFlashId(newTop.id);
27
        return next;
28
      });
29
    } catch {
30
    }
31
  }, []);
32
 
33
  useEffect(() => {
34
    const iv = setInterval(refresh, 15000);
35
    return () => clearInterval(iv);
36
  }, [refresh]);
37
 
38
  return (
39
    <section
40
      className="relative overflow-hidden border-y border-line"
41
      aria-label="The Register - live feed of official actions"
42
    >
43
      {}
44
      <div
45
        aria-hidden="true"
46
        className="pointer-events-none absolute inset-0"
47
        style={{ background: "var(--dk-hero-glow)" }}
48
      />
49
 
50
      <div className="relative mx-auto w-full max-w-register px-4 py-8 sm:px-6 sm:py-14">
51
        {}
52
        <div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
53
          <div>
54
            <div className="mb-3 flex items-center gap-2.5">
55
              {}
56
              <span className="live-dot animate-pulseDot" aria-hidden="true" />
57
              <span className="label text-paper">Live · The Register</span>
58
            </div>
59
            <h1
60
              className="display max-w-2xl text-paper"
61
              style={{ fontSize: "clamp(1.875rem, 7vw, 3rem)", lineHeight: 1 }}
62
            >
63
              What the government did this week
64
            </h1>
65
          </div>
66
          <p className="prose-lead max-w-sm text-ash">
67
            Each line is an official action, sealed to its source. No
68
            commentary. Order is the order of action.
69
          </p>
70
        </div>
71
 
72
        {rows.length === 0 ? (
73
          <EmptyRegister />
74
        ) : (
75
          <div
76
            className="mt-8 overflow-hidden rounded-[3px] border border-line bg-surface/60 backdrop-blur-sm"
77
            aria-label="Stamped ledger of official actions"
78
          >
79
            {}
80
            <div className="reg-row hidden grid-cols-[5.5rem_7.5rem_minmax(0,1fr)_6.5rem_minmax(0,5rem)] gap-x-2 px-4 py-2 sm:grid">
81
              <span className="label">Date</span>
82
              <span className="label">Body</span>
83
              <span className="label">Item</span>
84
              <span className="label">Action</span>
85
              <span className="label text-right">Result</span>
86
            </div>
87
 
88
            {rows.map((r) => {
89
              const isNew = r.id === flashId;
90
              return (
91
                <div key={r.id} className="border-t border-line first:border-t-0">
92
                  {
93
}
94
                  <RegisterRowInner row={r} flash={isNew} />
95
                </div>
96
              );
97
            })}
98
          </div>
99
        )}
100
 
101
        <div className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
102
          <p className="label text-ash-2">
103
            Ranked by action date, dollar magnitude, and people affected - never by coverage.
104
          </p>
105
          <Link
106
            href="/how-we-pick"
107
            className="focus-ring label tap w-fit rounded-[3px] px-1 text-ash underline underline-offset-4 hover:text-paper"
108
          >
109
            How we pick →
110
          </Link>
111
        </div>
112
      </div>
113
    </section>
114
  );
115
}
116
 
117
function EmptyRegister() {
118
  return (
119
    <div className="mt-8 rounded-[3px] border border-line bg-surface/60 p-8 text-center">
120
      <p className="display text-2xl text-paper">The register is quiet.</p>
121
      <p className="prose-lead mx-auto mt-3 max-w-md text-ash">
122
        No official actions are on record yet. The ledger fills as bills are
123
        filed, votes are taken, and awards are made - each line sealed to its
124
        government source.
125
      </p>
126
    </div>
127
  );
128
}
129
 
130
function RegisterRowInner({
131
  row,
132
  flash,
133
}: {
134
  row: FeedItem;
135
  flash?: boolean;
136
}) {
137
  const isPass = row.actionType === "PASSED";
138
  const isFail = row.actionType === "FAILED";
139
  return (
140
    <div className="reg-row grid grid-cols-1 gap-1 px-3 py-2.5 sm:grid-cols-[5.5rem_7.5rem_minmax(0,1fr)_6.5rem_minmax(0,5rem)] sm:gap-x-2 sm:items-center sm:px-4">
141
      <span
142
        className={clsx(
143
          "font-mono text-[12px] tabular-nums text-ash",
144
          flash && "animate-fadeUp"
145
        )}
146
      >
147
        {formatEt(row.actionDate) || formatDateShort(row.actionDate)}
148
      </span>
149
      <span className="font-mono text-[12px] uppercase tracking-wide text-ash-2">
150
        {row.chamber}
151
      </span>
152
      <span className="min-w-0 font-mono text-[13px] text-paper">
153
        {}
154
        {row.href ? (
155
          <Link
156
            href={row.href}
157
            className="focus-ring rounded-sm underline underline-offset-4 hover:text-paper"
158
          >
159
            <span className="block break-words">{row.title}</span>
160
          </Link>
161
        ) : (
162
          <span className="block break-words">{row.title}</span>
163
        )}
164
        {}
165
        <SourceLink
166
          url={row.sourceUrl}
167
          label="Source"
168
          className="mt-1"
169
        />
170
      </span>
171
      <span
172
        className="font-mono text-[12px] uppercase tracking-wider"
173
        style={{
174
          color: isPass
175
            ? "var(--dk-yea)"
176
            : isFail
177
              ? "var(--dk-nay)"
178
              : "var(--dk-ash)",
179
        }}
180
      >
181
        {row.actionType}
182
      </span>
183
      <span className="font-mono text-[12px] tabular-nums text-paper sm:text-right">
184
        {
185
}
186
        {row.amountText ? row.amountText : <span className="hidden sm:inline">{"-"}</span>}
187
      </span>
188
    </div>
189
  );
190
}