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 oauth mode, shows a "Sign in" button when logged out and a sign-out
7// action when logged in.
8
9import { Link, useMatch, NavLink } from 'react-router-dom'
10import { Bug, Plus, Sun, Moon, LogIn, LogOut } from 'lucide-react'
11import { cn } from '@/lib/utils'
12import { useAuth } from '@/lib/auth'
13import { useTheme } from '@/lib/theme'
14import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
15import { Button } from '@/components/ui/button'
16
17// SignOutButton sends a POST to /auth/logout and reloads the page.
18// A full reload is the simplest way to reset all Apollo cache + React state.
19function SignOutButton() {
20 function handleSignOut() {
21 fetch('/auth/logout', { method: 'POST', credentials: 'include' }).finally(
22 () => window.location.assign('/'),
23 )
24 }
25 return (
26 <Button variant="ghost" size="sm" onClick={handleSignOut} title="Sign out">
27 <LogOut className="size-4" />
28 </Button>
29 )
30}
31
32export function Header() {
33 const { user, mode, oauthProviders } = useAuth()
34 const { theme, toggle } = useTheme()
35
36 // Detect if we're inside a /:repo route and grab the slug.
37 // useMatch works from any component in the tree, unlike useParams which is
38 // scoped to the nearest Route element.
39 const repoMatch = useMatch({ path: '/:repo/*', end: false })
40 const repo = repoMatch?.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="sticky top-0 z-50 border-b border-border bg-background/95 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="flex items-center gap-2 font-semibold text-foreground">
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={`/${effectiveRepo}`}
59 end
60 className={({ isActive }) => cn(
61 'rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
62 isActive
63 ? 'bg-accent text-accent-foreground'
64 : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
65 )}
66 >
67 Code
68 </NavLink>
69 <NavLink
70 to={`/${effectiveRepo}/issues`}
71 className={({ isActive }) => cn(
72 'rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
73 isActive
74 ? 'bg-accent text-accent-foreground'
75 : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
76 )}
77 >
78 Issues
79 </NavLink>
80 </nav>
81 )}
82
83 <div className="ml-auto flex items-center gap-2">
84 {mode === 'readonly' && (
85 <span className="text-xs text-muted-foreground">Read only</span>
86 )}
87
88 <Button variant="ghost" size="icon" onClick={toggle} title="Toggle theme">
89 {theme === 'light' ? <Moon className="size-4" /> : <Sun className="size-4" />}
90 </Button>
91
92 {/* OAuth mode: show sign-in buttons when logged out */}
93 {mode === 'oauth' && !user && oauthProviders.map((provider) => (
94 <Button key={provider} asChild size="sm" variant="outline">
95 <a href={`/auth/login?provider=${provider}`}>
96 <LogIn className="size-4" />
97 Sign in with {providerLabel(provider)}
98 </a>
99 </Button>
100 ))}
101
102 {user && effectiveRepo && (
103 <>
104 <Button asChild size="sm" variant="outline">
105 <Link to={`/${effectiveRepo}/issues/new`}>
106 <Plus className="size-4" />
107 New issue
108 </Link>
109 </Button>
110 <Link to={`/${effectiveRepo}/user/${user.humanId}`}>
111 <Avatar className="size-7">
112 <AvatarImage src={user.avatarUrl ?? undefined} alt={user.displayName} />
113 <AvatarFallback className="text-xs">
114 {user.displayName.slice(0, 2).toUpperCase()}
115 </AvatarFallback>
116 </Avatar>
117 </Link>
118 </>
119 )}
120
121 {/* Sign out only shown in oauth mode when logged in */}
122 {mode === 'oauth' && user && <SignOutButton />}
123 </div>
124 </div>
125 </header>
126 )
127}
128
129function providerLabel(name: string): string {
130 const labels: Record<string, string> = { github: 'GitHub', gitlab: 'GitLab', gitea: 'Gitea' }
131 return labels[name] ?? name
132}