TypeScript 97.5%
SQL 1.4%
Python 0.8%
1"use client";2import * as React from "react";3import Link from "next/link";4import { useRouter } from "next/navigation";5import { Check, ChevronsUpDown, LogOut, Menu, Plus, Settings, User } from "lucide-react";6import { authClient } from "@/lib/auth-client";7import { selectProject } from "@/actions/projects";8import { Button } from "@/components/ui/button";9import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";10import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";11import { ThemeToggle } from "@/components/ui/theme";12import { Logo } from "@/components/ui/logo";13import { SidebarNav } from "./sidebar";14import { cn } from "@/lib/utils";1516export interface TopbarProps {17 user: { name: string; email: string; image?: string | null };18 organization: { id: string; name: string; plan: string };19 projects: Array<{ id: string; name: string; environment: string }>;20 currentProjectId: string;21 isAdmin: boolean;22}2324export function Topbar({ user, organization, projects, currentProjectId, isAdmin }: TopbarProps) {25 const router = useRouter();26 const [open, setOpen] = React.useState(false);27 const [pending, startTransition] = React.useTransition();28 const current = projects.find((p) => p.id === currentProjectId) ?? projects[0];2930 return (31 <header className="sticky top-0 z-30 flex h-14 items-center gap-3 border-b border-border bg-bg/85 px-4 backdrop-blur sm:px-6">32 <Button variant="ghost" size="icon-sm" className="lg:hidden -ml-1.5" onClick={() => setOpen(true)} aria-label="Open navigation">33 <Menu />34 </Button>35 <div className="lg:hidden">36 <Logo href="/dashboard" size="sm" />37 </div>38 <div className="hidden items-center gap-1.5 text-[13px] text-fg-muted lg:flex">39 <span className="truncate max-w-[180px]">{organization.name}</span>40 <span className="text-fg-subtle">/</span>41 </div>42 <DropdownMenu>43 <DropdownMenuTrigger asChild>44 <Button variant="outline" size="sm" className={cn("gap-2 font-medium max-w-[220px]", pending && "opacity-70")} aria-label="Switch project">45 <span className={cn("size-1.5 rounded-full", current?.environment === "development" ? "bg-warning" : "bg-success")} />46 <span className="truncate">{current?.name ?? "Project"}</span>47 <ChevronsUpDown className="size-3.5 text-fg-subtle" />48 </Button>49 </DropdownMenuTrigger>50 <DropdownMenuContent align="start" className="w-64">51 <DropdownMenuLabel>Projects</DropdownMenuLabel>52 {projects.map((p) => (53 <DropdownMenuItem54 key={p.id}55 onClick={() => startTransition(async () => { await selectProject(p.id); router.refresh(); })}56 className="justify-between"57 >58 <span className="flex items-center gap-2 truncate">59 <span className={cn("size-1.5 rounded-full", p.environment === "development" ? "bg-warning" : "bg-success")} />60 <span className="truncate">{p.name}</span>61 </span>62 {p.id === currentProjectId ? <Check className="size-4" /> : null}63 </DropdownMenuItem>64 ))}65 <DropdownMenuSeparator />66 <DropdownMenuItem asChild>67 <Link href="/dashboard/projects?new=1"><Plus /> New project</Link>68 </DropdownMenuItem>69 </DropdownMenuContent>70 </DropdownMenu>71 <span className="hidden sm:inline-flex rounded-full border border-border px-2 py-0.5 text-[11px] font-medium uppercase tracking-wide text-fg-subtle">{organization.plan}</span>7273 <div className="ml-auto flex items-center gap-1.5">74 <Button asChild variant="ghost" size="sm" className="hidden sm:inline-flex text-fg-muted">75 <Link href="/docs">Docs</Link>76 </Button>77 <ThemeToggle />78 <DropdownMenu>79 <DropdownMenuTrigger asChild>80 <button className="flex size-8 items-center justify-center rounded-full bg-fg text-bg text-[12px] font-semibold uppercase ring-offset-bg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40" aria-label="Account menu">81 {(user.name || user.email).slice(0, 1)}82 </button>83 </DropdownMenuTrigger>84 <DropdownMenuContent align="end" className="w-60">85 <div className="px-2 py-2">86 <div className="truncate text-[13px] font-medium">{user.name || "Account"}</div>87 <div className="truncate text-xs text-fg-muted">{user.email}</div>88 </div>89 <DropdownMenuSeparator />90 <DropdownMenuItem asChild><Link href="/dashboard/settings"><User /> Profile</Link></DropdownMenuItem>91 <DropdownMenuItem asChild><Link href="/dashboard/settings/security"><Settings /> Security</Link></DropdownMenuItem>92 {isAdmin ? <DropdownMenuItem asChild><Link href="/admin"><Settings /> Admin</Link></DropdownMenuItem> : null}93 <DropdownMenuSeparator />94 <DropdownMenuItem95 onClick={async () => {96 await authClient.signOut();97 router.push("/login");98 router.refresh();99 }}100 >101 <LogOut /> Sign out102 </DropdownMenuItem>103 </DropdownMenuContent>104 </DropdownMenu>105 </div>106107 <Dialog open={open} onOpenChange={setOpen}>108 <DialogContent size="sm" className="left-0 top-0 h-dvh max-w-[280px] translate-x-0 translate-y-0 rounded-none border-r p-4 data-[state=open]:animate-fade-in">109 <DialogTitle className="sr-only">Navigation</DialogTitle>110 <div className="mb-4 px-1"><Logo href="/dashboard" size="sm" /></div>111 <SidebarNav isAdmin={isAdmin} onNavigate={() => setOpen(false)} />112 </DialogContent>113 </Dialog>114 </header>115 );116}117