diff --git a/index.html b/index.html index 97d6aa3..f645fde 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,7 @@
- + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 556394a..987bd18 100644 --- a/src/App.tsx +++ b/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: ( -
- - Customer signs up for product updates -
- ) - }, - position: { x: 250, y: 0 }, - }, - { - id: '2', - data: { - label: ( -
- - Send email -
- ) - }, - position: { x: 250, y: 100 }, - }, - { - id: '3', - data: { - label:( -
- - Replied to conversation? -
- ) - }, - position: { x: 250, y: 200 }, - }, - { - id: '4', - type: 'output', - data: { - label: ( -
- - Send survey -
- ) - }, - position: { x: 100, y: 300 }, - }, - { - id: '5', - type: 'output', - data: { - label: ( -
- - Contact Exits -
- ) - }, - 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 ( - - - - +
+ + } /> + } /> + +
); } -export default Flow; - - +export default App; diff --git a/src/assets/css/style.css b/src/assets/css/style.css index ee659d3..69cc6b2 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -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; +} diff --git a/src/assets/icons/icon-birthday.svg b/src/assets/icons/icon-birthday.svg new file mode 100644 index 0000000..661286d --- /dev/null +++ b/src/assets/icons/icon-birthday.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/icon-buy.svg b/src/assets/icons/icon-buy.svg new file mode 100644 index 0000000..1e9b589 --- /dev/null +++ b/src/assets/icons/icon-buy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/icon-signs.svg b/src/assets/icons/icon-signs.svg new file mode 100644 index 0000000..217b07b --- /dev/null +++ b/src/assets/icons/icon-signs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/icon-slack.svg b/src/assets/icons/icon-slack.svg new file mode 100644 index 0000000..435faca --- /dev/null +++ b/src/assets/icons/icon-slack.svg @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/icon-tag.svg b/src/assets/icons/icon-tag.svg new file mode 100644 index 0000000..4e818dd --- /dev/null +++ b/src/assets/icons/icon-tag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/icon-wink.svg b/src/assets/icons/icon-wink.svg new file mode 100644 index 0000000..339bc11 --- /dev/null +++ b/src/assets/icons/icon-wink.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/icon-zalo.svg b/src/assets/icons/icon-zalo.svg new file mode 100644 index 0000000..0822f56 --- /dev/null +++ b/src/assets/icons/icon-zalo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/Customer-Journey_Animation.gif b/src/assets/images/Customer-Journey_Animation.gif new file mode 100644 index 0000000..edf390c Binary files /dev/null and b/src/assets/images/Customer-Journey_Animation.gif differ diff --git a/src/assets/images/ILLO_MiniSpot_2-Paths-v2_1080x1080.png b/src/assets/images/ILLO_MiniSpot_2-Paths-v2_1080x1080.png new file mode 100644 index 0000000..76f8e8d Binary files /dev/null and b/src/assets/images/ILLO_MiniSpot_2-Paths-v2_1080x1080.png differ diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..0994835 --- /dev/null +++ b/src/index.tsx @@ -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( + + + + + +); diff --git a/src/main.tsx b/src/main.tsx index dfd9c51..7348f43 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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( - + {/* + + */} + + ); diff --git a/src/nodes/BuildFlow.tsx b/src/nodes/BuildFlow.tsx new file mode 100644 index 0000000..e1e8c22 --- /dev/null +++ b/src/nodes/BuildFlow.tsx @@ -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: ( +
+
+ +
+
{selectedMainNode.name}
+
+ ), + }, + position: { ...selectedMainNode.position }, // Vị trí từ mainNode + }; + + // Tạo node add-node + const addNode = { + id: '2', + key: 'add-node', + type: 'default', + data: { + label: ( +
+ + Add a journey point +
+ ), + }, + 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 ? ( + <> + +
+

Building Flow: {flowName}

+ +
+ +

How will a contact start their journey?

+

+ 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. +

+
+ + + {showPopup && ( + setShowPopup(false)} + /> + )} +
+ + ) : ( + + + + )} + + ); +}; + +export default BuildFlow; diff --git a/src/nodes/CreateFlow.tsx b/src/nodes/CreateFlow.tsx new file mode 100644 index 0000000..c86240a --- /dev/null +++ b/src/nodes/CreateFlow.tsx @@ -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 ( +
+
+ +

Create a map of your contact's journey

+

+ 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. +

+
+ + setFlowName(e.target.value)} + /> + +
+
+
+ ); +}; + +export default CreateFlow; diff --git a/src/nodes/MenuFlow.tsx b/src/nodes/MenuFlow.tsx index 7764f45..d4a7e4a 100644 --- a/src/nodes/MenuFlow.tsx +++ b/src/nodes/MenuFlow.tsx @@ -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 ( <> -
+
{MenuOptions.map((option) => ( -
+
{option.title}
{option.list_option.map((items) => (
onDragStart(event, items)} > -
-
- +
+
+
-
{items.label}
+
{items.label}
))} diff --git a/src/nodes/NodeFlow.tsx b/src/nodes/NodeFlow.tsx new file mode 100644 index 0000000..ce4bf64 --- /dev/null +++ b/src/nodes/NodeFlow.tsx @@ -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': ` + + + + + `, + 'customer-sign': ` + + + + + `, + ifelse: ` + + + `, + 'contact-exists': ` + + + `, + 'send-survey': ` + + + + + `, +}; + +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: ( +
+
+ +
+
Contact Exists
+
+ ), + }, + position: { x: initialX - 150, y: nextY + 150 }, + }; + + const noNode = { + id: nanoid(8), + type: '', + key: 'send-survey', + data: { + label: ( +
+
+ +
+
Send survey
+
+ ), + }, + 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:
+
, + // 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:
Add Note
, // 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, +
+
+
+ +
+
{type.label}
+
+
+ + +
+ + +
+
+
+ ); + + 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: ( +
+ + Add a journey point +
+ ), + }, + 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 }, +
+ + Add a journey point +
+ ); + + 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 ( +
+
+ +
+ + + + + +
+
+ + {isTabbarVisible && ( + + )} + + {showPopup && ( + setShowPopup(false)} + /> + )} + + {showPopupSuccess && ( +
+
+
+
+ + +
+
+
+
+
+ Đã lưu lại tiến trình thành công ! +
+
+
+ )} +
+ ); +}; + +export default NodeFlow; diff --git a/src/nodes/data/data.ts b/src/nodes/data/data.ts new file mode 100644 index 0000000..a2b51a2 --- /dev/null +++ b/src/nodes/data/data.ts @@ -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: ( + `
    +
  • Send more relevant content +
  • Track people's interests
  • +
  • Turn new shoppers into regulars
` + ), + }, + { + id: nanoid(8), + name: 'Signs up for Email', + key: 'sign-up', + icon: '/src/assets/icons/icon-signs.svg', + description: ( + `
  • Welcome new signups
  • Introduce your brand
  • Turn new contacts into new customers
` + ) + }, + { + id: nanoid(8), + name: 'Birthday', + key: 'birthday', + icon: '/src/assets/icons/icon-birthday.svg', + description: + (`
  • Send a special discount
  • Offer a free gift
  • Just say 'happy birthday'
`), + }, + { + 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 }, + }, +]; \ No newline at end of file diff --git a/src/nodes/popup/PopupListFlow.tsx b/src/nodes/popup/PopupListFlow.tsx new file mode 100644 index 0000000..6572ecc --- /dev/null +++ b/src/nodes/popup/PopupListFlow.tsx @@ -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 ( + <> +
+
+
+ +

+ These starting points are popular with other Mailchimp customers. +

+

+ Some starting points are coming soon or require additional setup. +

+
+ {dataListFlow.map((option) => ( +
handleFlowClick(option.key)} + > +
+ {option.name} +

{option.name}

+
+
+
+ ))} +
+
+
+ + ); +}; + +export default PopupListFlow;