/
npronnikov
/
ProcessVisualizer
Обзор
Документация
Войти
/
npronnikov
/
ProcessVisualizer
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
frontend/src/components/layout/AppLayout.tsx
190 строк
6 KB
Madnick
Improve frontend card readability and navigation UX
26 май 2026, 14:04
26 май 2026, 14:04
9a3b6c0
Код
Авторство
О чём код?
import React, { useContext, useState } from 'react'; import { Layout, Space, Button, Dropdown, Tooltip, Drawer, Grid } from 'antd'; import { useNavigate, useLocation } from 'react-router-dom'; import { AppstoreOutlined, DashboardOutlined, UserOutlined, LogoutOutlined, SunOutlined, MoonOutlined, TeamOutlined, MenuOutlined, } from '@ant-design/icons'; import { ThemeContext, AuthContext } from '../../App'; const { Content: AntContent } = Layout; const { useBreakpoint } = Grid; const SIDEBAR_WIDTH = 228; const TOPBAR_HEIGHT = 64; interface AppLayoutProps { children: React.ReactNode; } const NavItem: React.FC<{ icon: React.ReactNode; label: string; active: boolean; onClick: () => void; compact?: boolean; }> = ({ icon, label, active, onClick, compact = false }) => ( <Tooltip title={compact ? label : undefined} placement="right"> <button onClick={onClick} className={`app-nav-item ${compact ? 'is-compact' : ''} ${active ? 'is-active' : ''}`} aria-current={active ? 'page' : undefined} > <span className="app-nav-icon">{icon}</span> <span className="app-nav-label">{label}</span> </button> </Tooltip> ); const AppLayout: React.FC<AppLayoutProps> = ({ children }) => { const navigate = useNavigate(); const location = useLocation(); const screens = useBreakpoint(); const isMobile = !screens.md; const { isDark, setIsDark } = useContext(ThemeContext); const { user, logout } = useContext(AuthContext); const [mobileNavOpen, setMobileNavOpen] = useState(false); const selectedKey = location.pathname === '/' ? '/' : location.pathname.startsWith('/groups') ? '/groups' : location.pathname.startsWith('/users') ? '/users' : '/'; const navItems = [ { key: '/', icon: <DashboardOutlined />, label: 'Дашборд' }, { key: '/groups', icon: <AppstoreOutlined />, label: 'Группы' }, ...(user?.isAdmin ? [{ key: '/users', icon: <TeamOutlined />, label: 'Пользователи' }] : []), ]; const textClr = isDark ? '#e2e8f0' : '#0f172a'; const accentClr = isDark ? '#00d4aa' : '#0ea5e9'; const handleNavigate = (target: string) => { navigate(target); setMobileNavOpen(false); }; return ( <Layout className="app-shell"> <header className="app-topbar meridian-header"> <div className="app-topbar-left"> {isMobile && ( <Button type="text" icon={<MenuOutlined />} onClick={() => setMobileNavOpen(true)} className="app-icon-btn" /> )} <div className="app-logo-pill" onClick={() => navigate('/')} role="button" tabIndex={0} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); navigate('/'); } }} > <img src="/assets/robot_no_bg.png" alt="logo" className="app-logo-icon" /> <span className="app-brand"> Авто<span style={{ color: accentClr }}>ном</span>и<span className="app-brand-k">К</span>о </span> </div> </div> <div className="app-topbar-right"> <Button type="text" icon={isDark ? <SunOutlined /> : <MoonOutlined />} onClick={() => setIsDark(!isDark)} className="app-icon-btn" /> {user && ( <Dropdown menu={{ items: [{ key: 'logout', icon: <LogoutOutlined />, label: 'Выйти', onClick: logout }], }} placement="bottomRight" > <button className="app-user-button"> <span className="app-user-avatar"> {user.name?.[0]?.toUpperCase() ?? <UserOutlined />} </span> <span className="app-user-name">{user.name}</span> </button> </Dropdown> )} </div> </header> <Layout className="app-body" style={{ marginTop: TOPBAR_HEIGHT }}> {!isMobile && ( <aside className="app-sidebar" style={{ width: SIDEBAR_WIDTH, top: TOPBAR_HEIGHT }}> <div className="app-sidebar-inner"> {navItems.map((item) => ( <NavItem key={item.key} icon={item.icon} label={item.label} active={selectedKey === item.key} onClick={() => handleNavigate(item.key)} /> ))} </div> </aside> )} <AntContent className="app-content" style={{ marginLeft: isMobile ? 0 : SIDEBAR_WIDTH, minHeight: `calc(100vh - ${TOPBAR_HEIGHT}px)`, }} > <div className="app-content-inner">{children}</div> </AntContent> </Layout> <Drawer title={<span style={{ color: textClr, fontWeight: 700 }}>Навигация</span>} placement="left" onClose={() => setMobileNavOpen(false)} open={mobileNavOpen} width={280} styles={{ header: { borderBottom: '1px solid var(--border)', background: 'var(--bg-card)' }, body: { padding: '14px', background: 'var(--bg-card)' }, }} > <Space direction="vertical" size={10} style={{ width: '100%' }}> {navItems.map((item) => ( <NavItem key={item.key} icon={item.icon} label={item.label} active={selectedKey === item.key} onClick={() => handleNavigate(item.key)} compact /> ))} <Button icon={<LogoutOutlined />} onClick={() => { setMobileNavOpen(false); logout(); }} danger style={{ marginTop: 10, borderRadius: 10, height: 40 }} > Выйти </Button> </Space> </Drawer> </Layout> ); }; export default AppLayout;