From d7bf7d69dfc567837fd9fbb2b97ce7e975ba6ad9 Mon Sep 17 00:00:00 2001 From: Mike de Geofroy Date: Sun, 1 Dec 2024 04:28:49 +0300 Subject: [PATCH] Added env variables and other shit --- .env | 1 + .env.example | 1 + src/App.tsx | 15 +- .../command/command-menu.component.tsx | 118 ++++++-- src/components/tree/custom-node.component.tsx | 43 ++- src/components/tree/flow.component.tsx | 285 ++++++++++-------- src/components/ui/command.tsx | 2 +- src/lib/tree-layout.ts | 69 +++-- src/shared/api/node.api.ts | 56 +++- src/shared/api/people.api.ts | 0 src/shared/constants.ts | 1 + src/shared/data.ts | 2 +- .../{ => interfaces}/person-node.interface.ts | 0 src/shared/interfaces/person.interface.ts | 22 ++ 14 files changed, 394 insertions(+), 221 deletions(-) create mode 100644 .env create mode 100644 .env.example create mode 100644 src/shared/api/people.api.ts create mode 100644 src/shared/constants.ts rename src/shared/{ => interfaces}/person-node.interface.ts (100%) create mode 100644 src/shared/interfaces/person.interface.ts diff --git a/.env b/.env new file mode 100644 index 0000000..8d60d30 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_API_URL='https://api.mts.shamps.dev' \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8d60d30 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +VITE_API_URL='https://api.mts.shamps.dev' \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index dc22cd6..e2a2eae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,15 @@ import '@xyflow/react/dist/style.css'; import Flow from './components/tree/flow.component'; import { CommandMenu } from './components/command/command-menu.component'; +import { ReactFlowProvider } from '@xyflow/react'; export default function App() { - return ( -
- - -
- ); + return ( +
+ + + + +
+ ); } diff --git a/src/components/command/command-menu.component.tsx b/src/components/command/command-menu.component.tsx index 8ec8336..3aa9d9a 100644 --- a/src/components/command/command-menu.component.tsx +++ b/src/components/command/command-menu.component.tsx @@ -1,32 +1,92 @@ -import { useEffect, useState } from "react" -import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "../ui/command" +import { useEffect, useState } from 'react'; +import { + CommandDialog, + CommandEmpty, + CommandInput, + CommandList, + CommandItem, +} from '../ui/command'; +import { Person } from '@/shared/interfaces/person.interface'; +import { getPersonNodePath } from '@/shared/api/node.api'; +import { API_URL } from '@/shared/constants'; export function CommandMenu() { - const [open, setOpen] = useState(false) - - useEffect(() => { - const down = (e: KeyboardEvent) => { - if (e.key === "k" && (e.metaKey || e.ctrlKey)) { - e.preventDefault() - setOpen((open) => !open) - } - } - document.addEventListener("keydown", down) - return () => document.removeEventListener("keydown", down) - }, []) - - return ( - - - - No results found. - - Calendar - Search Emoji - Calculator - - - - ) -} + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + const [error, setError] = useState(null); + + const handleQueryChange = (search: string) => { + setQuery(search); + }; + + useEffect(() => { + if (query.trim() === '') { + setResults([]); + return; + } + + const fetchResults = async () => { + setError(null); + + try { + const response = await fetch( + `${API_URL}/persons/search?text=${encodeURIComponent( + query + )}` + ); + const data = await response.json(); + setResults(data); + } catch { + setError('An error occurred while fetching the results.'); + } + }; + fetchResults(); + }, [query]); + + useEffect(() => { + console.log(results); + }, [results]); + + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((open) => !open); + } + }; + document.addEventListener('keydown', down); + return () => document.removeEventListener('keydown', down); + }, []); + + return ( + + + + {error && {error}} + {results.length === 0 && !error && ( + No results found. + )} + {results.map((result) => { + return ( + { + const path = await getPersonNodePath('8', result.id); + console.log(path); + setOpen(false); + }} + key={result.id} + > + {result.name} + + ); + })} + + + ); +} diff --git a/src/components/tree/custom-node.component.tsx b/src/components/tree/custom-node.component.tsx index 31a930d..710b28e 100644 --- a/src/components/tree/custom-node.component.tsx +++ b/src/components/tree/custom-node.component.tsx @@ -1,21 +1,38 @@ import { memo } from 'react'; -import { PersonNode } from '@/shared/person-node.interface'; +import { PersonNode } from '@/shared/interfaces/person-node.interface'; import { Handle, Position } from '@xyflow/react'; function CustomNode({ data }: { data: PersonNode }) { - return ( -
-
- -
-
{data.name}
-
{data.jobtitle}
-
-
- - + return ( +
+
+ +
+
{data.name}
+
{data.jobtitle}
- ); +
+ + +
+ ); } export default memo(CustomNode); diff --git a/src/components/tree/flow.component.tsx b/src/components/tree/flow.component.tsx index a1cf330..6d3241a 100644 --- a/src/components/tree/flow.component.tsx +++ b/src/components/tree/flow.component.tsx @@ -1,145 +1,178 @@ import { - ReactFlow, - useNodesState, - useEdgesState, - Background, - BackgroundVariant, - ConnectionLineType, - Node, + ReactFlow, + useNodesState, + useEdgesState, + Background, + BackgroundVariant, + ConnectionLineType, + Node, } from '@xyflow/react'; import '@xyflow/react/dist/base.css'; import CustomNode from './custom-node.component'; -import { data } from '@/shared/data'; import { getPersonNodeById } from '@/shared/api/node.api'; -import { PersonNode } from '@/shared/person-node.interface'; +import { PersonNode } from '@/shared/interfaces/person-node.interface'; import { getLayoutedElements } from '@/lib/tree-layout'; +import { useEffect } from 'react'; const nodeTypes = { - custom: CustomNode, + custom: CustomNode, }; -const initNodes: Node[] = [ - { - id: data[3].id, - type: 'custom', - data: data[3], - position: { x: 0, y: 50 }, - } -]; - const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements( - initNodes, - [] + [], + [] ); const Flow = () => { - const [nodes, setNodes, onNodesChange] = useNodesState>(layoutedNodes); - const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges); - - const loadNodesRecursively = async (node: PersonNode, currentDepth: number, maxDepth: number) => { - if (currentDepth >= maxDepth) { - return { newNodes: [], newEdges: [] }; - } - - const parentNodes = await Promise.all( - node.parents.map((id) => getPersonNodeById(id)) - ); - const childNodes = await Promise.all( - node.children.map((id) => getPersonNodeById(id)) - ); - - const newNodes = [ - ...parentNodes - .filter((parent) => !nodes.some((existingNode) => existingNode.id === parent.id)) - .map((parent) => ({ - id: parent.id, - type: 'custom', - data: parent, - position: { x: 0, y: 0}, - })), - ...childNodes - .filter((child) => !nodes.some((existingNode) => existingNode.id === child.id)) - .map((child) => ({ - id: child.id, - type: 'custom', - data: child, - position: { x: 0, y: 0}, - })), - ]; - - const newEdges = [ - ...parentNodes.map((parentNode) => ({ - id: `e-${parentNode.id}-${node.id}`, - source: parentNode.id, - target: node.id, - })), - ...childNodes.map((childNode) => ({ - id: `e-${node.id}-${childNode.id}`, - source: node.id, - target: childNode.id, - })) - ]; - - let allNewNodes = newNodes; - let allNewEdges = newEdges; - - for (const parentNode of parentNodes) { - const { newNodes: nestedNodes, newEdges: nestedEdges } = await loadNodesRecursively(parentNode, currentDepth + 1, maxDepth); - allNewNodes = [...allNewNodes, ...nestedNodes]; - allNewEdges = [...allNewEdges, ...nestedEdges]; - } - - for (const childNode of childNodes) { - const { newNodes: nestedNodes, newEdges: nestedEdges } = await loadNodesRecursively(childNode, currentDepth + 1, maxDepth); - allNewNodes = [...allNewNodes, ...nestedNodes]; - allNewEdges = [...allNewEdges, ...nestedEdges]; - } - - return { newNodes: allNewNodes, newEdges: allNewEdges }; - }; - - const onNodeDoubleClick = async (_: unknown, input: Node) => { - const node = input.data; - - const maxDepth = 1; - const { newNodes, newEdges } = await loadNodesRecursively(node, 0, maxDepth); - - const uniqueNodes = [ - ...new Map([...nodes, ...newNodes].map(node => [node.id, node])).values() - ]; - - const uniqueEdges = [ - ...new Map([...edges, ...newEdges].map(edge => [edge.id, edge])).values() - ]; - - const { nodes: updatedNodes, edges: updatedEdges } = getLayoutedElements(uniqueNodes, uniqueEdges); - - setNodes(updatedNodes); - setEdges(updatedEdges); - }; - - return ( - - - + const [nodes, setNodes, onNodesChange] = + useNodesState>(layoutedNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges); + + useEffect(() => { + getPersonNodeById('8').then((x) => { + const { nodes, edges } = getLayoutedElements( + [ + { + id: x.id, + type: 'custom', + data: x, + position: { x: 0, y: 0 }, + }, + ], + [] + ); + + setNodes(nodes); + setEdges(edges); + }); + }, [setEdges, setNodes]); + + const loadNodesRecursively = async ( + node: PersonNode, + currentDepth: number, + maxDepth: number + ) => { + if (currentDepth >= maxDepth) { + return { newNodes: [], newEdges: [] }; + } + + const parentNodes = await Promise.all( + node.parents.map((id) => getPersonNodeById(id)) + ); + const childNodes = await Promise.all( + node.children.map((id) => getPersonNodeById(id)) + ); + + const newNodes = [ + ...parentNodes + .filter( + (parent) => + !nodes.some((existingNode) => existingNode.id === parent.id) + ) + .map((parent) => ({ + id: parent.id, + type: 'custom', + data: parent, + position: { x: 0, y: 0 }, + })), + ...childNodes + .filter( + (child) => !nodes.some((existingNode) => existingNode.id === child.id) + ) + .map((child) => ({ + id: child.id, + type: 'custom', + data: child, + position: { x: 0, y: 0 }, + })), + ]; + + const newEdges = [ + ...parentNodes.map((parentNode) => ({ + id: `e-${parentNode.id}-${node.id}`, + source: parentNode.id, + target: node.id, + })), + ...childNodes.map((childNode) => ({ + id: `e-${node.id}-${childNode.id}`, + source: node.id, + target: childNode.id, + })), + ]; + + let allNewNodes = newNodes; + let allNewEdges = newEdges; + + for (const parentNode of parentNodes) { + const { newNodes: nestedNodes, newEdges: nestedEdges } = + await loadNodesRecursively(parentNode, currentDepth + 1, maxDepth); + allNewNodes = [...allNewNodes, ...nestedNodes]; + allNewEdges = [...allNewEdges, ...nestedEdges]; + } + + for (const childNode of childNodes) { + const { newNodes: nestedNodes, newEdges: nestedEdges } = + await loadNodesRecursively(childNode, currentDepth + 1, maxDepth); + allNewNodes = [...allNewNodes, ...nestedNodes]; + allNewEdges = [...allNewEdges, ...nestedEdges]; + } + + return { newNodes: allNewNodes, newEdges: allNewEdges }; + }; + + const onNodeDoubleClick = async (_: unknown, input: Node) => { + const node = input.data; + + const maxDepth = 1; + const { newNodes, newEdges } = await loadNodesRecursively( + node, + 0, + maxDepth + ); + + const uniqueNodes = [ + ...new Map( + [...nodes, ...newNodes].map((node) => [node.id, node]) + ).values(), + ]; + + const uniqueEdges = [ + ...new Map( + [...edges, ...newEdges].map((edge) => [edge.id, edge]) + ).values(), + ]; + + const { nodes: updatedNodes, edges: updatedEdges } = getLayoutedElements( + uniqueNodes, + uniqueEdges ); + + setNodes(updatedNodes); + setEdges(updatedEdges); + }; + + return ( + + + + ); }; export default Flow; diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index 4216fea..fce5b36 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -24,7 +24,7 @@ const CommandDialog = ({ children, ...props }: DialogProps) => { return ( - + {children} diff --git a/src/lib/tree-layout.ts b/src/lib/tree-layout.ts index 05c11cc..abc10d9 100644 --- a/src/lib/tree-layout.ts +++ b/src/lib/tree-layout.ts @@ -1,40 +1,43 @@ -import { PersonNode } from '@/shared/person-node.interface'; +import { PersonNode } from '@/shared/interfaces/person-node.interface'; import dagre from '@dagrejs/dagre'; import { Node, Edge, Position } from '@xyflow/react'; const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); -const nodeWidth = 180; -const nodeHeight = 40; - -export const getLayoutedElements = (nodes: Node[], edges: Edge[]) => { - dagreGraph.setGraph({ rankdir: 'TB' }); - - nodes.forEach((node: Node) => { - dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); - }); - - edges.forEach((edge: Edge) => { - dagreGraph.setEdge(edge.source, edge.target); - }); - - dagre.layout(dagreGraph); - - const newNodes: Node[] = nodes.map((node) => { - const nodeWithPosition = dagreGraph.node(node.id); - const position = { - x: nodeWithPosition.x - nodeWidth / 2, - y: nodeWithPosition.y - nodeHeight / 2, - }; - - return { - ...node, - targetPosition: Position.Top, - sourcePosition: Position.Bottom, - position, - }; - }); - - return { nodes: newNodes, edges }; +const nodeWidth = 320; +const nodeHeight = 80; + +export const getLayoutedElements = ( + nodes: Node[], + edges: Edge[] +) => { + dagreGraph.setGraph({ rankdir: 'TB' }); + + nodes.forEach((node: Node) => { + dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); + }); + + edges.forEach((edge: Edge) => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + dagre.layout(dagreGraph); + + const newNodes: Node[] = nodes.map((node) => { + const nodeWithPosition = dagreGraph.node(node.id); + const position = { + x: nodeWithPosition.x - nodeWidth / 2, + y: nodeWithPosition.y - nodeHeight / 2, + }; + + return { + ...node, + targetPosition: Position.Top, + sourcePosition: Position.Bottom, + position, + }; + }); + + return { nodes: newNodes, edges }; }; diff --git a/src/shared/api/node.api.ts b/src/shared/api/node.api.ts index 2976c67..873a602 100644 --- a/src/shared/api/node.api.ts +++ b/src/shared/api/node.api.ts @@ -1,15 +1,47 @@ -import { PersonNode } from "../person-node.interface"; +import { API_URL } from '../constants'; +import { PersonNode } from '../interfaces/person-node.interface'; -import { data } from '../data'; +export const getPersonNodeById = async (id: string): Promise => { + try { + const response = await fetch( + `${API_URL}/persons/nodes/${id}` + ); -export const getPersonNodeById = (id: string): Promise => { - return new Promise((resolve, reject) => { - const person = data.find(x => x.id == id); + if (!response.ok) { + throw new Error(`Error fetching person data: ${response.statusText}`); + } - if (person) { - resolve(person); - } else { - reject(); - } - }); -} + const person: PersonNode = await response.json(); + return person; + } catch (error) { + console.error('Error fetching person node:', error); + throw error; + } +}; + +export const getPersonNodePath = async ( + from: string, + to: string +): Promise => { + try { + const response = await fetch( + `${API_URL}/persons/nodes/path?from=${from}&to=${to}`, + { + method: 'GET', + headers: { + accept: 'application/json', + }, + } + ); + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + const data: PersonNode[] = await response.json(); + return data; + } catch (error) { + console.error('Error fetching person node path:', error); + throw error; + } +}; diff --git a/src/shared/api/people.api.ts b/src/shared/api/people.api.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/shared/constants.ts b/src/shared/constants.ts new file mode 100644 index 0000000..280b721 --- /dev/null +++ b/src/shared/constants.ts @@ -0,0 +1 @@ +export const API_URL = import.meta.env.VITE_API_URL; \ No newline at end of file diff --git a/src/shared/data.ts b/src/shared/data.ts index f86c693..b302d02 100644 --- a/src/shared/data.ts +++ b/src/shared/data.ts @@ -1,4 +1,4 @@ -import { PersonNode } from "./person-node.interface"; +import { PersonNode } from "./interfaces/person-node.interface"; export const data: PersonNode[] = [ { diff --git a/src/shared/person-node.interface.ts b/src/shared/interfaces/person-node.interface.ts similarity index 100% rename from src/shared/person-node.interface.ts rename to src/shared/interfaces/person-node.interface.ts diff --git a/src/shared/interfaces/person.interface.ts b/src/shared/interfaces/person.interface.ts new file mode 100644 index 0000000..9ed1a5a --- /dev/null +++ b/src/shared/interfaces/person.interface.ts @@ -0,0 +1,22 @@ +export interface ContactDetails { + email: string; + phone: string; +} + +export interface Person { + id: string; + surname: string; + name: string; + middle_name_rus: string; + jobtitle: string; + status: string; + contacts: ContactDetails; + working_hour: string; + workplace: string; + head: string; + children: string[] | null; + department: string; + division: string; + team: string[] | null; + about: string; +}