This commit is contained in:
2024-10-05 11:25:22 +07:00
commit e882b33118
57 changed files with 8083 additions and 0 deletions

19
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,19 @@
module.exports = {
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
],
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
settings: { react: { version: "18.2" } },
plugins: ["react-refresh"],
rules: {
"react-refresh/only-export-components": "warn",
},
globals: {
COLORS: true,
},
};

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

6
README.md Normal file
View File

@@ -0,0 +1,6 @@
# React Flow Drag
# **[View the Live Demo](https://react-flow-drag.vercel.app/)**
Experience the versatility of React Flow Drag, a robust tool for effortless drag-and-drop functionality in web applications. Explore its potential for creating highly interactive user interfaces.

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

5140
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^5.0.1",
"@contactlab/ds-tokens": "^3.3.0",
"antd": "^5.4.7",
"dagre": "^0.8.5",
"lodash": "^4.17.21",
"nanoid": "^5.0.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-flow-renderer": "^10.3.17",
"react-router-dom": "^6.26.2",
"reactflow": "^11.7.0",
"sass": "^1.62.1",
"uuid": "^9.0.0",
"zustand": "^4.3.8"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.38.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"vite": "^4.3.2"
}
}

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

85
src/App.jsx Normal file
View File

@@ -0,0 +1,85 @@
import React from "react";
import _ from "lodash";
import "antd/dist/reset.css";
import "./index.scss";
import { getIncomers, getOutgoers } from "react-flow-renderer";
// File Importing from folder
import Layout from "./Automation.jsx";
import { initialElements } from "./Data/Elements1.jsx";
import { getUpdatedElementsAfterNodeAddition } from "./Utils/WorkflowElementUtils.jsx";
import Sidebar from "./Sidebar/Sidebar";
const App = () => {
const [elements, setElements] = React.useState([]);
const onAddNodeCallback = ({ id, type }) => {
console.log(type);
setElements((elements) =>
getUpdatedElementsAfterNodeAddition({
elements,
targetEdgeId: id,
type,
onDeleteNodeCallback,
onNodeClickCallback,
onAddNodeCallback,
})
);
};
const onDeleteNodeCallback = (id) => {
setElements((elements) => {
const clonedElements = _.cloneDeep(elements);
const incomingEdges = clonedElements.filter((x) => x.target === id);
const outgoingEdges = clonedElements.filter((x) => x.source === id);
const updatedIncomingEdges = incomingEdges.map((x) => ({
...x,
target: outgoingEdges[0].target,
}));
const filteredElements = clonedElements.filter(
(x) =>
x.id !== id &&
x.target !== incomingEdges[0].target &&
x.source !== outgoingEdges[0].source
);
filteredElements.push(...updatedIncomingEdges);
return filteredElements;
});
};
const onNodeClickCallback = (id) => {
setElements((elements) => {
const currentNode = elements.find((x) => x.id === id);
const nodes = elements.filter((x) => x.position);
const edges = elements.filter((x) => !x.position);
console.error({
incomers: getIncomers(currentNode, nodes, edges),
outgoers: getOutgoers(currentNode, nodes, edges),
});
return elements;
});
alert(`You clicked the "${id}" node`);
};
React.useEffect(() => {
const nodes = initialElements
.filter((x) => !x.target)
.map((x) => ({
...x,
data: { ...x.data, onDeleteNodeCallback, onNodeClickCallback },
}));
const edges = initialElements
.filter((x) => x.target)
.map((x) => ({ ...x, data: { ...x.data, onAddNodeCallback } }));
setElements([...nodes, ...edges]);
}, []);
return (
<div className="App">
<Sidebar />
<Layout elements={elements} />
</div>
);
};
export default App;

184
src/Automation.css Normal file
View File

@@ -0,0 +1,184 @@
html,
body,
#root {
height: 100%;
margin: 0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
width: 1200px;
margin: 0 auto;
padding: 0 10px;
}
.menu-fixed .item-flow b {
margin-bottom: 10px;
display: block;
}
.menu-fixed .list-option.flex {
grid-gap: 8px;
display: grid;
grid-template-columns: repeat(2, 1fr);
}
.menu-fixed .list-option .items {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
border: 1px solid rgba(36, 28, 21, 0.15);
border-radius: 4px;
padding: 16px 8px;
background: #fff;
cursor: move;
margin-bottom: 10px;
}
.menu-fixed .list-option.flex .step {
flex-direction: column;
height: 48px;
justify-content: center;
}
.menu-fixed .list-option.flex .items {
justify-content: center;
}
.menu-fixed .list-option.flex .left {
border: 0;
width: auto;
}
.menu-fixed .list-option.flex .right {
width: 100%;
text-align: center;
}
.AutomationCanvas {
--edges-stroke-color: green;
width: calc(100% - 360px);
background-color: var(--color-base100);
}
.menu-fixed {
background: #fff;
border: initial;
border-radius: initial;
border-right: 1px solid rgba(36, 28, 21, 0.15);
-webkit-transition: -webkit-transform 0.2s ease-in;
transition: -webkit-transform 0.2s ease-in;
transition: transform 0.2s ease-in;
transition: transform 0.2s ease-in, -webkit-transform 0.2s ease-in;
width: 360px;
z-index: 9;
padding: 20px;
height: 100vh;
overflow-y: auto;
}
.react-flow__edge-path {
stroke: green;
}
.Controls {
bottom: 70px;
left: auto !important;
right: 30px;
}
.reactflow-wrapper {
height: 100vh;
background-color: var(--color-base100);
margin-top: 50px;
margin-left: 100px;
}
/* export const AutomationCanvas = css`
--edges-stroke-color: green;
width: 100vw;
height: 100vh;
background-color: var(--color-base100);
.react-flow__edge-path {
stroke: var(--edges-stroke-color);
}
`;
export const Controls = css`
top: 10px;
bottom: auto;
`; */
.node-dropzone {
width: 20px;
height: 20px;
border: 1px solid crimson;
background-color: rgb(255, 238, 0);
border-radius: 50px;
color: crimson;
cursor: pointer;
display: flex;
align-items: start;
justify-content: center;
}
.node-dropzone:hover {
background-color: greenyellow;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 10px;
width: auto;
border: 1px solid;
padding: 10px;
}
.SidebarItem {
background-color: blue;
padding: 10px;
color: white;
text-align: center;
}
.SidebarItem:hover {
background-color: rgb(153, 0, 255);
cursor: pointer;
}
.step {
display: flex;
align-items: center;
height: 30px;
}
.step .left {
width: 40px;
margin-right: 10px;
border-right: 1px solid #241c156e;
}
.step .right {
width: calc(100% - 30px);
text-align: left;
font-size: 14px;
}
.step .left img {
margin-top: 5px;
height: 20px;
width: 20px;
}
.NodeIcon svg {
width: 100%;
object-fit: contain;
}
.react-flow__edge {
outline: none;
}

137
src/Automation.jsx Normal file
View File

@@ -0,0 +1,137 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import ReactFlow, {
Controls,
MiniMap,
ReactFlowProvider,
useEdgesState,
useNodesState,
} from "reactflow";
// File imports
import "./Automation.css";
import { nodeTypes } from "./Nodes/index.jsx";
import { edgeTypes } from "./Edges/index.jsx";
import { getLayoutedElements } from "./Utils/WorkflowLayoutUtils.jsx";
import Sidebar from "./Sidebar/Sidebar";
import { getUpdatedElementsAfterNodeAddition } from "./Utils/WorkflowElementUtils";
export const Automation = (props) => {
const { elements } = props;
const reactFlowWrapper = useRef(null);
const [nodes, setNodes, onNodesChange] = useNodesState();
const [edges, setEdges, onEdgesChange] = useEdgesState();
const [reactFlowInstance, setReactFlowInstance] = useState(null);
useEffect(() => {
const layoutElements = getLayoutedElements(elements);
const layoutNodes = layoutElements.filter((x) => x.position);
const layoutEdges = layoutElements.filter((x) => !x.position);
setNodes(layoutNodes);
setEdges(layoutEdges);
}, [elements]);
const onConnect = useCallback(
(params) => setEdges((eds) => eds.concat(params)), // Modified: Concatenate edges
[setEdges]
);
// ============================>
// ==============================>
let id = 0;
const getId = () => `dndnode_${id++}`;
const onDrop = useCallback(
(event) => {
event.preventDefault();
console.log(reactFlowInstance);
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
const type = event.dataTransfer.getData("application/reactflow");
// check if the dropped element is valid
if (typeof type === "undefined" || !type) {
return;
}
const position = reactFlowInstance.project({
x: event.clientX - reactFlowBounds.left,
y: event.clientY - reactFlowBounds.top,
});
const newNode = {
id: getId(),
type,
position,
data: { label: `${type} node` },
};
setNodes((nds) => nds.concat(newNode));
// ===========
// setNodes((elements) =>
// getUpdatedElementsAfterNodeAddition({
// elements,
// newNode: newNode,
// targetEdgeId: "e1-2",
// })
// );
// ===========
},
[reactFlowInstance, setNodes]
);
const onDragOver = useCallback((event) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}, []);
// =================================>
return (
<div className="AutomationCanvas">
<ReactFlowProvider>
<div ref={reactFlowWrapper} className="reactflow-wrapper">
<ReactFlow
nodes={nodes}
edges={edges}
nodesDraggable={false}
nodesConnectable={false}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
// zoomOnScroll={false}
// zoomOnPinch={false}
// panOnScroll
// panOnDrag
// preventScrolling
onConnect={onConnect}
panOnScroll
onInit={setReactFlowInstance}
// onDrop={onDrop}
onDragOver={onDragOver}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
>
<Controls
// showInteractive={false}
className="Controls"
position="right-bottom"
/>
</ReactFlow>
</div>
</ReactFlowProvider>
</div>
);
};
const Layout = (props) => (
<ReactFlowProvider>
<Automation {...props} />
</ReactFlowProvider>
);
export default Layout;

124
src/BuildFlow.jsx Normal file
View File

@@ -0,0 +1,124 @@
import React, { useState, useCallback } from "react";
import { useLocation } from "react-router-dom";
import { nanoid } from "nanoid";
import "./assets/css/style.css";
import imageStart from "./assets/images/ILLO_MiniSpot_2-Paths-v2_1080x1080.png";
import PopupListFlow from "./Popups/PopupListFlow";
import { mainNode } from "./data/data";
import App from "./App";
const BuildFlow = () => {
const [showPopup, setShowPopup] = useState(true);
const location = useLocation();
const [selectedFlow, setSelectedFlow] = useState(null);
const query = new URLSearchParams(location.search);
const flowName = query.get("flowName");
const handleAddStartPointClick = () => {
setShowPopup(true);
};
const hidePopup = () => {
setShowPopup(false);
};
const handleSelectFlow = (flow) => {
setSelectedFlow(flow); // Lưu flow đã chọn
hidePopup(); // Ẩn popup sau khi chọn flow
// Tìm node khớp với flow trong mainNode
const selectedMainNode = mainNode.find((node) => node.key === flow);
if (selectedMainNode) {
// Tạo một node từ mainNode
const newNode = {
id: `${selectedMainNode.id}`, // Sử dụng id từ mainNode
type: "input",
data: {
label: (
<div className="step">
<div className="left">
<img
src={selectedMainNode.icon}
width="24px"
height="24px"
alt=""
/>
</div>
<div className="right"> {selectedMainNode.name}</div>
</div>
),
},
position: { ...selectedMainNode.position }, // Vị trí từ mainNode
};
// Tạo node add-node
const addNode = {
id: "2",
key: "add-node",
type: "default",
data: {
label: (
<div className="add-node">
<i>+</i> <span>Add a journey point</span>
</div>
),
},
position: {
x: selectedMainNode.position.x,
y: selectedMainNode.position.y + 180,
},
className: "contact-exits",
};
// Kết nối giữa node và add-node
const newEdge = {
id: `edge-${selectedMainNode.id}-add-node`,
source: `${selectedMainNode.id}`,
target: "2",
type: "custom",
data: { onAddNodeCallback },
};
// Cập nhật state với node mới, add-node, và edge
setNodes((nds) => [newNode, addNode]);
setEdges((eds) => [newEdge]);
}
};
return (
<>
{!selectedFlow ? (
<>
<div className="container">
<h2>Building Flow: {flowName}</h2>
<div className="content-start">
<img src={imageStart} className="imageStart" alt="" />
<h2 className="title">How will a contact start their journey?</h2>
<p className="note">
This is what kicks off your contact's journey. You choose the
starting point, then contacts who meet the criteria will enter
your map and begin their journey.
</p>
</div>
<button onClick={handleAddStartPointClick} className="btn-start">
Choose A Starting Point
</button>
{showPopup && (
<PopupListFlow
onSelectFlow={handleSelectFlow}
hidePopup={() => setShowPopup(false)}
/>
)}
</div>
</>
) : (
<App />
)}
</>
);
};
export default BuildFlow;

View File

@@ -0,0 +1,14 @@
import { Button } from "antd";
import { PlusOutlined } from "@ant-design/icons";
export const AddButton = (props) => {
const { onClick } = props;
return (
<Button
shape="circle"
size="small"
icon={<PlusOutlined />}
onClick={() => onClick(props)}
/>
);
};

View File

@@ -0,0 +1,76 @@
import { Button } from "antd";
import { useState, DragEvent, memo } from "react";
import { Handle, Position } from "react-flow-renderer";
import styles from "./nodes.module.scss";
const DropzoneNode = (props) => {
const [isDropzoneActive, setDropzoneActive] = useState(false);
const onDrop = () => {
setDropzoneActive(false);
};
const onDragOver = (evt) => {
evt.preventDefault();
};
const onDragEnter = () => {
setDropzoneActive(true);
};
const onDragLeave = () => {
setDropzoneActive(false);
};
return (
<div
className={`${styles.NextStepComponent} ${
isDropzoneActive ? styles.dropzoneNode : ""
}`}
onDrop={onDrop}
onDragOver={onDragOver}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
>
<Handle
type="target"
id="b"
className={styles.handle}
position={Position.Top}
style={{
background: "#ffff",
border: "1px dotted gray",
zIndex: "-1",
}}
/>
<Handle
type="source"
id="a"
position={Position.Bottom}
style={{
background: "#ffff",
border: "1px dotted gray",
padding: 0,
zIndex: "-1",
}}
/>
{isDropzoneActive && (
<p style={{ textAlign: "center" }}>Drop here</p>
)}
<div className={styles.cancelButton}>
<Button
style={{
margin: "-1px -19px",
padding: "0 0",
zIndex: "10",
}}
size="small"
></Button>
</div>
</div>
);
};
export default memo(DropzoneNode);

View File

@@ -0,0 +1,62 @@
// import { AddButton } from "../AddButton/AddButton.jsx";
// import { Menu, Dropdown } from "antd";
// import "./EdgeAddButton.scss";
// const EdgeAddButton = (props) => {
// const { style, data, id } = props;
// console.log(id);
// const content = (
// <Menu
// onClick={(event) => data.onAddNodeCallback({ id, type: event.key })}
// >
// <Menu.Item key="email">Email</Menu.Item>
// <Menu.Item key="sms">SMS</Menu.Item>
// <Menu.Item key="waitThenCheck">Rule</Menu.Item>
// <Menu.Item key="end">End</Menu.Item>
// </Menu>
// );
// return (
// <div className="EdgeAddButton" style={style}>
// <Dropdown overlay={content} trigger={["click"]}>
// <AddButton {...props} />
// </Dropdown>
// </div>
// );
// };
// export default EdgeAddButton;
// ============================
// import { AddButton } from "../AddButton/AddButton.jsx";
import { Menu, Dropdown, Button } from "antd";
import "./EdgeAddButton.scss";
import { useState } from "react";
import { Handle, Position } from "reactflow";
import styles from "./nodes.module.scss";
// import DropzoneNode from "./DropzoneNode.jsx";
const EdgeAddButton = (props) => {
const { style, data, id } = props;
const dropzone = <div className="node-dropzone"> +</div>;
return (
<div
className="EdgeAddButton"
style={style}
onDrop={(event) =>
data.onAddNodeCallback({
id,
type: event.dataTransfer.getData("nodeType"),
})
}
onDragOver={(event) => event.preventDefault()}
>
{dropzone}
</div>
);
};
export default EdgeAddButton;

View File

@@ -0,0 +1,5 @@
.EdgeAddButton {
display: flex;
align-items: center;
justify-content: center;
}

46
src/CreateFlow.jsx Normal file
View File

@@ -0,0 +1,46 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import "./assets/css/style.css";
import Customer from "./assets/images/Customer-Journey_Animation.gif";
const CreateFlow = () => {
const [flowName, setFlowName] = useState("");
const navigate = useNavigate();
const handleSave = (e) => {
e.preventDefault();
if (flowName.trim() !== "") {
// Chuyển hướng đến trang build với tham số build=1
navigate(`/build?build=1&flowName=${encodeURIComponent(flowName)}`);
} else {
alert("Bạn chưa nhập tên cho tiến trình");
}
};
return (
<div className="container">
<div className="content-create">
<img src={Customer} className="image-header" alt="" />
<h2 className="title">Create a map of your contact's journey</h2>
<p className="note">
Put your contacts on a path thats right for them. With a customer
journey, you can always be there for your contacts when they need you
most.
</p>
<form onSubmit={handleSave} className="style-form">
<label>Flow Name:</label>
<input
type="text"
value={flowName}
onChange={(e) => setFlowName(e.target.value)}
/>
<button type="submit" className="btn-submit">
Start Building
</button>
</form>
</div>
</div>
);
};
export default CreateFlow;

146
src/Data/Elements1.jsx Normal file
View File

@@ -0,0 +1,146 @@
const position = { x: 0, y: 0 };
const nodes = [
{
id: "1",
type: "source",
data: {
title: "Source",
},
position: {
x: 150,
y: 0,
},
style: {
width: 250,
},
},
// {
// id: "2",
// type: "email",
// data: {
// title: "Email",
// description: "Send message to contacts.",
// stats: {
// running: 18,
// error: 1,
// },
// },
// position,
// },
// {
// id: "3",
// type: "waitThenCheck",
// data: {
// title: "Replied to conversation?",
// description: "Replied to conversation?",
// stats: {
// running: 17,
// },
// },
// position,
// style: {
// width: 250,
// },
// },
// {
// id: "4",
// type: "email",
// data: {
// title: "Email",
// description: "Send message to contacts.",
// stats: {
// running: 3,
// },
// },
// position,
// },
// {
// id: "5",
// type: "sms",
// data: {
// title: "SMS",
// description: "Send SMS to contacts.",
// stats: {
// running: 14,
// },
// },
// position,
// },
{
id: "7",
type: "end",
data: {
title: "Contact Exits",
description: "",
},
position: {
x: 150,
y: 200,
},
},
// {
// id: "8",
// type: "end",
// data: {
// title: "Contact Exits",
// description: "",
// stats: {
// completed: 14,
// },
// },
// position,
// },
];
const edges = [
{
id: "e1-7",
source: "1",
target: "7",
type: "condition",
},
// {
// id: "e2-3",
// source: "2",
// target: "3",
// type: "condition",
// },
// {
// id: "e3-4",
// source: "3",
// target: "4",
// type: "condition",
// sourceHandle: "yes",
// data: {
// title: "Default condition",
// disabled: true,
// },
// },
// {
// id: "e3-5",
// source: "3",
// target: "5",
// type: "condition",
// sourceHandle: "no",
// data: {
// title: "Editable branch",
// },
// },
// {
// id: "e6-7",
// source: "5",
// target: "7",
// sourceHandle: "yes",
// type: "condition",
// },
// {
// id: "e4-8",
// source: "4",
// target: "8",
// sourceHandle: "no",
// type: "condition",
// },
];
export const initialElements = [...nodes, ...edges];

511
src/Data/Elements2.jsx Normal file
View File

