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:
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user