Skip to main content
Glama
DashboardSidebar.tsx9.84 kB
'use client'; import { Link } from '@components/Link/Link'; import { Button, Container, KeyboardShortcut, PopoverStatic, TabSelector, } from '@intlayer/design-system'; import { useDevice, useSession } from '@intlayer/design-system/hooks'; import { cn } from '@utils/cn'; import { AnimatePresence, motion } from 'framer-motion'; import { ArrowLeftToLine, Book, Building2, FileText, FolderKanban, type LucideIcon, PenTool, Shield, Tags, User, } from 'lucide-react'; import { useLocale } from 'next-intlayer'; import { type FC, useState } from 'react'; import { type ExternalLinks, PagesRoutes } from '@/Routes'; // Map icon names to components - must be done in client component const iconMap: Record<string, LucideIcon> = { PenTool, Book, FileText, Tags, FolderKanban, Building2, User, Shield, }; const shouldHaveOrganizationRoutes = [ PagesRoutes.Dashboard_Projects, PagesRoutes.Dashboard_Tags, ] as string[]; const shouldHaveProjectRoutes = [ PagesRoutes.Dashboard_Editor, PagesRoutes.Dashboard_Dictionaries, PagesRoutes.Dashboard_Tags, ] as string[]; const shouldHaveAdminRoutes = [PagesRoutes.Admin_Users] as string[]; export type SidebarNavigationItem = { key: string; href?: string | PagesRoutes | ExternalLinks; icon?: keyof typeof iconMap; label: string; title: string; items?: SidebarNavigationItem[]; }; export type DashboardSidebarProps = { className?: string; items: SidebarNavigationItem[]; collapseButtonLabel: string; }; const getCleanPath = (path: string): string => { // Remove leading "/" if present const cleanPath = path.startsWith('/') ? path.substring(1) : path; // Split the path into components const components = cleanPath.split('/'); // If more than two components, keep only the first two if (components.length > 2) { return components.slice(0, 2).join('/'); } // For single component "dashboard", you can choose to append "/" if (components.length === 1 && components[0] === 'dashboard') { return components[0]; } // Return the path as is for other cases return cleanPath; }; type FlatSidebarItem = SidebarNavigationItem & { level: number; isLastChild?: boolean; }; const filterItems = ( nodes: SidebarNavigationItem[], context: { hasOrganization: boolean; hasProject: boolean; isSuperAdmin: boolean; } ): SidebarNavigationItem[] => { return ( nodes .filter((el) => { const href = el.href as string; // Check permissions if href is restricted if (href) { if ( shouldHaveOrganizationRoutes.includes(href) && !context.hasOrganization ) return false; if (shouldHaveProjectRoutes.includes(href) && !context.hasProject) return false; if (shouldHaveAdminRoutes.includes(href) && !context.isSuperAdmin) return false; } return true; }) .map((item) => ({ ...item, items: item.items ? filterItems(item.items, context) : undefined, })) // Keep item if it has passed filter OR if it has children .filter((item) => { // If it has children, keep it. if (item.items && item.items.length > 0) return true; // If it has no children but had an href and passed the permission check, keep it. if (item.href) return true; return false; }) ); }; const flattenItems = ( nodes: SidebarNavigationItem[], level = 0 ): FlatSidebarItem[] => { const result: FlatSidebarItem[] = []; for (let i = 0; i < nodes.length; i++) { const item = nodes[i]; const isLastChild = i === nodes.length - 1; result.push({ ...item, level, isLastChild }); if (item.items) { result.push(...flattenItems(item.items, level + 1)); } } return result; }; export const DashboardSidebar: FC<DashboardSidebarProps> = ({ className, items, collapseButtonLabel, }) => { const [isCollapsed, setIsCollapsed] = useState(false); const { isMobile } = useDevice('sm'); const { pathWithoutLocale } = useLocale(); const { session } = useSession(); const { organization, project, roles } = session ?? {}; const isSuperAdmin = roles?.some((role: string) => role.toLowerCase() === 'admin') ?? false; // Filter navigation items based on session context const filteredNavItems = filterItems(items, { hasOrganization: !!organization, hasProject: !!project, isSuperAdmin, }); const flatNavItems = flattenItems(filteredNavItems); const cleanPath = getCleanPath(pathWithoutLocale); let activeKey = cleanPath; let maxLevel = -1; // Optimized active key lookup for (const item of flatNavItems) { if (item.href && getCleanPath(item.href) === cleanPath) { if (item.level > maxLevel) { maxLevel = item.level; activeKey = item.key; } } } if (isMobile) { return ( <nav className="fixed inset-x-0 bottom-0 z-50 border-neutral-200 border-t bg-card/80 px-2 py-2 backdrop-blur-sm dark:border-neutral-800"> <TabSelector selectedChoice={activeKey} tabs={flatNavItems.map((item) => { const IconComponent = item.icon ? iconMap[item.icon] : null; return ( <Link key={item.key} href={item.href ?? '#'} label={item.label} color="text" variant="invisible-link" className="flex flex-col items-center gap-1 px-2 py-1.5" aria-current={activeKey === item.key ? 'page' : undefined} > {IconComponent && <IconComponent className="size-5" />} <span className="text-[10px]">{item.title}</span> </Link> ); })} hoverable color="text" className="justify-around" /> </nav> ); } // Desktop: render sidebar return ( <aside className={cn( 'sticky top-0 z-40 shrink-0 transition-all duration-300', isCollapsed ? 'w-16' : 'w-54', className )} > <Container className={cn( 'flex h-full flex-col transition-all duration-300', isCollapsed ? 'p-2' : 'p-4' )} roundedSize="none" transparency="none" > <div className={cn( 'mb-6 flex w-full', isCollapsed ? 'justify-center' : 'justify-end' )} > <PopoverStatic identifier="dashboard-nav-collapse"> <Button Icon={ArrowLeftToLine} size="icon-md" variant="hoverable" className={cn([ 'p-3 transition-transform', isCollapsed && 'rotate-180', ])} color="text" label={collapseButtonLabel} onClick={() => setIsCollapsed((prev) => !prev)} /> <PopoverStatic.Detail identifier="dashboard-nav-collapse"> <KeyboardShortcut shortcut="Alt + ArrowLeft" onTriggered={() => setIsCollapsed((prev) => !prev)} size="sm" /> </PopoverStatic.Detail> </PopoverStatic> </div> <nav className="flex-1 overflow-y-auto"> <TabSelector selectedChoice={activeKey} tabs={flatNavItems.map((item) => { const IconComponent = item.icon ? iconMap[item.icon] : null; const isChild = item.level > 0; return ( <Link key={item.key} href={item.href ?? '#'} label={item.label} color="text" variant="invisible-link" className={cn( 'relative flex w-full items-center justify-center rounded-lg px-2 py-2', !isCollapsed && 'justify-start gap-3 px-4', // Indentation !isCollapsed && isChild && 'pl-10' )} aria-current={activeKey === item.key ? 'page' : undefined} > {/* Tree Visuals */} {!isCollapsed && isChild && ( <div className="absolute top-0 left-4 h-full w-4 scale-110"> <div className="pointer-events-none relative h-full w-4"> <div className="absolute top-0 left-0 h-1/2 w-3 rounded-bl-lg border-text/60 border-b border-l" /> {!item.isLastChild && ( <div className="absolute top-1/2 left-0 h-1/2 w-px bg-text/60" /> )} </div> </div> )} {IconComponent && ( <IconComponent className="size-4 shrink-0" /> )} <AnimatePresence initial={false}> {!isCollapsed && ( <motion.span initial={{ opacity: 0, width: 0 }} animate={{ opacity: 1, width: 'auto' }} exit={{ opacity: 0, width: 0 }} transition={{ duration: 0.2, ease: 'easeInOut' }} className="overflow-hidden truncate whitespace-nowrap" > {item.title} </motion.span> )} </AnimatePresence> </Link> ); })} hoverable color="text" orientation="vertical" className="flex-col gap-1" /> </nav> </Container> </aside> ); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server