● Zion Boggan
repos/Docket/components/site-header.tsx
zionboggan.com ↗
342 lines · tsx
History for this file →
1
"use client";
2
 
3
import { useEffect, useState } from "react";
4
import Link from "next/link";
5
import { usePathname } from "next/navigation";
6
import { CustodyMark, Wordmark } from "@/components/custody-mark";
7
import { DisplayToggle } from "@/components/display-toggle";
8
import { clsx } from "@/lib/cx";
9
 
10
 
11
const NAV: Array<{ href: string; label: string }> = [
12
  { href: "/",            label: "Home" },
13
  { href: "/bills",       label: "Bills" },
14
  { href: "/laws",        label: "Laws" },
15
  { href: "/votes",       label: "Votes" },
16
  { href: "/members",     label: "Reps" },
17
  { href: "/spending",    label: "Spending" },
18
  { href: "/states",      label: "States" },
19
];
20
 
21
function isActive(pathname: string | null, href: string): boolean {
22
  if (href === "/") return pathname === "/";
23
  return pathname === href || !!pathname?.startsWith(href + "/");
24
}
25
 
26
export function SiteHeader() {
27
  const pathname = usePathname();
28
  const [open, setOpen] = useState(false);
29
 
30
  useEffect(() => { setOpen(false); }, [pathname]);
31
 
32
  useEffect(() => {
33
    if (!open) return;
34
    const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); };
35
    document.addEventListener("keydown", onKey);
36
    const prev = document.body.style.overflow;
37
    document.body.style.overflow = "hidden";
38
    return () => {
39
      document.removeEventListener("keydown", onKey);
40
      document.body.style.overflow = prev;
41
    };
42
  }, [open]);
43
 
44
  return (
45
    <>
46
      <header
47
        className="sticky top-0 z-40"
48
        style={{
49
          background: "var(--dk-header-bg)",
50
          borderBottom: "1px solid var(--dk-header-border)",
51
          backdropFilter: "blur(14px) saturate(120%)",
52
          WebkitBackdropFilter: "blur(14px) saturate(120%)",
53
          height: "64px",
54
          display: "flex",
55
          alignItems: "center",
56
        }}
57
      >
58
        <div
59
          className="mx-auto w-full px-4 sm:px-7"
60
          style={{ maxWidth: "1180px", display: "flex", alignItems: "center", gap: "20px" }}
61
        >
62
          {}
63
          <Link
64
            href="/"
65
            className="focus-ring shrink-0 no-underline"
66
            aria-label="Docket - home"
67
            style={{ display: "inline-flex", alignItems: "center", gap: "10px", minHeight: "44px" }}
68
          >
69
            <CustodyMark size={28} />
70
            <Wordmark className="font-display text-paper" />
71
          </Link>
72
 
73
          {}
74
          <nav
75
            aria-label="Primary"
76
            className="hidden md:flex"
77
            style={{ marginLeft: "12px", gap: "0px" }}
78
          >
79
            {NAV.map((item) => {
80
              const active = isActive(pathname, item.href);
81
              return (
82
                <Link
83
                  key={item.href}
84
                  href={item.href}
85
                  aria-current={active ? "page" : undefined}
86
                  className="focus-ring no-underline"
87
                  style={{
88
                    fontFamily: "var(--font-mono), ui-monospace, monospace",
89
                    fontSize: "12.5px",
90
                    fontWeight: 400,
91
                    letterSpacing: "0.13em",
92
                    textTransform: "uppercase",
93
                    color: active ? "var(--dk-paper)" : "var(--dk-ash)",
94
                    display: "inline-flex",
95
                    alignItems: "center",
96
                    minHeight: "44px",
97
                    padding: "0 7px",
98
                    whiteSpace: "nowrap",
99
                    borderRadius: "6px",
100
                    position: "relative",
101
                    transition: "color 0.2s",
102
                    textDecoration: "none",
103
                  }}
104
                >
105
                  {item.label}
106
                  {active && (
107
                    <span
108
                      aria-hidden="true"
109
                      style={{
110
                        position: "absolute",
111
                        left: "7px",
112
                        right: "7px",
113
                        bottom: "2px",
114
                        height: "2px",
115
                        background: "var(--dk-signal)",
116
                        borderRadius: "2px",
117
                        boxShadow: "0 0 8px rgba(224,165,59,0.45)",
118
                      }}
119
                    />
120
                  )}
121
                </Link>
122
              );
123
            })}
124
          </nav>
125
 
126
          {}
127
          <div
128
            className="flex items-center"
129
            style={{ marginLeft: "auto", gap: "8px" }}
130
          >
131
            {}
132
            <Link
133
              href="/search"
134
              aria-label="Search Docket"
135
              className="focus-ring no-underline tap"
136
              style={{
137
                display: "inline-flex",
138
                alignItems: "center",
139
                gap: "7px",
140
                padding: "0 12px",
141
                height: "36px",
142
                border: "1px solid var(--dk-line)",
143
                borderRadius: "8px",
144
                background: "transparent",
145
                color: "var(--dk-ash)",
146
                fontFamily: "var(--font-mono), ui-monospace, monospace",
147
                fontSize: "12.5px",
148
                letterSpacing: "0.12em",
149
                textTransform: "uppercase",
150
                transition: "border-color 0.2s, color 0.2s",
151
              }}
152
            >
153
              <SearchIcon />
154
              <span className="hidden lg:inline">Search</span>
155
            </Link>
156
 
157
            <DisplayToggle />
158
 
159
            {}
160
            <button
161
              type="button"
162
              onClick={() => setOpen((v) => !v)}
163
              aria-label={open ? "Close menu" : "Open menu"}
164
              aria-expanded={open}
165
              aria-controls="primary-mobile-nav"
166
              className="focus-ring md:hidden"
167
              style={{
168
                display: "flex",
169
                alignItems: "center",
170
                justifyContent: "center",
171
                flexDirection: "column",
172
                gap: "4px",
173
                width: "44px",
174
                height: "44px",
175
                border: "1px solid var(--dk-line)",
176
                borderRadius: "8px",
177
                background: "transparent",
178
                cursor: "pointer",
179
              }}
180
            >
181
              <BurgerIcon open={open} />
182
            </button>
183
          </div>
184
        </div>
185
      </header>
186
 
187
      {}
188
      {open && (
189
        <>
190
          {}
191
          <div
192
            role="presentation"
193
            className="fixed inset-0 z-[99] md:hidden"
194
            style={{ background: "rgba(6,7,9,0.6)", backdropFilter: "blur(4px)" }}
195
            onClick={() => setOpen(false)}
196
          />
197
          {}
198
          <div
199
            id="primary-mobile-nav"
200
            className="fixed top-0 bottom-0 right-0 z-[100] md:hidden"
201
            style={{
202
              width: "min(82vw, 340px)",
203
              background: "var(--dk-surface)",
204
              borderLeft: "1px solid var(--dk-line)",
205
              paddingTop: "calc(64px + 16px)",
206
              paddingLeft: "24px",
207
              paddingRight: "24px",
208
              paddingBottom: "28px",
209
              display: "flex",
210
              flexDirection: "column",
211
              gap: "4px",
212
              overflowY: "auto",
213
              boxShadow: "-30px 0 60px -20px rgba(0,0,0,0.7)",
214
            }}
215
            role="dialog"
216
            aria-label="Primary navigation"
217
            aria-modal="true"
218
          >
219
            <nav aria-label="Primary mobile">
220
              <ul style={{ display: "flex", flexDirection: "column", gap: "0" }}>
221
                {NAV.map((item) => {
222
                  const active = isActive(pathname, item.href);
223
                  return (
224
                    <li key={item.href}>
225
                      <Link
226
                        href={item.href}
227
                        aria-current={active ? "page" : undefined}
228
                        className="focus-ring no-underline"
229
                        style={{
230
                          display: "flex",
231
                          alignItems: "center",
232
                          justifyContent: "space-between",
233
                          fontFamily: "var(--font-mono), ui-monospace, monospace",
234
                          fontSize: "14px",
235
                          letterSpacing: "0.12em",
236
                          textTransform: "uppercase",
237
                          color: active ? "var(--dk-paper)" : "var(--dk-ash)",
238
                          padding: "14px 4px",
239
                          borderBottom: "1px solid rgba(155,160,166,0.10)",
240
                          paddingLeft: active ? "12px" : "4px",
241
                          borderLeft: active ? "2px solid var(--dk-signal)" : "2px solid transparent",
242
                          transition: "color 0.2s",
243
                          textDecoration: "none",
244
                        }}
245
                      >
246
                        {item.label}
247
                      </Link>
248
                    </li>
249
                  );
250
                })}
251
                {}
252
                <li>
253
                  <Link
254
                    href="/search"
255
                    className="focus-ring no-underline"
256
                    style={{
257
                      display: "flex",
258
                      alignItems: "center",
259
                      gap: "12px",
260
                      fontFamily: "var(--font-mono), ui-monospace, monospace",
261
                      fontSize: "14px",
262
                      letterSpacing: "0.12em",
263
                      textTransform: "uppercase",
264
                      color: "var(--dk-ash)",
265
                      padding: "14px 4px",
266
                      borderBottom: "1px solid rgba(155,160,166,0.10)",
267
                      marginTop: "12px",
268
                      textDecoration: "none",
269
                    }}
270
                  >
271
                    <SearchIcon />
272
                    Search
273
                  </Link>
274
                </li>
275
              </ul>
276
            </nav>
277
          </div>
278
        </>
279
      )}
280
    </>
281
  );
