This commit is contained in:
2024-08-16 15:08:06 +07:00
commit 502e4877fc
22 changed files with 4263 additions and 0 deletions

128
src/App.tsx Normal file
View File

@@ -0,0 +1,128 @@
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],
);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
>
<Background />
</ReactFlow>
);
}
export default Flow;

11
src/edges/index.ts Normal file
View File

@@ -0,0 +1,11 @@
import type { Edge, EdgeTypes } from '@xyflow/react';
export const initialEdges: Edge[] = [
{ id: 'a->c', source: 'a', target: 'c', animated: true },
{ id: 'b->d', source: 'b', target: 'd' },
{ id: 'c->d', source: 'c', target: 'd', animated: true },
];
export const edgeTypes = {
// Add your custom edge types here!
} satisfies EdgeTypes;

122
src/index.css Normal file
View File

@@ -0,0 +1,122 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
}
html,
body,
#root {
height: 100%;
margin: 0;
}
.react-flow__node-input,
.react-flow__node-default,
.react-flow__node-output,
.react-flow__node-group {
width: 275px !important;
}
.react-flow__attribution {
display: none;
}
/* Popup Styling */
.popup {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 600px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.popup.hidden {
display: none;
}
.popup-content {
background: #fff;
padding: 20px;
border-radius: 5px;
width: 100%;
max-width: 600px;
position: relative;
}
.popup-close {
position: absolute;
top: 10px;
right: 10px;
font-size: 20px;
cursor: pointer;
}
form {
display: flex;
flex-direction: column;
}
form label {
font-weight: bold;
margin-bottom: 5px;
color: #333;
display: block;
}
form input,
form textarea,
form select {
padding: 10px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
width: 97%;
}
form textarea {
resize: vertical;
}
form button {
padding: 10px 15px;
background-color: #007bff;
border: none;
border-radius: 4px;
color: white;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
form button:hover {
background-color: #0056b3;
}
form .form-group {
margin-bottom: 15px;
}
form .form-group:last-child {
margin-bottom: 0;
}
#popup-form-fields {
width: 99%;
padding: 10px 0;
}
.popup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
}

12
src/main.tsx Normal file
View File

@@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import NoteFlow from './nodes/NoteFlow';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<NoteFlow />
</React.StrictMode>
);

293
src/nodes/NoteFlow.tsx Normal file
View File