@@ -0,0 +1,511 @@
const position = { x: 0, y: 0 };
const data = {
layoutNodes: [
{
id: "1",
type: "source",
data: {
title: "Source",
description: "Automations Database contacts",
stats: {
started: 0,
},
},
_position: {
x: 380.00037444075423,
y: 0,
},
style: {
width: 250,
},
target_position: "top",
source_position: "bottom",
position,
},
{
id: "3",
type: "waitThenCheck",
data: {
title: "Wait then Check",
description: "Check behaviour of the contacts.",
stats: {
running: 17,
},
},
_position: {
x: 380.00030212384667,
y: 372,
},
style: {
width: 250,
},
target_position: "top",
source_position: "bottom",
position,
},
{
id: "5",
type: "sms",
data: {
title: "SMS",
description: "Send SMS to contacts.",
stats: {
running: 14,
},
},
_position: {
x: 0.0006895893374535293,
y: 1246,
},
target_position: "top",
source_position: "bottom",
position,
},
{
id: "6",
type: "empty",
data: {},
_position: {
x: 380.00084311022334,
y: 1376,
},
height: 6,
mergeNodeOfParentId: "3",
target_position: "top",
source_position: "bottom",
position,
},
{
id: "7",
type: "end",
data: {
title: "End",
description: "Automation ends.",
stats: {
completed: 14,
},
},
_position: {
x: 380.0000154657833,
y: 1432,
},
target_position: "top",
source_position: "bottom",
position,
},
{
id: "848c3dec-1b01-4a3f-8b72-706d66a8cd70",
type: "sms",
data: {
title: "Sms",
description: "Send sms to contacts.",
},
_position: {
x: 530.0003565002155,
y: 502,
},
target_position: "top",
source_position: "bottom",
position,
},
{
id: "b288f125-ff39-466d-bab6-934c2fa07c03",
type: "waitThenCheck",
data: {
title: "New Rule",
description: "Check behaviour of the Rule",
},
_position: {
x: 530.000649770617,
y: 632,
},
target_position: "top",
source_position: "bottom",
position,
},
{
id: "7301b7b0-eaba-49dc-bf57-0c5fec2b192f",
type: "empty",
data: {},
_position: {
x: 380.000880810274,
y: 1134,
},
height: 6,
target_position: "top",
source_position: "bottom",
position,
},
{
id: "977c989f-9657-41e6-9617-08bd3c6fbd43",
type: "empty",
data: {},
_position: {
x: 760.0009293479523,
y: 1134,
},
height: 6,
target_position: "top",
source_position: "bottom",
position,
},
{
id: "316366e7-8c56-49c9-9803-5c30d937634e",
type: "empty",
data: {},
_position: {
x: 530.0002802883737,
y: 1190,
},
height: 6,
target_position: "top",
source_position: "bottom",
position,
},
{
id: "78919ba2-5f9e-4d1a-b729-444a738ea1f5",
type: "sms",
data: {
title: "Sms",
description: "Send sms to contacts.",
},
_position: {
x: 530.0009417226739,
y: 1246,
},
target_position: "top",
source_position: "bottom",
position,
},
{
id: "571a5ddf-f865-486b-9374-0196e842a9f9",
type: "waitThenCheck",
data: {
title: "New Rule",
description: "Check behaviour of the Rule",
},
_position: {
x: 380.0009084120236,
y: 762,
},
target_position: "top",
source_position: "bottom",
position,
},
{
id: "8c804560-48f1-47fd-9937-bc62124b8de4",
type: "empty",
data: {},
_position: {
x: 230.00023449374996,
y: 1022,
},
height: 6,
target_position: "top",
source_position: "bottom",
position,
},
{
id: "3d4f48ac-1dc7-4aff-8739-15659058e878",
type: "empty",
data: {},
_position: {
x: 530.0000020924601,
y: 1022,
},
height: 6,
target_position: "top",
source_position: "bottom",
position,
},
{
id: "d7651a34-9478-409a-a38c-64312d73e8d3",
type: "empty",
data: {},
_position: {
x: 380.0006605416041,
y: 1078,
},
height: 6,
target_position: "top",
source_position: "bottom",
position,
},
{
id: "4acdd729-39b9-42ce-adfe-a8d1068f821a",
type: "sms",
data: {
title: "Sms",
description: "Send sms to contacts.",
},
_position: {
x: 530.0004075241991,
y: 892,
},
target_position: "top",
source_position: "bottom",
position,
},
{
id: "e70a497c-1b42-4f9f-b92c-c4d6b1cc5d57",
type: "sms",
data: {
title: "Sms",
description: "Send sms to contacts.",
},
_position: {
x: 230.00060068647215,
y: 892,
},
target_position: "top",
source_position: "bottom",
position,
},
{
id: "528ba05d-9c5b-413e-a59f-7ca6914020c4",
type: "waitThenCheck",
data: {
title: "New Rule",
description: "Check behaviour of the Rule",
},
_position: {
x: 380.0001113668592,
y: 130,
},
target_position: "top",
source_position: "bottom",
position,
},
{
id: "cd485fbb-3d6f-4015-8717-822d3c433725",
type: "empty",
data: {},
_position: {
x: 230.00041223048268,
y: 260,
},
height: 6,
target_position: "top",
source_position: "bottom",
position,
},
{
id: "8f37b5f7-9cea-4085-89c9-bfa7db971e17",
type: "empty",
data: {},
_position: {
x: 530.0006256305971,
y: 260,
},
height: 6,
target_position: "top",
source_position: "bottom",
position,
},
{
id: "2202a162-e008-4653-ab23-1b780c839a26",
type: "empty",
data: {},
_position: {
x: 380.0006922939385,
y: 316,
},
height: 6,
target_position: "top",
source_position: "bottom",
position,
},
],
layoutEdges: [
{
id: "e3-4",
source: "3",
target: "848c3dec-1b01-4a3f-8b72-706d66a8cd70",
type: "condition",
data: {
title: "Default condition",
disabled: true,
},
},
{
id: "e3-5",
source: "3",
target: "5",
type: "condition",
data: {
title: "Editable branch",
},
},
{
id: "e5-6",
source: "5",
target: "6",
type: "condition",
data: {},
},
{
id: "e6-7",
source: "6",
target: "7",
type: "condition",
data: {},
},
{
id: "53706f28-e86a-4687-8e4e-9c41b0ab4cbc",
source: "b288f125-ff39-466d-bab6-934c2fa07c03",
target: "571a5ddf-f865-486b-9374-0196e842a9f9",
type: "condition",
data: {},
},
{
id: "6a5ea441-efa8-45a5-a0a5-e89ea942a714",
source: "7301b7b0-eaba-49dc-bf57-0c5fec2b192f",
target: "316366e7-8c56-49c9-9803-5c30d937634e",
type: "condition",
data: {
isAddButtonHidden: true,
},
},
{
id: "047285c3-4f2c-4d2d-b7e5-34be0cd03cd8",
source: "b288f125-ff39-466d-bab6-934c2fa07c03",
target: "977c989f-9657-41e6-9617-08bd3c6fbd43",
type: "condition",
data: {},
},
{
id: "ebd9bc4f-d5c9-47d5-b667-548c9f63ed35",
source: "977c989f-9657-41e6-9617-08bd3c6fbd43",
target: "316366e7-8c56-49c9-9803-5c30d937634e",
type: "condition",
data: {
isAddButtonHidden: true,
},
},
{
id: "c1f453c1-0d79-4bdd-bb3e-4995b43851c9",
source: "316366e7-8c56-49c9-9803-5c30d937634e",
target: "78919ba2-5f9e-4d1a-b729-444a738ea1f5",
type: "condition",
data: {},
mergeNodeOfParentId: "b288f125-ff39-466d-bab6-934c2fa07c03",
},
{
id: "3ce6e537-8efe-491c-bd67-bd8dd2c22453",
source: "78919ba2-5f9e-4d1a-b729-444a738ea1f5",
target: "6",
type: "condition",
data: {},
},
{
id: "2178e8ad-4da2-43b5-95fd-d7b8e813bb99",
source: "8c804560-48f1-47fd-9937-bc62124b8de4",
target: "d7651a34-9478-409a-a38c-64312d73e8d3",
type: "condition",
data: {
isAddButtonHidden: true,
},
},
{
id: "e6d0ce27-60c3-4fa9-a855-78d3eaf8e653",
source: "571a5ddf-f865-486b-9374-0196e842a9f9",
target: "4acdd729-39b9-42ce-adfe-a8d1068f821a",
type: "condition",
data: {},
},
{
id: "1b2e68f6-cada-45a6-9333-4d25bc3ec018",
source: "3d4f48ac-1dc7-4aff-8739-15659058e878",
target: "d7651a34-9478-409a-a38c-64312d73e8d3",
type: "condition",
data: {
isAddButtonHidden: true,
},
},
{
id: "fe2db513-d113-468e-b884-2be2385f0c74",
source: "d7651a34-9478-409a-a38c-64312d73e8d3",
target: "7301b7b0-eaba-49dc-bf57-0c5fec2b192f",
type: "condition",
data: {},
mergeNodeOfParentId: "571a5ddf-f865-486b-9374-0196e842a9f9",
},
{
id: "07773a27-04d1-470b-b7e0-36911475aece",
source: "4acdd729-39b9-42ce-adfe-a8d1068f821a",
target: "3d4f48ac-1dc7-4aff-8739-15659058e878",
type: "condition",
data: {},
},
{
id: "726de0f3-6c82-41a6-af03-c5ee43eaf195",
source: "e70a497c-1b42-4f9f-b92c-c4d6b1cc5d57",
target: "8c804560-48f1-47fd-9937-bc62124b8de4",
type: "condition",
data: {},
},
{
id: "e1-2",
source: "1",
target: "528ba05d-9c5b-413e-a59f-7ca6914020c4",
type: "condition",
data: {},
},
{
id: "9ef8742a-2d31-44bc-abbe-137ce9bdee57",
source: "848c3dec-1b01-4a3f-8b72-706d66a8cd70",
target: "b288f125-ff39-466d-bab6-934c2fa07c03",
type: "condition",
data: {},
},
{
id: "d3db8ba7-c6e6-4ac2-b024-be2b04468de4",
source: "571a5ddf-f865-486b-9374-0196e842a9f9",
target: "e70a497c-1b42-4f9f-b92c-c4d6b1cc5d57",
type: "condition",
data: {},
},
{
id: "d5693be9-7995-4b97-831f-928a9de65cdd",
source: "528ba05d-9c5b-413e-a59f-7ca6914020c4",
target: "cd485fbb-3d6f-4015-8717-822d3c433725",
type: "condition",
data: {},
},
{
id: "d21e4de4-b3f2-47ca-a597-cfcb94151f3b",
source: "cd485fbb-3d6f-4015-8717-822d3c433725",
target: "2202a162-e008-4653-ab23-1b780c839a26",
type: "condition",
data: {
isAddButtonHidden: true,
},
},
{
id: "d535bd4f-8e24-4530-9499-db8d220a7267",
source: "528ba05d-9c5b-413e-a59f-7ca6914020c4",
target: "8f37b5f7-9cea-4085-89c9-bfa7db971e17",
type: "condition",
data: {},
},
{
id: "68d0415b-959d-42f3-be7e-9e70b4611929",
source: "8f37b5f7-9cea-4085-89c9-bfa7db971e17",
target: "2202a162-e008-4653-ab23-1b780c839a26",
type: "condition",
data: {
isAddButtonHidden: true,
},
},
{
id: "383b4cf8-e73f-416d-93e6-753f5f14d0cb",
source: "2202a162-e008-4653-ab23-1b780c839a26",
target: "3",
type: "condition",
data: {},
mergeNodeOfParentId: "528ba05d-9c5b-413e-a59f-7ca6914020c4",
},
],
};
const initialElements = [...data.layoutNodes, ...data.layoutEdges];
export { initialElements };

