116 lines
4.9 KiB
TypeScript
116 lines
4.9 KiB
TypeScript
import { useState, useEffect, type ReactNode } from 'react';
|
||
import { NavLink } from 'react-router-dom';
|
||
import { useAuth } from '../context/AuthContext';
|
||
import { getBackendVersion } from '../api/version';
|
||
|
||
export function Layout({ children }: { children: ReactNode }) {
|
||
const { user, logout } = useAuth();
|
||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||
const [beVersion, setBeVersion] = useState<string | null>(null);
|
||
|
||
const closeDrawer = () => setDrawerOpen(false);
|
||
|
||
useEffect(() => {
|
||
getBackendVersion()
|
||
.then((r) => setBeVersion(r.version))
|
||
.catch(() => setBeVersion(null));
|
||
}, []);
|
||
|
||
return (
|
||
<div className="app-shell">
|
||
<button
|
||
type="button"
|
||
className="menu-button"
|
||
aria-label="Открыть меню"
|
||
onClick={() => setDrawerOpen(true)}
|
||
>
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<line x1="3" y1="6" x2="21" y2="6" />
|
||
<line x1="3" y1="12" x2="21" y2="12" />
|
||
<line x1="3" y1="18" x2="21" y2="18" />
|
||
</svg>
|
||
</button>
|
||
|
||
{drawerOpen && (
|
||
<div
|
||
className="app-shell__overlay"
|
||
aria-hidden="true"
|
||
onClick={closeDrawer}
|
||
/>
|
||
)}
|
||
|
||
<aside className={`sidebar ${drawerOpen ? 'sidebar--open' : ''}`}>
|
||
<div className="sidebar__brand">
|
||
<span className="sidebar__brand-icon">₽</span>
|
||
<span className="sidebar__brand-text">Семейный бюджет</span>
|
||
</div>
|
||
|
||
<nav className="sidebar__nav" aria-label="Основная навигация">
|
||
<NavLink
|
||
to="/history"
|
||
className={({ isActive }) =>
|
||
`sidebar__nav-link${isActive ? ' sidebar__nav-link--active' : ''}`
|
||
}
|
||
onClick={closeDrawer}
|
||
>
|
||
<svg className="sidebar__nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
||
<polyline points="14,2 14,8 20,8" />
|
||
<line x1="16" y1="13" x2="8" y2="13" />
|
||
<line x1="16" y1="17" x2="8" y2="17" />
|
||
<polyline points="10,9 9,9 8,9" />
|
||
</svg>
|
||
Операции
|
||
</NavLink>
|
||
|
||
<NavLink
|
||
to="/analytics"
|
||
className={({ isActive }) =>
|
||
`sidebar__nav-link${isActive ? ' sidebar__nav-link--active' : ''}`
|
||
}
|
||
onClick={closeDrawer}
|
||
>
|
||
<svg className="sidebar__nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<line x1="18" y1="20" x2="18" y2="10" />
|
||
<line x1="12" y1="20" x2="12" y2="4" />
|
||
<line x1="6" y1="20" x2="6" y2="14" />
|
||
</svg>
|
||
Аналитика
|
||
</NavLink>
|
||
|
||
<NavLink
|
||
to="/settings"
|
||
className={({ isActive }) =>
|
||
`sidebar__nav-link${isActive ? ' sidebar__nav-link--active' : ''}`
|
||
}
|
||
onClick={closeDrawer}
|
||
>
|
||
<svg className="sidebar__nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<circle cx="12" cy="12" r="3" />
|
||
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z" />
|
||
</svg>
|
||
Настройки
|
||
</NavLink>
|
||
</nav>
|
||
|
||
<div className="sidebar__footer">
|
||
<div className="sidebar__user-row">
|
||
<span className="sidebar__user">{user?.login}</span>
|
||
<button className="sidebar__logout" onClick={() => logout()}>
|
||
Выход
|
||
</button>
|
||
</div>
|
||
<div className="sidebar__meta">
|
||
<span className="sidebar__version">
|
||
FE {__FE_VERSION__} · BE {beVersion ?? '…'}
|
||
</span>
|
||
<span className="sidebar__copyright">© 2025–2026 Семейный бюджет</span>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<main className="app-shell__main">{children}</main>
|
||
</div>
|
||
);
|
||
}
|