180 lines
5.8 KiB
TypeScript
180 lines
5.8 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 } 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()
|
|
|
|
// ✅ Handle responsive behavior properly
|
|
useEffect(() => {
|
|
const handleResize = () => {
|
|
const mobile = window.innerWidth < 768
|
|
setIsMobile(mobile)
|
|
setSidebarOpen(!mobile) // open on desktop, closed on 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: 'Leave & Adjustments', path: '/dashboard/leave-adjustments', icon: <CalendarDays 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 py-1
|
|
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-64'
|
|
: 'w-20'}
|
|
`}
|
|
>
|
|
{/* Header */}
|
|
<div className="p-4 text-xl font-bold flex justify-between items-center border-b border-base-300">
|
|
{isSidebarOpen ? (
|
|
<span className="whitespace-nowrap">VA Tracker</span>
|
|
) : (
|
|
<span className="text-primary font-extrabold">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>
|
|
|
|
{/* Overlay for mobile */}
|
|
{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 transition-all duration-300 ease-in-out bg-base-200 ${
|
|
!isMobile ? (isSidebarOpen ? 'md:ml-0' : 'md:ml-0') : ''
|
|
}`}
|
|
>
|
|
{/* Navbar */}
|
|
<nav className="navbar bg-base-100 border-b border-t border-base-300 p-3 sticky top-0 z-30 flex justify-between items-center">
|
|
<div className="flex items-center">
|
|
<button
|
|
onClick={toggleSidebar}
|
|
className="btn btn-ghost btn-square"
|
|
aria-label="Toggle sidebar"
|
|
>
|
|
<Menu size={20} />
|
|
</button>
|
|
<span className="text-lg font-semibold ml-2 md:hidden">
|
|
{location.pathname.includes('projects') ? 'Projects' : 'Dashboard'}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<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-10 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 flex justify-center items-center">
|
|
<User size={22} />
|
|
</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
|