Sidebar
LayoutReactTailwindCollapsible sidebar with navigation links and user profile section.
Desktop
Mobile
1 "use client"; 2 3 import { useState } from "react"; 4 import Link from "next/link"; 5 import { usePathname } from "next/navigation"; 6 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 import { 8 faGauge, 9 faUsers, 10 faMapLocationDot, 11 faCode, 12 faXmark, 13 faAnglesLeft, 14 faAnglesRight, 15 faGear, 16 faClockRotateLeft, 17 faRightFromBracket, 18 } from "@fortawesome/free-solid-svg-icons"; 19 20 // 1. Define your nav links — each item is { label, icon, href }. 21 const navItems = [ 22 { label: "Dashboard", icon: faGauge, href: "/dashboard" }, 23 { label: "Users", icon: faUsers, href: "/users" }, 24 { label: "Map", icon: faMapLocationDot, href: "/map" }, 25 ]; 26 27 // 2. SidebarNav: a title bar (brand + collapse toggle) over the nav links. 28 // Pass hideHeader on mobile, where the drawer supplies its own top bar. 29 function SidebarNav({ 30 collapsed, 31 onToggleCollapse, 32 hideHeader, 33 }: { 34 collapsed: boolean; 35 onToggleCollapse: () => void; 36 hideHeader?: boolean; 37 }) { 38 const pathname = usePathname(); 39 40 return ( 41 <div className="flex h-full flex-col"> 42 {!hideHeader && ( 43 <div className="bg-panel-header flex h-12 shrink-0 items-center justify-between border-b px-4 shadow-sm"> 44 {!collapsed && ( 45 <Link 46 href="/" 47 className="text-sidebar-foreground flex items-center gap-2 text-sm font-bold tracking-tight" 48 > 49 <FontAwesomeIcon icon={faCode} className="text-primary h-4 w-4" /> 50 Turmerrific 51 </Link> 52 )} 53 <button 54 onClick={onToggleCollapse} 55 aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"} 56 className="text-sidebar-foreground/70 hover:text-sidebar-foreground transition-colors" 57 > 58 <FontAwesomeIcon icon={collapsed ? faAnglesRight : faAnglesLeft} className="h-3 w-3" /> 59 </button> 60 </div> 61 )} 62 63 <nav className="flex-1 space-y-1 p-2"> 64 {navItems.map((item) => { 65 const active = pathname === item.href; 66 return ( 67 <Link 68 key={item.href} 69 href={item.href} 70 title={item.label} 71 className={`flex items-center gap-3 rounded-lg px-3 py-2 text-sm ${ 72 collapsed ? "justify-center" : "" 73 } ${ 74 active 75 ? "bg-sidebar-primary text-sidebar-primary-foreground" 76 : "text-sidebar-foreground/70 hover:bg-sidebar-accent" 77 }`} 78 > 79 <FontAwesomeIcon icon={item.icon} className="h-4 w-4 shrink-0" /> 80 {!collapsed && item.label} 81 </Link> 82 ); 83 })} 84 </nav> 85 86 <SidebarFooter collapsed={collapsed} /> 87 </div> 88 ); 89 } 90 91 // User menu: a trigger (avatar + name + role) that toggles a popup with 92 // Settings, Change Log, and a destructive Log Out — same as the demo. 93 function SidebarFooter({ collapsed }: { collapsed: boolean }) { 94 const [open, setOpen] = useState(false); 95 96 return ( 97 <div className="border-sidebar-border relative shrink-0 border-t p-2"> 98 {open && ( 99 <div 100 role="menu" 101 className="bg-card absolute bottom-full left-2 z-10 mb-1 min-w-[180px] overflow-hidden rounded-md border shadow-lg" 102 > 103 <Link 104 href="/settings" 105 role="menuitem" 106 className="text-foreground hover:bg-muted flex items-center gap-2.5 px-3 py-2 text-sm transition-colors" 107 > 108 <FontAwesomeIcon icon={faGear} className="text-muted-foreground h-3.5 w-3.5" /> 109 Settings 110 </Link> 111 <Link 112 href="/changelog" 113 role="menuitem" 114 className="text-foreground hover:bg-muted flex items-center gap-2.5 px-3 py-2 text-sm transition-colors" 115 > 116 <FontAwesomeIcon icon={faClockRotateLeft} className="text-muted-foreground h-3.5 w-3.5" /> 117 Change Log 118 </Link> 119 <div className="bg-border h-px" /> 120 <Link 121 href="/login" 122 role="menuitem" 123 className="text-destructive hover:bg-destructive/10 flex items-center gap-2.5 px-3 py-2 text-sm transition-colors" 124 > 125 <FontAwesomeIcon icon={faRightFromBracket} className="text-destructive h-3.5 w-3.5" /> 126 Log Out 127 </Link> 128 </div> 129 )} 130 131 <button 132 type="button" 133 onClick={() => setOpen((v) => !v)} 134 aria-haspopup="menu" 135 aria-expanded={open} 136 className={`hover:bg-sidebar-accent flex w-full items-center rounded-lg transition-colors ${ 137 collapsed ? "justify-center p-1.5" : "gap-3 px-2 py-1.5" 138 }`} 139 > 140 <span className="bg-primary/20 flex h-7 w-7 shrink-0 items-center justify-center rounded-full"> 141 <span className="text-primary text-xs font-medium">D</span> 142 </span> 143 {!collapsed && ( 144 <span className="min-w-0 flex-1 text-left"> 145 <span className="text-sidebar-foreground block truncate text-sm font-medium"> 146 Demo User 147 </span> 148 <span className="text-sidebar-foreground/60 block truncate text-xs">HR Admin</span> 149 </span> 150 )} 151 </button> 152 </div> 153 ); 154 } 155 156 // 3. Desktop sidebar collapses between w-64 and w-12; mobile renders a 157 // full-screen drawer toggled by mobileOpen (its own top bar + close button). 158 function Sidebar({ mobileOpen, onClose }: { mobileOpen: boolean; onClose: () => void }) { 159 const [collapsed, setCollapsed] = useState(false); 160 const toggleCollapse = () => setCollapsed((v) => !v); 161 162 return ( 163 <> 164 {/* Desktop */} 165 <aside 166 className={`bg-sidebar hidden shrink-0 border-r transition-[width] md:block ${ 167 collapsed ? "w-12" : "w-64" 168 }`} 169 > 170 <SidebarNav collapsed={collapsed} onToggleCollapse={toggleCollapse} /> 171 </aside> 172 173 {/* Mobile — full-screen drawer */} 174 {mobileOpen && ( 175 <aside className="bg-sidebar fixed inset-0 z-[60] flex flex-col md:hidden"> 176 <div className="bg-panel-header flex h-12 shrink-0 items-center justify-between border-b px-4 shadow-sm"> 177 <span className="text-sidebar-foreground flex items-center gap-2 text-xl font-bold tracking-tight"> 178 <FontAwesomeIcon icon={faCode} className="text-primary" /> 179 Turmerrific 180 </span> 181 <button onClick={onClose} aria-label="Close menu" className="text-sidebar-foreground/70"> 182 <FontAwesomeIcon icon={faXmark} className="h-5 w-5" /> 183 </button> 184 </div> 185 <div className="flex-1 overflow-y-auto"> 186 <SidebarNav collapsed={false} onToggleCollapse={toggleCollapse} hideHeader /> 187 </div> 188 </aside> 189 )} 190 </> 191 ); 192 }