"use client"; import React from "react"; import Link from "next/link"; import { useParams, usePathname } from "next/navigation"; import { cn } from "@app/lib/cn"; import { useUserContext } from "@app/hooks/useUserContext"; import { Badge } from "@app/components/ui/badge"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useTranslations } from "next-intl"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@app/components/ui/collapsible"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; import { ChevronDown } from "lucide-react"; import { build } from "@server/build"; export type SidebarNavItem = { href?: string; title: string; icon?: React.ReactNode; showEE?: boolean; isBeta?: boolean; items?: SidebarNavItem[]; }; export type SidebarNavSection = { heading: string; items: SidebarNavItem[]; }; export interface SidebarNavProps extends React.HTMLAttributes { sections: SidebarNavSection[]; disabled?: boolean; onItemClick?: () => void; isCollapsed?: boolean; } type CollapsibleNavItemProps = { item: SidebarNavItem; level: number; isChildActive: boolean; isDisabled: boolean; isCollapsed: boolean; renderNavItem: (item: SidebarNavItem, level: number) => React.ReactNode; t: (key: string) => string; build: string; isUnlocked: () => boolean; }; function CollapsibleNavItem({ item, level, isChildActive, isDisabled, isCollapsed, renderNavItem, t, build, isUnlocked }: CollapsibleNavItemProps) { const [isOpen, setIsOpen] = React.useState(isChildActive); // Update open state when child active state changes React.useEffect(() => { if (isChildActive) { setIsOpen(true); } }, [isChildActive]); return (
{item.items!.map((childItem) => renderNavItem(childItem, level + 1) )}
); } export function SidebarNav({ className, sections, disabled = false, onItemClick, isCollapsed = false, ...props }: SidebarNavProps) { const pathname = usePathname(); const params = useParams(); const orgId = params.orgId as string; const niceId = params.niceId as string; const resourceId = params.resourceId as string; const userId = params.userId as string; const apiKeyId = params.apiKeyId as string; const clientId = params.clientId as string; const { licenseStatus, isUnlocked } = useLicenseStatusContext(); const { user } = useUserContext(); const t = useTranslations(); function hydrateHref(val?: string): string | undefined { if (!val) return undefined; return val .replace("{orgId}", orgId) .replace("{niceId}", niceId) .replace("{resourceId}", resourceId) .replace("{userId}", userId) .replace("{apiKeyId}", apiKeyId) .replace("{clientId}", clientId); } function isItemOrChildActive(item: SidebarNavItem): boolean { const hydratedHref = hydrateHref(item.href); if (hydratedHref && pathname.startsWith(hydratedHref)) { return true; } if (item.items) { return item.items.some((child) => isItemOrChildActive(child)); } return false; } const renderNavItem = ( item: SidebarNavItem, level: number = 0 ): React.ReactNode => { const hydratedHref = hydrateHref(item.href); const hasNestedItems = item.items && item.items.length > 0; const isActive = hydratedHref ? pathname.startsWith(hydratedHref) : false; const isChildActive = hasNestedItems ? isItemOrChildActive(item) : false; const isEE = build === "enterprise" && item.showEE && !isUnlocked(); const isDisabled = disabled || isEE; const tooltipText = item.showEE && !isUnlocked() ? `${t(item.title)} (${t("licenseBadge")})` : t(item.title); // If item has nested items, render as collapsible if (hasNestedItems && !isCollapsed) { return ( ); } // Regular item without nested items const itemContent = hydratedHref ? ( { if (isDisabled) { e.preventDefault(); } else if (onItemClick) { onItemClick(); } }} tabIndex={isDisabled ? -1 : undefined} aria-disabled={isDisabled} > {item.icon && ( {item.icon} )} {!isCollapsed && ( <>
{t(item.title)} {item.isBeta && ( {t("beta")} )}
{build === "enterprise" && item.showEE && !isUnlocked() && ( {t("licenseBadge")} )} )} ) : (
{item.icon && ( {item.icon} )}
{t(item.title)} {item.isBeta && ( {t("beta")} )}
{build === "enterprise" && item.showEE && !isUnlocked() && ( {t("licenseBadge")} )}
); if (isCollapsed) { // If item has nested items, show popover instead of tooltip if (hasNestedItems) { return (
{item.items!.map((childItem) => { const childHydratedHref = hydrateHref( childItem.href ); const childIsActive = childHydratedHref ? pathname.startsWith( childHydratedHref ) : false; const childIsEE = build === "enterprise" && childItem.showEE && !isUnlocked(); const childIsDisabled = disabled || childIsEE; if (!childHydratedHref) { return null; } return ( { if (childIsDisabled) { e.preventDefault(); } else if (onItemClick) { onItemClick(); } }} > {childItem.icon && ( {childItem.icon} )}
{t(childItem.title)} {childItem.isBeta && ( {t("beta")} )}
{build === "enterprise" && childItem.showEE && !isUnlocked() && ( {t("licenseBadge")} )} ); })}
); } // Regular item without nested items - show tooltip return ( {itemContent}

{tooltipText}

); } return ( {itemContent} ); }; return ( ); }