compassmock/src/components/org-switcher.tsx
Nicholai d7a3218ea0
fix(ui): make compass logo link to dashboard home (#96)
The logo in the org switcher is now a separate clickable link
to /dashboard (fullscreen chat). The org name + chevron still
opens the workspace dropdown independently.

Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
2026-02-16 01:27:57 -07:00

162 lines
4.8 KiB
TypeScript

"use client"
import * as React from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import {
IconBuilding,
IconCheck,
IconSelector,
IconUser,
} from "@tabler/icons-react"
import {
getUserOrganizations,
switchOrganization,
} from "@/app/actions/organizations"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import { cn } from "@/lib/utils"
type OrgInfo = {
readonly id: string
readonly name: string
readonly slug: string
readonly type: string
readonly role: string
}
export function OrgSwitcher({
activeOrgId,
activeOrgName,
}: {
readonly activeOrgId: string | null
readonly activeOrgName: string | null
}): React.ReactElement {
const router = useRouter()
const { isMobile } = useSidebar()
const [orgs, setOrgs] = React.useState<readonly OrgInfo[]>([])
const [isLoading, setIsLoading] = React.useState(false)
React.useEffect(() => {
async function loadOrgs(): Promise<void> {
const result = await getUserOrganizations()
setOrgs(result)
}
void loadOrgs()
}, [])
async function handleOrgSwitch(orgId: string): Promise<void> {
if (orgId === activeOrgId) return
setIsLoading(true)
const result = await switchOrganization(orgId)
if (result.success) {
router.refresh()
} else {
console.error("Failed to switch organization:", result.error)
setIsLoading(false)
}
}
const displayName = activeOrgName ?? "Compass"
const hasOrgs = orgs.length > 1
return (
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
size="lg"
className="gap-0 px-0 hover:bg-transparent active:bg-transparent"
asChild={false}
>
<Link
href="/dashboard"
className="flex shrink-0 items-center justify-center size-8 rounded-md hover:bg-sidebar-accent transition-colors"
aria-label="Compass home"
>
<span
className="!size-5 block bg-current"
style={{
maskImage: "url(/logo-black.png)",
maskSize: "contain",
maskRepeat: "no-repeat",
WebkitMaskImage: "url(/logo-black.png)",
WebkitMaskSize: "contain",
WebkitMaskRepeat: "no-repeat",
}}
/>
</Link>
<DropdownMenu>
<DropdownMenuTrigger asChild disabled={!hasOrgs}>
<button
className={cn(
"flex min-w-0 flex-1 items-center gap-1 rounded-md px-2 py-1 text-left",
"hover:bg-sidebar-accent transition-colors",
"data-[state=open]:bg-sidebar-accent",
"data-[state=open]:text-sidebar-accent-foreground",
!hasOrgs && "cursor-default hover:bg-transparent",
)}
>
<span className="truncate text-sm font-semibold">
{displayName}
</span>
{hasOrgs && (
<IconSelector
className="size-4 shrink-0 opacity-50"
/>
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="start"
sideOffset={4}
>
{orgs.map((org, i) => {
const isActive = org.id === activeOrgId
const OrgIcon =
org.type === "personal" ? IconUser : IconBuilding
return (
<React.Fragment key={org.id}>
{i > 0 && <DropdownMenuSeparator />}
<DropdownMenuItem
onClick={() => void handleOrgSwitch(org.id)}
disabled={isLoading}
className="gap-2 px-2 py-1.5"
>
<OrgIcon className="size-4 shrink-0 opacity-60" />
<span className="truncate font-medium">
{org.name}
</span>
{isActive && (
<IconCheck
className="ml-auto size-4 shrink-0 text-primary"
/>
)}
</DropdownMenuItem>
</React.Fragment>
)
})}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
)
}