up
This commit is contained in:
293
src/nodes/NoteFlow.tsx
Normal file
293
src/nodes/NoteFlow.tsx
Normal 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;
|
||||
|
||||
|
||||
25
src/nodes/PositionLoggerNode.tsx
Normal file
25
src/nodes/PositionLoggerNode.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
31
src/nodes/TextUpdaterNode.tsx
Normal file
31
src/nodes/TextUpdaterNode.tsx
Normal 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
26
src/nodes/index.ts
Normal 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
5
src/nodes/types.ts
Normal 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;
|
||||
|
||||
Reference in New Issue
Block a user