import React, { useRef, useMemo, useCallback, useState, useEffect } from 'react'; import { COMPONENTS } from '../../data/components'; import { useFlowStore } from '../../store/useFlowStore'; import { useAnimationQueue } from '../../hooks/useAnimationQueue'; import { SystemComponent } from './SystemComponent'; import { ConnectionLine } from './ConnectionLine'; import { AnimatedMessage } from './AnimatedMessage'; import { Icon } from '../shared/Icon'; const ALL_CONNECTIONS = [ ['rider-app', 'backend-api'], ['driver-app', 'backend-api'], ['backend-api', 'bullmq'], ['backend-api', 'database'], ['backend-api', 'pubsub'], ['backend-api', 'dac'], ['backend-api', 'ops-dashboard'], ['ops-dashboard', 'backend-api'], ]; export const FlowCanvas: React.FC = () => { const containerRef = useRef(null); const [dims, setDims] = useState({ width: 0, height: 0 }); const getCurrentStep = useFlowStore((s) => s.getCurrentStep); const speed = useFlowStore((s) => s.speed); const zoomLevel = useFlowStore((s) => s.zoomLevel); const zoomIn = useFlowStore((s) => s.zoomIn); const zoomOut = useFlowStore((s) => s.zoomOut); const resetZoom = useFlowStore((s) => s.resetZoom); const selectedPath = useFlowStore((s) => s.selectedPath); const currentStep = getCurrentStep(); const { completedTargets, animKey } = useAnimationQueue(); useEffect(() => { const el = containerRef.current; if (!el) return; const observer = new ResizeObserver((entries) => { const entry = entries[0]; if (entry) { setDims({ width: entry.contentRect.width, height: entry.contentRect.height, }); } }); observer.observe(el); return () => observer.disconnect(); }, []); const activeComponents = useMemo( () => new Set(currentStep?.activeComponents || []), [currentStep] ); const activeConnections = useMemo(() => { if (!currentStep) return new Set(); const conns = new Set(); for (const msg of currentStep.messages) { conns.add(`${msg.from}-${msg.to}`); conns.add(`${msg.to}-${msg.from}`); } return conns; }, [currentStep]); const isConnectionActive = useCallback( (from: string, to: string) => { return ( activeConnections.has(`${from}-${to}`) || activeConnections.has(`${to}-${from}`) ); }, [activeConnections] ); const messages = currentStep?.messages || []; // Optional loop-back arc, swept below the canvas to read as "the pipeline repeats". // Rendered only when both endpoint nodes exist (e.g. the SDLC-overview's // trust-log → product-hub cycle); a no-op for diagrams without them. const loopBack = useMemo(() => { if (dims.width === 0) return null; const a = COMPONENTS.find((c) => c.id === 'trust-log'); const b = COMPONENTS.find((c) => c.id === 'product-hub'); if (!a || !b) return null; const x1 = (a.position.x / 100) * dims.width; const y1 = (a.position.y / 100) * dims.height; const x2 = (b.position.x / 100) * dims.width; const y2 = (b.position.y / 100) * dims.height; const yb = dims.height * 0.985; return { d: `M ${x1} ${y1} C ${x1} ${yb}, ${x2} ${yb}, ${x2} ${y2}`, labelX: (x1 + x2) / 2, labelY: dims.height * 0.92, }; }, [dims]); return (
{/* Canvas Header Overlay */}

{selectedPath.label}

Live Simulation
{/* Zoomable content */}
{/* SVG layer for connection lines */} {dims.width > 0 && ( {ALL_CONNECTIONS.map(([from, to]) => ( ))} {/* Loop-back: the pipeline repeats per epic */} {loopBack && ( ↺ pipeline repeats per epic )} )} {/* Animated messages */} {dims.width > 0 && messages.map((msg) => ( ))} {/* System component nodes */} {COMPONENTS.map((comp) => ( ))}
{/* Current status badge */} {currentStep && (
Status
{currentStep.status}
{currentStep.bookingStatus}
)}
); };