1// Sticky top navigation bar. Adapts based on whether we're on the repo picker
2// page (root) or inside a specific repo:
3// - Root: shows logo only, no Code/Issues links
4// - Repo: shows Code + Issues nav links scoped to the current repo slug
5//
6// In external mode, shows a "Sign in" button when logged out and a sign-out
7// action when logged in.
8
9import { Link, useParams } from "@tanstack/react-router";
10import { Bug, Plus, Sun, Moon, LogIn, LogOut } from "lucide-react";
11
12import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
13import { Button } from "@/components/ui/button";
14import { ButtonLink, NavLink } from "@/components/ui/button-link";
15import { useAuth } from "@/lib/auth";
16import { useTheme } from "@/lib/theme";
17
18// SignOutButton sends a POST to /auth/logout and reloads the page.
19// A full reload is the simplest way to reset all Apollo cache + React state.
20function handleSignOut() {
21 void fetch("/auth/logout", { method: "POST", credentials: "include" }).finally(() =>
22 window.location.assign("/"),
23 );
24}
25
26function SignOutButton() {
27 return (
28 <Button variant="ghost" size="sm" onClick={handleSignOut} title="Sign out">
29 <LogOut className="size-4" />
30 </Button>
31 );
32}
33
34export function Header() {
35 const { user, mode, loginProviders } = useAuth();
36 const { theme, toggle } = useTheme();
37
38 // Detect if we're inside a /$repo route and grab the slug.
39 const params = useParams({ strict: false });
40 const repo = params.repo ?? null;
41
42 // Don't show repo nav on the /auth/* pages.
43 const effectiveRepo = repo === "auth" ? null : repo;
44
45 return (
46 <header className="border-border bg-background/95 sticky top-0 z-50 border-b backdrop-blur">
47 <div className="mx-auto flex h-14 max-w-screen-xl items-center gap-6 px-4">
48 {/* Logo always goes to the repo picker root */}
49 <Link to="/" className="text-foreground flex items-center gap-2 font-semibold">
50 <Bug className="size-4" />
51 <span>git-bug</span>
52 </Link>
53
54 {/* Repo-scoped nav links — only shown when inside a repo */}
55 {effectiveRepo && (
56 <nav className="flex items-center gap-1">
57 <NavLink
58 to="/$repo"
59 params={{ repo: effectiveRepo }}
60 search={{ ref: "", path: "", type: "tree" as const }}
61 activeOptions={{ exact: true }}
62 >
63 Code
64 </NavLink>
65 <NavLink
66 to="/$repo/issues"
67 params={{ repo: effectiveRepo }}
68 search={{ q: "status:open", after: "" }}
69 >
70 Issues
71 </NavLink>
72 </nav>
73 )}
74
75 <div className="ml-auto flex items-center gap-2">
76 {mode === "readonly" && <span className="text-muted-foreground text-xs">Read only</span>}
77
78 <Button variant="ghost" size="icon" onClick={toggle} title="Toggle theme">
79 {theme === "light" ? <Moon className="size-4" /> : <Sun className="size-4" />}
80 </Button>
81
82 {/* External mode: show sign-in buttons when logged out */}
83 {mode === "external" &&
84 !user &&
85 loginProviders.map((p) => (
86 <Button key={p} asChild size="sm">
87 <a href={`/auth/login?provider=${p}`}>
88 <LogIn className="size-4" />
89 Sign in with {providerLabel(p)}
90 </a>
91 </Button>
92 ))}
93
94 {user && effectiveRepo && (
95 <>
96 <ButtonLink to="/$repo/issues/new" params={{ repo: effectiveRepo }} size="sm">
97 <Plus className="size-4" />
98 New issue
99 </ButtonLink>
100 <Link to="/$repo/user/$id" params={{ repo: effectiveRepo, id: user.humanId }}>
101 <Avatar className="size-7">
102 <AvatarImage src={user.avatarUrl ?? undefined} alt={user.displayName} />
103 <AvatarFallback className="text-xs">
104 {user.displayName.slice(0, 2).toUpperCase()}
105 </AvatarFallback>
106 </Avatar>
107 </Link>
108 </>
109 )}
110
111 {/* Sign out only shown in external mode when logged in */}
112 {mode === "external" && user && <SignOutButton />}
113 </div>
114 </div>
115 </header>
116 );
117}
118
119function providerLabel(name: string): string {
120 const labels: Record<string, string> = { github: "GitHub", gitlab: "GitLab", gitea: "Gitea" };
121 return labels[name] ?? name;
122}