SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
5.8 KB · 117 lines tsx
Raw Blame History
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