up
@@ -10,7 +10,7 @@
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
143
src/App.tsx
@@ -1,136 +1,17 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
addEdge,
|
||||
applyEdgeChanges,
|
||||
applyNodeChanges,
|
||||
Background,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
|
||||
import TextUpdaterNode from './nodes/TextUpdaterNode.js';
|
||||
import { FiUser, FiMail, FiCheckCircle,FiList } from 'react-icons/fi';
|
||||
import {FaShuffle } from "react-icons/fa6";
|
||||
|
||||
const initialNodes = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: {
|
||||
label: (
|
||||
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
||||
<FiUser style={{ marginRight: 5, fontSize: '24px' }} />
|
||||
Customer signs up for product updates
|
||||
</div>
|
||||
)
|
||||
},
|
||||
position: { x: 250, y: 0 },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: {
|
||||
label: (
|
||||
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
||||
<FiMail style={{ marginRight: 5 }} />
|
||||
Send email
|
||||
</div>
|
||||
)
|
||||
},
|
||||
position: { x: 250, y: 100 },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: {
|
||||
label:(
|
||||
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
||||
<FaShuffle style={{ marginRight: 5, transform:`rotate(90deg)` }} />
|
||||
<span>Replied to conversation?</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
position: { x: 250, y: 200 },
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'output',
|
||||
data: {
|
||||
label: (
|
||||
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
||||
<FiCheckCircle style={{ marginRight: 5 }} />
|
||||
Send survey
|
||||
</div>
|
||||
)
|
||||
},
|
||||
position: { x: 100, y: 300 },
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'output',
|
||||
data: {
|
||||
label: (
|
||||
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
||||
<FiList style={{ marginRight: 5 }} />
|
||||
Contact Exits
|
||||
</div>
|
||||
)
|
||||
},
|
||||
position: { x: 400, y: 300 },
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges = [
|
||||
{ id: 'e1-2', source: '1', target: '2' },
|
||||
{ id: 'e2-3', source: '2', target: '3' },
|
||||
{ id: 'e3-4', source: '3', target: '4', label: 'No' },
|
||||
{ id: 'e3-5', source: '3', target: '5', label: 'Yes' },
|
||||
];
|
||||
|
||||
|
||||
// we define the nodeTypes outside of the component to prevent re-renderings
|
||||
// you could also use useMemo inside the component
|
||||
const nodeTypes = { textUpdater: TextUpdaterNode };
|
||||
|
||||
function Flow() {
|
||||
const [nodes, setNodes] = useState(initialNodes);
|
||||
const [edges, setEdges] = useState(initialEdges);
|
||||
|
||||
const onNodesChange = useCallback(
|
||||
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
[setNodes],
|
||||
);
|
||||
const onEdgesChange = useCallback(
|
||||
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
||||
[setEdges],
|
||||
);
|
||||
const onConnect = useCallback(
|
||||
(connection) => setEdges((eds) => addEdge(connection, eds)),
|
||||
[setEdges],
|
||||
);
|
||||
|
||||
const onNodesDelete = useCallback((deletedNodes) => {
|
||||
console.log('Các node đã bị xóa:', deletedNodes);
|
||||
|
||||
// Lọc lại danh sách nodes sau khi xóa
|
||||
setNodes((nds) => nds.filter((node) => !deletedNodes.some((deletedNode) => deletedNode.id === node.id)));
|
||||
}, []);
|
||||
import React from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import CreateFlow from '../src/nodes/CreateFlow';
|
||||
import BuildFlow from '../src/nodes/BuildFlow';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodesDelete={onNodesDelete}
|
||||
fitView
|
||||
>
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
|
||||
<div>
|
||||
<Routes>
|
||||
<Route path="/" element={<CreateFlow />} />
|
||||
<Route path="/build" element={<BuildFlow />} />
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Flow;
|
||||
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
height: 70px;
|
||||
border: 1px solid rgba(36, 28, 21, 0.15);
|
||||
border-radius: 4px;
|
||||
padding: 16px 8px;
|
||||
@@ -58,7 +58,7 @@
|
||||
}
|
||||
.PopupTabBar .itemTab span {
|
||||
margin-top: 5px;
|
||||
font-size: 13px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -212,3 +212,550 @@ foreignObject {
|
||||
.react-flow__node.btn-add-node .react-flow__handle {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.custom-node {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.custom-node .manageStep {
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.custom-node .btn-show {
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-node .actions {
|
||||
position: absolute;
|
||||
width: 100px;
|
||||
left: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.custom-node .actions.staticNode {
|
||||
display: block;
|
||||
}
|
||||
.custom-node .actions .btn-edit {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
text-align: left;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.custom-node .actions .btn-remove {
|
||||
background: #007c89;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
text-align: left;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.success-checkmark {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.success-checkmark .check-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
-webkit-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
border: 4px solid #007c89;
|
||||
}
|
||||
.success-checkmark .check-icon::before {
|
||||
top: 3px;
|
||||
left: -2px;
|
||||
width: 30px;
|
||||
-webkit-transform-origin: 100% 50%;
|
||||
-ms-transform-origin: 100% 50%;
|
||||
transform-origin: 100% 50%;
|
||||
border-radius: 100px 0 0 100px;
|
||||
}
|
||||
.success-checkmark .check-icon::after {
|
||||
top: 0;
|
||||
left: 30px;
|
||||
width: 60px;
|
||||
-webkit-transform-origin: 0 50%;
|
||||
-ms-transform-origin: 0 50%;
|
||||
transform-origin: 0 50%;
|
||||
border-radius: 0 100px 100px 0;
|
||||
-webkit-animation: rotate-circle 4.25s ease-in;
|
||||
animation: rotate-circle 4.25s ease-in;
|
||||
}
|
||||
.success-checkmark .check-icon::after,
|
||||
.success-checkmark .check-icon::before {
|
||||
content: '';
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
-webkit-transform: rotate(-45deg);
|
||||
-ms-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
.success-checkmark .check-icon .icon-line {
|
||||
height: 5px;
|
||||
background-color: #007c89;
|
||||
display: block;
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
.success-checkmark .check-icon .icon-line.line-tip {
|
||||
top: 46px;
|
||||
left: 14px;
|
||||
width: 25px;
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
-webkit-animation: icon-line-tip 0.75s;
|
||||
animation: icon-line-tip 0.75s;
|
||||
}
|
||||
.success-checkmark .check-icon .icon-line.line-long {
|
||||
top: 38px;
|
||||
right: 8px;
|
||||
width: 47px;
|
||||
-webkit-transform: rotate(-45deg);
|
||||
-ms-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
-webkit-animation: icon-line-long 0.75s;
|
||||
animation: icon-line-long 0.75s;
|
||||
}
|
||||
.success-checkmark .check-icon .icon-circle {
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
z-index: 10;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
-webkit-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
border: 4px solid #007c89;
|
||||
}
|
||||
.success-checkmark .check-icon .icon-fix {
|
||||
top: 8px;
|
||||
width: 5px;
|
||||
left: 26px;
|
||||
z-index: 1;
|
||||
height: 85px;
|
||||
position: absolute;
|
||||
-webkit-transform: rotate(-45deg);
|
||||
-ms-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
background-color: #fff;
|
||||
}
|
||||
@-webkit-keyframes rotate-circle {
|
||||
0% {
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
5% {
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
12% {
|
||||
-webkit-transform: rotate(-405deg);
|
||||
transform: rotate(-405deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(-405deg);
|
||||
transform: rotate(-405deg);
|
||||
}
|
||||
}
|
||||
@keyframes rotate-circle {
|
||||
0% {
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
5% {
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
12% {
|
||||
-webkit-transform: rotate(-405deg);
|
||||
transform: rotate(-405deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(-405deg);
|
||||
transform: rotate(-405deg);
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes icon-line-tip {
|
||||
0% {
|
||||
width: 0;
|
||||
left: 1px;
|
||||
top: 19px;
|
||||
}
|
||||
54% {
|
||||
width: 0;
|
||||
left: 1px;
|
||||
top: 19px;
|
||||
}
|
||||
70% {
|
||||
width: 50px;
|
||||
left: -8px;
|
||||
top: 37px;
|
||||
}
|
||||
84% {
|
||||
width: 17px;
|
||||
left: 21px;
|
||||
top: 48px;
|
||||
}
|
||||
100% {
|
||||
width: 25px;
|
||||
left: 14px;
|
||||
top: 45px;
|
||||
}
|
||||
}
|
||||
@keyframes icon-line-tip {
|
||||
0% {
|
||||
width: 0;
|
||||
left: 1px;
|
||||
top: 19px;
|
||||
}
|
||||
54% {
|
||||
width: 0;
|
||||
left: 1px;
|
||||
top: 19px;
|
||||
}
|
||||
70% {
|
||||
width: 50px;
|
||||
left: -8px;
|
||||
top: 37px;
|
||||
}
|
||||
84% {
|
||||
width: 17px;
|
||||
left: 21px;
|
||||
top: 48px;
|
||||
}
|
||||
100% {
|
||||
width: 25px;
|
||||
left: 14px;
|
||||
top: 45px;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes icon-line-long {
|
||||
0% {
|
||||
width: 0;
|
||||
right: 46px;
|
||||
top: 54px;
|
||||
}
|
||||
65% {
|
||||
width: 0;
|
||||
right: 46px;
|
||||
top: 54px;
|
||||
}
|
||||
84% {
|
||||
width: 55px;
|
||||
right: 0;
|
||||
top: 35px;
|
||||
}
|
||||
100% {
|
||||
width: 47px;
|
||||
right: 8px;
|
||||
top: 38px;
|
||||
}
|
||||
}
|
||||
@keyframes icon-line-long {
|
||||
0% {
|
||||
width: 0;
|
||||
right: 46px;
|
||||
top: 54px;
|
||||
}
|
||||
65% {
|
||||
width: 0;
|
||||
right: 46px;
|
||||
top: 54px;
|
||||
}
|
||||
84% {
|
||||
width: 55px;
|
||||
right: 0;
|
||||
top: 35px;
|
||||
}
|
||||
100% {
|
||||
width: 47px;
|
||||
right: 8px;
|
||||
top: 38px;
|
||||
}
|
||||
}
|
||||
.success-save {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 99999;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: block;
|
||||
}
|
||||
.content-container {
|
||||
background: #fff;
|
||||
max-width: calc(100% - 40px);
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
max-height: 260px;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
.content-text {
|
||||
font-weight: 600;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.success-save .content-container {
|
||||
max-width: 500px;
|
||||
min-height: 300px;
|
||||
}
|
||||
.text-30 {
|
||||
font-size: 30px;
|
||||
}
|
||||
.content-text {
|
||||
font-weight: 600;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
1
src/assets/icons/icon-birthday.svg
Normal 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 |
1
src/assets/icons/icon-buy.svg
Normal 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 |
1
src/assets/icons/icon-signs.svg
Normal 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 |
18
src/assets/icons/icon-slack.svg
Normal 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 |
1
src/assets/icons/icon-tag.svg
Normal 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 |
1
src/assets/icons/icon-wink.svg
Normal 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 |
1
src/assets/icons/icon-zalo.svg
Normal 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 |
BIN
src/assets/images/Customer-Journey_Animation.gif
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
src/assets/images/ILLO_MiniSpot_2-Paths-v2_1080x1080.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
22
src/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
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 './index.css';
|
||||
|
||||
import Flow from './nodes/Test';
|
||||
|
||||
// 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 as HTMLElement);
|
||||
|
||||
// Render ứng dụng
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -2,14 +2,16 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { ReactFlowProvider } from '@xyflow/react';
|
||||
|
||||
import App from './App';
|
||||
import NoteFlow from './nodes/NoteFlow';
|
||||
import './index.css';
|
||||
import NoteFlowNew from './nodes/NodeFlowNew';
|
||||
import FlowArea from './nodes/NodeMove';
|
||||
import CreateFlow from './nodes/CreateFlow';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
{/* <ReactFlowProvider>
|
||||
<NoteFlow />
|
||||
</ReactFlowProvider> */}
|
||||
|
||||
<CreateFlow />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
128
src/nodes/BuildFlow.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { ReactFlow, ReactFlowProvider } from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import '../assets/css/style.css';
|
||||
import imageStart from '../assets/images/ILLO_MiniSpot_2-Paths-v2_1080x1080.png';
|
||||
import PopupListFlow from '../nodes/popup/PopupListFlow';
|
||||
import { mainNode } from './data/data';
|
||||
import NodeFlow from './NodeFlow';
|
||||
|
||||
const BuildFlow = () => {
|
||||
const [nodes, setNodes] = useState([]);
|
||||
const [edges, setEdges] = useState([]);
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
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 + 100,
|
||||
},
|
||||
};
|
||||
|
||||
// Kết nối giữa node và add-node
|
||||
const newEdge = {
|
||||
id: `edge-${selectedMainNode.id}-add-node`,
|
||||
source: `${selectedMainNode.id}`,
|
||||
target: '2',
|
||||
};
|
||||
|
||||
// Cập nhật state với node mới, add-node, và edge
|
||||
setNodes((nds) => [newNode, addNode]);
|
||||
setEdges((eds) => [newEdge]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!selectedFlow ? (
|
||||
<>
|
||||
<ReactFlow />
|
||||
<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>
|
||||
</>
|
||||
) : (
|
||||
<ReactFlowProvider>
|
||||
<NodeFlow initialNodes={nodes} initialEdges={edges} />
|
||||
</ReactFlowProvider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BuildFlow;
|
||||
46
src/nodes/CreateFlow.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import '../assets/css/test.css';
|
||||
import Customer from '../assets/images/Customer-Journey_Animation.gif';
|
||||
|
||||
const CreateFlow = () => {
|
||||
const [flowName, setFlowName] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSave = (e: React.FormEvent) => {
|
||||
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 that’s 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;
|
||||
@@ -12,7 +12,7 @@ const MenuOptions = [
|
||||
label: 'Time delay',
|
||||
id: nanoid(8),
|
||||
type: 'default',
|
||||
icon: '/src/assets/icons/icon-time.svg'
|
||||
icon: '/src/assets/icons/icon-time.svg',
|
||||
},
|
||||
{
|
||||
key: 'ifelse',
|
||||
@@ -20,16 +20,16 @@ const MenuOptions = [
|
||||
id: nanoid(8),
|
||||
type: 'default',
|
||||
icon: '/src/assets/icons/icon-if.svg',
|
||||
position: { x: 400, y: 290 }
|
||||
position: { x: 400, y: 290 },
|
||||
},
|
||||
{
|
||||
key: 'wait',
|
||||
label: 'Wait for trigger',
|
||||
id: nanoid(8),
|
||||
type: 'default',
|
||||
icon: '/src/assets/icons/icon-wait.svg'
|
||||
}
|
||||
]
|
||||
icon: '/src/assets/icons/icon-wait.svg',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
option: 'action',
|
||||
@@ -40,21 +40,37 @@ const MenuOptions = [
|
||||
label: 'Send email',
|
||||
id: nanoid(8),
|
||||
type: '',
|
||||
icon: '/src/assets/icons/icon-email.svg'
|
||||
icon: '/src/assets/icons/icon-email.svg',
|
||||
},
|
||||
{
|
||||
key: 'send-sms',
|
||||
label: 'Send sms',
|
||||
id: nanoid(8),
|
||||
type: '',
|
||||
icon: '/src/assets/icons/icon-phone.svg'
|
||||
icon: '/src/assets/icons/icon-phone.svg',
|
||||
},
|
||||
{
|
||||
key: 'send-slack',
|
||||
branchKey: 'no',
|
||||
label: 'Send Slack',
|
||||
id: nanoid(8),
|
||||
type: 'default',
|
||||
icon: '/src/assets/icons/icon-slack.svg',
|
||||
},
|
||||
{
|
||||
key: 'send-zalo',
|
||||
branchKey: 'no',
|
||||
label: 'Send Zalo',
|
||||
id: nanoid(8),
|
||||
type: 'default',
|
||||
icon: '/src/assets/icons/icon-zalo.svg',
|
||||
},
|
||||
{
|
||||
key: 'send-survey-email',
|
||||
label: 'Send survey in email',
|
||||
id: nanoid(8),
|
||||
type: '',
|
||||
icon: '/src/assets/icons/icon-list.svg'
|
||||
icon: '/src/assets/icons/icon-list.svg',
|
||||
},
|
||||
{
|
||||
key: 'contact-exists',
|
||||
@@ -62,7 +78,7 @@ const MenuOptions = [
|
||||
label: 'Contact Exists',
|
||||
id: nanoid(8),
|
||||
type: 'output',
|
||||
icon: '/src/assets/icons/icon-check.svg'
|
||||
icon: '/src/assets/icons/icon-check.svg',
|
||||
},
|
||||
{
|
||||
key: 'send-survey',
|
||||
@@ -70,10 +86,10 @@ const MenuOptions = [
|
||||
label: 'Send survey',
|
||||
id: nanoid(8),
|
||||
type: 'output',
|
||||
icon: '/src/assets/icons/icon-list.svg'
|
||||
}
|
||||
]
|
||||
}
|
||||
icon: '/src/assets/icons/icon-list.svg',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const MenuFlow = () => {
|
||||
@@ -87,26 +103,26 @@ const MenuFlow = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='menu-fixed'>
|
||||
<div className="menu-fixed">
|
||||
<b></b>
|
||||
{MenuOptions.map((option) => (
|
||||
<div className='item-flow' key={option.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'
|
||||
className="items"
|
||||
draggable="true"
|
||||
key={items.id}
|
||||
onDragStart={(event) => onDragStart(event, items)}
|
||||
>
|
||||
<div className='step'>
|
||||
<div className='left'>
|
||||
<img src={items.icon} width='24px' height='24px' alt='' />
|
||||
<div className="step">
|
||||
<div className="left">
|
||||
<img src={items.icon} width="24px" height="24px" alt="" />
|
||||
</div>
|
||||
<div className='right'>{items.label}</div>
|
||||
<div className="right">{items.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
659
src/nodes/NodeFlow.tsx
Normal file
@@ -0,0 +1,659 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
addEdge,
|
||||
Controls,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
MarkerType,
|
||||
useReactFlow,
|
||||
ReactFlowProvider,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import '../assets/css/style.css';
|
||||
import Tabbar from './Tabbar.tsx';
|
||||
import ShowPopup from './ShowPopup.tsx';
|
||||
import { nanoid } from 'nanoid';
|
||||
import CustomEdge from './CustomEdge';
|
||||
import MenuFlow from './MenuFlow';
|
||||
|
||||
// Nội dung cho mỗi node
|
||||
const nodeContents = {
|
||||
'send-email': `
|
||||
<label for="email-subject">Subject:</label>
|
||||
<input type="text" id="email-subject" name="email-subject" placeholder="Enter subject">
|
||||
<label for="email-body">Body:</label>
|
||||
<textarea id="email-body" name="email-body" rows="4" placeholder="Enter email body"></textarea>
|
||||
`,
|
||||
'customer-sign': `
|
||||
<label for="customer-name">Customer Name:</label>
|
||||
<input type="text" id="customer-name" name="customer-name" placeholder="Enter customer name">
|
||||
<label for="customer-email">Customer Email:</label>
|
||||
<input type="email" id="customer-email" name="customer-email" placeholder="Enter customer email">
|
||||
`,
|
||||
ifelse: `
|
||||
<label for="reply-status">Reply Status:</label>
|
||||
<select id="reply-status" name="reply-status">
|
||||
<option value="replied">Replied</option>
|
||||
<option value="not-replied">Not Replied</option>
|
||||
</select>
|
||||
`,
|
||||
'contact-exists': `
|
||||
<label for="contact-id">Contact ID:</label>
|
||||
<input type="text" id="contact-id" name="contact-id" placeholder="Enter contact ID">
|
||||
`,
|
||||
'send-survey': `
|
||||
<label for="survey-question">Survey Question:</label>
|
||||
<input type="text" id="survey-question" name="survey-question" placeholder="Enter survey question">
|
||||
<label for="survey-options">Options (comma-separated):</label>
|
||||
<input type="text" id="survey-options" name="survey-options" placeholder="Option 1, Option 2, ...">
|
||||
`,
|
||||
};
|
||||
|
||||
const NodeFlow = ({ initialNodes, initialEdges }) => {
|
||||
const [nodes, setNodes] = useState(initialNodes);
|
||||
const [edges, setEdges] = useState(initialEdges);
|
||||
const [showTabBar, setShowTabBar] = useState(false);
|
||||
const [lastNodeId, setLastNodeId] = useState(nodes[0].id);
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
const [selectedNodeContent, setSelectedNodeContent] = useState('');
|
||||
const [addNodeCallback, setAddNodeCallback] = useState(() => () => {});
|
||||
const [isTabbarVisible, setIsTabbarVisible] = useState(false);
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const [showActions, setShowActions] = useState(null);
|
||||
const [showPopupSuccess, setShowPopupSuccess] = useState(false);
|
||||
|
||||
const handleAddNoteClick = () => {
|
||||
setShowTabBar(true);
|
||||
setIsTabbarVisible(true);
|
||||
setAddNodeCallback(() => (selectedNode) => {
|
||||
addNodeBetween(selectedNode, sourceId, targetId);
|
||||
});
|
||||
};
|
||||
const closeTabbar = () => {
|
||||
setIsTabbarVisible(false); // Ẩn tabbar khi click close
|
||||
};
|
||||
|
||||
const onNodeClick = (event, node) => {
|
||||
if (node.key === 'add-node') {
|
||||
// Check if the clicked node is "Add Node"
|
||||
setShowTabBar(true);
|
||||
setIsTabbarVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const getNextYPosition = (xPosition, spacing) => {
|
||||
const nodesInColumn = nodes.filter((node) => node.position.x === xPosition);
|
||||
if (nodesInColumn.length === 0) return 0; // Return 0 if there are no nodes
|
||||
|
||||
const lastNode = nodesInColumn[nodesInColumn.length - 1];
|
||||
return lastNode.position.y + spacing;
|
||||
};
|
||||
|
||||
const addNode = (option) => {
|
||||
const newNodeId = nanoid(8); // Generate a unique ID for the new node
|
||||
const addNoteNode = nodes.find((node) => node.key === 'add-node');
|
||||
const initialX = addNoteNode?.position.x;
|
||||
|
||||
const nextY =
|
||||
nodes.length > 2
|
||||
? getNextYPosition(initialX, 120)
|
||||
: addNoteNode?.position.y;
|
||||
|
||||
// Remove existing edges from addNoteNode
|
||||
setEdges((eds) =>
|
||||
eds.filter((edge) => edge.source !== '2' && edge.target !== '2')
|
||||
);
|
||||
|
||||
if (option.id === '4') {
|
||||
const repliedNode = {
|
||||
id: newNodeId,
|
||||
key: option.key,
|
||||
data: { label: option.htmlNode },
|
||||
position: { x: initialX, y: nextY },
|
||||
};
|
||||
|
||||
const yesNode = {
|
||||
id: nanoid(8),
|
||||
type: 'output',
|
||||
key: 'contact-exists',
|
||||
data: {
|
||||
label: (
|
||||
<div className="step">
|
||||
<div className="left">
|
||||
<img
|
||||
src="/src/assets/icons/icon-check.svg"
|
||||
width="24px"
|
||||
height="24px"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="right">Contact Exists</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
position: { x: initialX - 150, y: nextY + 150 },
|
||||
};
|
||||
|
||||
const noNode = {
|
||||
id: nanoid(8),
|
||||
type: '',
|
||||
key: 'send-survey',
|
||||
data: {
|
||||
label: (
|
||||
<div className="step">
|
||||
<div className="left">
|
||||
<img
|
||||
src="/src/assets/icons/icon-list.svg"
|
||||
width="24px"
|
||||
height="24px"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="right">Send survey</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
position: { x: initialX + 150, y: nextY + 150 },
|
||||
};
|
||||
|
||||
// Di chuyển node add-note sang nhánh no
|
||||
setNodes((nds) =>
|
||||
nds.map((node) =>
|
||||
node.id === '2'
|
||||
? {
|
||||
...node,
|
||||
position: {
|
||||
x: noNode.position.x,
|
||||
y: noNode.position.y + 150,
|
||||
},
|
||||
}
|
||||
: node
|
||||
)
|
||||
);
|
||||
|
||||
// Thêm các node mới (replied, yes, no)
|
||||
setNodes((nds) => [...nds, repliedNode, yesNode, noNode]);
|
||||
|
||||
// Kết nối các edges giữa các node
|
||||
setEdges((eds) => [
|
||||
...eds,
|
||||
{
|
||||
id: nanoid(8),
|
||||
source: repliedNode.id,
|
||||
target: yesNode.id,
|
||||
label: 'yes',
|
||||
},
|
||||
{
|
||||
id: nanoid(8),
|
||||
source: repliedNode.id,
|
||||
target: noNode.id,
|
||||
label: 'no',
|
||||
},
|
||||
{ id: nanoid(8), source: lastNodeId, target: repliedNode.id },
|
||||
{ id: nanoid(8), source: noNode.id, target: addNoteNode.id },
|
||||
]);
|
||||
|
||||
setLastNodeId(noNode.id);
|
||||
} else {
|
||||
// Add new node logic
|
||||
if (addNoteNode) {
|
||||
const initialX = addNoteNode.position.x;
|
||||
setNodes((nds) =>
|
||||
nds.map((node) =>
|
||||
node.key === 'add-node'
|
||||
? {
|
||||
...node,
|
||||
position: {
|
||||
x: initialX,
|
||||
y: nextY + 130,
|
||||
},
|
||||
}
|
||||
: node
|
||||
)
|
||||
);
|
||||
}
|
||||
const newNode = {
|
||||
id: newNodeId,
|
||||
key: option.key,
|
||||
data: { label: option.htmlNode },
|
||||
position: {
|
||||
x: initialX,
|
||||
y: nodes.length > 2 ? nextY : addNoteNode.position.y,
|
||||
},
|
||||
};
|
||||
|
||||
const newEdge = {
|
||||
id: nanoid(8),
|
||||
source: lastNodeId,
|
||||
target: newNodeId,
|
||||
// data: {
|
||||
// label: <div onClick={handleAddNoteClick}>+</div>,
|
||||
// onClick: handleAddNoteClick
|
||||
// }
|
||||
};
|
||||
|
||||
setNodes((nds) => [...nds, newNode]);
|
||||
setEdges((eds) => [
|
||||
...eds,
|
||||
newEdge,
|
||||
{ id: nanoid(8), source: newNodeId, target: addNoteNode.id },
|
||||
]);
|
||||
|
||||
// setEdges((eds) => [
|
||||
// ...eds,
|
||||
// {
|
||||
// id: nanoid(8),
|
||||
// source: lastNodeId,
|
||||
// data: {
|
||||
// label: <div onClick={handleAddNoteClick}>Add Note</div>, // Use HTML or React component
|
||||
// onClick: handleAddNoteClick
|
||||
// },
|
||||
// target: newNodeId
|
||||
// },
|
||||
// { id: nanoid(8), source: newNodeId, target: addNoteNode.id }
|
||||
// ]);
|
||||
|
||||
// Cuộn tới node mới thêm
|
||||
setLastNodeId(newNodeId);
|
||||
}
|
||||
|
||||
setShowTabBar(false);
|
||||
};
|
||||
|
||||
const hidePopup = () => {
|
||||
setShowPopup(false);
|
||||
};
|
||||
|
||||
const addPopup = (node) => {
|
||||
const content = nodeContents[node.id]; // Lấy nội dung từ nodeContents dựa trên ID node
|
||||
setSelectedNodeContent(content); // Cập nhật nội dung được chọn
|
||||
};
|
||||
|
||||
const onDrop = (event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const type = JSON.parse(
|
||||
event.dataTransfer.getData('application/reactflow')
|
||||
);
|
||||
const position = screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
|
||||
const newNodeId = nanoid(8);
|
||||
const addNodeId = '2';
|
||||
const addNode = nodes.find((node) => node.id === addNodeId);
|
||||
const targetEdge = edges.find(
|
||||
(edge) => edge?.source === '2' || edge?.target === '2'
|
||||
);
|
||||
const targetNodeId =
|
||||
targetEdge?.source === '2' ? targetEdge?.target : targetEdge?.source;
|
||||
const targetNode = nodes.find((node) => node.id === targetNodeId);
|
||||
|
||||
const newNodePosition = targetNode
|
||||
? { x: targetNode.position.x, y: targetNode.position.y + 110 }
|
||||
: position;
|
||||
|
||||
const createNewNode = (
|
||||
nodeId: string,
|
||||
nodePosition: any,
|
||||
nodeLabel: JSX.Element
|
||||
) => ({
|
||||
id: nodeId,
|
||||
key: type.key,
|
||||
type: type.type || 'default',
|
||||
data: { label: nodeLabel },
|
||||
position: nodePosition,
|
||||
});
|
||||
|
||||
const newNode = createNewNode(
|
||||
newNodeId,
|
||||
newNodePosition,
|
||||
<div className="custom-node">
|
||||
<div className="step">
|
||||
<div className="left">
|
||||
<img src={type.icon} width="24px" height="24px" alt="" />
|
||||
</div>
|
||||
<div className="right">{type.label}</div>
|
||||
</div>
|
||||
<div className="manageStep">
|
||||
<button className="btn-show" onClick={() => toggleActions(newNodeId)}>
|
||||
<img
|
||||
src="/src/assets/icons/icon-wink.svg"
|
||||
width="20px"
|
||||
height="20px"
|
||||
alt="wink"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div className={`actions actions-${newNodeId}`}>
|
||||
<button
|
||||
onClick={() => handleEditNode(newNodeId)}
|
||||
className="btn-edit"
|
||||
>
|
||||
Sửa
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteNode(newNodeId)}
|
||||
className="btn-remove"
|
||||
>
|
||||
Xóa
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
setEdges((eds) =>
|
||||
eds.filter((edge) => edge.source !== '2' && edge.target !== '2')
|
||||
);
|
||||
|
||||
if (type.key === 'ifelse') {
|
||||
const yesNodeId = nanoid(8);
|
||||
const noNodeId = nanoid(8);
|
||||
|
||||
const createBranchNode = (id: string, xOffset: number) => ({
|
||||
id,
|
||||
key: 'add-node',
|
||||
data: {
|
||||
label: (
|
||||
<div className="add-node">
|
||||
<i>+</i> <span>Add a journey point</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
position: {
|
||||
x: newNode.position.x + xOffset,
|
||||
y: newNode.position.y + 150,
|
||||
},
|
||||
});
|
||||
|
||||
const yesNode = createBranchNode(yesNodeId, -200);
|
||||
const noNode = createBranchNode(noNodeId, 200);
|
||||
|
||||
const ifelseEdges = [
|
||||
{ id: nanoid(8), source: targetNodeId, target: newNodeId },
|
||||
{
|
||||
id: nanoid(8),
|
||||
source: newNodeId,
|
||||
target: yesNodeId,
|
||||
type: 'custom',
|
||||
data: { label: 'Yes' },
|
||||
},
|
||||
{
|
||||
id: nanoid(8),
|
||||
source: newNodeId,
|
||||
target: noNodeId,
|
||||
type: 'custom',
|
||||
data: { label: 'No' },
|
||||
},
|
||||
];
|
||||
|
||||
setNodes((nds) => [
|
||||
...nds.filter((node) => node.id !== addNodeId),
|
||||
newNode,
|
||||
yesNode,
|
||||
noNode,
|
||||
]);
|
||||
setEdges((eds) => [...eds, ...ifelseEdges]);
|
||||
} else {
|
||||
if (targetNode) {
|
||||
const updatedEdges = [
|
||||
{ id: nanoid(8), source: targetNodeId, target: newNodeId },
|
||||
{ id: nanoid(8), source: newNodeId, target: '2' },
|
||||
];
|
||||
|
||||
// Cập nhật nodes
|
||||
setNodes((nds) => {
|
||||
const updatedNodes = [...nds, newNode];
|
||||
|
||||
if (addNode) {
|
||||
// Tính toán vị trí cho addNode nằm dưới newNode
|
||||
const addNodePosition = {
|
||||
x: newNode.position.x,
|
||||
y: newNode.position.y + 110, // Đặt add-node dưới newNode
|
||||
};
|
||||
|
||||
// Cập nhật vị trí của addNode
|
||||
const addNodeUpdated = updatedNodes.map((node) =>
|
||||
node.id === addNodeId
|
||||
? { ...node, position: addNodePosition }
|
||||
: node
|
||||
);
|
||||
|
||||
// Đảm bảo rằng add-node nằm dưới newNode
|
||||
return addNodeUpdated.sort((a, b) => {
|
||||
if (a.id === newNodeId) return -1; // Đặt newNode lên trước
|
||||
if (b.id === newNodeId) return 1; // Đặt addNode xuống dưới
|
||||
return 0; // Giữ nguyên thứ tự cho các node khác
|
||||
});
|
||||
}
|
||||
|
||||
return updatedNodes;
|
||||
});
|
||||
|
||||
// Cập nhật edges
|
||||
setEdges((eds) => [...eds, ...updatedEdges]);
|
||||
|
||||
if (addNode && type.key === 'contact-exists') {
|
||||
// Nếu key là 'contact-exists', xóa addNode
|
||||
setNodes((nds) => nds.filter((node) => node.id !== addNodeId));
|
||||
}
|
||||
} else {
|
||||
const newNodeNew = createNewNode(
|
||||
newNodeId,
|
||||
position,
|
||||
newNode.data.label
|
||||
);
|
||||
let closestNode = null;
|
||||
let closestDistance = Infinity;
|
||||
const threshold = 150;
|
||||
|
||||
nodes.forEach((node) => {
|
||||
const distance = getDistance(newNodeNew.position, node.position);
|
||||
if (distance < closestDistance && distance <= threshold) {
|
||||
closestDistance = distance;
|
||||
closestNode = node;
|
||||
}
|
||||
});
|
||||
|
||||
if (closestNode) {
|
||||
const updatedEdges = edges.map((edge) => {
|
||||
if (edge.source === closestNode.id)
|
||||
return { ...edge, source: newNodeNew.id };
|
||||
if (edge.target === closestNode.id)
|
||||
return { ...edge, target: newNodeNew.id };
|
||||
return edge;
|
||||
});
|
||||
|
||||
if (newNodeNew.type !== 'output') {
|
||||
const addNodeNew = createNewNode(
|
||||
nanoid(8),
|
||||
{ x: closestNode.position.x, y: closestNode.position.y + 100 },
|
||||
<div className="add-node">
|
||||
<i>+</i> <span>Add a journey point</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
setNodes((nds) => [
|
||||
...nds.map((n) =>
|
||||
n.id === closestNode.id
|
||||
? { ...newNodeNew, position: closestNode.position }
|
||||
: n
|
||||
),
|
||||
addNodeNew,
|
||||
]);
|
||||
setEdges([
|
||||
...updatedEdges,
|
||||
{ id: nanoid(8), source: newNodeNew.id, target: addNodeNew.id },
|
||||
]);
|
||||
} else {
|
||||
setNodes((nds) => [
|
||||
...nds.map((n) =>
|
||||
n.id === closestNode.id
|
||||
? { ...newNodeNew, position: closestNode.position }
|
||||
: n
|
||||
),
|
||||
]);
|
||||
setEdges(updatedEdges);
|
||||
}
|
||||
} else {
|
||||
setNodes((nds) => [...nds, newNodeNew]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Updated nodes:', nodes);
|
||||
};
|
||||
|
||||
const onDragOver = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}, []);
|
||||
|
||||
const defaultEdgeOptions = {
|
||||
style: { strokeWidth: 1.5, stroke: 'black' },
|
||||
type: 'floating',
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: 'black',
|
||||
},
|
||||
};
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params) => setEdges((eds) => addEdge(params, eds)),
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
// Hàm tính khoảng cách giữa hai node
|
||||
const getDistance = (pos1, pos2) => {
|
||||
const dx = pos1.x - pos2.x;
|
||||
const dy = pos1.y - pos2.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
};
|
||||
|
||||
const toggleActions = (nodeId: string) => {
|
||||
setShowActions((prev) => (prev === nodeId ? null : nodeId));
|
||||
const actionsElement = document.querySelector(`.actions-${nodeId}`);
|
||||
if (actionsElement) {
|
||||
// Toggle class 'show' cho phần tử actions
|
||||
actionsElement.classList.toggle('staticNode');
|
||||
}
|
||||
};
|
||||
|
||||
// Hàm xóa node
|
||||
const handleDeleteNode = useCallback(
|
||||
(nodeId) => {
|
||||
// Tìm tất cả các edges có nodeId là source hoặc target
|
||||
const connectedEdges = edges.filter(
|
||||
(edge) => edge.source === nodeId || edge.target === nodeId
|
||||
);
|
||||
|
||||
console.log(connectedEdges);
|
||||
|
||||
// Lấy các nodes đang nối với node bị xóa
|
||||
const sourceNode = connectedEdges.find(
|
||||
(edge) => edge.target === nodeId
|
||||
)?.source;
|
||||
const targetNode = connectedEdges.find(
|
||||
(edge) => edge.source === nodeId
|
||||
)?.target;
|
||||
|
||||
// Cập nhật nodes: xóa node bị xóa
|
||||
setNodes((nds) => nds.filter((node) => node.id !== nodeId));
|
||||
|
||||
// Cập nhật edges: xóa các cạnh kết nối với node bị xóa
|
||||
setEdges((eds) =>
|
||||
eds.filter((edge) => edge.source !== nodeId && edge.target !== nodeId)
|
||||
);
|
||||
|
||||
// Nếu có cả sourceNode và targetNode, tạo một edge mới nối 2 node này
|
||||
if (sourceNode && targetNode) {
|
||||
setEdges((eds) => [
|
||||
...eds,
|
||||
{
|
||||
id: `new-edge-${sourceNode}-${targetNode}`,
|
||||
source: sourceNode,
|
||||
target: targetNode,
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
[edges, setNodes, setEdges]
|
||||
);
|
||||
|
||||
// Hàm sửa node
|
||||
const handleEditNode = (nodeId: string) => {
|
||||
// Logic chỉnh sửa node
|
||||
alert(`Sửa node: ${nodeId}`);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
setShowPopupSuccess(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setShowPopupSuccess(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ overflowY: 'auto', position: 'relative' }}>
|
||||
<div className="flex-flow">
|
||||
<MenuFlow />
|
||||
<div className="panelContainer">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
edgeTypes={{ custom: CustomEdge }}
|
||||
onNodeClick={onNodeClick}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
panOnScroll
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
onConnect={onConnect}
|
||||
>
|
||||
<Controls position="right-bottom" />
|
||||
<Background variant={BackgroundVariant.Lines} color="#ccccc" />
|
||||
</ReactFlow>
|
||||
<button className="btn-save" onClick={() => handleSave()}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isTabbarVisible && (
|
||||
<Tabbar addNode={addNode} closeTabbar={closeTabbar} />
|
||||
)}
|
||||
|
||||
{showPopup && (
|
||||
<ShowPopup
|
||||
selectedNodeContent={selectedNodeContent}
|
||||
hidePopup={() => setShowPopup(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showPopupSuccess && (
|
||||
<div className="success-save">
|
||||
<div className="content-container">
|
||||
<div className="success-checkmark">
|
||||
<div className="check-icon">
|
||||
<span className="icon-line line-tip"></span>
|
||||
<span className="icon-line line-long"></span>
|
||||
<div className="icon-circle"></div>
|
||||
<div className="icon-fix"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center content-text text-30">
|
||||
Đã lưu lại tiến trình thành công !
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeFlow;
|
||||
90
src/nodes/data/data.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export const dataListFlow = [
|
||||
{
|
||||
id: nanoid(8),
|
||||
name: 'Tag added',
|
||||
key: 'tag',
|
||||
icon: '/src/assets/icons/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/icons/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/icons/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/icons/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/icons/icon-buy.svg',
|
||||
description: 'You haven’t connected your store.',
|
||||
},
|
||||
];
|
||||
|
||||
export const mainNode = [
|
||||
{
|
||||
key: 'sign-up',
|
||||
id: nanoid(8),
|
||||
type: 'input',
|
||||
icon: '/src/assets/icons/icon-signs.svg',
|
||||
name: 'Signs up for Email',
|
||||
position: { x: 250, y: 100 },
|
||||
},
|
||||
{
|
||||
id: nanoid(8),
|
||||
key: 'tag',
|
||||
type: 'input',
|
||||
icon: '/src/assets/icons/icon-tag.svg',
|
||||
name: 'Set a tag',
|
||||
position: { x: 250, y: 100 },
|
||||
},
|
||||
{
|
||||
id: nanoid(8),
|
||||
key: 'birthday',
|
||||
type: 'default',
|
||||
icon: '/src/assets/icons/icon-birthday.svg',
|
||||
name: 'Contact Birthday',
|
||||
position: { x: 250, y: 100 },
|
||||
},
|
||||
{
|
||||
id: nanoid(8),
|
||||
key: 'send-an-email',
|
||||
type: 'default',
|
||||
icon: '/src/assets/icons/icon-email.svg',
|
||||
name: 'Sent an email',
|
||||
position: { x: 250, y: 100 },
|
||||
},
|
||||
{
|
||||
id: nanoid(8),
|
||||
key: 'buy-product',
|
||||
type: 'default',
|
||||
icon: '/src/assets/icons/icon-buy.svg',
|
||||
name: 'Buys a specific product',
|
||||
position: { x: 250, y: 100 },
|
||||
},
|
||||
];
|
||||
62
src/nodes/popup/PopupListFlow.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { describe } from 'node:test';
|
||||
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;
|
||||