@@ -1,5 +1,6 @@
|
|||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { useNavigate, useLocation } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
import { FaCode } from "react-icons/fa";
|
import { FaCode, FaChevronDown } from "react-icons/fa";
|
||||||
import {
|
import {
|
||||||
FaHome,
|
FaHome,
|
||||||
FaServer,
|
FaServer,
|
||||||
@@ -8,32 +9,63 @@ import {
|
|||||||
FaRocket,
|
FaRocket,
|
||||||
FaKey,
|
FaKey,
|
||||||
FaFileAlt,
|
FaFileAlt,
|
||||||
FaSun,
|
FaPalette,
|
||||||
FaMoon,
|
FaSignOutAlt,
|
||||||
|
FaShieldAlt,
|
||||||
} from "react-icons/fa";
|
} from "react-icons/fa";
|
||||||
import { useAuthStore } from "@/modules/auth/store/useAuthStore";
|
import { useAuthStore } from "@/modules/auth/store/useAuthStore";
|
||||||
import { useThemeStore } from "@/modules/theme-bw/stores/theme.store";
|
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 = () => {
|
export const Navigation = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { user, logout } = useAuthStore();
|
const { user, logout } = useAuthStore();
|
||||||
const { toggleTheme, theme } = useThemeStore();
|
const { setTheme } = useThemeStore();
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
const isDark = theme === "dark";
|
const [themePickerOpen, setThemePickerOpen] = useState(false);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const currentTheme = getCurrentTheme();
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ path: "/", label: "Главная", icon: FaHome },
|
|
||||||
{ path: "/add-agents", label: "Агенты", icon: FaServer },
|
|
||||||
{ path: "/templates", label: "Шаблоны", icon: FaCode },
|
{ path: "/templates", label: "Шаблоны", icon: FaCode },
|
||||||
{ path: "/add-agents", label: "Деплой", icon: FaRocket },
|
{ path: "/add-agents", label: "Деплой", icon: FaRocket },
|
||||||
{ path: "/registration", label: "Регистрация", icon: FaKey },
|
{ path: "/registration", label: "Регистрация", icon: FaKey },
|
||||||
{ path: "/logs", label: "Логи", icon: FaFileAlt },
|
{ path: "/logs", label: "Логи", icon: FaFileAlt },
|
||||||
{ path: "/admin", label: "Админка", icon: FaUsers, adminOnly: true },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const isActive = (path: string) => location.pathname === path;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex-shrink-0 border-b"
|
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 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 flex-1 mx-4 overflow-x-auto scrollbar-hide">
|
||||||
<div className="flex items-center gap-1 whitespace-nowrap">
|
<div className="flex items-center gap-1 whitespace-nowrap">
|
||||||
{navItems
|
{navItems
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
if (item.adminOnly && !user?.permission_admin) return false;
|
if ((item as any).adminOnly && !user?.permission_admin)
|
||||||
|
return false;
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
@@ -87,81 +120,210 @@ export const Navigation = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Профиль пользователя */}
|
{/* Профиль пользователя — дропдаун */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="relative" ref={dropdownRef}>
|
||||||
{user && (
|
<button
|
||||||
<div className="flex items-center gap-2">
|
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
|
<div
|
||||||
className="w-8 h-8 rounded-full flex items-center justify-center"
|
className="px-4 py-3 border-b"
|
||||||
style={{ backgroundColor: "var(--bg-secondary)" }}
|
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>
|
</div>
|
||||||
<span
|
|
||||||
className="text-xs"
|
{/* Theme selector */}
|
||||||
style={{ color: "var(--text-secondary)" }}
|
<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}
|
<FaSignOutAlt size={12} />
|
||||||
</span>
|
<span>Выйти</span>
|
||||||
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user