Initial commit: TopSysOps site + admin panel
Multi-page public site (Next.js), Node/Express backend with Postgres, and Vite admin/account app, restructured from a single-page layout into dedicated service, promotions, cases, about, FAQ, and blog pages for SEO. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013otXCiTZsxdZ4SJKZ9wUky
This commit is contained in:
commit
130c5382cd
108 files changed
+7146
No files matched your search
@@ -0,0 +1,98 @@
|
||||
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-5xl">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user