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 { Bug, Plus, Sun, Moon, LogIn, LogOut } from "lucide-react";
10import { Link, useMatch, NavLink } from "react-router";
11
12import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
13import { Button } from "@/components/ui/button";
14import { useAuth } from "@/lib/auth";
15import { useTheme } from "@/lib/theme";
16import { cn } from "@/lib/utils";
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 SignOutButton() {
21 function handleSignOut() {
22 void fetch("/auth/logout", { method: "POST", credentials: "include" }).finally(() =>
23 window.location.assign("/"),
24 );
25 }
26 return (
27 <Button variant="ghost" size="sm" onClick={handleSignOut} title="Sign out">
28 <LogOut className="size-4" />
29 </Button>
30 );
31}
32
33export function Header() {
34 const { user, mode, loginProviders } = useAuth();
35 const { theme, toggle } = useTheme();
36
37 // Detect if we're inside a /:repo route and grab the slug.
38 // useMatch works from any component in the tree, unlike useParams which is
39 // scoped to the nearest Route element.
40 const repoMatch = useMatch({ path: "/:repo/*", end: false });
41 const repo = repoMatch?.params.repo ?? null;
42
43 // Don't show repo nav on the /auth/* pages.
44 const effectiveRepo = repo === "auth" ? null : repo;
45
46 return (
47 <header className="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur">
48 <div className="mx-auto flex h-14 max-w-screen-xl items-center gap-6 px-4">
49 {/* Logo always goes to the repo picker root */}
50 <Link to="/" className="flex items-center gap-2 font-semibold text-foreground">
51 <Bug className="size-4" />
52 <span>git-bug</span>
53 </Link>
54
55 {/* Repo-scoped nav links — only shown when inside a repo */}
56 {effectiveRepo && (
57 <nav className="flex items-center gap-1">
58 <NavLink
59 to={`/${effectiveRepo}`}
60 end
61 className={({ isActive }) =>
62 cn(
63 "rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
64 isActive
65 ? "bg-accent text-accent-foreground"
66 : "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
67 )
68 }
69 >
70 Code
71 </NavLink>
72 <NavLink
73 to={`/${effectiveRepo}/issues`}
74 className={({ isActive }) =>
75 cn(
76 "rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
77 isActive
78 ? "bg-accent text-accent-foreground"
79 : "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
80 )
81 }
82 >
83 Issues
84 </NavLink>
85 </nav>
86 )}
87
88 <div className="ml-auto flex items-center gap-2">
89 {mode === "readonly" && <span className="text-xs text-muted-foreground">Read only</span>}
90
91 <Button variant="ghost" size="icon" onClick={toggle} title="Toggle theme">
92 {theme === "light" ? <Moon className="size-4" /> : <Sun className="size-4" />}
93 </Button>
94
95 {/* External mode: show sign-in buttons when logged out */}
96 {mode === "external" &&
97 !user &&
98 loginProviders.map((p) => (
99 <Button key={p} asChild size="sm">
100 <a href={`/auth/login?provider=${p}`}>
101 <LogIn className="size-4" />
102 Sign in with {providerLabel(p)}
103 </a>
104 </Button>
105 ))}
106
107 {user && effectiveRepo && (
108 <>
109 <Button asChild size="sm">
110 <Link to={`/${effectiveRepo}/issues/new`}>
111 <Plus className="size-4" />
112 New issue
113 </Link>
114 </Button>
115 <Link to={`/${effectiveRepo}/user/${user.humanId}`}>
116 <Avatar className="size-7">
117 <AvatarImage src={user.avatarUrl ?? undefined} alt={user.displayName} />
118 <AvatarFallback className="text-xs">
119 {user.displayName.slice(0, 2).toUpperCase()}
120 </AvatarFallback>
121 </Avatar>
122 </Link>
123 </>
124 )}
125
126 {/* Sign out only shown in external mode when logged in */}
127 {mode === "external" && user && <SignOutButton />}
128 </div>
129 </div>
130 </header>
131 );
132}
133
134function providerLabel(name: string): string {
135 const labels: Record<string, string> = { github: "GitHub", gitlab: "GitLab", gitea: "Gitea" };
136 return labels[name] ?? name;
137}