Files
clube67_newwhats.local/clube67/newwhats.local/frontend/pages/flow-builder/index.tsx
T

226 lines
10 KiB
TypeScript

import React, { useCallback, useRef } from 'react';
import {
ReactFlow,
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
useReactFlow,
addEdge,
Connection,
ReactFlowProvider,
Node,
Edge,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { ArrowLeft, Save, Play, MessageSquare, Zap, Clock, Image as ImageIcon } from 'lucide-react';
import Link from 'next/link';
// --- Custom Nodes Types Implementation ---
import MessageNode from './_components/MessageNode';
import TriggerNode from './_components/TriggerNode';
const nodeTypes = {
messageNode: MessageNode,
triggerNode: TriggerNode,
};
// Initial state for the canvas
const initialNodes: Node[] = [
{
id: 'start-trigger',
type: 'triggerNode',
position: { x: 250, y: 150 },
data: { label: 'Keyword Trigger', keyword: 'hello' },
},
];
const initialEdges: Edge[] = [];
function FlowBuilder() {
const { screenToFlowPosition } = useReactFlow();
// Contador via ref: persiste entre re-renders sem resetar no HMR
const nodeIdRef = useRef(0);
const getId = useCallback(() => `node-${nodeIdRef.current++}`, []);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Connection) => {
setEdges((eds) => addEdge({ ...params, animated: true }, eds));
}, [setEdges]);
const onDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, []);
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
const type = event.dataTransfer.getData('application/reactflow');
if (!type) return;
// screenToFlowPosition em @xyflow/react v12 recebe coordenadas de tela
// (clientX/Y) e desconta internamente a posição do container.
// NÃO subtrair getBoundingClientRect() — isso duplicaria o offset.
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
const newNode: Node = {
id: getId(),
type,
position,
data: { label: `${type} node` },
};
setNodes((nds) => nds.concat(newNode));
},
[screenToFlowPosition, getId, setNodes]
);
const onDragStart = (event: React.DragEvent, nodeType: string) => {
event.dataTransfer.setData('application/reactflow', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
return (
<div className="h-screen w-full flex flex-col bg-[#070b14] overflow-hidden">
{/* Header */}
<header className="h-16 px-6 border-b border-white/5 bg-white/[0.02] flex items-center justify-between z-10">
<div className="flex items-center gap-4">
<Link href="/dashboard" className="p-2 -ml-2 rounded-lg text-slate-400 hover:text-white hover:bg-white/5 transition-all">
<ArrowLeft size={18} />
</Link>
<div className="h-8 w-[1px] bg-white/10" />
<div>
<h1 className="text-white font-bold text-sm">Main Customer Flow</h1>
<p className="text-xs text-brand-400">Status: Draft</p>
</div>
</div>
<div className="flex items-center gap-3">
<button className="flex items-center gap-2 px-4 py-2 rounded-lg bg-white/5 text-white text-sm font-semibold hover:bg-white/10 transition-all border border-white/10">
<Play size={14} className="text-slate-300" />
Test Flow
</button>
<button className="flex items-center gap-2 px-4 py-2 rounded-lg bg-brand-500 text-white text-sm font-bold shadow-lg shadow-brand-500/20 hover:bg-brand-600 transition-all">
<Save size={14} />
Publish Builder
</button>
</div>
</header>
<div className="flex-1 flex w-full h-full relative">
{/* Sidebar Toolbar */}
<aside className="w-72 bg-white/[0.02] border-r border-white/5 p-4 flex flex-col gap-6 z-10 overflow-y-auto">
<div>
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-widest mb-4 ml-1">Triggers</h3>
<div className="space-y-2">
<div
onDragStart={(event) => onDragStart(event, 'triggerNode')}
draggable
className="p-3 bg-white/5 border border-white/10 rounded-xl cursor-grab hover:bg-white/10 hover:border-brand-500/50 transition-all flex items-center gap-3 group"
>
<div className="w-8 h-8 rounded-lg bg-brand-500/20 flex items-center justify-center">
<Zap size={16} className="text-brand-400" />
</div>
<div>
<p className="text-sm font-medium text-white group-hover:text-brand-400 transition-colors">Keyword</p>
<p className="text-[11px] text-slate-400">Trigger on exact match</p>
</div>
</div>
</div>
</div>
<div>
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-widest mb-4 ml-1">Messages</h3>
<div className="space-y-2">
<div
onDragStart={(event) => onDragStart(event, 'messageNode')}
draggable
className="p-3 bg-white/5 border border-white/10 rounded-xl cursor-grab hover:bg-white/10 hover:border-blue-500/50 transition-all flex items-center gap-3 group"
>
<div className="w-8 h-8 rounded-lg bg-blue-500/20 flex items-center justify-center">
<MessageSquare size={16} className="text-blue-400" />
</div>
<div>
<p className="text-sm font-medium text-white group-hover:text-blue-400 transition-colors">Text Message</p>
<p className="text-[11px] text-slate-400">Send standard WhatsApp text</p>
</div>
</div>
<div className="p-3 bg-white/5 border border-white/10 rounded-xl cursor-not-allowed opacity-50 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center">
<ImageIcon size={16} className="text-purple-400" />
</div>
<div>
<p className="text-sm font-medium text-white">Media/Doc</p>
<p className="text-[11px] text-slate-400">Coming soon</p>
</div>
</div>
</div>
</div>
<div>
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-widest mb-4 ml-1">Actions</h3>
<div className="space-y-2">
<div className="p-3 bg-white/5 border border-white/10 rounded-xl cursor-not-allowed opacity-50 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-orange-500/20 flex items-center justify-center">
<Clock size={16} className="text-orange-400" />
</div>
<div>
<p className="text-sm font-medium text-white">Delay</p>
<p className="text-[11px] text-slate-400">Wait X seconds</p>
</div>
</div>
</div>
</div>
</aside>
{/* ReactFlow Canvas */}
<div className="flex-1 h-full">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onDrop={onDrop}
onDragOver={onDragOver}
nodeTypes={nodeTypes}
fitView
className="bg-[#04060b]"
minZoom={0.2}
maxZoom={2}
>
<Background color="#ffffff" gap={20} size={1} />
<Controls className="bg-white/5 border-white/10 fill-white !text-white" />
<MiniMap
nodeColor={(n: any) => {
if (n.type === 'triggerNode') return '#10b981';
return '#3b82f6';
}}
maskColor="rgba(0,0,0,0.7)"
className="bg-black/50 border border-white/10 rounded-xl !w-40"
/>
</ReactFlow>
</div>
</div>
</div>
);
}
// Wrap with Provider
export default function FlowBuilderPage() {
return (
<ReactFlowProvider>
<FlowBuilder />
</ReactFlowProvider>
);
}