Add separate CP admin panel at cp.hotelsync.ru
- CpLoginPage: dark themed login page with email/password auth, only for cp subdomain - CpLayout: separate dark sidebar layout with Shield icon, nav for Обзор/Отели/Пользователи/Тарифы/Мониторинг/Настройки - App.tsx: detect hostname — cp.hotelsync.ru renders CP app, app.hotelsync.ru renders PMS (no /admin route) - CpGuard: protects CP routes, redirects to /login if not authenticated - Admin panel completely removed from app.hotelsync.ru Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
141
src/layouts/CpLayout.tsx
Normal file
141
src/layouts/CpLayout.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { useState } from 'react'
|
||||
import { NavLink, Outlet, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
Building2, Users, BarChart2, CreditCard, Settings,
|
||||
Shield, LogOut, Menu, X, Bell, Activity,
|
||||
ChevronRight,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '../lib/utils'
|
||||
|
||||
const NAV = [
|
||||
{ to: '/', icon: BarChart2, label: 'Обзор' },
|
||||
{ to: '/hotels', icon: Building2, label: 'Отели' },
|
||||
{ to: '/cp-users', icon: Users, label: 'Пользователи' },
|
||||
{ to: '/billing', icon: CreditCard, label: 'Тарифы и оплата' },
|
||||
{ to: '/monitoring', icon: Activity, label: 'Мониторинг' },
|
||||
{ to: '/cp-settings', icon: Settings, label: 'Настройки' },
|
||||
]
|
||||
|
||||
export function CpLayout() {
|
||||
const navigate = useNavigate()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const stored = localStorage.getItem('cpAuth')
|
||||
const admin = stored ? JSON.parse(stored) : null
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('cpAuth')
|
||||
navigate('/login', { replace: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-slate-950 text-slate-100">
|
||||
{/* Mobile overlay */}
|
||||
{open && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/60 z-40 lg:hidden"
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside className={cn(
|
||||
'fixed left-0 top-0 bottom-0 z-50 w-60 flex flex-col bg-slate-900 border-r border-slate-800 transition-transform duration-200',
|
||||
'lg:translate-x-0 lg:static lg:z-auto',
|
||||
open ? 'translate-x-0' : '-translate-x-full',
|
||||
)}>
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3 px-5 py-4 border-b border-slate-800">
|
||||
<div className="w-8 h-8 rounded-lg bg-brand-600 flex items-center justify-center shrink-0">
|
||||
<Shield size={16} className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-white leading-none">HotelSync</p>
|
||||
<p className="text-[10px] text-slate-500 mt-0.5">Control Panel</p>
|
||||
</div>
|
||||
<button onClick={() => setOpen(false)} className="ml-auto lg:hidden text-slate-500 hover:text-slate-300">
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav className="flex-1 px-3 py-4 space-y-0.5 overflow-y-auto">
|
||||
{NAV.map(item => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
end={item.to === '/'}
|
||||
onClick={() => setOpen(false)}
|
||||
className={({ isActive }) => cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-brand-600/20 text-brand-400'
|
||||
: 'text-slate-400 hover:bg-slate-800 hover:text-slate-200',
|
||||
)}
|
||||
>
|
||||
<item.icon size={16} />
|
||||
{item.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* User footer */}
|
||||
<div className="px-3 pb-4 border-t border-slate-800 pt-3">
|
||||
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl">
|
||||
<div className="w-8 h-8 rounded-lg bg-brand-700 flex items-center justify-center shrink-0">
|
||||
<Shield size={14} className="text-brand-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs font-medium text-slate-200 truncate">{admin?.email ?? 'admin'}</p>
|
||||
<p className="text-[10px] text-slate-500">Super Admin</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={logout}
|
||||
title="Выйти"
|
||||
className="text-slate-600 hover:text-red-400 transition-colors"
|
||||
>
|
||||
<LogOut size={15} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main area */}
|
||||
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||
{/* Topbar */}
|
||||
<header className="flex items-center gap-4 px-4 md:px-6 py-3 border-b border-slate-800 bg-slate-900 shrink-0">
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="lg:hidden text-slate-400 hover:text-slate-200 transition-colors"
|
||||
>
|
||||
<Menu size={20} />
|
||||
</button>
|
||||
|
||||
{/* Breadcrumb placeholder */}
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-500">
|
||||
<span className="text-slate-400 font-medium">cp.hotelsync.ru</span>
|
||||
<ChevronRight size={12} />
|
||||
<span>Control Panel</span>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
{/* Status indicator */}
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-emerald-500/10 border border-emerald-500/20">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
|
||||
<span className="text-[11px] text-emerald-400 font-medium">Система в норме</span>
|
||||
</div>
|
||||
|
||||
<button className="relative text-slate-400 hover:text-slate-200 transition-colors p-1.5">
|
||||
<Bell size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page content */}
|
||||
<main className="flex-1 overflow-y-auto bg-slate-950">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user