feat: add AdminLayout with dark sidebar and sticky header

This commit is contained in:
2026-05-07 20:15:28 +03:00
parent 5628f45c71
commit 735d0a88cb
2 changed files with 176 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
import React, { useState, useEffect } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import { Plus } from 'lucide-react';
import Sidebar from './Sidebar';
import { supabase } from '../../lib/supabase';
const PAGE_TITLES = {
'/admin/kanban': 'Канбан',
'/admin/analytics': 'Аналитика',
'/admin/completed': 'Завершённые заказы',
};
const AdminLayout = () => {
const location = useLocation();
const title = PAGE_TITLES[location.pathname] || 'Панель управления';
const [showNewOrder, setShowNewOrder] = useState(false);
const [newCount, setNewCount] = useState(0);
const refreshBadge = () => {
supabase
.from('orders')
.select('id', { count: 'exact', head: true })
.eq('status', 'новая')
.then(({ count }) => setNewCount(count || 0));
};
useEffect(() => {
refreshBadge();
const channel = supabase
.channel('badge-count')
.on('postgres_changes', { event: '*', schema: 'public', table: 'orders' }, refreshBadge)
.subscribe();
return () => supabase.removeChannel(channel);
}, []);
return (
<div style={{ display: 'flex', minHeight: '100vh', background: 'var(--bg-light)' }}>
<Sidebar />
<div style={{ marginLeft: '240px', flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
{/* Header */}
<header style={{
height: '60px', background: '#fff',
borderBottom: '1px solid var(--border-light)',
display: 'flex', alignItems: 'center',
justifyContent: 'space-between', padding: '0 28px',
position: 'sticky', top: 0, zIndex: 50,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<h1 style={{ fontFamily: 'var(--font-heading)', fontSize: '1.05rem', fontWeight: 700, color: 'var(--text-main)' }}>
{title}
</h1>
{newCount > 0 && (
<span style={{
background: '#ef4444', color: '#fff',
borderRadius: '50px', padding: '2px 9px',
fontSize: '0.72rem', fontWeight: 700,
}}>
{newCount} новых
</span>
)}
</div>
<button
onClick={() => setShowNewOrder(true)}
className="btn-primary"
style={{ padding: '8px 18px', fontSize: '0.88rem', display: 'flex', alignItems: 'center', gap: '7px' }}
>
<Plus size={15} />
Заявка
</button>
</header>
{/* Page content */}
<main style={{ flex: 1, padding: '28px', overflow: 'auto' }}>
<Outlet context={{ showNewOrder, setShowNewOrder }} />
</main>
</div>
</div>
);
};
export default AdminLayout;
+95
View File
@@ -0,0 +1,95 @@
import React from 'react';
import { NavLink, useNavigate } from 'react-router-dom';
import { LayoutDashboard, BarChart2, CheckSquare, Shield, LogOut } from 'lucide-react';
import { useAuth } from '../auth/useAuth';
const NAV_ITEMS = [
{ to: '/admin/kanban', icon: LayoutDashboard, label: 'Канбан' },
{ to: '/admin/analytics', icon: BarChart2, label: 'Аналитика' },
{ to: '/admin/completed', icon: CheckSquare, label: 'Завершённые' },
];
const Sidebar = () => {
const { session, logout } = useAuth();
const navigate = useNavigate();
const handleLogout = async () => {
await logout();
navigate('/admin/login');
};
return (
<aside style={{
width: '240px', minHeight: '100vh',
background: '#0f172a',
display: 'flex', flexDirection: 'column',
position: 'fixed', top: 0, left: 0,
zIndex: 100,
borderRight: '1px solid rgba(255,255,255,0.06)',
}}>
{/* Logo */}
<div style={{ padding: '22px 20px', borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<Shield size={20} color="var(--accent-blue)" />
<span style={{ fontFamily: 'var(--font-heading)', fontWeight: 800, color: '#fff', fontSize: '1.05rem' }}>
Осколкам.<span style={{ color: 'var(--accent-blue)' }}>Нет</span>
</span>
</div>
<p style={{ fontSize: '0.72rem', color: 'rgba(255,255,255,0.35)', marginTop: '3px', marginLeft: '30px' }}>
Панель управления
</p>
</div>
{/* Navigation */}
<nav style={{ flex: 1, padding: '14px 10px' }}>
{NAV_ITEMS.map(({ to, icon: Icon, label }) => (
<NavLink
key={to}
to={to}
style={({ isActive }) => ({
display: 'flex', alignItems: 'center', gap: '12px',
padding: '11px 14px', borderRadius: '8px', marginBottom: '2px',
color: isActive ? '#fff' : 'rgba(255,255,255,0.45)',
background: isActive ? 'rgba(14, 165, 233, 0.15)' : 'transparent',
textDecoration: 'none',
fontFamily: 'var(--font-heading)', fontWeight: 600, fontSize: '0.88rem',
transition: 'all 0.15s',
borderLeft: isActive ? '3px solid var(--accent-blue)' : '3px solid transparent',
})}
>
<Icon size={17} />
{label}
</NavLink>
))}
</nav>
{/* User + logout */}
<div style={{ padding: '14px 18px', borderTop: '1px solid rgba(255,255,255,0.06)' }}>
<p style={{
fontSize: '0.75rem', color: 'rgba(255,255,255,0.35)',
marginBottom: '10px', overflow: 'hidden',
textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
{session?.user?.email}
</p>
<button
onClick={handleLogout}
style={{
display: 'flex', alignItems: 'center', gap: '8px',
background: 'none', border: 'none', cursor: 'pointer',
color: 'rgba(255,255,255,0.45)',
fontFamily: 'var(--font-body)', fontSize: '0.82rem', padding: '4px 0',
transition: 'color 0.15s',
}}
onMouseEnter={(e) => e.currentTarget.style.color = '#fff'}
onMouseLeave={(e) => e.currentTarget.style.color = 'rgba(255,255,255,0.45)'}
>
<LogOut size={15} />
Выйти
</button>
</div>
</aside>
);
};
export default Sidebar;