From adbb0ee368d7e76cbec496a84c36408aebbd9234 Mon Sep 17 00:00:00 2001 From: nikita Date: Sat, 4 Apr 2026 06:05:51 +0300 Subject: [PATCH] feat: page tempaltes --- .../layout/navigation/navigation.tsx | 15 +- .../src/app/providers/routing/routing.tsx | 2 + .../src/modules/ide/components/FilePicker.tsx | 65 ++++++ .../modules/ide/components/FilePickerItem.tsx | 156 +++++++++++++++ frontend/src/modules/ide/components/index.ts | 2 + frontend/src/modules/ide/index.ts | 2 + .../modules/ide/store/useFilePickerStore.ts | 57 ++++++ frontend/src/pages/ide.page.tsx | 11 +- frontend/src/pages/templates.page.tsx | 189 ++++++++++++++++++ 9 files changed, 490 insertions(+), 9 deletions(-) create mode 100644 frontend/src/modules/ide/components/FilePicker.tsx create mode 100644 frontend/src/modules/ide/components/FilePickerItem.tsx create mode 100644 frontend/src/modules/ide/store/useFilePickerStore.ts create mode 100644 frontend/src/pages/templates.page.tsx diff --git a/frontend/src/app/providers/layout/navigation/navigation.tsx b/frontend/src/app/providers/layout/navigation/navigation.tsx index 3c7a3be..7cd745e 100644 --- a/frontend/src/app/providers/layout/navigation/navigation.tsx +++ b/frontend/src/app/providers/layout/navigation/navigation.tsx @@ -1,5 +1,5 @@ import { useNavigate, useLocation } from "react-router-dom"; -import { FaHome, FaServer, FaPalette, FaUser } from "react-icons/fa"; +import { FaHome, FaServer, FaPalette, FaUser, FaCode } from "react-icons/fa"; import { useAuthStore } from "@/modules/auth/store/useAuthStore"; export const Navigation = () => { @@ -10,6 +10,7 @@ export const Navigation = () => { const navItems = [ { path: "/", label: "Главная", icon: FaHome }, { path: "/add-agents", label: "Агенты", icon: FaServer }, + { path: "/templates", label: "Шаблоны", icon: FaCode }, { path: "/themes", label: "Темы", icon: FaPalette }, ]; @@ -50,11 +51,14 @@ export const Navigation = () => { className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all" style={{ backgroundColor: active ? "var(--accent)" : "transparent", - color: active ? "var(--accent-text)" : "var(--text-secondary)", + color: active + ? "var(--accent-text)" + : "var(--text-secondary)", }} onMouseEnter={(e) => { if (!active) { - e.currentTarget.style.backgroundColor = "var(--bg-secondary)"; + e.currentTarget.style.backgroundColor = + "var(--bg-secondary)"; e.currentTarget.style.color = "var(--text-primary)"; } }} @@ -82,7 +86,10 @@ export const Navigation = () => { > - + {user.name} diff --git a/frontend/src/app/providers/routing/routing.tsx b/frontend/src/app/providers/routing/routing.tsx index 131f5f6..05b6b27 100644 --- a/frontend/src/app/providers/routing/routing.tsx +++ b/frontend/src/app/providers/routing/routing.tsx @@ -9,6 +9,7 @@ import { RegisterPage } from "@/pages/register.page"; import { DefaultLayout } from "@/shared/layouts/DefaultLayout"; import { AddAgentsPage } from "@/pages/add-agents.page"; import { IDEPage } from "@/pages/ide.page"; +import { TemplatesPage } from "@/pages/templates.page"; export const mockGraphData: GraphData = { nodes: [ @@ -120,6 +121,7 @@ export const Routing = () => { } /> } /> } /> + } /> } /> diff --git a/frontend/src/modules/ide/components/FilePicker.tsx b/frontend/src/modules/ide/components/FilePicker.tsx new file mode 100644 index 0000000..e6441c9 --- /dev/null +++ b/frontend/src/modules/ide/components/FilePicker.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import type { FileNode } from "../types"; +import { FilePickerItem } from "./FilePickerItem"; +import { useFilePickerStore } from "../store/useFilePickerStore"; + +interface FilePickerProps { + files: FileNode; +} + +const FilePickerTree: React.FC<{ node: FileNode; level: number }> = ({ + node, + level, +}) => { + const expandedFolders = useFilePickerStore((s) => s.expandedFolders); + const selectedPaths = useFilePickerStore((s) => s.selectedPaths); + const toggleSelection = useFilePickerStore((s) => s.toggleSelection); + const toggleFolder = useFilePickerStore((s) => s.toggleFolder); + + const nodePath = node.path || node.name; + const isExpanded = expandedFolders.has(nodePath); + const isSelected = node.type === "file" && selectedPaths.has(nodePath); + + if (node.type === "file") { + return ( + + ); + } + + return ( + <> + + {node.children?.map((child, idx) => ( + + ))} + + + ); +}; + +export const FilePicker: React.FC = ({ files }) => { + return ( +
+ +
+ ); +}; diff --git a/frontend/src/modules/ide/components/FilePickerItem.tsx b/frontend/src/modules/ide/components/FilePickerItem.tsx new file mode 100644 index 0000000..39aeddd --- /dev/null +++ b/frontend/src/modules/ide/components/FilePickerItem.tsx @@ -0,0 +1,156 @@ +import React from "react"; +import { + FiChevronRight, + FiChevronDown, + FiFile, + FiFolder, +} from "react-icons/fi"; + +interface FilePickerItemProps { + name: string; + type: "file" | "folder"; + path: string; + isSelected?: boolean; + isExpanded?: boolean; + children?: React.ReactNode; + level: number; + onToggleSelect?: (path: string) => void; + onToggleFolder?: (path: string) => void; +} + +export const FilePickerItem: React.FC = ({ + name, + type, + path, + isSelected, + isExpanded, + children, + level, + onToggleSelect, + onToggleFolder, +}) => { + const isFolder = type === "folder"; + const extension = name.includes(".") + ? name.split(".").pop()?.toUpperCase() + : ""; + const paddingLeft = 12 + level * 20; + + return ( +
+
{ + if (isFolder && onToggleFolder) { + onToggleFolder(path); + } else if (!isFolder && onToggleSelect) { + onToggleSelect(path); + } + }} + onMouseEnter={(e) => { + e.currentTarget.style.backgroundColor = "#2a2a2a"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.backgroundColor = "transparent"; + }} + > + {/* Folder expand icon */} + {isFolder && ( + + {isExpanded ? ( + + ) : ( + + )} + + )} + + {/* File/Folder icon */} + + {isFolder ? ( + + ) : ( + + )} + + + {/* Name */} + + {name} + + + {/* Extension badge — только у файлов */} + {!isFolder && extension && ( + + {extension} + + )} + + {/* Checkbox — только у файлов */} + {!isFolder && onToggleSelect && ( +
{ + e.stopPropagation(); + onToggleSelect(path); + }} + > + {isSelected && ( + + + + )} +
+ )} +
+ + {/* Children */} + {isFolder && isExpanded && children} +
+ ); +}; diff --git a/frontend/src/modules/ide/components/index.ts b/frontend/src/modules/ide/components/index.ts index 95f10f9..7a2a8e9 100644 --- a/frontend/src/modules/ide/components/index.ts +++ b/frontend/src/modules/ide/components/index.ts @@ -6,3 +6,5 @@ export { TabBar } from "./TabBar"; export { CodeEditor } from "./CodeEditor"; export { TitleBar } from "./TitleBar"; export { StatusBar } from "./StatusBar"; +export { FilePickerItem } from "./FilePickerItem"; +export { FilePicker } from "./FilePicker"; diff --git a/frontend/src/modules/ide/index.ts b/frontend/src/modules/ide/index.ts index caeba36..294db1d 100644 --- a/frontend/src/modules/ide/index.ts +++ b/frontend/src/modules/ide/index.ts @@ -1,3 +1,5 @@ export { IDE } from "./IDE"; +export { FilePicker } from "./components/FilePicker"; export { useIDEStore, initialFiles } from "./store/useIDEStore"; +export { useFilePickerStore } from "./store/useFilePickerStore"; export type { FileNode } from "./types"; diff --git a/frontend/src/modules/ide/store/useFilePickerStore.ts b/frontend/src/modules/ide/store/useFilePickerStore.ts new file mode 100644 index 0000000..e6b9e74 --- /dev/null +++ b/frontend/src/modules/ide/store/useFilePickerStore.ts @@ -0,0 +1,57 @@ +import { create } from "zustand"; + +interface FilePickerState { + selectedPaths: Set; + expandedFolders: Set; + + toggleSelection: (path: string) => void; + selectAll: (paths: string[]) => void; + clearSelection: () => void; + toggleFolder: (path: string) => void; + getSelectedPaths: () => string[]; +} + +export const useFilePickerStore = create((set, get) => ({ + selectedPaths: new Set(), + expandedFolders: new Set(), + + toggleSelection: (path: string) => { + set((state) => { + const newSet = new Set(state.selectedPaths); + if (newSet.has(path)) { + newSet.delete(path); + } else { + newSet.add(path); + } + return { selectedPaths: newSet }; + }); + }, + + selectAll: (paths: string[]) => { + set((state) => { + const newSet = new Set(state.selectedPaths); + paths.forEach((p) => newSet.add(p)); + return { selectedPaths: newSet }; + }); + }, + + clearSelection: () => { + set({ selectedPaths: new Set() }); + }, + + toggleFolder: (path: string) => { + set((state) => { + const newSet = new Set(state.expandedFolders); + if (newSet.has(path)) { + newSet.delete(path); + } else { + newSet.add(path); + } + return { expandedFolders: newSet }; + }); + }, + + getSelectedPaths: () => { + return Array.from(get().selectedPaths); + }, +})); diff --git a/frontend/src/pages/ide.page.tsx b/frontend/src/pages/ide.page.tsx index 7097165..d518e94 100644 --- a/frontend/src/pages/ide.page.tsx +++ b/frontend/src/pages/ide.page.tsx @@ -1,14 +1,15 @@ -import { useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import { IDE } from "../modules/ide"; +import type { FileNode } from "../modules/ide"; export const IDEPage = () => { const navigate = useNavigate(); + const location = useLocation(); + const files: FileNode | undefined = location.state?.files; + return (
- navigate("/home")} - initialFiles={{ name: "тест", type: "folder" }} - /> + navigate("/templates")} initialFiles={files} />
); }; diff --git a/frontend/src/pages/templates.page.tsx b/frontend/src/pages/templates.page.tsx new file mode 100644 index 0000000..2cc3561 --- /dev/null +++ b/frontend/src/pages/templates.page.tsx @@ -0,0 +1,189 @@ +import { useNavigate } from "react-router-dom"; +import { FiEdit3, FiPlay } from "react-icons/fi"; +import { FilePicker, useFilePickerStore } from "../modules/ide"; +import type { FileNode } from "../modules/ide"; + +const mockFiles: FileNode = { + name: "templates", + type: "folder", + children: [ + { + name: "python-basic", + type: "folder", + children: [ + { + name: "src", + type: "folder", + children: [ + { + name: "main.py", + type: "file", + content: + 'print("Hello, World!")\n\ndef main():\n print("Welcome!")\n\nif __name__ == "__main__":\n main()', + }, + { + name: "utils.py", + type: "file", + content: "def helper():\n return 42", + }, + ], + }, + { + name: "README.md", + type: "file", + content: "# Python Project\n\nA basic Python project.", + }, + ], + }, + { + name: "react-starter", + type: "folder", + children: [ + { + name: "src", + type: "folder", + children: [ + { + name: "App.tsx", + type: "file", + content: + 'import React from "react";\n\nexport const App: React.FC = () => {\n return
Hello React!
;\n};', + }, + { + name: "index.tsx", + type: "file", + content: + 'import React from "react";\nimport { createRoot } from "react-dom/client";\nimport { App } from "./App";\n\ncreateRoot(document.getElementById("root")!).render();', + }, + ], + }, + { + name: "package.json", + type: "file", + content: '{\n "name": "react-project",\n "version": "1.0.0"\n}', + }, + ], + }, + { + name: "node-api", + type: "folder", + children: [ + { + name: "src", + type: "folder", + children: [ + { + name: "index.js", + type: "file", + content: + 'const express = require("express");\nconst app = express();\nconst PORT = 3000;\n\napp.get("/", (req, res) => {\n res.json({ message: "Hello!" });\n});\n\napp.listen(PORT, () => {\n console.log(`Server running on port ${PORT}`);\n});', + }, + ], + }, + { + name: "package.json", + type: "file", + content: + '{\n "name": "api-project",\n "dependencies": {\n "express": "^4.18.0"\n }\n}', + }, + ], + }, + { + name: "html-css", + type: "folder", + children: [ + { + name: "index.html", + type: "file", + content: + '\n\n\n My Landing\n \n\n\n

Welcome!

\n\n', + }, + { + name: "styles.css", + type: "file", + content: + "body {\n font-family: sans-serif;\n margin: 0;\n padding: 2rem;\n background: #f5f5f5;\n}\n\nh1 {\n color: #333;\n}", + }, + ], + }, + ], +}; + +export const TemplatesPage = () => { + const navigate = useNavigate(); + const selectedPaths = useFilePickerStore((s) => s.selectedPaths); + + return ( +
+ {/* Floating header */} +
+ {/* Running scripts counter */} +
+ + + {selectedPaths.size} script{selectedPaths.size !== 1 ? "s" : ""}{" "} + running + +
+ + {/* Open in Editor button */} + +
+ + {/* File Picker */} +
+ +
+
+ ); +};