Files
calypso/frontend/src/components/Layout.tsx
2025-12-27 16:58:19 +00:00

182 lines
6.4 KiB
TypeScript

import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom'
import { useAuthStore } from '@/store/auth'
import {
LogOut,
Menu,
LayoutDashboard,
HardDrive,
Database,
Network,
Settings,
Bell,
Server,
Users,
Archive
} from 'lucide-react'
import { useState, useEffect } from 'react'
export default function Layout() {
const { user, clearAuth } = useAuthStore()
const navigate = useNavigate()
const location = useLocation()
const [sidebarOpen, setSidebarOpen] = useState(false)
// Set sidebar open by default on desktop, closed on mobile
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 1024) {
setSidebarOpen(true)
} else {
setSidebarOpen(false)
}
}
handleResize() // Set initial state
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
const handleLogout = () => {
clearAuth()
navigate('/login')
}
const navigation = [
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
{ name: 'Storage', href: '/storage', icon: HardDrive },
{ name: 'Tape Libraries', href: '/tape', icon: Database },
{ name: 'iSCSI Management', href: '/iscsi', icon: Network },
{ name: 'Backup Management', href: '/backup', icon: Archive },
{ name: 'Tasks', href: '/tasks', icon: Settings },
{ name: 'Alerts', href: '/alerts', icon: Bell },
{ name: 'System', href: '/system', icon: Server },
]
if (user?.roles.includes('admin')) {
navigation.push({ name: 'User Management', href: '/iam', icon: Users })
}
const isActive = (href: string) => {
if (href === '/') {
return location.pathname === '/'
}
return location.pathname.startsWith(href)
}
return (
<div className="h-screen bg-background-dark flex overflow-hidden">
{/* Mobile backdrop overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar */}
<div
className={`fixed inset-y-0 left-0 z-50 w-64 bg-background-dark border-r border-border-dark text-white transition-transform duration-300 ${
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex flex-col h-full">
{/* Header */}
<div className="flex items-center justify-between px-6 py-5 border-b border-border-dark">
<div className="flex items-center gap-3">
<img
src="/logo.png"
alt="Calypso Logo"
className="w-10 h-10 object-contain"
onError={(e) => {
// Fallback to text if image not found
const target = e.target as HTMLImageElement
target.style.display = 'none'
const fallback = target.nextElementSibling as HTMLElement
if (fallback) fallback.style.display = 'flex'
}}
/>
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center hidden">
<span className="text-white font-bold text-sm">C</span>
</div>
<div className="flex flex-col">
<h1 className="text-xl font-black text-white font-display tracking-tight">Calypso</h1>
<p className="text-[10px] text-text-secondary leading-tight">Dev Release V.1</p>
</div>
</div>
<button
onClick={() => setSidebarOpen(false)}
className="lg:hidden text-text-secondary hover:text-white transition-colors"
>
<Menu className="h-5 w-5" />
</button>
</div>
{/* Navigation */}
<nav className="flex-1 px-3 py-4 space-y-1 overflow-y-auto custom-scrollbar">
{navigation.map((item) => {
const Icon = item.icon
const active = isActive(item.href)
return (
<Link
key={item.name}
to={item.href}
className={`flex items-center gap-3 px-4 py-2.5 rounded-lg transition-all ${
active
? 'bg-primary/20 text-primary border-l-2 border-primary'
: 'text-text-secondary hover:bg-card-dark hover:text-white'
}`}
>
<Icon className={`h-5 w-5 ${active ? 'text-primary' : ''}`} />
<span className={`text-sm font-medium ${active ? 'font-semibold' : ''}`}>
{item.name}
</span>
</Link>
)
})}
</nav>
{/* Footer */}
<div className="p-4 border-t border-border-dark bg-[#0d1419]">
<Link
to="/profile"
className="mb-3 px-2 py-2 rounded-lg hover:bg-card-dark transition-colors block"
>
<p className="text-sm font-semibold text-white mb-0.5">{user?.username}</p>
<p className="text-xs text-text-secondary font-mono">
{user?.roles.join(', ').toUpperCase()}
</p>
</Link>
<button
onClick={handleLogout}
className="w-full flex items-center gap-2 px-4 py-2.5 rounded-lg text-text-secondary hover:bg-card-dark hover:text-white transition-colors border border-border-dark"
>
<LogOut className="h-4 w-4" />
<span className="text-sm font-medium">Logout</span>
</button>
</div>
</div>
</div>
{/* Main content */}
<div className={`transition-all duration-300 flex-1 flex flex-col overflow-hidden ${sidebarOpen ? 'lg:ml-64' : 'ml-0'} bg-background-dark`}>
{/* Top bar with burger menu button */}
<div className="flex-none lg:hidden bg-background-dark border-b border-border-dark px-4 py-3">
<button
onClick={() => setSidebarOpen(true)}
className="text-text-secondary hover:text-white transition-colors"
aria-label="Open menu"
>
<Menu className="h-6 w-6" />
</button>
</div>
{/* Page content */}
<main className="flex-1 overflow-hidden">
<Outlet />
</main>
</div>
</div>
)
}