102 lines
3.9 KiB
TypeScript
102 lines
3.9 KiB
TypeScript
import { ReactNode, useState } from 'react'
|
|
import { Link, useLocation } from 'react-router-dom'
|
|
import { ThemeToggle } from './ThemeToggle'
|
|
import { Button } from './ui/Button'
|
|
import { useAuth } from '../context/AuthContext'
|
|
|
|
interface LayoutProps {
|
|
children: ReactNode
|
|
}
|
|
|
|
export default function Layout({ children }: LayoutProps) {
|
|
const location = useLocation()
|
|
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
const { logout } = useAuth()
|
|
|
|
const navigation = [
|
|
{ name: 'Dashboard', path: '/', icon: '📊' },
|
|
{ name: 'Proxy Hosts', path: '/proxy-hosts', icon: '🌐' },
|
|
{ name: 'Remote Servers', path: '/remote-servers', icon: '🖥️' },
|
|
{ name: 'Certificates', path: '/certificates', icon: '🔒' },
|
|
{ name: 'Import Caddyfile', path: '/import', icon: '📥' },
|
|
{ name: 'Settings', path: '/settings', icon: '⚙️' },
|
|
]
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 dark:bg-dark-bg flex transition-colors duration-200">
|
|
{/* Mobile Header */}
|
|
<div className="lg:hidden fixed top-0 left-0 right-0 h-16 bg-white dark:bg-dark-sidebar border-b border-gray-200 dark:border-gray-800 flex items-center justify-between px-4 z-40">
|
|
<h1 className="text-lg font-bold text-gray-900 dark:text-white">CPM+</h1>
|
|
<div className="flex items-center gap-2">
|
|
<ThemeToggle />
|
|
<Button variant="ghost" size="sm" onClick={() => setSidebarOpen(!sidebarOpen)}>
|
|
{sidebarOpen ? '✕' : '☰'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sidebar */}
|
|
<aside className={`
|
|
fixed lg:static inset-y-0 left-0 z-30 w-64 transform transition-transform duration-200 ease-in-out
|
|
bg-white dark:bg-dark-sidebar border-r border-gray-200 dark:border-gray-800 flex flex-col
|
|
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
|
|
`}>
|
|
<div className="p-6 hidden lg:flex items-center justify-between">
|
|
<h1 className="text-xl font-bold text-gray-900 dark:text-white">CPM+</h1>
|
|
<ThemeToggle />
|
|
</div>
|
|
|
|
<nav className="flex-1 px-4 space-y-1 mt-16 lg:mt-0">
|
|
{navigation.map((item) => {
|
|
const isActive = location.pathname === item.path
|
|
return (
|
|
<Link
|
|
key={item.path}
|
|
to={item.path}
|
|
onClick={() => setSidebarOpen(false)}
|
|
className={`flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${
|
|
isActive
|
|
? 'bg-blue-100 text-blue-700 dark:bg-blue-active dark:text-white'
|
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white'
|
|
}`}
|
|
>
|
|
<span className="text-lg">{item.icon}</span>
|
|
{item.name}
|
|
</Link>
|
|
)
|
|
})}
|
|
|
|
<button
|
|
onClick={() => {
|
|
setSidebarOpen(false)
|
|
logout()
|
|
}}
|
|
className="w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors text-gray-600 dark:text-gray-400 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-600 dark:hover:text-red-400"
|
|
>
|
|
<span className="text-lg">🚪</span>
|
|
Logout
|
|
</button>
|
|
</nav>
|
|
<div className="p-4 border-t border-gray-200 dark:border-gray-800">
|
|
<div className="text-xs text-gray-500 dark:text-gray-500">
|
|
Version 0.1.0
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Overlay for mobile */}
|
|
{sidebarOpen && (
|
|
<div
|
|
className="fixed inset-0 bg-black/50 z-20 lg:hidden"
|
|
onClick={() => setSidebarOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Main content */}
|
|
<main className="flex-1 overflow-auto pt-16 lg:pt-0">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|