282
}
283
 
284
function SearchIcon() {
285
  return (
286
    <svg
287
      aria-hidden="true"
288
      width="16"
289
      height="16"
290
      viewBox="0 0 16 16"
291
      fill="none"
292
      stroke="currentColor"
293
      strokeWidth="1.5"
294
      strokeLinecap="round"
295
      strokeLinejoin="round"
296
      style={{ flexShrink: 0 }}
297
    >
298
      <circle cx="6.5" cy="6.5" r="4.5" />
299
      <line x1="10.5" y1="10.5" x2="14" y2="14" />
300
    </svg>
301
  );
302
}
303
 
304
function BurgerIcon({ open }: { open: boolean }) {
305
  return (
306
    <>
307
      <span
308
        style={{
309
          display: "block",
310
          width: "18px",
311
          height: "2px",
312
          background: "var(--dk-paper)",
313
          borderRadius: "2px",
314
          transform: open ? "translateY(6px) rotate(45deg)" : "none",
315
          transition: "transform 0.25s",
316
        }}
317
      />
318
      <span
319
        style={{
320
          display: "block",
321
          width: "18px",
322
          height: "2px",
323
          background: "var(--dk-paper)",
324
          borderRadius: "2px",
325
          opacity: open ? 0 : 1,
326
          transition: "opacity 0.2s",
327
        }}
328
      />
329
      <span
330
        style={{
331
          display: "block",
332
          width: "18px",
333
          height: "2px",
334
          background: "var(--dk-paper)",
335
          borderRadius: "2px",
336
          transform: open ? "translateY(-6px) rotate(-45deg)" : "none",
337
          transition: "transform 0.25s",
338
        }}
339
      />
340
    </>
341
  );
342
}