"use client"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect } from "react"; import { cn } from "@/lib/utils"; import { useAuth } from "@/hooks/use-auth"; import { useTotalUnread } from "@/hooks/use-total-unread"; import { useUnreadNotifications } from "@/hooks/use-unread-notifications"; import { Bell, Crown, GitBranch, LayoutDashboard, LogOut, MessageSquare, Radio, Settings, Shield, User, UserCog, Users, UsersRound, Workflow, X, Zap, } from "lucide-react"; import type { AccountRole } from "@/lib/auth/roles"; // Per-role chip metadata used in the sidebar's account strip + the // Members tab roster. Keeping this near both consumers in a single // place avoids drift between the two surfaces — when a designer // wants to recolour "agent" rows, this is the one diff. const ROLE_CHIP: Record< AccountRole, { icon: typeof Crown; label: string; className: string } > = { owner: { icon: Crown, label: "Owner", // Amber: scarce, immutable, "the boss" — gets visual emphasis. className: "border-amber-500/40 bg-amber-500/10 text-amber-300", }, admin: { icon: Shield, label: "Admin", // Primary-tinted: significant but not as scarce as owner. className: "border-primary/40 bg-primary/10 text-primary", }, agent: { icon: UserCog, label: "Agent", // Neutral slate: the operational default. className: "border-border bg-muted text-foreground", }, viewer: { icon: User, label: "Viewer", // Muted slate: read-only role; visually quieter than agent. className: "border-border bg-card text-muted-foreground", }, }; import { Avatar, AvatarFallback, AvatarImage, } from "@/components/ui/avatar"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; interface NavItem { href: string; label: string; icon: typeof LayoutDashboard; /** * When true, the nav row renders a small "Beta" chip after the label. * Purely informational — doesn't affect routing or access. */ beta?: boolean; } const navItems: NavItem[] = [ { href: "/dashboard", label: "Dashboard", icon: LayoutDashboard }, { href: "/inbox", label: "Inbox", icon: MessageSquare }, { href: "/notifications", label: "Notifications", icon: Bell }, { href: "/contacts", label: "Contacts", icon: Users }, { href: "/pipelines", label: "Pipelines", icon: GitBranch }, { href: "/broadcasts", label: "Broadcasts", icon: Radio }, { href: "/automations", label: "Automations", icon: Zap }, { href: "/flows", label: "Flows", icon: Workflow, beta: true }, ]; const bottomNavItems = [ { href: "/settings", label: "Settings", icon: Settings }, ]; interface SidebarProps { /** Controlled on mobile by the Header's hamburger button. Ignored on lg+. */ open?: boolean; onClose?: () => void; } export function Sidebar({ open = false, onClose }: SidebarProps) { const pathname = usePathname(); const { profile, profileLoading, account, accountRole, signOut } = useAuth(); const totalUnread = useTotalUnread(); const unreadNotifications = useUnreadNotifications(); // Only surface the account-name strip when it actually carries // information. A solo user's personal account is named after them // (the 017 signup trigger seeds it from `full_name`), so showing it // here would just duplicate the user name in the footer below. Once // the account is renamed or the user joins a shared account, the // name diverges and the strip becomes meaningful — that's the signal // we gate on. Wait for the profile fetch to settle first, otherwise // the strip flashes in once the row resolves (a layout jump). const showAccountStrip = !profileLoading && !!account?.name && account.name !== profile?.full_name; // Close the drawer when route changes — users opened it to navigate, // so once they pick a destination the drawer should get out of the way. useEffect(() => { onClose?.(); // Only pathname drives this — onClose identity doesn't need to re-run it. // eslint-disable-next-line react-hooks/exhaustive-deps }, [pathname]); // Lock body scroll and allow Escape to close while the drawer is open on // mobile. No-ops on desktop because the sidebar isn't positioned there. useEffect(() => { if (!open) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose?.(); }; window.addEventListener("keydown", onKey); return () => { document.body.style.overflow = prev; window.removeEventListener("keydown", onKey); }; }, [open, onClose]); return ( <> {/* Backdrop — only exists on mobile and only when open. Clicking it closes the drawer. Hidden from lg+ since the sidebar is part of the main flex row there. */} {/* Main navigation */} {/* User section */}
{/* Account name display — surfaced only when the account name differs from the user's own name (see `showAccountStrip`). For a default solo account the two match, so we hide it to avoid duplicating the user name below; for renamed or shared accounts it tells the user which account they're acting in. */} {showAccountStrip && account?.name ? (
{/* `title=` exposes the full name on hover when it gets truncated (long account names + narrow sidebars). Cheap a11y win. */} {account.name} {accountRole ? ( // Always render the chip — owners used to be // invisible here, which made them indistinguishable // from admins at a glance. Now everyone sees their // role (with a colour cue) regardless of tier. (() => { const meta = ROLE_CHIP[accountRole]; const Icon = meta.icon; return ( {meta.label} ); })() ) : null}
) : null} {profile?.avatar_url ? ( ) : null} {profile?.full_name?.charAt(0)?.toUpperCase() ?? profile?.email?.charAt(0)?.toUpperCase() ?? "U"}

{profile?.full_name ?? "User"}

{profile?.email ?? ""}

} > Profile } > Settings Sign out
); }