|
| 1 | +import { useState } from "react"; |
| 2 | +import { Outlet, NavLink, useLocation } from "react-router-dom"; |
| 3 | +import { useAuth } from "../context/AuthContext"; |
| 4 | +import { Home, FolderOpen, Globe, Settings, ChevronLeft, ChevronRight, LogOut } from "lucide-react"; |
| 5 | + |
| 6 | +const NAV_ITEMS = [ |
| 7 | + { to: "/dashboard", label: "Overview", icon: Home, end: true }, |
| 8 | + { to: "/dashboard/projects", label: "All Projects", icon: FolderOpen }, |
| 9 | + { to: "/dashboard/community", label: "Community", icon: Globe }, |
| 10 | + { to: "/dashboard/settings", label: "Settings", icon: Settings }, |
| 11 | +]; |
| 12 | + |
| 13 | +function DashboardLayout() { |
| 14 | + const [isCollapsed, setIsCollapsed] = useState(false); |
| 15 | + const { user, logOut } = useAuth(); |
| 16 | + const location = useLocation(); |
| 17 | + |
| 18 | + // Generate breadcrumbs from pathname |
| 19 | + const getBreadcrumbs = () => { |
| 20 | + const paths = location.pathname.split("/").filter(Boolean); |
| 21 | + return paths.map((path, index) => ({ |
| 22 | + label: path.charAt(0).toUpperCase() + path.slice(1), |
| 23 | + isLast: index === paths.length - 1, |
| 24 | + })); |
| 25 | + }; |
| 26 | + |
| 27 | + const breadcrumbs = getBreadcrumbs(); |
| 28 | + |
| 29 | + return ( |
| 30 | + <div className="min-h-screen flex bg-gray-900"> |
| 31 | + {/* Sidebar */} |
| 32 | + <aside |
| 33 | + className={`fixed left-0 top-0 h-full bg-slate-900 border-r border-slate-800 flex flex-col transition-all duration-300 z-40 ${isCollapsed ? "w-16" : "w-60" |
| 34 | + }`} |
| 35 | + > |
| 36 | + {/* Logo */} |
| 37 | + <div className={`h-14 flex items-center border-b border-slate-800 ${isCollapsed ? "justify-center px-2" : "px-4"}`}> |
| 38 | + <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center flex-shrink-0"> |
| 39 | + <span className="text-white font-bold text-sm"></></span> |
| 40 | + </div> |
| 41 | + {!isCollapsed && ( |
| 42 | + <span className="ml-3 text-lg font-bold text-white tracking-tight">CodeSplit</span> |
| 43 | + )} |
| 44 | + </div> |
| 45 | + |
| 46 | + {/* Navigation */} |
| 47 | + <nav className="flex-1 py-4 px-2 space-y-1"> |
| 48 | + {NAV_ITEMS.map((item) => ( |
| 49 | + <NavLink |
| 50 | + key={item.to} |
| 51 | + to={item.to} |
| 52 | + end={item.end} |
| 53 | + className={({ isActive }) => |
| 54 | + `flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all group relative ${isActive |
| 55 | + ? "bg-slate-700/50 text-white" |
| 56 | + : "text-slate-400 hover:text-white hover:bg-slate-800" |
| 57 | + } ${isCollapsed ? "justify-center" : ""}` |
| 58 | + } |
| 59 | + > |
| 60 | + {({ isActive }) => ( |
| 61 | + <> |
| 62 | + {isActive && ( |
| 63 | + <div className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-5 bg-blue-500 rounded-r" /> |
| 64 | + )} |
| 65 | + <item.icon className="w-5 h-5 flex-shrink-0" /> |
| 66 | + {!isCollapsed && ( |
| 67 | + <span className="text-sm font-medium">{item.label}</span> |
| 68 | + )} |
| 69 | + {isCollapsed && ( |
| 70 | + <div className="absolute left-full ml-2 px-2 py-1 bg-slate-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 pointer-events-none whitespace-nowrap z-50 transition-opacity"> |
| 71 | + {item.label} |
| 72 | + </div> |
| 73 | + )} |
| 74 | + </> |
| 75 | + )} |
| 76 | + </NavLink> |
| 77 | + ))} |
| 78 | + </nav> |
| 79 | + |
| 80 | + {/* Collapse Toggle */} |
| 81 | + <div className="p-2 border-t border-slate-800"> |
| 82 | + <button |
| 83 | + onClick={() => setIsCollapsed(!isCollapsed)} |
| 84 | + className="w-full flex items-center justify-center gap-2 px-3 py-2 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition-colors" |
| 85 | + title={isCollapsed ? "Expand sidebar" : "Collapse sidebar"} |
| 86 | + > |
| 87 | + {isCollapsed ? ( |
| 88 | + <ChevronRight className="w-5 h-5" /> |
| 89 | + ) : ( |
| 90 | + <> |
| 91 | + <ChevronLeft className="w-5 h-5" /> |
| 92 | + <span className="text-sm">Collapse</span> |
| 93 | + </> |
| 94 | + )} |
| 95 | + </button> |
| 96 | + </div> |
| 97 | + </aside> |
| 98 | + |
| 99 | + {/* Main Area */} |
| 100 | + <div className={`flex-1 flex flex-col transition-all duration-300 ${isCollapsed ? "ml-16" : "ml-60"}`}> |
| 101 | + {/* Top Header */} |
| 102 | + <header className="sticky top-0 z-30 h-14 bg-slate-900/80 backdrop-blur-md border-b border-slate-800 flex items-center justify-between px-6"> |
| 103 | + {/* Breadcrumbs */} |
| 104 | + <nav className="flex items-center gap-2 text-sm"> |
| 105 | + {breadcrumbs.map((crumb, index) => ( |
| 106 | + <span key={index} className="flex items-center gap-2"> |
| 107 | + {index > 0 && <span className="text-slate-600">/</span>} |
| 108 | + <span className={crumb.isLast ? "text-white font-medium" : "text-slate-400"}> |
| 109 | + {crumb.label} |
| 110 | + </span> |
| 111 | + </span> |
| 112 | + ))} |
| 113 | + </nav> |
| 114 | + |
| 115 | + {/* User Section */} |
| 116 | + <div className="flex items-center gap-3"> |
| 117 | + {user && ( |
| 118 | + <div className="flex items-center gap-3"> |
| 119 | + <img |
| 120 | + src={user.photoURL || "https://via.placeholder.com/32"} |
| 121 | + alt={user.displayName || "User"} |
| 122 | + className="w-8 h-8 rounded-full border border-slate-700" |
| 123 | + /> |
| 124 | + <button |
| 125 | + onClick={logOut} |
| 126 | + className="p-2 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition-colors" |
| 127 | + title="Logout" |
| 128 | + > |
| 129 | + <LogOut className="w-4 h-4" /> |
| 130 | + </button> |
| 131 | + </div> |
| 132 | + )} |
| 133 | + </div> |
| 134 | + </header> |
| 135 | + |
| 136 | + {/* Page Content */} |
| 137 | + <main className="flex-1 p-6 overflow-y-auto"> |
| 138 | + <Outlet /> |
| 139 | + </main> |
| 140 | + </div> |
| 141 | + </div> |
| 142 | + ); |
| 143 | +} |
| 144 | + |
| 145 | +export default DashboardLayout; |
0 commit comments