feat(client): move components
This commit is contained in:
@@ -4,46 +4,23 @@ import {
|
|||||||
Controls,
|
Controls,
|
||||||
useNodesState,
|
useNodesState,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
Handle,
|
Node,
|
||||||
Position,
|
Edge,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import IfElseNode from "./nodes/IfElseNode";
|
import { useState } from "react";
|
||||||
import { useCallback, useEffect } from "react";
|
|
||||||
import customEdgeStyle from "./edges/defaultEdgeStyle";
|
import customEdgeStyle from "./edges/defaultEdgeStyle";
|
||||||
import { message } from "antd";
|
import { Dropdown, DropdownProps } from "antd";
|
||||||
import AppropriationNode from "./nodes/AppropriationNode";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { edgeTitleGenerator } from "@/utils/edge";
|
||||||
function CustomNode({ data }: { data: any }) {
|
import IfElseNode from "./nodes/IfElseNode";
|
||||||
return (
|
import AppropriationNode from "./nodes/AppropriationNode";
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: 10,
|
|
||||||
border: "1px solid #ccc",
|
|
||||||
borderRadius: 5,
|
|
||||||
backgroundColor: "white",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div>{data.label}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Handle type="source" position={Position.Right} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Handle type="source" position={Position.Bottom} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeTypes = {
|
const nodeTypes = {
|
||||||
custom: CustomNode,
|
|
||||||
ifElse: IfElseNode,
|
ifElse: IfElseNode,
|
||||||
appropriation: AppropriationNode,
|
appropriation: AppropriationNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialNodes = [
|
const initialNodes: Node[] = [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
type: "ifElse",
|
type: "ifElse",
|
||||||
@@ -52,9 +29,9 @@ const initialNodes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "2",
|
id: "2",
|
||||||
type: "default",
|
type: "appropriation",
|
||||||
position: { x: 400, y: 100 },
|
position: { x: 100, y: 300 },
|
||||||
data: { label: "Приём" },
|
data: { value: "Выбрать {{account.email}}" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "3",
|
id: "3",
|
||||||
@@ -64,14 +41,24 @@ const initialNodes = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const initialEdges = [
|
const initialEdges: Edge[] = [
|
||||||
{
|
// {
|
||||||
id: "e1-2",
|
// id: "e1-3",
|
||||||
source: "1",
|
// source: "1",
|
||||||
target: "2",
|
// sourceHandle: "1",
|
||||||
label: "A1",
|
// target: "3",
|
||||||
...customEdgeStyle,
|
// targetHandle: null,
|
||||||
},
|
// label: "A1",
|
||||||
|
// ...customEdgeStyle,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "e1-2",
|
||||||
|
// source: "1",
|
||||||
|
// sourceHandle: "2",
|
||||||
|
// target: "2",
|
||||||
|
// label: "B1",
|
||||||
|
// ...customEdgeStyle,
|
||||||
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface ReactFlowDrawerProps {
|
interface ReactFlowDrawerProps {
|
||||||
@@ -84,56 +71,117 @@ export default function ReactFlowDrawer({ showDrawer }: ReactFlowDrawerProps) {
|
|||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
|
||||||
const onConnect = useCallback(
|
const [menuVisible, setMenuVisible] = useState(false);
|
||||||
(con: { source: any; target: any }) => {
|
const [selectedHandleId, setSelectedHandleId] = useState<string | null>(null);
|
||||||
const exists = edges.some(
|
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
|
||||||
(edge) => edge.source === con.source && edge.target === con.target
|
|
||||||
);
|
|
||||||
|
|
||||||
if (exists) {
|
const handleOpenChange: DropdownProps["onOpenChange"] = (nextOpen, info) => {
|
||||||
message.warning("Edge already exists");
|
if (info.source === "trigger" || nextOpen) {
|
||||||
return;
|
setMenuVisible(nextOpen);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent, node: Node) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const handleElement = target.closest(".react-flow__handle") as HTMLElement;
|
||||||
|
|
||||||
|
if (!handleElement) return;
|
||||||
|
|
||||||
|
const handleId = handleElement.getAttribute("data-handleid");
|
||||||
|
if (!handleId) return;
|
||||||
|
|
||||||
|
setSelectedHandleId(`${node.id}-${handleId}`);
|
||||||
|
|
||||||
|
const flowWrapper = document.querySelector(".react-flow") as HTMLElement;
|
||||||
|
if (flowWrapper) {
|
||||||
|
const wrapperRect = flowWrapper.getBoundingClientRect();
|
||||||
|
const handleRect = handleElement.getBoundingClientRect();
|
||||||
|
setMenuPosition({
|
||||||
|
x: handleRect.right - wrapperRect.left,
|
||||||
|
y: handleRect.top - wrapperRect.top,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newEdge = {
|
setMenuVisible(true);
|
||||||
...con,
|
};
|
||||||
|
|
||||||
|
const handleMenuItemClick = (targetNodeId: string) => {
|
||||||
|
if (!selectedHandleId) return;
|
||||||
|
|
||||||
|
const [sourceNodeId, sourceHandleId] = selectedHandleId.split("-");
|
||||||
|
|
||||||
|
const label = edgeTitleGenerator(edges.length + 1);
|
||||||
|
|
||||||
|
const newEdge: Edge = {
|
||||||
|
id: `e${sourceNodeId}-${sourceHandleId}-${targetNodeId}`,
|
||||||
|
source: sourceNodeId,
|
||||||
|
sourceHandle: sourceHandleId,
|
||||||
|
target: targetNodeId,
|
||||||
|
label,
|
||||||
...customEdgeStyle,
|
...customEdgeStyle,
|
||||||
id: `e${con.source}-${con.target}`,
|
|
||||||
label: "A1",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setEdges((eds) => [...eds, newEdge]);
|
setEdges((eds) => [...eds, newEdge]);
|
||||||
},
|
setMenuVisible(false);
|
||||||
[edges, setEdges]
|
setSelectedHandleId(null);
|
||||||
);
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const menuItems = nodes
|
||||||
console.log("Nodes changed:", nodes);
|
.filter((node) => node.id !== selectedHandleId?.split("-")[0]) // исключаем текущий узел
|
||||||
}, [nodes]);
|
.map((node) => ({
|
||||||
|
key: node.id,
|
||||||
useEffect(() => {
|
label: t("connectTo", { nodeId: node.id }),
|
||||||
console.log("Edges changed:", edges);
|
onClick: () => handleMenuItemClick(node.id),
|
||||||
}, [edges]);
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
// onNodesChange={onNodesChange}
|
|
||||||
// onEdgesChange={onEdgesChange}
|
|
||||||
// onConnect={onConnect}
|
|
||||||
nodesDraggable={false}
|
nodesDraggable={false}
|
||||||
elementsSelectable={false}
|
elementsSelectable={false}
|
||||||
nodesConnectable={false}
|
nodesConnectable={false}
|
||||||
onNodeClick={(e) => {
|
onNodeClick={(event, node) => {
|
||||||
console.log(e, "node clicked");
|
const target = event.target as HTMLElement;
|
||||||
|
|
||||||
|
if (!target.closest(".react-flow__handle")) {
|
||||||
|
console.log("node clicked");
|
||||||
showDrawer();
|
showDrawer();
|
||||||
|
} else {
|
||||||
|
handleClick(event, node);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
fitView
|
fitView
|
||||||
>
|
>
|
||||||
<Background />
|
<Background color="#F2F2F2" />
|
||||||
<Controls />
|
<Controls
|
||||||
|
position="bottom-center"
|
||||||
|
orientation="horizontal"
|
||||||
|
showInteractive={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{menuVisible && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: `${menuPosition.x}px`,
|
||||||
|
top: `${menuPosition.y}px`,
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dropdown
|
||||||
|
menu={{ items: menuItems }}
|
||||||
|
open={menuVisible}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
placement="bottom"
|
||||||
|
>
|
||||||
|
<div style={{ width: 1, height: 1 }} />
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
213
client/src/components/drawers/DrawerTitles.tsx
Normal file
213
client/src/components/drawers/DrawerTitles.tsx
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import { User } from "@/types/user";
|
||||||
|
import { Avatar, Typography } from "antd";
|
||||||
|
import { TFunction } from "i18next";
|
||||||
|
|
||||||
|
interface DrawerTitleProps {
|
||||||
|
closeDrawer: () => void;
|
||||||
|
t: TFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserEditDrawerTitleProps extends DrawerTitleProps {
|
||||||
|
login?: string;
|
||||||
|
name?: string;
|
||||||
|
email?: string | null;
|
||||||
|
user: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserCreateDrawerTitleProps extends DrawerTitleProps {}
|
||||||
|
|
||||||
|
interface NodeEditDrawerTitleProps extends DrawerTitleProps {}
|
||||||
|
|
||||||
|
const UserEditDrawerTitle = ({
|
||||||
|
closeDrawer,
|
||||||
|
login,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
user,
|
||||||
|
t,
|
||||||
|
}: UserEditDrawerTitleProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={closeDrawer}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/arrow_back.svg"
|
||||||
|
alt="close_drawer"
|
||||||
|
style={{ height: "16px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 12, flex: 1 }}>
|
||||||
|
<Avatar
|
||||||
|
src={
|
||||||
|
login ? `https://gamma.heado.ru/go/ava?name=${login}` : undefined
|
||||||
|
}
|
||||||
|
size={40}
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography.Text
|
||||||
|
strong
|
||||||
|
style={{ display: "block", fontSize: "20px" }}
|
||||||
|
>
|
||||||
|
{name} {login === user?.login ? t("you") : ""}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 14 }}>
|
||||||
|
{email}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/delete.svg"
|
||||||
|
alt="delete"
|
||||||
|
style={{ height: "18px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserCreateDrawerTitle = ({
|
||||||
|
closeDrawer,
|
||||||
|
t,
|
||||||
|
}: UserCreateDrawerTitleProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={closeDrawer}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/arrow_back.svg"
|
||||||
|
alt="close_drawer"
|
||||||
|
style={{ height: "16px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "12px",
|
||||||
|
flex: 1,
|
||||||
|
fontSize: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("newAccount")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
}}
|
||||||
|
onClick={closeDrawer}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/delete.svg"
|
||||||
|
alt="delete"
|
||||||
|
style={{ height: "18px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodeEditDrawerTitle = ({ closeDrawer, t }: NodeEditDrawerTitleProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={closeDrawer}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/arrow_back.svg"
|
||||||
|
alt="close_drawer"
|
||||||
|
style={{ height: "16px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "12px",
|
||||||
|
flex: 1,
|
||||||
|
fontSize: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("editNode")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
}}
|
||||||
|
onClick={closeDrawer}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/delete.svg"
|
||||||
|
alt="delete"
|
||||||
|
style={{ height: "18px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserEditDrawerTitle, UserCreateDrawerTitle, NodeEditDrawerTitle };
|
@@ -1,11 +1,13 @@
|
|||||||
import { Handle, Node, NodeProps, Position } from "@xyflow/react";
|
import { Handle, Node, NodeProps, Position } from "@xyflow/react";
|
||||||
import DEFAULT_HANDLE_STYLE from "./defaultHandleStyle";
|
import DEFAULT_HANDLE_STYLE from "./defaultHandleStyle";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type AppropriationNodeData = { value: string };
|
type AppropriationNodeData = { value: string };
|
||||||
|
|
||||||
export default function AppropriationNode({
|
export default function AppropriationNode({
|
||||||
data,
|
data,
|
||||||
}: NodeProps<Node & AppropriationNodeData>) {
|
}: NodeProps<Node & AppropriationNodeData>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -30,8 +32,8 @@ export default function AppropriationNode({
|
|||||||
style={{ height: "24px", width: "24px" }}
|
style={{ height: "24px", width: "24px" }}
|
||||||
src="/icons/node/calculate.svg"
|
src="/icons/node/calculate.svg"
|
||||||
alt="appropriation logo"
|
alt="appropriation logo"
|
||||||
/>{" "}
|
/>
|
||||||
ПРИСВОЕНИЕ
|
{t("appropriationNode")}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
||||||
<div
|
<div
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
import { Handle, NodeProps, Position, Node } from "@xyflow/react";
|
import { Handle, NodeProps, Position, Node } from "@xyflow/react";
|
||||||
import DEFAULT_HANDLE_STYLE from "./defaultHandleStyle";
|
import DEFAULT_HANDLE_STYLE from "./defaultHandleStyle";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type IfElseNodeData = {
|
interface IfElseNodeProps extends Node {
|
||||||
condition: string;
|
condition: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function IfElseNode({ data }: NodeProps<Node & IfElseNodeData>) {
|
export default function IfElseNode({ id, data }: NodeProps<IfElseNodeProps>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
border: "0px solid",
|
border: "0px solid",
|
||||||
@@ -31,8 +34,8 @@ export default function IfElseNode({ data }: NodeProps<Node & IfElseNodeData>) {
|
|||||||
style={{ height: "24px", width: "24px" }}
|
style={{ height: "24px", width: "24px" }}
|
||||||
src="/icons/node/ifElse.svg"
|
src="/icons/node/ifElse.svg"
|
||||||
alt="if else logo"
|
alt="if else logo"
|
||||||
/>{" "}
|
/>
|
||||||
ЕСЛИ - ТО
|
{t("ifElseNode")}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
||||||
|
|
||||||
@@ -45,7 +48,7 @@ export default function IfElseNode({ data }: NodeProps<Node & IfElseNodeData>) {
|
|||||||
height: "48px",
|
height: "48px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Если {data.condition as string}, то
|
{t("conditionIf", { condition: data.condition })}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
||||||
|
|
||||||
@@ -58,25 +61,23 @@ export default function IfElseNode({ data }: NodeProps<Node & IfElseNodeData>) {
|
|||||||
height: "48px",
|
height: "48px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Иначе
|
{t("conditionElse")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Handle type="target" position={Position.Top} id="input" />
|
<Handle type="target" position={Position.Top} id="input" />
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id="true"
|
id="1"
|
||||||
style={{ ...DEFAULT_HANDLE_STYLE }}
|
style={{ ...DEFAULT_HANDLE_STYLE }}
|
||||||
/>
|
/>
|
||||||
<Handle
|
<Handle
|
||||||
onClick={() => {
|
|
||||||
console.log("click");
|
|
||||||
}}
|
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Bottom}
|
position={Position.Bottom}
|
||||||
id="false"
|
id="2"
|
||||||
style={{ ...DEFAULT_HANDLE_STYLE }}
|
style={{ ...DEFAULT_HANDLE_STYLE }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user