@@ -1,5 +1,6 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { FaCode } from "react-icons/fa";
|
||||
import { FaCode, FaChevronDown } from "react-icons/fa";
|
||||
import {
|
||||
FaHome,
|
||||
FaServer,
|
||||
@@ -8,32 +9,63 @@ import {
|
||||
FaRocket,
|
||||
FaKey,
|
||||
FaFileAlt,
|
||||
FaSun,
|
||||
FaMoon,
|
||||
FaPalette,
|
||||
FaSignOutAlt,
|
||||
FaShieldAlt,
|
||||
} from "react-icons/fa";
|
||||
import { useAuthStore } from "@/modules/auth/store/useAuthStore";
|
||||
import { useThemeStore } from "@/modules/theme-bw/stores/theme.store";
|
||||
import { themes } from "@/modules/theme-changer/config/theme.config";
|
||||
import {
|
||||
applyTheme,
|
||||
getCurrentTheme,
|
||||
} from "@/modules/theme-changer/utils/apply.theme";
|
||||
|
||||
export const Navigation = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user, logout } = useAuthStore();
|
||||
const { toggleTheme, theme } = useThemeStore();
|
||||
|
||||
const isDark = theme === "dark";
|
||||
const { setTheme } = useThemeStore();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [themePickerOpen, setThemePickerOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const currentTheme = getCurrentTheme();
|
||||
|
||||
const navItems = [
|
||||
{ path: "/", label: "Главная", icon: FaHome },
|
||||
{ path: "/add-agents", label: "Агенты", icon: FaServer },
|
||||
{ path: "/templates", label: "Шаблоны", icon: FaCode },
|
||||
{ path: "/add-agents", label: "Деплой", icon: FaRocket },
|
||||
{ path: "/registration", label: "Регистрация", icon: FaKey },
|
||||
{ path: "/logs", label: "Логи", icon: FaFileAlt },
|
||||
{ path: "/admin", label: "Админка", icon: FaUsers, adminOnly: true },
|
||||
];
|
||||
|
||||
const isActive = (path: string) => location.pathname === path;
|
||||
|
||||
// Закрытие дропдауна при клике вне
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setDropdownOpen(false);
|
||||
setThemePickerOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate("/auth");
|
||||
};
|
||||
|
||||
const handleThemeChange = (themeId: string) => {
|
||||
applyTheme(themeId);
|
||||
setTheme(themeId as any);
|
||||
setThemePickerOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-shrink-0 border-b"
|
||||
@@ -43,12 +75,13 @@ export const Navigation = () => {
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-2.5">
|
||||
{/* Навигация с горизонтальным скроллом */}
|
||||
{/* Навигация */}
|
||||
<div className="flex items-center flex-1 mx-4 overflow-x-auto scrollbar-hide">
|
||||
<div className="flex items-center gap-1 whitespace-nowrap">
|
||||
{navItems
|
||||
.filter((item) => {
|
||||
if (item.adminOnly && !user?.permission_admin) return false;
|
||||
if ((item as any).adminOnly && !user?.permission_admin)
|
||||
return false;
|
||||
return true;
|
||||
})
|
||||
.map((item) => {
|
||||
@@ -87,81 +120,210 @@ export const Navigation = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Профиль пользователя */}
|
||||
<div className="flex items-center gap-2">
|
||||
{user && (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Профиль пользователя — дропдаун */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => 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)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-7 h-7 rounded-full flex items-center justify-center"
|
||||
style={{ backgroundColor: "var(--accent)" }}
|
||||
>
|
||||
<FaUser size={11} style={{ color: "var(--accent-text)" }} />
|
||||
</div>
|
||||
<span
|
||||
className="text-xs font-medium"
|
||||
style={{ color: "var(--text-primary)" }}
|
||||
>
|
||||
{user?.name || user?.login || "Пользователь"}
|
||||
</span>
|
||||
<FaChevronDown
|
||||
size={10}
|
||||
style={{
|
||||
color: "var(--text-secondary)",
|
||||
transform: dropdownOpen ? "rotate(180deg)" : "rotate(0)",
|
||||
transition: "transform 0.2s",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Dropdown menu */}
|
||||
{dropdownOpen && (
|
||||
<div
|
||||
className="absolute right-0 top-full mt-2 rounded-lg shadow-xl border z-50"
|
||||
style={{
|
||||
backgroundColor: "var(--card-bg)",
|
||||
borderColor: "var(--border)",
|
||||
minWidth: "220px",
|
||||
}}
|
||||
>
|
||||
{/* User info */}
|
||||
<div
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center"
|
||||
style={{ backgroundColor: "var(--bg-secondary)" }}
|
||||
className="px-4 py-3 border-b"
|
||||
style={{ borderColor: "var(--border)" }}
|
||||
>
|
||||
<FaUser size={12} style={{ color: "var(--accent)" }} />
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center"
|
||||
style={{ backgroundColor: "var(--accent)" }}
|
||||
>
|
||||
<FaUser size={12} style={{ color: "var(--accent-text)" }} />
|
||||
</div>
|
||||
<div>
|
||||
<p
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: "var(--text-primary)" }}
|
||||
>
|
||||
{user?.name || user?.login}
|
||||
</p>
|
||||
<p
|
||||
className="text-[10px]"
|
||||
style={{ color: "var(--text-muted)" }}
|
||||
>
|
||||
{user?.login}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: "var(--text-secondary)" }}
|
||||
|
||||
{/* Theme selector */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => 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";
|
||||
}}
|
||||
>
|
||||
<FaPalette
|
||||
size={12}
|
||||
style={{ color: "var(--text-secondary)" }}
|
||||
/>
|
||||
<span className="flex-1 text-left">
|
||||
Тема: {themes.find((t) => t.id === currentTheme)?.name}
|
||||
</span>
|
||||
<FaChevronDown
|
||||
size={9}
|
||||
style={{
|
||||
color: "var(--text-muted)",
|
||||
transform: themePickerOpen
|
||||
? "rotate(180deg)"
|
||||
: "rotate(0)",
|
||||
transition: "transform 0.2s",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Theme sub-dropdown */}
|
||||
{themePickerOpen && (
|
||||
<div
|
||||
className="absolute right-full top-0 mr-1 rounded-lg shadow-xl border z-50"
|
||||
style={{
|
||||
backgroundColor: "var(--card-bg)",
|
||||
borderColor: "var(--border)",
|
||||
minWidth: "180px",
|
||||
}}
|
||||
>
|
||||
{themes.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => 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";
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-4 h-4 rounded-full border"
|
||||
style={{
|
||||
backgroundColor: t.colors.primary,
|
||||
borderColor: "var(--border)",
|
||||
}}
|
||||
/>
|
||||
<span>{t.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Admin (if admin) */}
|
||||
{user?.permission_admin && (
|
||||
<button
|
||||
onClick={() => {
|
||||
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";
|
||||
}}
|
||||
>
|
||||
<FaShieldAlt size={12} style={{ color: "#f59e0b" }} />
|
||||
<span>Админка</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Divider */}
|
||||
<div
|
||||
className="my-1 border-b"
|
||||
style={{ borderColor: "var(--border)" }}
|
||||
/>
|
||||
|
||||
{/* Logout */}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 text-xs transition-colors rounded-b-lg"
|
||||
style={{ color: "var(--error-text)" }}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor =
|
||||
"rgba(239, 68, 68, 0.1)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
}}
|
||||
>
|
||||
{user.name}
|
||||
</span>
|
||||
<FaSignOutAlt size={12} />
|
||||
<span>Выйти</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Переключатель темы */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-1.5 rounded-lg transition-colors"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-secondary)",
|
||||
color: isDark ? "#fbbf24" : "#3b82f6",
|
||||
border: "1px solid var(--border)",
|
||||
}}
|
||||
title={isDark ? "Светлая тема" : "Тёмная тема"}
|
||||
>
|
||||
{isDark ? <FaSun size={12} /> : <FaMoon size={12} />}
|
||||
</button>
|
||||
|
||||
{/* Админка */}
|
||||
<button
|
||||
onClick={() => navigate("/admin")}
|
||||
className="p-1.5 rounded-lg transition-colors"
|
||||
style={{
|
||||
backgroundColor:
|
||||
location.pathname === "/admin"
|
||||
? "var(--accent)"
|
||||
: "var(--bg-secondary)",
|
||||
color:
|
||||
location.pathname === "/admin"
|
||||
? "var(--accent-text)"
|
||||
: "var(--text-secondary)",
|
||||
border: "1px solid var(--border)",
|
||||
}}
|
||||
title="Админка"
|
||||
>
|
||||
<FaUsers size={12} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
logout();
|
||||
navigate("/auth");
|
||||
}}
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
|
||||
style={{
|
||||
backgroundColor: "var(--error-bg)",
|
||||
color: "var(--error-text)",
|
||||
border: "1px solid var(--error-border)",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "var(--error-text)";
|
||||
e.currentTarget.style.color = "#fff";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "var(--error-bg)";
|
||||
e.currentTarget.style.color = "var(--error-text)";
|
||||
}}
|
||||
>
|
||||
Выйти
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user