Separate CP admin panel into independent Vite project at /cp/

- New /cp/ Vite project with own package.json, Dockerfile, tailwind config
- Migrated CpLoginPage, CpLayout, AdminDashboard → cp/src/ (self-contained)
- cp/src/lib/utils.ts has only what CP needs (cn, PLAN_LABELS, PLAN_COLORS)
- cp/src/data/mockData.ts has Hotel/User types and mock data
- cp/ builds to nginx static container (hotelsync-cp)
- docker-compose.yml: added cp service
- nginx: added cp.hotelsync.ru → hotelsync-cp:80 (SSL block ready for certbot)
- deploy.yml: builds hotelsync-cp:latest and restarts container on push
- Cleaned src/App.tsx: removed IS_CP branch, CpLoginPage/CpLayout/AdminDashboard imports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 01:03:28 +03:00
parent 5429b272f9
commit 526a9eefcf
21 changed files with 801 additions and 43 deletions

139
cp/src/layouts/CpLayout.tsx Normal file
View File

@@ -0,0 +1,139 @@
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>
<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">
<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>
)
}