92
src/Data/Menu.jsx Normal file
View File

@@ -0,0 +1,92 @@
import { nanoid } from "nanoid";
// nội dung Menu
const MenuOptions = [
{
option: "rule",
title: "Rules",
list_option: [
{
key: "time-delay",
label: "Time delay",
id: nanoid(8),
type: "default",
icon: "./src/assets/images/icon-time.svg",
},
{
key: "waitThenCheck",
label: "Replied to conversation?",
id: nanoid(8),
type: "selectorNode",
icon: "./src/assets/images/icon-if.svg",
position: { x: 400, y: 290 },
},
{
key: "wait",
label: "Wait for trigger",
id: nanoid(8),
type: "default",
icon: "./src/assets/images/icon-wait.svg",
},
],
},
{
option: "action",
title: "Actions",
list_option: [
{
key: "email",
label: "Send email",
id: nanoid(8),
type: "default",
icon: "./src/assets/images/icon-email.svg",
},
{
key: "sms",
label: "Send sms",
id: nanoid(8),
type: "default",
icon: "./src/assets/images/icon-phone.svg",
},
{
key: "slack",
label: "Send Slack",
id: nanoid(8),
type: "default",
icon: "./src/assets/images/icon-slack.svg",
},
{
key: "zalo",
label: "Send Zalo",
id: nanoid(8),
type: "default",
icon: "./src/assets/images/icon-zalo.svg",
},
{
key: "survey-email",
label: "Send survey in email",
id: nanoid(8),
type: "default",
icon: "./src/assets/images/icon-list.svg",
},
{
key: "end",
branchKey: "yes",
label: "Contact Exits",
id: nanoid(8),
type: "output",
icon: "./src/assets/images/icon-check.svg",
},
{
key: "survey",
branchKey: "no",
label: "Send survey",
id: nanoid(8),
type: "output",
icon: "./src/assets/images/icon-list.svg",
},
],
},
];
export { MenuOptions };

85
src/Data/data.jsx Normal file
View File

@@ -0,0 +1,85 @@
import { nanoid } from "nanoid";
export const dataListFlow = [
{
id: nanoid(8),
name: "Tag added",
key: "tag",
icon: "./src/assets/images/icon-tag.svg",
description: `<ul>
<li>Send more relevant content</ li >
<li>Track people's interests</li>
<li> Turn new shoppers into regulars </li></ul>`,
},
{
id: nanoid(8),
name: "Signs up for Email",
key: "sign-up",
icon: "./src/assets/images/icon-signs.svg",
description: `<ul><li>Welcome new signups</li><li>Introduce your brand</li><li>Turn new contacts into new customers</li></ul>`,
},
{
id: nanoid(8),
name: "Birthday",
key: "birthday",
icon: "./src/assets/images/icon-birthday.svg",
description: `<ul class="description-1lOza"><li>Send a special discount</li><li>Offer a free gift</li><li>Just say 'happy birthday'</li></ul>`,
},
{
id: nanoid(8),
name: "Sent an email",
key: "send-an-email",
icon: "./src/assets/images/icon-email.svg",
description: "Contacts will enter the map when sent a bulk email.",
},
{
id: nanoid(8),
name: "Buys a specific product",
key: "buy-product",
icon: "./src/assets/images/icon-buy.svg",
description: "You havent connected your store.",
},
];
export const mainNode = [
{
key: "sign-up",
id: nanoid(8),
type: "input",
icon: "./src/assets/images/icon-signs.svg",
name: "Signs up for Email",
position: { x: 250, y: 100 },
},
{
id: nanoid(8),
key: "tag",
type: "input",
icon: "./src/assets/images/icon-tag.svg",
name: "Set a tag",
position: { x: 250, y: 100 },
},
{
id: nanoid(8),
key: "birthday",
type: "default",
icon: "./src/assets/images/icon-birthday.svg",
name: "Contact Birthday",
position: { x: 250, y: 100 },
},
{
id: nanoid(8),
key: "send-an-email",
type: "default",
icon: "./src/assets/images/icon-email.svg",
name: "Sent an email",
position: { x: 250, y: 100 },
},
{
id: nanoid(8),
key: "buy-product",
type: "default",
icon: "./src/assets/images/icon-buy.svg",
name: "Buys a specific product",
position: { x: 250, y: 100 },
},
];

71
src/Edges/Edges.jsx Normal file
View File

@@ -0,0 +1,71 @@
import EdgeAddButton from "../Buttons/EdgeAddButton/EdgeAddButton.jsx";
import "./Style.scss";
import {
getEdgeCenter,
getBezierPath,
getMarkerEnd,
} from "react-flow-renderer";
const [buttonWidth, buttonHeight] = [100, 40];
export const Condition = (props) => {
const {
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
arrowHeadType,
markerEndId,
data,
} = props;
const edgePath = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
const [edgeCenterX, edgeCenterY] = getEdgeCenter({
sourceX,
sourceY,
targetX,
targetY,
});
const { isAddButtonHidden } = data;
return (
<>
<path
id={id}
d={edgePath}
markerEnd={markerEnd}
className="react-flow__edge-path"
/>
{isAddButtonHidden ? null : (
<>
<foreignObject
width={buttonWidth}
height={buttonHeight}
x={edgeCenterX - buttonWidth / 2}
y={edgeCenterY - buttonHeight / 2}
requiredExtensions="http://www.w3.org/1999/xhtml"
>
<EdgeAddButton
{...props}
onClick={() => console.log("clicked")}
style={{ width: buttonWidth, height: buttonHeight }}
/>
</foreignObject>
</>
)}
</>
);
};

19
src/Edges/Style.scss Normal file
View File

@@ -0,0 +1,19 @@
.EdgeButton {
padding: 2px;
border: 0;
background-color: var(--color-base100);
cursor: pointer;
font-size: 12px;
text-align: center;
color: var(--color-accent);
transform: color 0.2s ease;
&:hover {
color: var(--color-accent700);
}
&[disabled] {
color: var(--color-base600);
cursor: not-allowed;
}
}

5
src/Edges/index.jsx Normal file
View File

@@ -0,0 +1,5 @@
import * as Edge from "./Edges";
export const edgeTypes = {
condition: Edge.Condition,
};

98
src/Nodes/Base.jsx Normal file
View File

@@ -0,0 +1,98 @@
/* eslint-disable default-case */
import * as colors from "@contactlab/ds-tokens/constants/colors";
import {
DatabaseOutlined,
MailOutlined,
CommentOutlined,
CheckCircleOutlined,
FlagOutlined,
CloseOutlined,
CheckCircleFilled,
SlackOutlined,
} from "@ant-design/icons";
import "./Style.scss";
export const BaseNode = ({
type,
data,
selected,
disabled,
onNodeClick,
onCloseIconClick,
additionalClassName,
}) => {
const content = (
<>
{getIconSrc(type)}
<div className="NodeContent">
<div className="NodeTitle">{data.title}</div>
</div>
</>
);
return (
<div
data-selected={selected}
aria-disabled={disabled}
className={`NodeInnerWrapper ${additionalClassName}`}
style={{ color: getColor(type) }}
{...(onNodeClick && { onClick: () => onNodeClick(type, data) })}
>
{content}
<CloseOutlined className="closeIcon" onClick={onCloseIconClick} />
</div>
);
};
export const EmptyBaseNode = () => {
return <div className="EmptyNodeInnerWrapper"></div>;
};
// --- Helpers
const getColor = (type) => {
switch (type) {
case "source":
return colors.success;
case "email":
return colors.accent;
case "sms":
return colors.accent;
case "slack":
return colors.accent;
case "zalo":
return colors.accent;
case "waitThenCheck":
return colors.warning;
case "end":
return colors.base;
default:
return colors.base;
}
};
const getIconSrc = (type) => {
const color = getColor(type);
switch (type) {
case "source":
return <DatabaseOutlined className="NodeIcon" style={{ color }} />;
case "email":
return <MailOutlined className="NodeIcon" style={{ color }} />;
case "sms":
return <CommentOutlined className="NodeIcon" style={{ color }} />;
case "slack":
return <SlackOutlined className="NodeIcon" style={{ color }} />;
case "zalo":
return (
<img
className="NodeIcon"
src="./src/assets/images/icon-zalo.svg"
style={{ width: 40, height: 40 }}
/>
);
case "waitThenCheck":
return <CheckCircleOutlined className="NodeIcon" style={{ color }} />;
case "end":
return <CheckCircleFilled className="NodeIcon" style={{ color }} />;
}
};

