From 57b43da2e31896f0204bfeb7d9eef80172986287 Mon Sep 17 00:00:00 2001
From: NikitaTorbenko <2015nekitciti@gmail.com>
Date: Sat, 4 Apr 2026 03:07:45 +0300
Subject: [PATCH] feat: add layout
---
frontend/src/app/providers/layout/layout.tsx | 31 ++
.../layout/navigation/navigation.tsx | 116 +++++++
.../app/providers/layout/sidebar/sidebar.tsx | 294 ++++++++++++++++++
.../app/providers/layout/store/agent.store.ts | 34 ++
.../src/app/providers/routing/routing.tsx | 11 +-
.../src/modules/agent/types/agent.types.ts | 9 +-
frontend/src/shared/layouts/DefaultLayout.tsx | 93 +-----
7 files changed, 499 insertions(+), 89 deletions(-)
create mode 100644 frontend/src/app/providers/layout/layout.tsx
create mode 100644 frontend/src/app/providers/layout/navigation/navigation.tsx
create mode 100644 frontend/src/app/providers/layout/sidebar/sidebar.tsx
create mode 100644 frontend/src/app/providers/layout/store/agent.store.ts
diff --git a/frontend/src/app/providers/layout/layout.tsx b/frontend/src/app/providers/layout/layout.tsx
new file mode 100644
index 0000000..4210ac4
--- /dev/null
+++ b/frontend/src/app/providers/layout/layout.tsx
@@ -0,0 +1,31 @@
+import { useState, useEffect, type ReactNode } from "react";
+import { Sidebar } from "@/app/providers/layout/sidebar/sidebar";
+import { Navigation } from "@/app/providers/layout/navigation/navigation";
+import { useAgentStore } from "@/app/providers/layout/store/agent.store";
+
+export const Layout = ({ children }: { children: ReactNode }) => {
+ const [isOpen, setOpen] = useState(true);
+ const { fetchAgents } = useAgentStore();
+
+ useEffect(() => {
+ fetchAgents();
+ }, [fetchAgents]);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ fetchAgents();
+ }, 30000);
+
+ return () => clearInterval(interval);
+ }, [fetchAgents]);
+
+ return (
+
+
setOpen(!isOpen)} />
+
+
+ );
+};
diff --git a/frontend/src/app/providers/layout/navigation/navigation.tsx b/frontend/src/app/providers/layout/navigation/navigation.tsx
new file mode 100644
index 0000000..3c7a3be
--- /dev/null
+++ b/frontend/src/app/providers/layout/navigation/navigation.tsx
@@ -0,0 +1,116 @@
+import { useNavigate, useLocation } from "react-router-dom";
+import { FaHome, FaServer, FaPalette, FaUser } from "react-icons/fa";
+import { useAuthStore } from "@/modules/auth/store/useAuthStore";
+
+export const Navigation = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { user, logout } = useAuthStore();
+
+ const navItems = [
+ { path: "/", label: "Главная", icon: FaHome },
+ { path: "/add-agents", label: "Агенты", icon: FaServer },
+ { path: "/themes", label: "Темы", icon: FaPalette },
+ ];
+
+ const isActive = (path: string) => location.pathname === path;
+
+ return (
+
+
+ {/* Логотип */}
+
navigate("/")}
+ >
+
+
+ HellreigN
+
+
+
+ {/* Навигация */}
+
+ {navItems.map((item) => {
+ const Icon = item.icon;
+ const active = isActive(item.path);
+ return (
+
+ );
+ })}
+
+
+ {/* Профиль пользователя */}
+
+ {user && (
+
+
+
+
+
+ {user.name}
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/frontend/src/app/providers/layout/sidebar/sidebar.tsx b/frontend/src/app/providers/layout/sidebar/sidebar.tsx
new file mode 100644
index 0000000..5f20adf
--- /dev/null
+++ b/frontend/src/app/providers/layout/sidebar/sidebar.tsx
@@ -0,0 +1,294 @@
+import React, { useMemo, useState } from "react";
+import { FaBars, FaMicrochip, FaTimes, FaSpinner, FaCopy, FaCheck } from "react-icons/fa";
+import { useAgentStore } from "@/app/providers/layout/store/agent.store";
+import { useAuthStore } from "@/modules/auth/store/useAuthStore";
+
+interface SidebarProps {
+ isOpen?: boolean;
+ onToggle?: () => void;
+}
+
+export const Sidebar: React.FC = ({ isOpen = true, onToggle }) => {
+ const { agents, isLoading, error, fetchAgents } = useAgentStore();
+ const { token } = useAuthStore();
+ const [searchQuery, setSearchQuery] = useState("");
+ const [copied, setCopied] = useState(false);
+ const [showTokenModal, setShowTokenModal] = useState(false);
+
+ const filteredAgents = useMemo(() => {
+ if (!searchQuery) return agents;
+ const query = searchQuery.toLowerCase();
+ return agents.filter(
+ (agent) =>
+ agent.name.toLowerCase().includes(query) ||
+ agent.services.some((s) => s.name.toLowerCase().includes(query))
+ );
+ }, [agents, searchQuery]);
+
+ const handleCopyToken = () => {
+ if (token) {
+ navigator.clipboard.writeText(token);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ };
+
+ if (!isOpen) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+ {/* Overlay для мобильных */}
+
+
+
+
+ {/* Modal токена */}
+ {showTokenModal && (
+ setShowTokenModal(false)}
+ >
+
e.stopPropagation()}
+ >
+
+
+
+
+ Ваш токен доступа
+
+
+
+
+
+
+
+
+
+
+ {token || "Токен не найден"}
+
+ {token && (
+
+ )}
+
+
+
+
+
+
+
+ )}
+ >
+ );
+};
diff --git a/frontend/src/app/providers/layout/store/agent.store.ts b/frontend/src/app/providers/layout/store/agent.store.ts
new file mode 100644
index 0000000..33f4352
--- /dev/null
+++ b/frontend/src/app/providers/layout/store/agent.store.ts
@@ -0,0 +1,34 @@
+import { create } from "zustand";
+import { agentApiService } from "@/modules/agent/api/agent.api.service";
+import type { AgentInfo } from "@/modules/agent/types/agent.types";
+
+interface AgentState {
+ agents: AgentInfo[];
+ isLoading: boolean;
+ error: string | null;
+ fetchAgents: () => Promise;
+ removeAgent: (name: string) => void;
+}
+
+export const useAgentStore = create()((set, get) => ({
+ agents: [],
+ isLoading: false,
+ error: null,
+
+ fetchAgents: async () => {
+ set({ isLoading: true, error: null });
+ try {
+ const agents = await agentApiService.getAgents();
+ set({ agents, isLoading: false });
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : "Failed to fetch agents",
+ isLoading: false,
+ });
+ }
+ },
+
+ removeAgent: (name: string) => {
+ set({ agents: get().agents.filter((a) => a.name !== name) });
+ },
+}));
diff --git a/frontend/src/app/providers/routing/routing.tsx b/frontend/src/app/providers/routing/routing.tsx
index af4b471..7d8d4f8 100644
--- a/frontend/src/app/providers/routing/routing.tsx
+++ b/frontend/src/app/providers/routing/routing.tsx
@@ -17,15 +17,18 @@ export const Routing = () => {
}
>
+ {/* Публичные маршруты */}
+ } />
+ } />
+
+ {/* Защищённые маршруты с Layout */}
}>
} />
- } />
- } />
} />
} />
-
- } />
+
+ } />
);
diff --git a/frontend/src/modules/agent/types/agent.types.ts b/frontend/src/modules/agent/types/agent.types.ts
index ce0aa51..7d0e342 100644
--- a/frontend/src/modules/agent/types/agent.types.ts
+++ b/frontend/src/modules/agent/types/agent.types.ts
@@ -1,6 +1,11 @@
+export interface AgentService {
+ name: string;
+ status: string;
+}
+
export interface AgentInfo {
- label: string;
- services: string[];
+ name: string;
+ services: AgentService[];
token: string;
}
diff --git a/frontend/src/shared/layouts/DefaultLayout.tsx b/frontend/src/shared/layouts/DefaultLayout.tsx
index 3a3e439..9d21dbe 100644
--- a/frontend/src/shared/layouts/DefaultLayout.tsx
+++ b/frontend/src/shared/layouts/DefaultLayout.tsx
@@ -1,90 +1,17 @@
import { useAuthStore } from "@/modules/auth/store/useAuthStore";
-import { ThemeToggle } from "@/modules/theme-bw/ui/ThemeToggle";
-import React from "react";
-import { Outlet, useNavigate } from "react-router-dom";
+import { Navigate, Outlet } from "react-router-dom";
+import { Layout } from "@/app/providers/layout/layout";
-interface DefaultLayoutProps {
- children?: React.ReactNode;
-}
+export const DefaultLayout = () => {
+ const { token } = useAuthStore();
-export const DefaultLayout: React.FC = ({ children }) => {
- const { user, logout } = useAuthStore();
- const navigate = useNavigate();
-
- const handleLogout = () => {
- logout();
- navigate("/auth");
- };
+ if (!token) {
+ return ;
+ }
return (
-
- {/* Header */}
-
-
-
- {/* Logo */}
-
navigate("/")}
- >
- HellreigN
-
-
- {/* Right side */}
-
-
- {user && (
-
-
- {user.firstName} {user.lastName}
-
-
-
- )}
-
-
-
-
-
- {/* Main content */}
-
{children || }
-
- {/* Footer */}
-
-
+
+
+
);
};