diff --git a/frontend/src/app/providers/layout/layout.tsx b/frontend/src/app/providers/layout/layout.tsx
index 8a54aea..e24408e 100644
--- a/frontend/src/app/providers/layout/layout.tsx
+++ b/frontend/src/app/providers/layout/layout.tsx
@@ -1,6 +1,9 @@
import { useState, useEffect, type ReactNode } from "react";
import { Sidebar } from "@/app/providers/layout/sidebar/sidebar";
-import { Navigation } from "@/app/providers/layout/navigation/navigation";
+import {
+ Navigation,
+ BottomNav,
+} from "@/app/providers/layout/navigation/navigation";
import { useAgentStore } from "@/app/providers/layout/store/agent.store";
export const Layout = ({ children }: { children: ReactNode }) => {
@@ -8,6 +11,9 @@ export const Layout = ({ children }: { children: ReactNode }) => {
const [isMobile, setIsMobile] = useState(() =>
typeof window !== "undefined" ? window.innerWidth < 856 : false,
);
+ const [isVerySmall, setIsVerySmall] = useState(() =>
+ typeof window !== "undefined" ? window.innerWidth < 600 : false,
+ );
const { fetchAgents } = useAgentStore();
const sidebarOpen = isMobile ? mobileOpen : true;
@@ -19,6 +25,7 @@ export const Layout = ({ children }: { children: ReactNode }) => {
if (!mobile) {
setMobileOpen(false);
}
+ setIsVerySmall(window.innerWidth < 600);
};
window.addEventListener("resize", handleResize);
@@ -55,8 +62,13 @@ export const Layout = ({ children }: { children: ReactNode }) => {
isMobile={isMobile}
/>
-
+
{children}
+ {isVerySmall &&
}
);
diff --git a/frontend/src/app/providers/layout/navigation/navigation.tsx b/frontend/src/app/providers/layout/navigation/navigation.tsx
index 289c62c..a5deef1 100644
--- a/frontend/src/app/providers/layout/navigation/navigation.tsx
+++ b/frontend/src/app/providers/layout/navigation/navigation.tsx
@@ -24,11 +24,13 @@ import {
interface NavigationProps {
onToggleSidebar?: () => void;
isMobile?: boolean;
+ isVerySmall?: boolean;
}
export const Navigation: React.FC = ({
onToggleSidebar,
isMobile,
+ isVerySmall = false,
}) => {
const navigate = useNavigate();
const location = useLocation();
@@ -48,7 +50,6 @@ export const Navigation: React.FC = ({
const isActive = (path: string) => location.pathname === path;
- // Закрытие дропдауна при клике вне
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (
@@ -74,281 +75,349 @@ export const Navigation: React.FC = ({
setThemePickerOpen(false);
};
+ const renderNavItems = (showLabels: boolean, iconSize: number) => (
+
+ {navItems
+ .filter((item) => {
+ if ((item as any).adminOnly && !user?.permission_admin) return false;
+ return true;
+ })
+ .map((item) => {
+ const Icon = item.icon;
+ const active = isActive(item.path);
+ return (
+ navigate(item.path)}
+ className="flex items-center gap-1.5 px-3 py-2 rounded-lg font-medium transition-all flex-shrink-0"
+ style={{
+ backgroundColor: active ? "var(--accent)" : "transparent",
+ color: active ? "var(--accent-text)" : "var(--text-secondary)",
+ }}
+ onMouseEnter={(e) => {
+ if (!active) {
+ e.currentTarget.style.backgroundColor = "var(--bg-secondary)";
+ e.currentTarget.style.color = "var(--text-primary)";
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (!active) {
+ e.currentTarget.style.backgroundColor = "transparent";
+ e.currentTarget.style.color = "var(--text-secondary)";
+ }
+ }}
+ title={item.label}
+ >
+
+ {showLabels && {item.label} }
+
+ );
+ })}
+
+ );
+
+ return (
+ <>
+ {/* Верхний бар */}
+
+
+ {/* Бургер — только на мобильных */}
+ {isMobile && (
+
+
+
+ )}
+
+ {/* Название по центру — только на очень маленьких экранах */}
+ {isVerySmall && (
+
+
+ HellreigN
+
+
+ )}
+
+ {/* Навигация — только если НЕ очень маленький экран */}
+ {!isVerySmall && (
+
+ {renderNavItems(true, 12)}
+
+ )}
+
+ {/* Профиль пользователя — дропдаун */}
+
+
setDropdownOpen(!dropdownOpen)}
+ className="flex items-center gap-2 px-3 py-1.5 rounded-lg transition-all"
+ style={{
+ backgroundColor: dropdownOpen
+ ? "var(--bg-secondary)"
+ : "transparent",
+ border: "1px solid var(--border)",
+ }}
+ >
+
+
+
+
+ {user?.name || user?.login || "Пользователь"}
+
+
+
+
+ {dropdownOpen && (
+
+
+
+
+
+
+
+
+ {user?.name || user?.login}
+
+
+ {user?.login}
+
+
+
+
+
+
+
setThemePickerOpen(!themePickerOpen)}
+ className="w-full flex items-center gap-3 px-4 py-2.5 text-xs transition-colors"
+ style={{ color: "var(--text-primary)" }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.backgroundColor =
+ "var(--bg-secondary)";
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = "transparent";
+ }}
+ >
+
+
+ Тема: {themes.find((t) => t.id === currentTheme)?.name}
+
+
+
+
+ {themePickerOpen && (
+
+ {themes.map((t) => (
+
handleThemeChange(t.id)}
+ className="w-full flex items-center gap-3 px-4 py-2 text-xs transition-colors first:rounded-t-lg last:rounded-b-lg"
+ style={{
+ color:
+ currentTheme === t.id
+ ? "var(--accent)"
+ : "var(--text-primary)",
+ backgroundColor:
+ currentTheme === t.id
+ ? "var(--bg-secondary)"
+ : "transparent",
+ }}
+ onMouseEnter={(e) => {
+ if (currentTheme !== t.id) {
+ e.currentTarget.style.backgroundColor =
+ "var(--bg-secondary)";
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (currentTheme !== t.id) {
+ e.currentTarget.style.backgroundColor =
+ "transparent";
+ }
+ }}
+ >
+
+ {t.name}
+
+ ))}
+
+ )}
+
+
+ {user?.permission_admin && (
+
{
+ setDropdownOpen(false);
+ navigate("/admin");
+ }}
+ className="w-full flex items-center gap-3 px-4 py-2.5 text-xs transition-colors"
+ style={{ color: "var(--text-primary)" }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.backgroundColor =
+ "var(--bg-secondary)";
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = "transparent";
+ }}
+ >
+
+ Админка
+
+ )}
+
+
+
+
{
+ e.currentTarget.style.backgroundColor =
+ "rgba(239, 68, 68, 0.1)";
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = "transparent";
+ }}
+ >
+
+ Выйти
+
+
+ )}
+
+
+
+ >
+ );
+};
+
+export const BottomNav: React.FC = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { user } = useAuthStore();
+
+ const navItems = [
+ { path: "/templates", label: "Шаблоны", icon: FaCode },
+ { path: "/add-agents", label: "Деплой", icon: FaRocket },
+ { path: "/registration", label: "Регистрация", icon: FaKey },
+ { path: "/logs", label: "Логи", icon: FaFileAlt },
+ ];
+
+ const isActive = (path: string) => location.pathname === path;
+
return (
-
- {/* Бургер — только на мобильных */}
- {isMobile && (
-
-
-
- )}
-
- {/* Навигация */}
-
-
- {navItems
- .filter((item) => {
- if ((item as any).adminOnly && !user?.permission_admin)
- return false;
- return true;
- })
- .map((item) => {
- const Icon = item.icon;
- const active = isActive(item.path);
- return (
- navigate(item.path)}
- className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all flex-shrink-0"
- style={{
- backgroundColor: active ? "var(--accent)" : "transparent",
- color: active
- ? "var(--accent-text)"
- : "var(--text-secondary)",
- }}
- onMouseEnter={(e) => {
- if (!active) {
- e.currentTarget.style.backgroundColor =
- "var(--bg-secondary)";
- e.currentTarget.style.color = "var(--text-primary)";
- }
- }}
- onMouseLeave={(e) => {
- if (!active) {
- e.currentTarget.style.backgroundColor = "transparent";
- e.currentTarget.style.color = "var(--text-secondary)";
- }
- }}
- >
-
- {item.label}
-
- );
- })}
-
-
-
- {/* Профиль пользователя — дропдаун */}
-
-
setDropdownOpen(!dropdownOpen)}
- className="flex items-center gap-2 px-3 py-1.5 rounded-lg transition-all"
- style={{
- backgroundColor: dropdownOpen
- ? "var(--bg-secondary)"
- : "transparent",
- border: "1px solid var(--border)",
- }}
- >
-
-
-
-
- {user?.name || user?.login || "Пользователь"}
-
-
-
-
- {/* Dropdown menu */}
- {dropdownOpen && (
-
- {/* User info */}
-
-
-
-
-
-
-
- {user?.name || user?.login}
-
-
- {user?.login}
-
-
-
-
-
- {/* Theme selector */}
-
-
setThemePickerOpen(!themePickerOpen)}
- className="w-full flex items-center gap-3 px-4 py-2.5 text-xs transition-colors"
- style={{ color: "var(--text-primary)" }}
- onMouseEnter={(e) => {
- e.currentTarget.style.backgroundColor =
- "var(--bg-secondary)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.backgroundColor = "transparent";
- }}
- >
-
-
- Тема: {themes.find((t) => t.id === currentTheme)?.name}
-
-
-
-
- {/* Theme sub-dropdown */}
- {themePickerOpen && (
-
- {themes.map((t) => (
-
handleThemeChange(t.id)}
- className="w-full flex items-center gap-3 px-4 py-2 text-xs transition-colors first:rounded-t-lg last:rounded-b-lg"
- style={{
- color:
- currentTheme === t.id
- ? "var(--accent)"
- : "var(--text-primary)",
- backgroundColor:
- currentTheme === t.id
- ? "var(--bg-secondary)"
- : "transparent",
- }}
- onMouseEnter={(e) => {
- if (currentTheme !== t.id) {
- e.currentTarget.style.backgroundColor =
- "var(--bg-secondary)";
- }
- }}
- onMouseLeave={(e) => {
- if (currentTheme !== t.id) {
- e.currentTarget.style.backgroundColor =
- "transparent";
- }
- }}
- >
-
- {t.name}
-
- ))}
-
- )}
-
-
- {/* Admin (if admin) */}
- {user?.permission_admin && (
-
{
- setDropdownOpen(false);
- navigate("/admin");
- }}
- className="w-full flex items-center gap-3 px-4 py-2.5 text-xs transition-colors"
- style={{ color: "var(--text-primary)" }}
- onMouseEnter={(e) => {
- e.currentTarget.style.backgroundColor =
- "var(--bg-secondary)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.backgroundColor = "transparent";
- }}
- >
-
- Админка
-
- )}
-
- {/* Divider */}
-
-
- {/* Logout */}
+
+ {navItems
+ .filter((item) => {
+ if ((item as any).adminOnly && !user?.permission_admin)
+ return false;
+ return true;
+ })
+ .map((item) => {
+ const Icon = item.icon;
+ const active = isActive(item.path);
+ return (
{
- e.currentTarget.style.backgroundColor =
- "rgba(239, 68, 68, 0.1)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.backgroundColor = "transparent";
+ key={item.path}
+ onClick={() => navigate(item.path)}
+ className="flex items-center justify-center p-3 rounded-lg transition-all"
+ style={{
+ backgroundColor: active ? "var(--accent)" : "transparent",
+ color: active
+ ? "var(--accent-text)"
+ : "var(--text-secondary)",
}}
+ title={item.label}
>
-
- Выйти
+
-
- )}
-
+ );
+ })}
);