125
src/Nodes/Nodes.jsx Normal file
View File

@@ -0,0 +1,125 @@
import React from "react";
import { Handle, Position } from "reactflow";
import { BaseNode, EmptyBaseNode } from "./Base";
// import { Handle, Position } from "react-flow-renderer";
import "./Style.scss";
const handleNodeClick = (props) => {
const { data, id } = props;
data.onNodeClickCallback(id);
};
const onCloseIconClick = (event, props) => {
event.stopPropagation();
const { data, id } = props;
data.onDeleteNodeCallback(id);
};
export const Source = (props) => (
<div className="NodeWrapper">
<BaseNode
{...props}
onNodeClick={() => handleNodeClick(props)}
onCloseIconClick={(event) => onCloseIconClick(event, props)}
/>
<Handle type="source" position={Position.Bottom} className="NodePort" />
</div>
);
export const Action = (props) => (
<div className="NodeWrapper">
<Handle type="target" position={Position.Top} className="NodePort" />
<BaseNode
{...props}
onNodeClick={() => handleNodeClick(props)}
onCloseIconClick={(event) => onCloseIconClick(event, props)}
/>
<Handle type="source" position={Position.Bottom} className="NodePort" />
</div>
);
export const Condition = (props) => (
<div className="NodeWrapper">
<Handle type="target" position={Position.Top} className="NodePort" />
<BaseNode
{...props}
additionalClassName="ConditionNode"
onNodeClick={() => handleNodeClick(props)}
onCloseIconClick={(event) => onCloseIconClick(event, props)}
/>
<Handle
id="yes"
type="source"
position={Position.Bottom}
style={{ bottom: 0, left: 45, background: "#555" }}
className="NodePort"
/>
<div
style={{
position: "absolute",
left: 30,
bottom: -9,
background: "#000",
color: "#fff",
fontSize: 11,
paddingLeft: 5,
paddingRight: 5,
paddingTop: 3,
paddingBottom: 3,
borderRadius: 5,
}}
>
Yes
</div>
<Handle
id="no"
type="source"
position={Position.Bottom}
style={{ bottom: 0, left: 220, background: "#555" }}
className="NodePort"
/>
<div
style={{
position: "absolute",
right: 25,
bottom: -10,
background: "#000",
color: "#fff",
paddingLeft: 5,
paddingRight: 5,
paddingTop: 3,
paddingBottom: 3,
fontSize: 11,
borderRadius: 5,
}}
>
No
</div>
</div>
);
export const End = (props) => (
<div className="NodeWrapper">
<Handle type="target" position={Position.Top} className="NodePort" />
<BaseNode {...props} disabled={true} />
</div>
);
export const Empty = (props) => (
<div className="NodeWrapper">
<Handle
type="target"
position={Position.Top}
className="NodePort"
// style={{ opacity: 0 }}
/>
<EmptyBaseNode {...props} disabled={true} />
<Handle
type="source"
position={Position.Bottom}
className="NodePort"
style={{ opacity: 0 }}
/>
</div>
);

88
src/Nodes/Style.scss Normal file
View File

@@ -0,0 +1,88 @@
.NodeWrapper {
width: 250px;
display: flex;
flex-direction: column;
align-items: center;
}
.NodeInnerWrapper {
position: relative;
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
border-left-width: 3px;
border-left-style: solid;
border-left-color: currentColor;
border-radius: 2px;
background-color: var(--color-white);
box-shadow: var(--elevation-z1);
transition: box-shadow 0.2s ease-in-out;
cursor: pointer;
width: 100%;
height: 50px;
&[aria-disabled="true"] {
cursor: not-allowed;
}
&:not([aria-disabled="true"]):hover {
box-shadow: var(--elevation-z2);
}
&:not([aria-disabled="true"])[data-selected="true"] {
box-shadow:
inset 0px 0px 0px 1px currentColor,
var(--elevation-z3);
}
&.ConditionNode {
// width: 350px;
}
> .closeIcon {
position: absolute;
top: 5px;
right: 5px;
> svg {
color: red;
}
}
}
.EmptyNodeInnerWrapper {
width: 1px;
height: 1px;
background-color: var(--edges-stroke-color);
}
.NodeContent {
display: flex;
flex-direction: column;
}
.NodeIcon {
font-size: 18px;
padding: 4px;
}
.NodeTitle {
color: var(--color-base900);
font-size: 14px;
text-transform: upppercase;
}
.NodeDesc {
color: var(--color-base700);
font-size: 12px;
}
.NodePort {
&.react-flow__handle {
border: 0;
background-color: var(--edges-stroke-color);
}
// &[data-handlepos="bottom"] {
// width: 1px;
// background-color: var(--edges-stroke-color);
// }
}

12
src/Nodes/index.jsx Normal file
View File

@@ -0,0 +1,12 @@
import * as Node from "./Nodes";
export const nodeTypes = {
source: Node.Source,
email: Node.Action,
sms: Node.Action,
slack: Node.Action,
zalo: Node.Action,
waitThenCheck: Node.Condition,
end: Node.End,
empty: Node.Empty,
};

View File

@@ -0,0 +1,61 @@
import { useState, useEffect } from "react";
import { dataListFlow } from "../Data/data";
const PopupListFlow = ({ hidePopup, onSelectFlow }) => {
const [showPopup, setShowPopup] = useState(true);
const handleOverlayClick = () => {
setShowPopup(false);
};
const handleFlowClick = (flow) => {
onSelectFlow(flow); // Gọi callback với flow đã chọn
setShowPopup(false); // Ẩn popup
};
useEffect(() => {
if (!showPopup) {
hidePopup(); // Đảm bảo khi popup bị ẩn, nó được xử lý ở component cha
}
}, [showPopup, hidePopup]);
return (
<>
<div className="popup-overlay" onClick={handleOverlayClick}></div>
<div className="popup-list-flow ">
<div className="popup-content">
<button onClick={handleOverlayClick} className="close">
x
</button>
<h3 className="title">
These starting points are popular with other Mailchimp customers.
</h3>
<p className="note">
Some starting points are coming soon or require additional setup.
</p>
<div className="ListNodeMain d-flex flex-wrap">
{dataListFlow.map((option) => (
<div
className="item-node"
key={option.id}
onClick={() => handleFlowClick(option.key)}
>
<div className="triggerName d-flex align-items">
<img className="icon" src={option.icon} alt={option.name} />
<p className="name">{option.name}</p>
</div>
<div
className="description"
dangerouslySetInnerHTML={{ __html: option.description }}
/>
</div>
))}
</div>
</div>
</div>
</>
);
};
export default PopupListFlow;

17
src/RouterFlow.jsx Normal file
View File

@@ -0,0 +1,17 @@
import React from "react";
import { Routes, Route } from "react-router-dom";
import CreateFlow from "./CreateFlow";
import BuildFlow from "./BuildFlow";
function RouterFlow() {
return (
<div>
<Routes>
<Route path="/" element={<CreateFlow />} />
<Route path="/build" element={<BuildFlow />} />
</Routes>
</div>
);
}
export default RouterFlow;

57
src/Sidebar/Sidebar.css Normal file
View File

@@ -0,0 +1,57 @@
.dndflow {
flex-direction: column;
display: flex;
flex-grow: 1;
height: 100%;
}
.dndflow aside {
border-right: 1px solid #eee;
padding: 15px 10px;
font-size: 12px;
background: #fcfcfc;
}
.dndflow aside .description {
margin-bottom: 10px;
}
.dndflow .dndnode {
height: 20px;
padding: 4px;
border: 1px solid #1a192b;
border-radius: 2px;
margin-bottom: 10px;
display: flex;
justify-content: center;
align-items: center;
cursor: grab;
}
.dndflow .dndnode.input {
border-color: #0041d0;
}
.dndflow .dndnode.output {
border-color: #ff0072;
}
.dndflow {
flex-grow: 1;
height: 100vh;
}
.dndflow .selectall {
margin-top: 10px;
}
@media screen and (min-width: 768px) {
.dndflow {
flex-direction: row;
}
.dndflow aside {
width: 20%;
max-width: 250px;
}
}

44
src/Sidebar/Sidebar.jsx Normal file
View File

