Skip to content
File

Blob: src/client/components/header.tsx

typescript84 lines
1import { GitBranch, LogIn, User } from "lucide-react";
2 
3import { IslandHost } from "@/client/server/IslandHost";
4import { ThemeToggleIsland } from "@/client/islands/theme-toggle";
5import type { Viewer } from "@/client/server/viewer";
6 
7type HeaderProps = {
8 currentView?: string;
9 viewer?: Viewer | null;
10};
11 
12const navLinkClass = (isActive: boolean): string =>
13 [
14 "flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-canvas",
15 isActive
16 ? "bg-accent-500/10 text-accent-500 dark:text-accent-400"
17 : "text-zinc-500 hover:bg-zinc-100 hover:text-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200",
18 ].join(" ");
19 
20const signOutButtonClass =
21 "flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm text-zinc-500 hover:text-zinc-800 dark:text-zinc-500 dark:hover:text-zinc-200 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-500/50";
22 
23function AnonymousNav({ currentView }: { currentView?: string }) {
24 // Single primary call-to-action: the impeccable.md "confident restraint"
25 // principle says avoid extra nav noise on the anonymous header.
26 return (
27 <a href="/auth" className={navLinkClass(currentView === "auth-signin")}>
28 <LogIn className="h-3.5 w-3.5" aria-hidden="true" />
29 Sign in
30 </a>
31 );
32}
33 
34function SignedInNav({ currentView, viewer }: { currentView?: string; viewer: Viewer }) {
35 return (
36 <>
37 <a href="/auth/account" className={navLinkClass(currentView === "account")}>
38 <User className="h-3.5 w-3.5" aria-hidden="true" />
39 {viewer.primaryNamespaceSlug ? `@${viewer.primaryNamespaceSlug}` : "Account"}
40 </a>
41 {/* Sign-out is a same-origin POST; the form preserves Lax cookies. */}
42 <form method="post" action="/auth/sign-out" className="contents">
43 <button type="submit" className={signOutButtonClass}>
44 Sign out
45 </button>
46 </form>
47 </>
48 );
49}
50 
51export function Header({ currentView, viewer }: HeaderProps) {
52 return (
53 <header className="sticky top-0 z-50 w-full border-b border-zinc-300 bg-white/80 backdrop-blur-sm dark:border-zinc-800/60 dark:bg-zinc-900/95">
54 <div className="mx-auto flex w-full max-w-7xl items-center justify-between gap-4 px-4 py-3 sm:px-6">
55 <nav className="flex items-center gap-1.5" aria-label="Primary">
56 <a href="/" className="group flex items-center gap-3 transition-opacity hover:opacity-80">
57 <span className="inline-grid h-9 w-9 place-items-center">
58 <GitBranch
59 className="h-6 w-6 text-accent-500 transition-transform duration-200 group-hover:-rotate-6 dark:text-accent-400"
60 strokeWidth={2}
61 aria-hidden="true"
62 />
63 </span>
64 <span className="hidden sm:block">
65 <strong className="block text-sm font-semibold text-zinc-900 dark:text-zinc-100">
66 git-on-cloudflare
67 </strong>
68 <small className="block text-xs text-zinc-400">Git hosting on Cloudflare</small>
69 </span>
70 </a>
71 {viewer ? (
72 <SignedInNav currentView={currentView} viewer={viewer} />
73 ) : (
74 <AnonymousNav currentView={currentView} />
75 )}
76 </nav>
77 <IslandHost name="theme-toggle" props={{}}>
78 <ThemeToggleIsland />
79 </IslandHost>
80 </div>
81 </header>
82 );
83}