feat: page tempaltes

This commit is contained in:
nikita
2026-04-04 06:05:51 +03:00
parent 43e16b1360
commit adbb0ee368
9 changed files with 490 additions and 9 deletions
@@ -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 = () => {
>
<FaUser size={12} style={{ color: "var(--accent)" }} />
</div>
<span className="text-xs" style={{ color: "var(--text-secondary)" }}>
<span
className="text-xs"
style={{ color: "var(--text-secondary)" }}
>
{user.name}
</span>
</div>
@@ -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 = () => {
<Route path="/themes" element={<ThemesPage />} />
<Route path="/add-agents" element={<AddAgentsPage />} />
<Route path="/IDE" element={<IDEPage />} />
<Route path="/templates" element={<TemplatesPage />} />
</Route>
<Route path="/test" element={<TestPage />} />
@@ -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 (
<FilePickerItem
name={node.name}
type="file"
path={nodePath}
isSelected={isSelected}
level={level}
onToggleSelect={toggleSelection}
/>
);
}
return (
<>
<FilePickerItem
name={node.name}
type="folder"
path={nodePath}
isExpanded={isExpanded}
level={level}
onToggleFolder={toggleFolder}
>
{node.children?.map((child, idx) => (
<FilePickerTree key={idx} node={child} level={level + 1} />
))}
</FilePickerItem>
</>
);
};
export const FilePicker: React.FC<FilePickerProps> = ({ files }) => {
return (
<div
style={{
height: "100%",
overflowY: "auto",
}}
>
<FilePickerTree node={files} level={0} />
</div>
);
};
@@ -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<FilePickerItemProps> = ({
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 (
<div>
<div
style={{
display: "flex",
alignItems: "center",
paddingLeft: `${paddingLeft}px`,
paddingRight: "12px",
height: "36px",
borderBottom: "1px solid #1a1a1a",
cursor: "pointer",
transition: "background-color 0.1s",
gap: "8px",
}}
onClick={() => {
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 && (
<span style={{ color: "#858585", display: "flex", flexShrink: 0 }}>
{isExpanded ? (
<FiChevronDown size={14} />
) : (
<FiChevronRight size={14} />
)}
</span>
)}
{/* File/Folder icon */}
<span style={{ display: "flex", flexShrink: 0 }}>
{isFolder ? (
<FiFolder size={15} color="#dcb67a" />
) : (
<FiFile size={15} color="#858585" />
)}
</span>
{/* Name */}
<span
style={{
flex: 1,
color: "#cccccc",
fontSize: "13px",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{name}
</span>
{/* Extension badge — только у файлов */}
{!isFolder && extension && (
<span
style={{
color: "#858585",
fontSize: "11px",
fontFamily: "monospace",
padding: "2px 6px",
backgroundColor: "#2a2a2a",
borderRadius: "3px",
flexShrink: 0,
}}
>
{extension}
</span>
)}
{/* Checkbox — только у файлов */}
{!isFolder && onToggleSelect && (
<div
style={{
width: "18px",
height: "18px",
border: isSelected ? "2px solid #0e639c" : "2px solid #555",
borderRadius: "3px",
backgroundColor: isSelected ? "#0e639c" : "transparent",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
transition: "all 0.15s",
}}
onClick={(e) => {
e.stopPropagation();
onToggleSelect(path);
}}
>
{isSelected && (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<path
d="M2 6L5 9L10 3"
stroke="white"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</div>
)}
</div>
{/* Children */}
{isFolder && isExpanded && children}
</div>
);
};
@@ -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";
+2
View File
@@ -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";
@@ -0,0 +1,57 @@
import { create } from "zustand";
interface FilePickerState {
selectedPaths: Set<string>;
expandedFolders: Set<string>;
toggleSelection: (path: string) => void;
selectAll: (paths: string[]) => void;
clearSelection: () => void;
toggleFolder: (path: string) => void;
getSelectedPaths: () => string[];
}
export const useFilePickerStore = create<FilePickerState>((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);
},
}));
+6 -5
View File
@@ -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 (
<div className="absolute top-0 left-0 w-full h-full z-90">
<IDE
onBack={() => navigate("/home")}
initialFiles={{ name: "тест", type: "folder" }}
/>
<IDE onBack={() => navigate("/templates")} initialFiles={files} />
</div>
);
};
+189
View File
@@ -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 <div>Hello React!</div>;\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(<App />);',
},
],
},
{
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:
'<!DOCTYPE html>\n<html>\n<head>\n <title>My Landing</title>\n <link rel="stylesheet" href="styles.css">\n</head>\n<body>\n <h1>Welcome!</h1>\n</body>\n</html>',
},
{
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 (
<div
style={{
height: "100vh",
position: "relative",
}}
>
{/* Floating header */}
<div
style={{
position: "absolute",
top: "16px",
right: "16px",
zIndex: 10,
display: "flex",
alignItems: "center",
gap: "16px",
}}
>
{/* Running scripts counter */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: "6px 12px",
backgroundColor: "#1a1a1a",
borderRadius: "4px",
border: "1px solid #2a2a2a",
}}
>
<FiPlay size={13} color="#61c454" />
<span style={{ fontSize: "12px", color: "#858585" }}>
{selectedPaths.size} script{selectedPaths.size !== 1 ? "s" : ""}{" "}
running
</span>
</div>
{/* Open in Editor button */}
<button
onClick={() => navigate("/ide")}
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: "6px 16px",
backgroundColor: "#0e639c",
border: "none",
borderRadius: "4px",
color: "#ffffff",
cursor: "pointer",
fontSize: "12px",
fontWeight: 500,
transition: "all 0.15s",
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = "#1177bb";
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = "#0e639c";
}}
>
<FiEdit3 size={14} />
Open Editor
</button>
</div>
{/* File Picker */}
<div style={{ height: "100%", overflow: "hidden" }}>
<FilePicker files={mockFiles} />
</div>
</div>
);
};