203 lines
6.2 KiB
TypeScript
203 lines
6.2 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import {
|
|
Outlet,
|
|
NavLink,
|
|
useNavigate,
|
|
useLocation,
|
|
} from "react-router-dom";
|
|
import {
|
|
Bell,
|
|
Menu,
|
|
LogOut,
|
|
User,
|
|
Settings,
|
|
Home,
|
|
FolderKanban,
|
|
X,
|
|
CalendarDays,
|
|
SettingsIcon,
|
|
TimerIcon,
|
|
HelpCircleIcon,
|
|
} from "lucide-react";
|
|
|
|
interface User {
|
|
email: string;
|
|
password: string;
|
|
}
|
|
|
|
interface DashboardLayoutProps {
|
|
user: User | null;
|
|
setUser: React.Dispatch<React.SetStateAction<User | null>>;
|
|
}
|
|
|
|
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
|
const [isSidebarOpen, setSidebarOpen] = useState(true);
|
|
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
useEffect(() => {
|
|
const handleResize = () => {
|
|
const mobile = window.innerWidth < 768;
|
|
setIsMobile(mobile);
|
|
setSidebarOpen(!mobile);
|
|
};
|
|
|
|
handleResize();
|
|
window.addEventListener("resize", handleResize);
|
|
return () => window.removeEventListener("resize", handleResize);
|
|
}, []);
|
|
|
|
const handleLogout = () => {
|
|
localStorage.removeItem("user");
|
|
setUser(null);
|
|
navigate("/login");
|
|
};
|
|
|
|
const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
|
|
|
|
const closeSidebarOnMobile = () => {
|
|
if (isMobile) setSidebarOpen(false);
|
|
};
|
|
|
|
const menuItems = [
|
|
{ label: "Dashboard", path: "/dashboard", icon: <Home size={18} />, exact: true },
|
|
{ label: "Projects", path: "/dashboard/projects", icon: <FolderKanban size={18} /> },
|
|
{ label: "PTO", path: "/dashboard/leave-adjustments", icon: <CalendarDays size={18} /> },
|
|
{ label: "Timesheet", path: "/dashboard/timesheet", icon: <TimerIcon size={18} /> },
|
|
{ label: "Settings", path: "/dashboard/settings", icon: <SettingsIcon size={18} /> },
|
|
{ label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> },
|
|
];
|
|
|
|
return (
|
|
<div className="flex h-screen bg-base-200 overflow-hidden">
|
|
{/* Sidebar */}
|
|
<aside
|
|
className={`
|
|
fixed md:relative inset-y-0 left-0 z-40 transform
|
|
bg-base-100 border-r border-t border-base-300 transition-all duration-300 ease-in-out
|
|
${
|
|
isMobile
|
|
? isSidebarOpen
|
|
? "translate-x-0 w-64"
|
|
: "-translate-x-full w-64"
|
|
: isSidebarOpen
|
|
? "w-50"
|
|
: "w-20"
|
|
}
|
|
`}
|
|
>
|
|
{/* Sidebar Header */}
|
|
<div className="h-14 text-xl font-bold flex justify-between items-center border-b border-base-300">
|
|
{isSidebarOpen ? (
|
|
<span className="whitespace-nowrap mx-auto">VA Tracker</span>
|
|
) : (
|
|
<span className="text-primary font-extrabold mx-auto">VA</span>
|
|
)}
|
|
{isMobile && (
|
|
<button
|
|
onClick={toggleSidebar}
|
|
className="btn btn-ghost btn-sm md:hidden"
|
|
aria-label="Close sidebar"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Menu */}
|
|
<ul className="menu px-2 py-4">
|
|
{menuItems.map((item) => (
|
|
<li key={item.label}>
|
|
<NavLink
|
|
to={item.path}
|
|
end={item.exact}
|
|
className={({ isActive }) =>
|
|
`flex items-center gap-3 my-1 font-medium transition-all duration-150 ${
|
|
isActive
|
|
? "bg-primary text-primary-content rounded-lg"
|
|
: "hover:bg-base-300 rounded-lg"
|
|
}`
|
|
}
|
|
onClick={closeSidebarOnMobile}
|
|
>
|
|
<div className="min-w-[24px] flex justify-center">{item.icon}</div>
|
|
{isSidebarOpen && <span>{item.label}</span>}
|
|
</NavLink>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</aside>
|
|
|
|
{/* Mobile overlay */}
|
|
{isMobile && isSidebarOpen && (
|
|
<div
|
|
className="fixed inset-0 bg-black/20 backdrop-blur-[1px] z-30 md:hidden"
|
|
onClick={closeSidebarOnMobile}
|
|
></div>
|
|
)}
|
|
|
|
{/* Main Content */}
|
|
<div className="flex flex-col flex-1 bg-base-200 transition-all duration-300 ease-in-out">
|
|
{/* Navbar */}
|
|
<nav className="h-14 bg-base-100 border-b border-t border-base-300 px-4 flex justify-between items-center sticky top-0 z-30">
|
|
{/* Left */}
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={toggleSidebar}
|
|
className="btn btn-ghost btn-square"
|
|
aria-label="Toggle sidebar"
|
|
>
|
|
<Menu size={20} />
|
|
</button>
|
|
<span className="text-lg font-semibold md:hidden">
|
|
{location.pathname.includes("projects")
|
|
? "Projects"
|
|
: location.pathname.includes("leave-adjustments")
|
|
? "Leave & Adjustments"
|
|
: "Dashboard"}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Right */}
|
|
<div className="flex items-center gap-1">
|
|
<button className="btn btn-ghost btn-circle">
|
|
<Bell size={20} />
|
|
</button>
|
|
|
|
<div className="dropdown dropdown-end">
|
|
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
|
|
<div className="w-8 h-8 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 flex justify-center items-center">
|
|
<User size={18} className="text-base-content" />
|
|
</div>
|
|
</label>
|
|
<ul
|
|
tabIndex={0}
|
|
className="mt-3 p-2 shadow menu menu-sm dropdown-content bg-base-200 rounded-box w-52"
|
|
>
|
|
<li>
|
|
<a>
|
|
<Settings size={16} /> Settings
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a onClick={handleLogout} className="text-error">
|
|
<LogOut size={16} /> Logout
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
{/* Page Content */}
|
|
<main className="flex-1 overflow-y-auto p-4 md:px-6 no-scrollbar">
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DashboardLayout;
|