@@ -0,0 +1,44 @@
// =================================================================================================
import React from "react";
import { MenuOptions } from "../Data/Menu";
const Sidebar = ({ onAddNodeCallback }) => {
// const onDragStart = (event, nodeType) => {
// event.dataTransfer.setData("application/reactflow", nodeType);
// };
const onDragStart = (event, nodeType) => {
// event.dataTransfer.setData("application/reactflow", "custom"); // Set a custom dataTransfer type
event.dataTransfer.setData("nodeType", nodeType); // Store the node type in a custom data attribute
};
return (
<div className="menu-fixed">
{MenuOptions.map((option) => (
<div className="item-flow" key={option.option}>
<b>{option.title}</b>
<div
className={`list-option ${option.option == "rule" ? "flex" : ""}`}
>
{option.list_option.map((items) => (
<div
className="items"
draggable="true"
key={items.id}
onDragStart={(event) => onDragStart(event, items.key)}
>
<div className="step">
<div className="left">
<img src={items.icon} width="24px" height="24px" alt="" />
</div>
<div className="right">{items.label}</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
);
};
export default Sidebar;

View File

@@ -0,0 +1,224 @@
import { v4 as uuidv4 } from "uuid";
import _ from "lodash";
import { MenuOptions } from "../Data/Menu";
const position = { x: 0, y: 0 };
const getTitleAndDescription = (type) => {
let title = [];
MenuOptions.forEach((option) => {
option.list_option.forEach((item) => {
if (type === item.key) {
title.push(item.label);
}
});
});
return { title };
};
const getUpdatedElementsAfterActionNodeAddition = ({
elements,
newNodeId,
targetNodeId,
onAddNodeCallback,
}) => {
const clonedElements = _.cloneDeep(elements);
const newEdge = {
id: uuidv4(),
source: newNodeId,
target: targetNodeId,
type: "condition",
data: { onAddNodeCallback },
};
clonedElements.push(newEdge);
return clonedElements;
};
const getUpdatedElementsAfterEndNodeAddition = () => {};
const getUpdatedElementsAfterRuleNodeAdditon = ({
elements,
newNodeId,
targetNodeId,
onAddNodeCallback,
}) => {
const clonedElements = _.cloneDeep(elements);
const emptyNode1Id = uuidv4();
const emptyNode2Id = uuidv4();
const endNodeId = uuidv4();
// Define the end node for 'no' branch
const endNode = {
id: endNodeId,
type: "end",
data: {
title: "End",
description: "Automation ends.",
stats: {
completed: 14,
},
},
position, // Node end sẽ ở vị trí của nhánh no
};
// Move the existing target node to the 'yes' branch
const ruleNodeToYesBranchEdge = {
id: uuidv4(),
source: newNodeId,
target: targetNodeId, // Existing target node moves to yes branch
type: "condition",
sourceHandle: "yes",
data: { onAddNodeCallback },
};
// Connect the rule node to the empty node in 'no' branch, which ends in the end node
const ruleNodeToNoBranchEdge = {
id: uuidv4(),
source: newNodeId,
target: endNodeId, // This goes to the 'no' branch
type: "condition",
sourceHandle: "no",
data: { onAddNodeCallback },
};
// Add everything to cloned elements
clonedElements.push(ruleNodeToYesBranchEdge, ruleNodeToNoBranchEdge, endNode);
return clonedElements;
};
const getUpdatedElementsAfterNodeAddition = ({
elements,
targetEdgeId,
type,
onDeleteNodeCallback,
onNodeClickCallback,
onAddNodeCallback,
position,
}) => {
const newNodeId = uuidv4();
const { title } = getTitleAndDescription(type);
const newNode = {
id: newNodeId,
type,
data: {
title,
onNodeClickCallback,
onDeleteNodeCallback,
},
position,
};
const clonedElements = _.cloneDeep(elements);
const targetEdgeIndex = clonedElements.findIndex(
(x) => x.id === targetEdgeId
);
if (targetEdgeIndex === -1) {
console.error("Target edge is undefined.");
return elements;
}
const targetEdge = elements[targetEdgeIndex];
// Check if targetEdge is defined before accessing its properties
if (targetEdge) {
const { target: targetNodeId } = targetEdge;
const updatedTargetEdge = { ...targetEdge, target: newNodeId };
clonedElements[targetEdgeIndex] = updatedTargetEdge;
clonedElements.push(newNode);
console.log(position);
console.log(updatedTargetEdge);
switch (type) {
case "end":
// return getUpdatedElementsAfterEndNodeAddition();
return getUpdatedElementsAfterActionNodeAddition({
elements: clonedElements,
newNodeId,
newNode,
targetNodeId,
onAddNodeCallback,
});
case "waitThenCheck":
return getUpdatedElementsAfterRuleNodeAdditon({
elements: clonedElements,
newNodeId,
targetNodeId,
onAddNodeCallback,
});
default:
return getUpdatedElementsAfterActionNodeAddition({
elements: clonedElements,
newNodeId,
newNode,
targetNodeId,
onAddNodeCallback,
});
}
} else {
// Handle the case when targetEdge is undefined
console.error("Target edge is undefined.");
return elements; // Return the original elements array
}
};
// const getUpdatedElementsAfterNodeAddition = ({
// elements,
// targetEdgeId,
// type,
// onDeleteNodeCallback,
// onNodeClickCallback,
// onAddNodeCallback,
// }) => {
// const newNodeId = uuidv4();
// const { title, description } = getTitleAndDescription(type);
// const newNode = {
// id: newNodeId,
// type,
// data: {
// title,
// description,
// onNodeClickCallback,
// onDeleteNodeCallback,
// },
// position,
// };
// const clonedElements = _.cloneDeep(elements);
// const targetEdgeIndex = clonedElements.findIndex(
// (x) => x.id === targetEdgeId
// );
// const targetEdge = elements[targetEdgeIndex];
// const { target: targetNodeId } = targetEdge;
// const updatedTargetEdge = { ...targetEdge, target: newNodeId };
// clonedElements[targetEdgeIndex] = updatedTargetEdge;
// clonedElements.push(newNode);
// switch (type) {
// case "end":
// return getUpdatedElementsAfterEndNodeAddition();
// case "waitThenCheck":
// return getUpdatedElementsAfterRuleNodeAdditon({
// elements: clonedElements,
// newNodeId,
// targetNodeId,
// onAddNodeCallback,
// });
// default:
// return getUpdatedElementsAfterActionNodeAddition({
// elements: clonedElements,
// newNodeId,
// newNode,
// targetNodeId,
// onAddNodeCallback,
// });
// }
// };
export { getUpdatedElementsAfterNodeAddition };

View File

@@ -0,0 +1,45 @@
import dagre from "dagre";
import _ from "lodash";
import { isNode } from "reactflow";
const nodeWidth = 250;
const nodeHeight = 80;
const getLayoutedElements = (_elements) => {
const elements = _.cloneDeep(_elements);
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
dagreGraph.setGraph({ rankdir: "TB" });
elements.forEach((el) => {
if (isNode(el)) {
dagreGraph.setNode(el.id, {
width: el.width || nodeWidth,
height: el.height || nodeHeight,
});
} else {
dagreGraph.setEdge(el.source, el.target);
}
});
dagre.layout(dagreGraph);
return elements.map((el) => {
if (isNode(el)) {
const nodeWithPosition = dagreGraph.node(el.id);
el.targetPosition = "top";
el.sourcePosition = "bottom";
el.position = {
x:
nodeWithPosition.x -
(el.width || nodeWidth) / 2 +
Math.random() / 1000,
y: nodeWithPosition.y - (el.height || nodeHeight) / 2,
};
}
return el;
});
};
export { getLayoutedElements };

226
src/assets/css/style.css Normal file
View File

@@ -0,0 +1,226 @@
.container {
width: 1200px;
margin: 0 auto;
padding: 0 10px;
}
.content-create {
width: 640px;
margin: 0 auto;
}
.content-create .title {
text-align: center;
font-size: 20px;
}
.content-create .note {
text-align: center;
color: gray;
}
.style-form {
border: 1px solid rgba(36, 28, 21, 0.15);
border-radius: 4px;
-webkit-box-shadow: 0px 12px 24px rgba(36, 28, 21, 0.16);
box-shadow: 0px 12px 24px rgba(36, 28, 21, 0.16);
padding: 60px;
}
.style-form label {
font-weight: 600;
display: block;
margin-bottom: 10px;
}
.style-form input {
-webkit-box-flex: 1;
background-color: transparent;
border: 1px solid #222;
outline: none;
padding: 10px 16px;
width: 100%;
border-radius: 5px;
}
.style-form .btn-submit {
background-color: #007c89;
color: #fff;
text-decoration: none;
text-align: center;
margin: 20px auto;
width: 120px;
height: 45px;
border-radius: 50px;
outline: none;
border: 0;
display: block;
}
.content-create .image-header {
width: 200px;
margin-top: 30px;
text-align: center;
margin: 0 auto;
display: block;
}
.content-start {
width: 500px;
margin: 0 auto;
}
.content-start .imageStart {
width: 200px;
display: block;
margin: 20px auto 10px auto;
}
.content-start h2 {
display: block;
font-size: 20px;
text-align: center;
}
.content-start .note {
font-style: italic;
text-align: center;
color: gray;
}
.btn-start {
display: block;
position: relative;
margin: 30px auto 0 auto;
width: 300px;
height: 50px;
background: #007c89;
color: #fff;
border-radius: 50px;
outline: none;
border: 0;
cursor: pointer;
font-weight: 700;
}
.btn-start::after {
position: absolute;
content: "";
left: 50%;
bottom: -50px;
width: 2px;
height: 50px;
background: #222;
}
.popup-list-flow {
left: 50%;
position: fixed;
width: 1000px;
top: 50%;
transform: translate(-50%, -50%);
z-index: 1000;
}
.popup-list-flow .popup-content {
background: #efeeea;
position: relative;
padding: 0;
overflow-x: hidden;
}
.popup-list-flow .close {
position: absolute;
top: 10px;
right: 10px;
background: transparent;
font-size: 25px;
outline: 0;
border: 0;
cursor: pointer;
}
.popup-list-flow .title,
.popup-list-flow .note {
display: block;
margin: 0;
padding-left: 20px;
}
.popup-list-flow .title {
padding-top: 20px;
}
.d-flex {
display: flex;
}
.flex-wrap {
flex-wrap: wrap;
}
.align-items {
align-items: center;
}
.ListNodeMain {
padding: 20px;
margin-right: -16px;
}
.ListNodeMain .item-node {
width: calc(100% / 3 - 16px);
background: #fff;
border: 2px solid rgba(36, 28, 21, 0.15);
border-radius: 8px;
cursor: pointer;
padding: 20px;
margin-right: 16px;
margin-bottom: 16px;
}
.ListNodeMain .item-node .icon {
width: 24px;
height: 24px;
margin-right: 10px;
}
.ListNodeMain .triggerName {
margin-bottom: 10px;
}
.ListNodeMain .item-node .name {
font-weight: 700;
}
.ListNodeMain .item-node .description {
font-size: 11px;
line-height: 20px;
}
.ListNodeMain .item-node ul li {
margin-left: 20px;
}
.popup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.popup-header {
border-bottom: 1px solid rgba(36, 28, 21, 0.15);
padding: 10px 20px;
position: relative;
}
.popup-header .close-popup {
position: absolute;
right: 10px;
top: 0;
font-size: 25px;
font-weight: 600;
cursor: pointer;
}
.popup-footer {
border-top: 1px solid rgba(36, 28, 21, 0.15);
padding: 12px 16px;
text-align: right;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true" class="wink-icon iconPositioning-2AitU icon-3YgtG"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 1l1.106 2.211a1.236 1.236 0 11-2.212 0L8 1zm4 0l1.106 2.211a1.236 1.236 0 11-2.212 0L12 1zM7 9V6h2v3h2V6h2v3h2V6h2v3h3.75A2.25 2.25 0 0123 11.25V14a3.99 3.99 0 01-1 2.646V22a1 1 0 01-1 1H3a1 1 0 01-1-1v-5.354A3.985 3.985 0 011 14v-2.75A2.25 2.25 0 013.25 9H7zM4 21h16v-3.126A4.01 4.01 0 0115.535 16h-.07A3.998 3.998 0 0112 18a3.998 3.998 0 01-3.465-2h-.07A3.998 3.998 0 014 17.874V21zM17.106 3.211L16 1l-1.106 2.211a1.236 1.236 0 102.212 0zM7 14a2 2 0 11-4 0v-2.75a.25.25 0 01.25-.25h17.5a.25.25 0 01.25.25V14a2 2 0 11-4 0h-3a2 2 0 11-4 0H7z"></path></svg>

After

Width:  |  Height:  |  Size: 756 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true" class="wink-icon iconPositioning-2AitU icon-3YgtG" disabled=""><path fill-rule="evenodd" clip-rule="evenodd" d="M14 8V5a4 4 0 10-8 0v3H1v15h22V8h-4V5a4 4 0 00-5.076-3.854c.518.527.93 1.158 1.203 1.858A2 2 0 0117 5v3h-3zm-4-5a2 2 0 00-2 2v3h4V5a2 2 0 00-2-2zm7 7v11H3V10h3v2h2v-2h4v2h2v-2h3z"></path></svg>

After

Width:  |  Height:  |  Size: 402 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg>

After

Width:  |  Height:  |  Size: 447 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true" class="wink-icon icon-3SKgm"><path fill-rule="evenodd" clip-rule="evenodd" d="M21 3H3a2 2 0 00-2 2v14a2 2 0 002 2h18a2 2 0 002-2V5a2 2 0 00-2-2zM3 5h18v.57L12.101 14h-.203L3 5.57V5zm0 3.325v7.891l4.219-3.894L3 8.325zM3 19v-.062L8.674 13.7l2.428 2.3h1.796l2.428-2.3L21 18.938V19H3zm18-2.784V8.325l-4.219 3.997L21 16.216z"></path></svg>

After

Width:  |  Height:  |  Size: 431 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true" class="wink-icon exitIndicatorIcon-1gdYp"><path d="M7 2a2 2 0 00-2 2v5h2V4h12v16H7v-5H5v5a2 2 0 002 2h12a2 2 0 002-2V4a2 2 0 00-2-2H7z"></path><path d="M11.743 6.331l-1.486 1.338 3.181 3.535L2 11v2l11.438-.204-3.181 3.535 1.486 1.338L16.845 12l-5.102-5.669z"></path></svg>

After

Width:  |  Height:  |  Size: 369 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true" role="presentation" class="wink-icon icon-ECm3x"><path d="M13 2v2c0 1.316.323 2.097.738 2.65.443.591 1.045 1.004 1.817 1.518l.05.033c.719.48 1.591 1.06 2.258 1.949.71.947 1.137 2.166 1.137 3.85v2h3l-4 6-4-6h3v-2c0-1.316-.323-2.097-.738-2.65-.443-.591-1.045-1.004-1.817-1.518l-.05-.033c-.719-.48-1.591-1.06-2.258-1.949C11.427 6.903 11 5.684 11 4V2h2zM9.685 9.291c.184-.173.372-.353.562-.544L8.832 7.332c-.167.169-.34.335-.517.502-.121.114-.246.228-.372.345C6.585 9.43 5 10.89 5 13.667V16H2l4 6 4-6H7v-2.333c0-1.873.95-2.758 2.347-4.06l.338-.316z"></path></svg>

After

Width:  |  Height:  |  Size: 656 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 144a48 48 0 1 0 0-96 48 48 0 1 0 0 96zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L192 64zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-288 0zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-288 0zM64 464a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm48-208a48 48 0 1 0 -96 0 48 48 0 1 0 96 0z"/></svg>

After

Width:  |  Height:  |  Size: 658 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true" role="presentation"
class="wink-icon icon-ECm3x">
<path d="M13 19a1 1 0 11-2 0 1 1 0 012 0z"></path>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M6 1a2 2 0 00-2 2v18a2 2 0 002 2h12a2 2 0 002-2V3a2 2 0 00-2-2H6zm3 2H6v18h12V3h-3v1a1 1 0 01-1 1h-4a1 1 0 01-1-1V3z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 404 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true" class="wink-icon iconPositioning-2AitU icon-3YgtG"><g clip-path="url(#svg__d4767__clip0)"><path fill-rule="evenodd" clip-rule="evenodd" d="M15 8.5c0 3.13-1.547 5.814-3.94 6.937.219.734 1.01 1.027 1.897 1.356C14.359 17.313 16 17.922 16 20.5v2H1v-2c0-2.578 1.641-3.187 3.043-3.707.887-.329 1.678-.622 1.897-1.356C3.547 14.314 2 11.63 2 8.5 2 4.358 4.364 1 8.5 1S15 4.358 15 8.5zM8.5 14c2.39 0 4.5-2.14 4.5-5.5 0-1.71-.489-3.108-1.248-4.039C11.027 3.573 9.972 3 8.5 3c-1.473 0-2.527.573-3.252 1.461C4.488 5.391 4 6.791 4 8.5 4 11.86 6.11 14 8.5 14z"></path><path d="M21 4v3h3v2h-3v3h-2V9h-3V7h3V4h2z"></path></g><defs><clipPath id="svg__d4767__clip0"><path d="M0 0h24v24H0z"></path></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 800 B

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 31.444 31.443"
xml:space="preserve">
<g>
<path d="M31.202,16.369c-0.62-1.388-2.249-2.011-3.637-1.391l-1.325,0.594l-3.396-7.591l1.325-0.592
c1.388-0.622,2.01-2.25,1.389-3.637c-0.62-1.389-2.248-2.012-3.637-1.39l-1.324,0.593l-0.593-1.326
c-0.621-1.388-2.249-2.009-3.637-1.388c-1.388,0.62-2.009,2.247-1.389,3.637l0.593,1.325L7.98,8.598L7.388,7.273
c-0.621-1.39-2.249-2.009-3.637-1.39C2.363,6.504,1.742,8.132,2.362,9.52l0.592,1.324L1.63,11.438
c-1.388,0.621-2.01,2.247-1.389,3.636c0.62,1.388,2.249,2.01,3.637,1.39l1.325-0.594l3.394,7.592l-1.325,0.592
c-1.388,0.621-2.009,2.25-1.389,3.637c0.621,1.389,2.249,2.011,3.637,1.391l1.324-0.593l0.593,1.325
c0.621,1.389,2.249,2.01,3.637,1.389c1.387-0.62,2.009-2.248,1.388-3.636l-0.591-1.326l7.591-3.394l0.592,1.321
c0.621,1.391,2.248,2.013,3.637,1.392c1.388-0.619,2.01-2.248,1.389-3.637l-0.592-1.324l1.323-0.594
C31.201,19.384,31.823,17.757,31.202,16.369z M13.623,21.215l-3.395-7.593l7.591-3.394l3.395,7.591L13.623,21.215z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" focusable="false" aria-hidden="true" class="wink-icon iconPositioning-2AitU icon-3YgtG"><path d="M8 10a2 2 0 100-4 2 2 0 000 4z"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M12.414 2H3.586L2 3.586v8.828l11 11L23.414 13l-11-11zm-8 2L4 4.414v7.172l9 9L20.586 13l-9-9H4.414z"></path></svg>

After

Width:  |  Height:  |  Size: 358 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true" role="presentation" class="wink-icon icon-ECm3x"><path d="M11 5v7.5a1 1 0 00.486.857l5 3 1.029-1.714L13 11.933V5h-2z"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M23 12c0 6.075-4.925 11-11 11S1 18.075 1 12 5.925 1 12 1s11 4.925 11 11zm-2 0a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="28" viewBox="0 0 448 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512l388.6 0c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304l-91.4 0z"/></svg>

After

Width:  |  Height:  |  Size: 437 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true" role="presentation" class="wink-icon icon-ECm3x"><path d="M11 8H9v8h2V8zm4 0h-2v8h2V8z"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M23 12c0 6.075-4.925 11-11 11S1 18.075 1 12 5.925 1 12 1s11 4.925 11 11zm-2 0a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>

After

Width:  |  Height:  |  Size: 361 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true" class="wink-icon"><path d="M14 5a2 2 0 11-4 0 2 2 0 014 0zm0 7a2 2 0 11-4 0 2 2 0 014 0zm-2 9a2 2 0 100-4 2 2 0 000 4z"></path></svg>

After

Width:  |  Height:  |  Size: 230 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"><path d="M 9 4 C 6.2504839 4 4 6.2504839 4 9 L 4 41 C 4 43.749516 6.2504839 46 9 46 L 41 46 C 43.749516 46 46 43.749516 46 41 L 46 9 C 46 6.2504839 43.749516 4 41 4 L 9 4 z M 9 6 L 15.580078 6 C 12.00899 9.7156859 10 14.518083 10 19.5 C 10 24.66 12.110156 29.599844 15.910156 33.339844 C 16.030156 33.549844 16.129922 34.579531 15.669922 35.769531 C 15.379922 36.519531 14.799687 37.499141 13.679688 37.869141 C 13.249688 38.009141 12.97 38.430859 13 38.880859 C 13.03 39.330859 13.360781 39.710781 13.800781 39.800781 C 16.670781 40.370781 18.529297 39.510078 20.029297 38.830078 C 21.379297 38.210078 22.270625 37.789609 23.640625 38.349609 C 26.440625 39.439609 29.42 40 32.5 40 C 36.593685 40 40.531459 39.000731 44 37.113281 L 44 41 C 44 42.668484 42.668484 44 41 44 L 9 44 C 7.3315161 44 6 42.668484 6 41 L 6 9 C 6 7.3315161 7.3315161 6 9 6 z M 33 15 C 33.55 15 34 15.45 34 16 L 34 25 C 34 25.55 33.55 26 33 26 C 32.45 26 32 25.55 32 25 L 32 16 C 32 15.45 32.45 15 33 15 z M 18 16 L 23 16 C 23.36 16 23.700859 16.199531 23.880859 16.519531 C 24.050859 16.829531 24.039609 17.219297 23.849609 17.529297 L 19.800781 24 L 23 24 C 23.55 24 24 24.45 24 25 C 24 25.55 23.55 26 23 26 L 18 26 C 17.64 26 17.299141 25.800469 17.119141 25.480469 C 16.949141 25.170469 16.960391 24.780703 17.150391 24.470703 L 21.199219 18 L 18 18 C 17.45 18 17 17.55 17 17 C 17 16.45 17.45 16 18 16 z M 27.5 19 C 28.11 19 28.679453 19.169219 29.189453 19.449219 C 29.369453 19.189219 29.65 19 30 19 C 30.55 19 31 19.45 31 20 L 31 25 C 31 25.55 30.55 26 30 26 C 29.65 26 29.369453 25.810781 29.189453 25.550781 C 28.679453 25.830781 28.11 26 27.5 26 C 25.57 26 24 24.43 24 22.5 C 24 20.57 25.57 19 27.5 19 z M 38.5 19 C 40.43 19 42 20.57 42 22.5 C 42 24.43 40.43 26 38.5 26 C 36.57 26 35 24.43 35 22.5 C 35 20.57 36.57 19 38.5 19 z M 27.5 21 C 27.39625 21 27.29502 21.011309 27.197266 21.03125 C 27.001758 21.071133 26.819727 21.148164 26.660156 21.255859 C 26.500586 21.363555 26.363555 21.500586 26.255859 21.660156 C 26.148164 21.819727 26.071133 22.001758 26.03125 22.197266 C 26.011309 22.29502 26 22.39625 26 22.5 C 26 22.60375 26.011309 22.70498 26.03125 22.802734 C 26.051191 22.900488 26.079297 22.994219 26.117188 23.083984 C 26.155078 23.17375 26.202012 23.260059 26.255859 23.339844 C 26.309707 23.419629 26.371641 23.492734 26.439453 23.560547 C 26.507266 23.628359 26.580371 23.690293 26.660156 23.744141 C 26.819727 23.851836 27.001758 23.928867 27.197266 23.96875 C 27.29502 23.988691 27.39625 24 27.5 24 C 27.60375 24 27.70498 23.988691 27.802734 23.96875 C 28.487012 23.82916 29 23.22625 29 22.5 C 29 21.67 28.33 21 27.5 21 z M 38.5 21 C 38.39625 21 38.29502 21.011309 38.197266 21.03125 C 38.099512 21.051191 38.005781 21.079297 37.916016 21.117188 C 37.82625 21.155078 37.739941 21.202012 37.660156 21.255859 C 37.580371 21.309707 37.507266 21.371641 37.439453 21.439453 C 37.303828 21.575078 37.192969 21.736484 37.117188 21.916016 C 37.079297 22.005781 37.051191 22.099512 37.03125 22.197266 C 37.011309 22.29502 37 22.39625 37 22.5 C 37 22.60375 37.011309 22.70498 37.03125 22.802734 C 37.051191 22.900488 37.079297 22.994219 37.117188 23.083984 C 37.155078 23.17375 37.202012 23.260059 37.255859 23.339844 C 37.309707 23.419629 37.371641 23.492734 37.439453 23.560547 C 37.507266 23.628359 37.580371 23.690293 37.660156 23.744141 C 37.739941 23.797988 37.82625 23.844922 37.916016 23.882812 C 38.005781 23.920703 38.099512 23.948809 38.197266 23.96875 C 38.29502 23.988691 38.39625 24 38.5 24 C 38.60375 24 38.70498 23.988691 38.802734 23.96875 C 39.487012 23.82916 40 23.22625 40 22.5 C 40 21.67 39.33 21 38.5 21 z"/></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

106
src/index.scss Normal file
View File

@@ -0,0 +1,106 @@
/* prettier-ignore */
html,
body,
#root {
height: 100%;
margin: 0;
}
:root {
/* 1. The Contactlab Marketing Cloud color palette */
--color-accent: #1890ff;
--color-accent100: #e6f7ff;
--color-accent200: #bae7ff;
--color-accent300: #91d5ff;
--color-accent400: #69c0ff;
--color-accent500: #40a9ff;
--color-accent600: #1890ff;
--color-accent700: #096dd9;
--color-accent800: #0050b3;
--color-accent900: #003a8c;
--color-base: #141414;
--color-base100: #fafafa;
--color-base200: #f5f5f5;
--color-base300: #f0f0f0;
--color-base400: #d9d9d9;
--color-base500: #bfbfbf;
--color-base600: #8c8c8c;
--color-base700: #595959;
--color-base800: #434343;
--color-base900: #141414;
--color-info: #1890ff;
--color-success: #52c41a;
--color-warning: #faad14;
--color-error: #f5222d;
--color-white: #fff;
--color-black: #000;
--color-fluo-base300: #e9e8ff;
--color-fluo-base600: #b4afff;
--color-fluo-base900: #7168ff;
--color-fluo-accent300: #e1f9ef;
--color-fluo-accent600: #9bebd0;
--color-fluo-accent900: #00c9bd;
/* 2. The Contactlab Marketing Cloud elevations */
--elevation-z1: 0 1px 3px 0 rgba(0, 20, 32, 0.12);
--elevation-z2: 0 3px 6px 0 rgba(0, 20, 32, 0.04), 0 3px 6px 0 rgba(0, 20, 32, 0.12);
--elevation-z3: 0 6px 6px 0 rgba(0, 20, 32, 0.09), 0 10px 20px 0 rgba(0, 20, 32, 0.13);
--elevation-z4: 0 10px 10px 0 rgba(0, 20, 32, 0.05), 0 14px 28px 0 rgba(0, 20, 32, 0.16);
--elevation-z5: 0 19px 38px 0 rgba(0, 20, 32, 0.16), 0 15px 12px 0 rgba(0, 20, 32, 0.12);
--elevation-accent-z1: 0 1px 3px 0 rgba(3, 145, 236, 0.12);
--elevation-accent-z2: 0 3px 6px 0 rgba(3, 145, 236, 0.04), 0 3px 6px 0 rgba(3, 145, 236, 0.12);
--elevation-accent-z3: 0 6px 6px 0 rgba(3, 145, 236, 0.09), 0 10px 20px 0 rgba(3, 145, 236, 0.13);
--elevation-accent-z4: 0 10px 10px 0 rgba(3, 145, 236, 0.05), 0 14px 28px 0 rgba(3, 145, 236, 0.16);
--elevation-accent-z5: 0 19px 38px 0 rgba(3, 145, 236, 0.16), 0 15px 12px 0 rgba(3, 145, 236, 0.12);
/* 3. The Contactlab Marketing Cloud transitions */
--elevation-transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
/* 4. Document styles */
--mc-body-foreground: var(--color-base);
--mc-body-background: var(--color-base100);
--mc-scroll-padding: 100px;
--mc-scroll-behavior: smooth;
/* 5. a11y */
--mc-selection-foreground: var(--color-white);
--mc-selection-background: rgba(24, 144, 255, 0.8);
/* 6. Headings */
--mc-heading-foreground: var(--color-base);
--mc-subheading-foreground: var(--color-base800);
/* 7. Links */
--mc-link-foreground: var(--color-accent);
--mc-link-active-foreground: var(--color-accent500);
/* 8. <code> */
--mc-code-border-radius: 2px;
--mc-code-background: var(--color-base300);
/* 9. <hr> */
--mc-hr-background: var(--color-base300);
/* 10. <mark> */
--mc-mark-foreground: var(--color-white);
--mc-mark-background: var(--color-accent);
/* 11. <small> */
--mc-small-foreground: var(--color-base600);
}
.App {
display: flex;
justify-content: center;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
}

21
src/main.jsx Normal file
View File

@@ -0,0 +1,21 @@
import React from "react";
import ReactDOM from "react-dom/client"; // Sử dụng từ react-dom/client trong React 18
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import RouterFlow from "./RouterFlow";
// Lấy root element từ file HTML
const rootElement = document.getElementById("root");
// Sử dụng createRoot thay vì ReactDOM.render
const root = ReactDOM.createRoot(rootElement);
// Render ứng dụng
root.render(
<React.StrictMode>
<BrowserRouter>
<RouterFlow />
</BrowserRouter>
</React.StrictMode>
);

7
vite.config.js Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})

View File

@@ -0,0 +1,10 @@
// vite.config.js
import { defineConfig } from "file:///C:/Users/tiepb/Downloads/Company/work_2024/react-flow-drag/node_modules/vite/dist/node/index.js";
import react from "file:///C:/Users/tiepb/Downloads/Company/work_2024/react-flow-drag/node_modules/@vitejs/plugin-react/dist/index.mjs";
var vite_config_default = defineConfig({
plugins: [react()]
});
export {
vite_config_default as default
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFx0aWVwYlxcXFxEb3dubG9hZHNcXFxcQ29tcGFueVxcXFx3b3JrXzIwMjRcXFxccmVhY3QtZmxvdy1kcmFnXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFx0aWVwYlxcXFxEb3dubG9hZHNcXFxcQ29tcGFueVxcXFx3b3JrXzIwMjRcXFxccmVhY3QtZmxvdy1kcmFnXFxcXHZpdGUuY29uZmlnLmpzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9DOi9Vc2Vycy90aWVwYi9Eb3dubG9hZHMvQ29tcGFueS93b3JrXzIwMjQvcmVhY3QtZmxvdy1kcmFnL3ZpdGUuY29uZmlnLmpzXCI7aW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSdcclxuaW1wb3J0IHJlYWN0IGZyb20gJ0B2aXRlanMvcGx1Z2luLXJlYWN0J1xyXG5cclxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cclxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcclxuICBwbHVnaW5zOiBbcmVhY3QoKV0sXHJcbn0pXHJcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBOFcsU0FBUyxvQkFBb0I7QUFDM1ksT0FBTyxXQUFXO0FBR2xCLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQzFCLFNBQVMsQ0FBQyxNQUFNLENBQUM7QUFDbkIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K