Compare commits
2 Commits
9f6defd25c
...
c175461634
| Author | SHA1 | Date | |
|---|---|---|---|
| c175461634 | |||
| 5b90447984 |
@@ -1,6 +1,9 @@
|
|||||||
import { useState, useEffect, type ReactNode } from "react";
|
import { useState, useEffect, type ReactNode } from "react";
|
||||||
import { Sidebar } from "@/app/providers/layout/sidebar/sidebar";
|
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";
|
import { useAgentStore } from "@/app/providers/layout/store/agent.store";
|
||||||
|
|
||||||
export const Layout = ({ children }: { children: ReactNode }) => {
|
export const Layout = ({ children }: { children: ReactNode }) => {
|
||||||
@@ -8,6 +11,9 @@ export const Layout = ({ children }: { children: ReactNode }) => {
|
|||||||
const [isMobile, setIsMobile] = useState(() =>
|
const [isMobile, setIsMobile] = useState(() =>
|
||||||
typeof window !== "undefined" ? window.innerWidth < 856 : false,
|
typeof window !== "undefined" ? window.innerWidth < 856 : false,
|
||||||
);
|
);
|
||||||
|
const [isVerySmall, setIsVerySmall] = useState(() =>
|
||||||
|
typeof window !== "undefined" ? window.innerWidth < 600 : false,
|
||||||
|
);
|
||||||
const { fetchAgents } = useAgentStore();
|
const { fetchAgents } = useAgentStore();
|
||||||
|
|
||||||
const sidebarOpen = isMobile ? mobileOpen : true;
|
const sidebarOpen = isMobile ? mobileOpen : true;
|
||||||
@@ -19,6 +25,7 @@ export const Layout = ({ children }: { children: ReactNode }) => {
|
|||||||
if (!mobile) {
|
if (!mobile) {
|
||||||
setMobileOpen(false);
|
setMobileOpen(false);
|
||||||
}
|
}
|
||||||
|
setIsVerySmall(window.innerWidth < 600);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener("resize", handleResize);
|
||||||
@@ -55,8 +62,13 @@ export const Layout = ({ children }: { children: ReactNode }) => {
|
|||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||||
<Navigation onToggleSidebar={toggleSidebar} isMobile={isMobile} />
|
<Navigation
|
||||||
|
onToggleSidebar={toggleSidebar}
|
||||||
|
isMobile={isMobile}
|
||||||
|
isVerySmall={isVerySmall}
|
||||||
|
/>
|
||||||
<div className="flex-1 overflow-auto p-4">{children}</div>
|
<div className="flex-1 overflow-auto p-4">{children}</div>
|
||||||
|
{isVerySmall && <BottomNav />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,11 +24,13 @@ import {
|
|||||||
interface NavigationProps {
|
interface NavigationProps {
|
||||||
onToggleSidebar?: () => void;
|
onToggleSidebar?: () => void;
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
|
isVerySmall?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Navigation: React.FC<NavigationProps> = ({
|
export const Navigation: React.FC<NavigationProps> = ({
|
||||||
onToggleSidebar,
|
onToggleSidebar,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
isVerySmall = false,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -48,7 +50,6 @@ export const Navigation: React.FC<NavigationProps> = ({
|
|||||||
|
|
||||||
const isActive = (path: string) => location.pathname === path;
|
const isActive = (path: string) => location.pathname === path;
|
||||||
|
|
||||||
// Закрытие дропдауна при клике вне
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
if (
|
if (
|
||||||
@@ -74,7 +75,50 @@ export const Navigation: React.FC<NavigationProps> = ({
|
|||||||
setThemePickerOpen(false);
|
setThemePickerOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderNavItems = (showLabels: boolean, iconSize: number) => (
|
||||||
|
<div className="flex items-center gap-1 whitespace-nowrap">
|
||||||
|
{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 (
|
return (
|
||||||
|
<button
|
||||||
|
key={item.path}
|
||||||
|
onClick={() => 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}
|
||||||
|
>
|
||||||
|
<Icon size={iconSize} />
|
||||||
|
{showLabels && <span className="text-xs">{item.label}</span>}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Верхний бар */}
|
||||||
<div
|
<div
|
||||||
className="flex-shrink-0 border-b"
|
className="flex-shrink-0 border-b"
|
||||||
style={{
|
style={{
|
||||||
@@ -99,50 +143,24 @@ export const Navigation: React.FC<NavigationProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Навигация */}
|
{/* Название по центру — только на очень маленьких экранах */}
|
||||||
<div className="flex items-center flex-1 mx-4 overflow-x-auto scrollbar-hide">
|
{isVerySmall && (
|
||||||
<div className="flex items-center gap-1 whitespace-nowrap">
|
<div className="flex-1 text-center mx-4">
|
||||||
{navItems
|
<span
|
||||||
.filter((item) => {
|
className="text-sm font-bold"
|
||||||
if ((item as any).adminOnly && !user?.permission_admin)
|
style={{ color: "var(--text-primary)" }}
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.map((item) => {
|
|
||||||
const Icon = item.icon;
|
|
||||||
const active = isActive(item.path);
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={item.path}
|
|
||||||
onClick={() => 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)";
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Icon size={12} />
|
HellreigN
|
||||||
<span>{item.label}</span>
|
</span>
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Навигация — только если НЕ очень маленький экран */}
|
||||||
|
{!isVerySmall && (
|
||||||
|
<div className="flex items-center flex-1 mx-4 overflow-x-auto scrollbar-hide">
|
||||||
|
{renderNavItems(true, 12)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Профиль пользователя — дропдаун */}
|
{/* Профиль пользователя — дропдаун */}
|
||||||
<div className="relative" ref={dropdownRef}>
|
<div className="relative" ref={dropdownRef}>
|
||||||
@@ -178,7 +196,6 @@ export const Navigation: React.FC<NavigationProps> = ({
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Dropdown menu */}
|
|
||||||
{dropdownOpen && (
|
{dropdownOpen && (
|
||||||
<div
|
<div
|
||||||
className="absolute right-0 top-full mt-2 rounded-lg shadow-xl border z-50"
|
className="absolute right-0 top-full mt-2 rounded-lg shadow-xl border z-50"
|
||||||
@@ -188,7 +205,6 @@ export const Navigation: React.FC<NavigationProps> = ({
|
|||||||
minWidth: "220px",
|
minWidth: "220px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* User info */}
|
|
||||||
<div
|
<div
|
||||||
className="px-4 py-3 border-b"
|
className="px-4 py-3 border-b"
|
||||||
style={{ borderColor: "var(--border)" }}
|
style={{ borderColor: "var(--border)" }}
|
||||||
@@ -198,7 +214,10 @@ export const Navigation: React.FC<NavigationProps> = ({
|
|||||||
className="w-8 h-8 rounded-full flex items-center justify-center"
|
className="w-8 h-8 rounded-full flex items-center justify-center"
|
||||||
style={{ backgroundColor: "var(--accent)" }}
|
style={{ backgroundColor: "var(--accent)" }}
|
||||||
>
|
>
|
||||||
<FaUser size={12} style={{ color: "var(--accent-text)" }} />
|
<FaUser
|
||||||
|
size={12}
|
||||||
|
style={{ color: "var(--accent-text)" }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
@@ -217,7 +236,6 @@ export const Navigation: React.FC<NavigationProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Theme selector */}
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => setThemePickerOpen(!themePickerOpen)}
|
onClick={() => setThemePickerOpen(!themePickerOpen)}
|
||||||
@@ -250,7 +268,6 @@ export const Navigation: React.FC<NavigationProps> = ({
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Theme sub-dropdown */}
|
|
||||||
{themePickerOpen && (
|
{themePickerOpen && (
|
||||||
<div
|
<div
|
||||||
className="absolute right-full top-0 mr-1 rounded-lg shadow-xl border z-50"
|
className="absolute right-full top-0 mr-1 rounded-lg shadow-xl border z-50"
|
||||||
@@ -302,7 +319,6 @@ export const Navigation: React.FC<NavigationProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Admin (if admin) */}
|
|
||||||
{user?.permission_admin && (
|
{user?.permission_admin && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -324,13 +340,11 @@ export const Navigation: React.FC<NavigationProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div
|
<div
|
||||||
className="my-1 border-b"
|
className="my-1 border-b"
|
||||||
style={{ borderColor: "var(--border)" }}
|
style={{ borderColor: "var(--border)" }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Logout */}
|
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="w-full flex items-center gap-3 px-4 py-2.5 text-xs transition-colors rounded-b-lg"
|
className="w-full flex items-center gap-3 px-4 py-2.5 text-xs transition-colors rounded-b-lg"
|
||||||
@@ -351,5 +365,60 @@ export const Navigation: React.FC<NavigationProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className="flex-shrink-0 border-t"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--card-bg)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-around px-2 py-2">
|
||||||
|
{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 (
|
||||||
|
<button
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
<Icon size={20} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export const Routing = () => {
|
|||||||
<Route path="/register" element={<RegisterPage />} />
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
|
|
||||||
<Route element={<DefaultLayout />}>
|
<Route element={<DefaultLayout />}>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<TemplatesPage />} />
|
||||||
<Route path="/add-agents" element={<AddAgentsPage />} />
|
<Route path="/add-agents" element={<AddAgentsPage />} />
|
||||||
<Route path="/registration" element={<RegistrationTokenPage />} />
|
<Route path="/registration" element={<RegistrationTokenPage />} />
|
||||||
<Route path="/logs" element={<LogsPage />} />
|
<Route path="/logs" element={<LogsPage />} />
|
||||||
@@ -130,7 +130,7 @@ export const Routing = () => {
|
|||||||
<Route path="/IDE" element={<IDEPage />} />
|
<Route path="/IDE" element={<IDEPage />} />
|
||||||
<Route path="/templates" element={<TemplatesPage />} />
|
<Route path="/templates" element={<TemplatesPage />} />
|
||||||
<Route path="/graphs" element={<GraphsPage />} />
|
<Route path="/graphs" element={<GraphsPage />} />
|
||||||
<Route path="/dashboard" element={<DashboardPage />} />
|
{/* <Route path="/dashboard" element={<DashboardPage />} /> */}
|
||||||
<Route
|
<Route
|
||||||
path="/dashboard/:agentLabel"
|
path="/dashboard/:agentLabel"
|
||||||
element={<AgentDashboardPage />}
|
element={<AgentDashboardPage />}
|
||||||
|
|||||||
Reference in New Issue
Block a user