99 lines
3.7 KiB
React
99 lines
3.7 KiB
React
import { useEffect, useState } from 'react';
|
|
import { NavLink, Navigate, Outlet, useNavigate } from 'react-router-dom';
|
|
import { LayoutDashboard, MessageSquare, Tag, Terminal, Settings as SettingsIcon, LogOut, Loader2, Briefcase, HelpCircle, Bot, Users, MapPin, Newspaper } from 'lucide-react';
|
|
import { api } from '../lib/api.js';
|
|
|
|
function cx(...args) {
|
|
return args.filter(Boolean).join(' ');
|
|
}
|
|
|
|
const NAV = [
|
|
{ to: '/admin/dashboard', label: 'Дашборд', icon: LayoutDashboard },
|
|
{ to: '/admin/reviews', label: 'Отзывы', icon: MessageSquare },
|
|
{ to: '/admin/customers', label: 'Клиенты', icon: Users },
|
|
{ to: '/admin/services', label: 'Прайс-лист', icon: Terminal },
|
|
{ to: '/admin/promotions', label: 'Акции', icon: Tag },
|
|
{ to: '/admin/cases', label: 'Кейсы', icon: Briefcase },
|
|
{ to: '/admin/faqs', label: 'FAQ', icon: HelpCircle },
|
|
{ to: '/admin/blog', label: 'Блог', icon: Newspaper },
|
|
{ to: '/admin/contact', label: 'Контакты', icon: MapPin },
|
|
{ to: '/admin/ai', label: 'ИИ-ассистент', icon: Bot },
|
|
{ to: '/admin/settings', label: 'Настройки', icon: SettingsIcon },
|
|
];
|
|
|
|
export default function AdminLayout() {
|
|
const navigate = useNavigate();
|
|
const [status, setStatus] = useState('checking'); // checking | authed | anon
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
api
|
|
.me()
|
|
.then(() => !cancelled && setStatus('authed'))
|
|
.catch(() => !cancelled && setStatus('anon'));
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
async function handleLogout() {
|
|
try {
|
|
await api.logout();
|
|
} finally {
|
|
navigate('/admin');
|
|
}
|
|
}
|
|
|
|
if (status === 'checking') {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-950">
|
|
<Loader2 className="h-6 w-6 animate-spin text-emerald-500" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (status === 'anon') {
|
|
return <Navigate to="/admin" replace />;
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen flex flex-col md:flex-row bg-slate-50 dark:bg-slate-950">
|
|
<aside className="md:w-56 border-b md:border-b-0 md:border-r p-4 flex md:flex-col gap-2 overflow-x-auto md:overflow-x-visible border-slate-200 dark:border-slate-900">
|
|
<a href="/" className="hidden md:flex items-center gap-2 mb-6 px-2">
|
|
<Terminal className="h-4 w-4 text-emerald-500" />
|
|
<span className="text-sm font-bold font-display">TopSysOps</span>
|
|
<span className="text-xs text-slate-400 dark:text-slate-500 font-mono">admin</span>
|
|
</a>
|
|
{NAV.map((item) => {
|
|
const Icon = item.icon;
|
|
return (
|
|
<NavLink
|
|
key={item.to}
|
|
to={item.to}
|
|
className={({ isActive }) =>
|
|
cx(
|
|
'flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors flex-shrink-0 whitespace-nowrap',
|
|
isActive
|
|
? 'bg-emerald-500 text-white'
|
|
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-900'
|
|
)
|
|
}
|
|
>
|
|
<Icon className="h-4 w-4" /> {item.label}
|
|
</NavLink>
|
|
);
|
|
})}
|
|
<button
|
|
onClick={handleLogout}
|
|
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium flex-shrink-0 whitespace-nowrap md:mt-auto text-slate-500 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-900"
|
|
>
|
|
<LogOut className="h-4 w-4" /> Выйти
|
|
</button>
|
|
</aside>
|
|
<main className="flex-1 p-5 sm:p-8 max-w-[1800px]">
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|