@@ -0,0 +1,293 @@
import { useState } from 'react';
import {ReactFlow, addEdge, Controls, Background, MarkerType } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { FiUser, FiMail, FiCheckCircle,FiList } from 'react-icons/fi';
import {FaShuffle } from "react-icons/fa6";
import { event } from 'jquery';
const initialNode = {
id: '1',
type: 'input',
data: {
label: (
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
<FiUser style={{ marginRight: 5 }} />
<span>Customer signs up for product updates</span>
</div>
)
},
position: { x: 250, y: 100 },
};
const tabBarOptions = [
{
label: 'Send email',
id: '2',
type: '',
html: (
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
<FiMail style={{ marginRight: 5 }} />
Send email
</div>
),
position: { x: 250, y: 200 },
},
{
label: 'Replied to conversation?',
id: '3',
type: '',
html: (
<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: 290 },
},
{
label: 'Send survey',
id: '4',
type: 'output',
html: (
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
<FiCheckCircle style={{ marginRight: 5 }} />
Send survey
</div>
),
position: { x: 100, y: 400 },
},
{
label: 'Contact Exists',
id: '5',
type: 'output',
html: (
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
<FiList style={{ marginRight: 5 }} />
Contact Exits
</div>
),
position: { x: 400, y: 400 },
},
];
// Nội dung cho mỗi node
const nodeContents = {
'2': `
<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>
`,
'1': `
<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">
`,
'3': `
<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>
`,
'5': `
<label for="contact-id">Contact ID:</label>
<input type="text" id="contact-id" name="contact-id" placeholder="Enter contact ID">
`,
'4': `
<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 NoteFlow = () => {
const [nodes, setNodes] = useState([initialNode]);
const [edges, setEdges] = useState([]);
const [showTabBar, setShowTabBar] = useState(false);
const [lastNodeId, setLastNodeId] = useState('1');
const [showAdditionalOptions, setShowAdditionalOptions] = useState(false);
const [selectedNodeContent, setSelectedNodeContent] = useState('');
const [showPopup, setShowPopup] = useState(false);
const handleAddNoteClick = () => {
setShowTabBar(true);
};
const handleTabClick = (option) => {
setShowTabBar(false);
const newNode = {
id: option.id,
data: { label: option.html },
position: { x: option.position.x, y: option.position.y },
};
setNodes((nds) => [...nds, newNode]);
setEdges((eds) => addEdge({ id: `e${lastNodeId}-${option.id}`, source: lastNodeId, target: option.id, animated: false }, eds));
setLastNodeId(option.id);
if (option.id === '3') {
const yesNode = {
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: 400 },
};
const noNode = {
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: 400 },
};
setNodes((nds) => [...nds, yesNode, noNode]);
setEdges((eds) => [
...eds,
{ id: 'e3-4', source: '3', target: '4', label: 'yes', animated: false,},
{ id: 'e3-5', source: '3', target: '5', label: 'no', animated: false},
]);
}
};
const handleAdditionalOptionClick = (option) => {
setShowAdditionalOptions(false);
const surveyNode = {
id: option.id,
data: { label: 'Send survey' },
position: { x: 100, y: 150 * (nodes.length + 1) },
};
const contactExistsNode = {
id: '5',
data: { label: 'Contact Exists' },
position: { x: 400, y: 150 * (nodes.length + 1) },
};
setNodes((nds) => [...nds, surveyNode, contactExistsNode]);
setEdges((eds) => [
...eds,
{ id: `e${lastNodeId}-${option.id}`, source: lastNodeId, target: option.id, label: 'yes', animated: true },
{ id: `e${option.id}-5`, source: option.id, target: '5', label: 'no', animated: true },
]);
};
const onNodeClick = (event, node) => {
const content = nodeContents[node.id];
setSelectedNodeContent(content);
setShowPopup(true);
};
const hideenPopup = () => {
setShowPopup(false);
}
const onConnect = (params) => setEdges((eds) => addEdge(params, eds));
return (
<div style={{ height: '100vh', width: '100%' }}>
<ReactFlow nodes={nodes} edges={edges} onConnect={onConnect} onNodeClick={onNodeClick}>
<Controls />
<Background />
</ReactFlow>
<button
onClick={handleAddNoteClick}
style={{
position: 'absolute',
top: 10,
left: 10,
zIndex: 10,
padding: '10px',
}}
>
Add Note
</button>
{showTabBar && (
<div
style={{
position: 'absolute',
top: 50,
left: 10,
zIndex: 10,
background: 'white',
border: '1px solid #ccc',
padding: '10px',
}}
>
{tabBarOptions.map((option) => (
<div
key={option.id}
onClick={() => handleTabClick(option)}
style={{
padding: '10px',
cursor: 'pointer',
borderBottom: '1px solid #ddd',
}}
>
{option.label}
</div>
))}
</div>
)}
{showAdditionalOptions && (
<div className='popup'
>
{additionalOptions.map((option) => (
<div
key={option.id}
onClick={() => handleAdditionalOptionClick(option)}
style={{
padding: '10px',
cursor: 'pointer',
borderBottom: '1px solid #ddd',
}}
>
{option.label}
</div>
))}
</div>
)}
{showPopup && (
<>
<div className="popup-overlay" onClick={hideenPopup}></div>
<div
className={`popup global-popup ${showPopup ? '' : 'hidden'}`}
>
<div className="popup-content">
<div className="conent-form">
<form>
<div dangerouslySetInnerHTML={{ __html: selectedNodeContent }} />
<button onClick={() => setShowPopup(false)}>Close</button>
</form>
</div>
</div>
</div>
</>
)}
</div>
);
};
export default NoteFlow;

View File

@@ -0,0 +1,25 @@
import { Handle, Position, type NodeProps } from '@xyflow/react';
import { type PositionLoggerNode } from './types';
export function PositionLoggerNode({
positionAbsoluteX,
positionAbsoluteY,
data,
}: NodeProps<PositionLoggerNode>) {
const x = `${Math.round(positionAbsoluteX)}px`;
const y = `${Math.round(positionAbsoluteY)}px`;
return (
// We add this class to use the same styles as React Flow's default nodes.
<div className="react-flow__node-default">
{data.label && <div>{data.label}</div>}
<div>
{x} {y}
</div>
<Handle type="source" position={Position.Bottom} />
</div>
);
}

View File

@@ -0,0 +1,31 @@
import { useCallback } from 'react';
import { Handle, Position } from '@xyflow/react';
const handleStyle = { left: 10 };
function TextUpdaterNode({ data, isConnectable }) {
const onChange = useCallback((evt) => {
console.log(evt.target.value);
}, []);
return (
<div className="text-updater-node">
<div className="customer-sign-up node" data-node="customerSignsUpNode"><p><i className="fa-regular fa-circle-play"></i> Customer signs up for product updates</p></div>
<Handle
type="source"
position={Position.Bottom}
id="a"
style={handleStyle}
isConnectable={isConnectable}
/>
<Handle
type="source"
position={Position.Bottom}
id="b"
isConnectable={isConnectable}
/>
</div>
);
}
export default TextUpdaterNode;

26
src/nodes/index.ts Normal file
View File

@@ -0,0 +1,26 @@
import type { NodeTypes } from '@xyflow/react';
import { PositionLoggerNode } from './PositionLoggerNode';
import { AppNode } from './types';
export const initialNodes: AppNode[] = [
{ id: 'a', type: 'input', position: { x: 0, y: 0 }, data: { label: 'wire' } },
{
id: 'b',
type: 'position-logger',
position: { x: -100, y: 100 },
data: { label: 'drag me!' },
},
{ id: 'c', position: { x: 100, y: 100 }, data: { label: 'your ideas' } },
{
id: 'd',
type: 'output',
position: { x: 0, y: 200 },
data: { label: 'with React Flow' },
},
];
export const nodeTypes = {
'position-logger': PositionLoggerNode,
// Add any of your custom nodes here!
} satisfies NodeTypes;

5
src/nodes/types.ts Normal file
View File

@@ -0,0 +1,5 @@
import type { Node, BuiltInNode } from '@xyflow/react';
export type PositionLoggerNode = Node<{ label: string }, 'position-logger'>;
export type AppNode = BuiltInNode | PositionLoggerNode;

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />