'use client' import { memo, useState, useCallback, useMemo } from 'react' import { Handle, Position } from '@xyflow/react' import type { NodeProps } from '@xyflow/react' import type { OutcomeNodeData } from '@/lib/types' import { useConnectionState } from '../ConnectionContext' function OutcomeNode({ data, selected, id }: NodeProps) { const nodeData = data as OutcomeNodeData const { label, description, fundingReceived, fundingTarget, status } = nodeData const [isExpanded, setIsExpanded] = useState(false) const connectingFrom = useConnectionState() const progress = fundingTarget > 0 ? Math.min(100, (fundingReceived / fundingTarget) * 100) : 0 const isFunded = fundingReceived >= fundingTarget const isPartial = fundingReceived > 0 && fundingReceived < fundingTarget // Handle highlighting: outcome targets glow when dragging from spending or stream handles const isTargetHighlighted = useMemo(() => { if (!connectingFrom || connectingFrom.nodeId === id) return false const handleId = connectingFrom.handleId if (handleId === 'spending-out') return true if (handleId === 'stream-out') return true return false }, [connectingFrom, id]) const statusColors = { 'not-started': { bg: 'bg-slate-100', text: 'text-slate-600', border: 'border-slate-300' }, 'in-progress': { bg: 'bg-blue-100', text: 'text-blue-700', border: 'border-blue-300' }, 'completed': { bg: 'bg-emerald-100', text: 'text-emerald-700', border: 'border-emerald-300' }, 'blocked': { bg: 'bg-red-100', text: 'text-red-700', border: 'border-red-300' }, } const colors = statusColors[status] || statusColors['not-started'] const handleDoubleClick = useCallback((e: React.MouseEvent) => { e.stopPropagation() setIsExpanded(true) }, []) const handleCloseExpanded = useCallback(() => { setIsExpanded(false) }, []) return ( <>
{label} {nodeData.source?.type === 'rvote' && nodeData.source.rvoteSpaceSlug && nodeData.source.rvoteProposalId ? ( e.stopPropagation()} > rVote • score +{nodeData.source.rvoteProposalScore ?? 0} ) : nodeData.source?.type === 'rvote' ? ( rVote • score +{nodeData.source.rvoteProposalScore ?? 0} ) : null}
{description && (

{description}

)}
{status.replace('-', ' ')} {isFunded && ( )}
Funding ${Math.floor(fundingReceived).toLocaleString()} / ${fundingTarget.toLocaleString()}
{progress.toFixed(0)}%
Double-click for details
{/* Expanded Detail Modal */} {isExpanded && (
e.stopPropagation()} > {/* Header */}

{label}

{status.replace('-', ' ')} {nodeData.source?.type === 'rvote' && nodeData.source.rvoteSpaceSlug && nodeData.source.rvoteProposalId && ( rVote )}
{/* Description */} {description && (
Description

{description}

)} {/* Funding Progress */}
Funding Progress
${Math.floor(fundingReceived).toLocaleString()} / ${fundingTarget.toLocaleString()}
{progress.toFixed(1)}%
{/* rVote Details */} {nodeData.source?.type === 'rvote' && (
rVote Details
Score +{nodeData.source.rvoteProposalScore ?? 0}
{nodeData.source.rvoteProposalStatus && (
Status {nodeData.source.rvoteProposalStatus}
)} {nodeData.source.rvoteSpaceSlug && nodeData.source.rvoteProposalId && ( View on rVote )}
)} {/* Milestones */} {nodeData.milestones && nodeData.milestones.length > 0 && (
Milestones
{nodeData.milestones.map((milestone, i) => (
{milestone.completedAt ? ( ) : (
)} {milestone.label}
))}
)} {/* Completion Info */} {nodeData.completedAt && (
Completed {new Date(nodeData.completedAt).toLocaleDateString()}
)} {/* Close Button */}
)} ) } export default memo(OutcomeNode)