Files
gh-christianlouis-inboxconv…/frontend/src/components/DashboardLayout.tsx
T
Christian Krakau-Louis dd91110ff1 Merge pull request #83 from christianlouis/copilot/add-admin-interface-for-users
Fix test_app_title assertion after InboxRescue rebrand
2026-03-26 17:18:00 +01:00

245 lines
9.7 KiB
TypeScript

'use client';
import { useState } from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { useAuthStore } from '@/store/authStore';
import {
LayoutDashboard,
Mail,
Settings,
LogOut,
Menu,
X,
User,
Shield,
Users,
CreditCard
} from 'lucide-react';
interface DashboardLayoutProps {
children: React.ReactNode;
}
export function DashboardLayout({ children }: DashboardLayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const pathname = usePathname();
const router = useRouter();
const { user, logout } = useAuthStore();
const handleLogout = () => {
logout();
router.push('/login');
};
const navigation = [
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
{ name: 'Mail Accounts', href: '/accounts', icon: Mail },
{ name: 'Settings', href: '/settings', icon: Settings },
];
const adminNavigation = user?.is_superuser
? [
{ name: 'Admin Overview', href: '/admin', icon: Shield },
{ name: 'Manage Users', href: '/admin/users', icon: Users },
{ name: 'Manage Plans', href: '/admin/plans', icon: CreditCard },
]
: [];
const allNavItems = [...navigation, ...adminNavigation];
return (
<div className="min-h-screen bg-gray-50">
{/* Sidebar for desktop */}
<div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col">
<div className="flex flex-col flex-grow bg-white border-r border-gray-200">
<div className="flex items-center h-16 flex-shrink-0 px-4 border-b border-gray-200">
<h1 className="text-xl font-bold text-gray-900">InboxRescue</h1>
</div>
<nav className="flex-1 px-2 py-4 space-y-1">
{navigation.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={`flex items-center px-4 py-2 text-sm font-medium rounded-md transition-colors ${
isActive
? 'bg-blue-50 text-blue-600'
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900'
}`}
>
<item.icon className={`mr-3 h-5 w-5 ${isActive ? 'text-blue-600' : 'text-gray-400'}`} />
{item.name}
</Link>
);
})}
{adminNavigation.length > 0 && (
<>
<div className="pt-4 pb-1 px-4">
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Admin</p>
</div>
{adminNavigation.map((item) => {
const isActive = pathname === item.href || pathname.startsWith(item.href + '/');
return (
<Link
key={item.name}
href={item.href}
className={`flex items-center px-4 py-2 text-sm font-medium rounded-md transition-colors ${
isActive
? 'bg-purple-50 text-purple-600'
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900'
}`}
>
<item.icon className={`mr-3 h-5 w-5 ${isActive ? 'text-purple-600' : 'text-gray-400'}`} />
{item.name}
</Link>
);
})}
</>
)}
</nav>
<div className="flex-shrink-0 border-t border-gray-200 p-4">
<button
onClick={handleLogout}
className="flex items-center w-full px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-50 hover:text-gray-900 transition-colors"
>
<LogOut className="mr-3 h-5 w-5 text-gray-400" />
Logout
</button>
</div>
</div>
</div>
{/* Mobile sidebar */}
{sidebarOpen && (
<div className="fixed inset-0 z-40 lg:hidden">
<div className="fixed inset-0 bg-gray-600/75" onClick={() => setSidebarOpen(false)} />
<div className="fixed inset-y-0 left-0 flex w-64 flex-col bg-white">
<div className="flex items-center justify-between h-16 px-4 border-b border-gray-200">
<h1 className="text-xl font-bold text-gray-900">InboxRescue</h1>
<button onClick={() => setSidebarOpen(false)} className="text-gray-500 hover:text-gray-700">
<X className="h-6 w-6" />
</button>
</div>
<nav className="flex-1 px-2 py-4 space-y-1">
{navigation.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
onClick={() => setSidebarOpen(false)}
className={`flex items-center px-4 py-2 text-sm font-medium rounded-md transition-colors ${
isActive
? 'bg-blue-50 text-blue-600'
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900'
}`}
>
<item.icon className={`mr-3 h-5 w-5 ${isActive ? 'text-blue-600' : 'text-gray-400'}`} />
{item.name}
</Link>
);
})}
{adminNavigation.length > 0 && (
<>
<div className="pt-4 pb-1 px-4">
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Admin</p>
</div>
{adminNavigation.map((item) => {
const isActive = pathname === item.href || pathname.startsWith(item.href + '/');
return (
<Link
key={item.name}
href={item.href}
onClick={() => setSidebarOpen(false)}
className={`flex items-center px-4 py-2 text-sm font-medium rounded-md transition-colors ${
isActive
? 'bg-purple-50 text-purple-600'
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900'
}`}
>
<item.icon className={`mr-3 h-5 w-5 ${isActive ? 'text-purple-600' : 'text-gray-400'}`} />
{item.name}
</Link>
);
})}
</>
)}
</nav>
<div className="flex-shrink-0 border-t border-gray-200 p-4">
<button
onClick={handleLogout}
className="flex items-center w-full px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-50 hover:text-gray-900 transition-colors"
>
<LogOut className="mr-3 h-5 w-5 text-gray-400" />
Logout
</button>
</div>
</div>
</div>
)}
{/* Main content */}
<div className="lg:pl-64 flex flex-col flex-1">
{/* Top bar */}
<div className="sticky top-0 z-10 flex h-16 flex-shrink-0 bg-white border-b border-gray-200">
<button
type="button"
className="border-r border-gray-200 px-4 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 lg:hidden"
onClick={() => setSidebarOpen(true)}
>
<Menu className="h-6 w-6" />
</button>
<div className="flex flex-1 justify-between px-4 sm:px-6 lg:px-8">
<div className="flex flex-1 items-center">
<h2 className="text-lg font-semibold text-gray-900">
{allNavItems.find((item) => item.href === pathname)?.name || 'Dashboard'}
</h2>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className={`flex h-8 w-8 items-center justify-center rounded-full text-white ${user?.is_superuser ? 'bg-purple-600' : 'bg-blue-600'}`}>
{user?.is_superuser ? <Shield className="h-4 w-4" /> : <User className="h-4 w-4" />}
</div>
<div className="hidden sm:block">
<p className="text-sm font-medium text-gray-900">{user?.full_name}</p>
<p className="text-xs text-gray-500">
{user?.email}
{user?.is_superuser && (
<span className="ml-1 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-700">
Admin
</span>
)}
</p>
</div>
</div>
</div>
</div>
</div>
{/* Page content */}
<main className="flex-1">
<div className="py-6">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
{children}
</div>
</div>
</main>
{/* Footer */}
<footer className="border-t border-gray-200 bg-white py-4 px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-7xl flex flex-wrap justify-center gap-4 text-xs text-gray-400">
<Link href="/impressum" className="hover:text-gray-600 transition-colors">
Impressum
</Link>
<Link href="/datenschutz" className="hover:text-gray-600 transition-colors">
Datenschutz
</Link>
</div>
</footer>
</div>
</div>
);
}