'use client'; /** * Unified user menu - used by the topbar avatar AND the sidebar-footer * profile row. Encapsulates: * - Profile / Settings / Notification preferences links * - Dark-mode toggle * - Sign out * - Inline port switcher (when the user has access to >1 port) * * Everywhere a "click my profile" affordance lives, it should mount this * component. That keeps the menu items consistent regardless of which * trigger the user reached for. */ import { useRouter } from 'next/navigation'; import { useQueryClient } from '@tanstack/react-query'; import { Moon, Sun, LogOut, User, Settings, Bell, Check, Building2 } from 'lucide-react'; import { type ReactNode } from 'react'; import { useUIStore } from '@/stores/ui-store'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import type { Port } from '@/lib/db/schema/ports'; interface UserMenuProps { /** Element rendered as the dropdown trigger. Must be a single React node * that can receive a click handler (asChild semantics). */ trigger: ReactNode; /** "start" anchors menu under a sidebar-footer trigger (left edge); * "end" anchors under a top-right avatar. Forwarded to Radix. */ align?: 'start' | 'end'; user?: { name: string; email: string }; /** Ports the user has access to. When length > 1, renders a port-switcher * group inside the menu. When ≤ 1, the switcher is omitted. */ ports?: Port[]; } export function UserMenu({ trigger, align = 'end', user, ports }: UserMenuProps) { const router = useRouter(); const queryClient = useQueryClient(); const currentPortId = useUIStore((s) => s.currentPortId); const currentPortSlug = useUIStore((s) => s.currentPortSlug); const setPort = useUIStore((s) => s.setPort); const darkMode = useUIStore((s) => s.darkMode); const toggleDarkMode = useUIStore((s) => s.toggleDarkMode); const base = currentPortSlug ? `/${currentPortSlug}` : ''; const showPortSwitcher = ports && ports.length > 1; function handleToggleDarkMode() { toggleDarkMode(); document.documentElement.classList.toggle('dark'); } function handlePortChange(port: Port) { if (port.id === currentPortId) return; setPort(port.id, port.slug); // All cached queries are port-scoped - invalidate so they refetch with // the new X-Port-Id header. queryClient.invalidateQueries(); // eslint-disable-next-line @typescript-eslint/no-explicit-any router.push(`/${port.slug}/dashboard` as any); } return ( {trigger}
{user?.name ?? 'My Account'}
{user?.email && (
{user.email}
)}
{showPortSwitcher && ( <> {/* Port list lives in a submenu so the user has to actively hover/ click "Switch port" to see them - the main menu stays compact regardless of how many ports the operator has access to. */} Switch port {ports!.map((port) => { const active = port.id === currentPortId; return ( handlePortChange(port)} className={active ? 'bg-accent/40' : undefined} > {port.name} {active && } ); })} )} {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} router.push(`${base}/settings/profile` as any)}> Profile {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} router.push(`${base}/settings` as any)}> Settings router.push(`${base}/settings#notifications` as any)} > Notification preferences {darkMode ? ( <> Light Mode ) : ( <> Dark Mode )} router.push('/api/auth/sign-out')} > Sign Out
); }