feat: mobile responsive sidebar drawer

Collapses sidebar into slide-over drawer on mobile (< 768px) with
hamburger toggle, overlay backdrop, and route-change auto-close.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 17:19:01 +03:00
parent ef472f74d8
commit 8b29b68449
3 changed files with 73 additions and 6 deletions
+39 -3
View File
@@ -1,8 +1,9 @@
import { useState, useEffect } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import { Plus } from 'lucide-react';
import { Plus, Menu } from 'lucide-react';
import Sidebar from './Sidebar';
import { supabase } from '../../lib/supabase';
import { useIsMobile } from '../../hooks/useIsMobile';
const PAGE_TITLES = {
'/admin/kanban': 'Канбан',
@@ -15,6 +16,8 @@ const AdminLayout = () => {
const title = PAGE_TITLES[location.pathname] || 'Панель управления';
const [showNewOrder, setShowNewOrder] = useState(false);
const [newCount, setNewCount] = useState(0);
const [sidebarOpen, setSidebarOpen] = useState(false);
const isMobile = useIsMobile();
useEffect(() => {
const refreshBadge = () => {
@@ -33,10 +36,31 @@ const AdminLayout = () => {
return () => supabase.removeChannel(channel);
}, []);
useEffect(() => {
setSidebarOpen(false);
}, [location.pathname]);
return (
<div className="admin-panel" style={{ display: 'flex', minHeight: '100vh' }}>
<Sidebar />
<div style={{ marginLeft: '240px', flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
{isMobile && sidebarOpen && (
<div
onClick={() => setSidebarOpen(false)}
style={{
position: 'fixed', inset: 0,
background: 'rgba(0,0,0,0.4)',
zIndex: 99,
}}
/>
)}
<Sidebar
isOpen={sidebarOpen}
onClose={() => setSidebarOpen(false)}
isMobile={isMobile}
/>
<div style={{
marginLeft: isMobile ? 0 : '240px',
flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0,
}}>
{/* Header */}
<header style={{
height: '60px', background: '#fff',
@@ -46,6 +70,18 @@ const AdminLayout = () => {
position: 'sticky', top: 0, zIndex: 50,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{isMobile && (
<button
onClick={() => setSidebarOpen(true)}
aria-label="Открыть меню"
style={{
background: 'none', border: 'none', cursor: 'pointer',
color: 'var(--text-main)', padding: '4px', display: 'flex',
}}
>
<Menu size={22} />
</button>
)}
<h1 style={{ fontFamily: 'var(--font-heading)', fontSize: '1.05rem', fontWeight: 700, color: 'var(--text-main)' }}>
{title}
</h1>
+22 -3
View File
@@ -1,5 +1,5 @@
import { NavLink, useNavigate } from 'react-router-dom';
import { LayoutDashboard, BarChart2, CheckSquare, Shield, LogOut, Users, Settings, BookUser } from 'lucide-react';
import { LayoutDashboard, BarChart2, CheckSquare, Shield, LogOut, Users, Settings, BookUser, X } from 'lucide-react';
import { useAuth } from '../auth/useAuth';
const NAV_ITEMS = [
@@ -11,7 +11,7 @@ const NAV_ITEMS = [
{ to: '/admin/settings', icon: Settings, label: 'Настройки' },
];
const Sidebar = () => {
const Sidebar = ({ isOpen, onClose, isMobile }) => {
const { session, logout } = useAuth();
const navigate = useNavigate();
@@ -28,9 +28,11 @@ const Sidebar = () => {
position: 'fixed', top: 0, left: 0,
zIndex: 100,
borderRight: '1px solid #E9ECEF',
transform: isMobile && !isOpen ? 'translateX(-100%)' : 'translateX(0)',
transition: 'transform 0.25s ease',
}}>
{/* Logo */}
<div style={{ padding: '22px 20px', borderBottom: '1px solid #E9ECEF' }}>
<div style={{ padding: '22px 20px', borderBottom: '1px solid #E9ECEF', position: 'relative' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<Shield size={20} color="#4361EE" />
<span style={{ fontFamily: 'var(--font-heading)', fontWeight: 800, color: '#1E1E2D', fontSize: '1.05rem' }}>
@@ -40,6 +42,23 @@ const Sidebar = () => {
<p style={{ fontSize: '0.72rem', color: '#8C9097', marginTop: '3px', marginLeft: '30px' }}>
Панель управления
</p>
{isMobile && (
<button
onClick={onClose}
aria-label="Закрыть меню"
style={{
position: 'absolute', top: '50%', right: '14px',
transform: 'translateY(-50%)',
background: 'none', border: 'none', cursor: 'pointer',
color: '#8C9097', display: 'flex', padding: '4px',
borderRadius: '4px',
}}
onMouseEnter={(e) => e.currentTarget.style.color = '#1E1E2D'}
onMouseLeave={(e) => e.currentTarget.style.color = '#8C9097'}
>
<X size={18} />
</button>
)}
</div>
{/* Navigation */}
+12
View File
@@ -0,0 +1,12 @@
import { useState, useEffect } from 'react';
export function useIsMobile(breakpoint = 768) {
const [isMobile, setIsMobile] = useState(() => window.innerWidth < breakpoint);
useEffect(() => {
const mq = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
const handler = (e) => setIsMobile(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, [breakpoint]);
return isMobile;
}