feat: add AdminLayout with dark sidebar and sticky header
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user