From 0adf8bfff0de7fb1b2c9a4479ffe8e34ab83cce0 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Fri, 16 Jan 2026 17:36:30 +0000 Subject: [PATCH] Generated by Spark: To cope with the 1:1 mapping, we can add more connection points as needed, always ensure theres a spare blank one. --- src/components/FeatureIdeaCloud.tsx | 211 +++++++++++++++++++--------- 1 file changed, 148 insertions(+), 63 deletions(-) diff --git a/src/components/FeatureIdeaCloud.tsx b/src/components/FeatureIdeaCloud.tsx index eb58cc5..a594135 100644 --- a/src/components/FeatureIdeaCloud.tsx +++ b/src/components/FeatureIdeaCloud.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useRef } from 'react' +import { useState, useEffect, useCallback, useRef, ReactElement } from 'react' import { useKV } from '@github/spark/hooks' import ReactFlow, { Node, @@ -217,33 +217,64 @@ function GroupNode({ data, selected }: NodeProps) { ) } -function IdeaNode({ data, selected }: NodeProps) { +function IdeaNode({ data, selected, id }: NodeProps & { id: string }) { + const [connectionCounts, setConnectionCounts] = useState>({ + left: 0, + right: 0, + top: 0, + bottom: 0, + }) + + useEffect(() => { + const updateConnectionCounts = (event: CustomEvent) => { + const { nodeId, counts } = event.detail + if (nodeId === id) { + setConnectionCounts(counts) + } + } + + window.addEventListener('updateConnectionCounts' as any, updateConnectionCounts as EventListener) + return () => { + window.removeEventListener('updateConnectionCounts' as any, updateConnectionCounts as EventListener) + } + }, [id]) + + const generateHandles = (position: Position, type: 'source' | 'target', side: string) => { + const count = connectionCounts[side] || 0 + const totalHandles = Math.max(2, count + 1) + const handles: ReactElement[] = [] + + for (let i = 0; i < totalHandles; i++) { + const handleId = `${side}-${i}` + const isVertical = position === Position.Top || position === Position.Bottom + const positionStyle = isVertical + ? { left: `${((i + 1) / (totalHandles + 1)) * 100}%` } + : { top: `${((i + 1) / (totalHandles + 1)) * 100}%` } + + handles.push( + + ) + } + + return handles + } + return (
- - - - + {generateHandles(Position.Left, 'target', 'left')} + {generateHandles(Position.Right, 'source', 'right')} + {generateHandles(Position.Top, 'target', 'top')} + {generateHandles(Position.Bottom, 'source', 'bottom')}
@@ -292,8 +323,8 @@ export function FeatureIdeaCloud() { id: 'edge-1', source: 'idea-1', target: 'idea-8', - sourceHandle: 'right', - targetHandle: 'left', + sourceHandle: 'right-0', + targetHandle: 'left-0', type: 'default', animated: false, data: { label: 'requires' }, @@ -304,8 +335,8 @@ export function FeatureIdeaCloud() { id: 'edge-2', source: 'idea-2', target: 'idea-4', - sourceHandle: 'bottom', - targetHandle: 'top', + sourceHandle: 'bottom-0', + targetHandle: 'top-0', type: 'default', data: { label: 'works with' }, markerEnd: { type: MarkerType.ArrowClosed, color: '#a78bfa', width: 20, height: 20 }, @@ -315,8 +346,8 @@ export function FeatureIdeaCloud() { id: 'edge-3', source: 'idea-8', target: 'idea-5', - sourceHandle: 'bottom', - targetHandle: 'left', + sourceHandle: 'bottom-0', + targetHandle: 'left-0', type: 'default', data: { label: 'includes' }, markerEnd: { type: MarkerType.ArrowClosed, color: '#a78bfa', width: 20, height: 20 }, @@ -342,6 +373,42 @@ export function FeatureIdeaCloud() { const safeEdges = savedEdges || [] const safeNodePositions = savedNodePositions || {} + const updateNodeConnectionCounts = useCallback((edges: Edge[]) => { + const nodeConnectionMap = new Map>>() + + edges.forEach(edge => { + const sourceHandle = edge.sourceHandle || 'default' + const targetHandle = edge.targetHandle || 'default' + + if (!nodeConnectionMap.has(edge.source)) { + nodeConnectionMap.set(edge.source, { left: new Set(), right: new Set(), top: new Set(), bottom: new Set() }) + } + if (!nodeConnectionMap.has(edge.target)) { + nodeConnectionMap.set(edge.target, { left: new Set(), right: new Set(), top: new Set(), bottom: new Set() }) + } + + const sourceSide = sourceHandle.split('-')[0] + const targetSide = targetHandle.split('-')[0] + + nodeConnectionMap.get(edge.source)![sourceSide].add(sourceHandle) + nodeConnectionMap.get(edge.target)![targetSide].add(targetHandle) + }) + + nodeConnectionMap.forEach((connections, nodeId) => { + const counts = { + left: connections.left.size, + right: connections.right.size, + top: connections.top.size, + bottom: connections.bottom.size, + } + + const event = new CustomEvent('updateConnectionCounts', { + detail: { nodeId, counts } + }) + window.dispatchEvent(event) + }) + }, []) + useEffect(() => { if (!ideas || ideas.length === 0) { setIdeas(SEED_IDEAS) @@ -376,7 +443,8 @@ export function FeatureIdeaCloud() { useEffect(() => { setEdges(safeEdges) - }, [safeEdges, setEdges]) + updateNodeConnectionCounts(safeEdges) + }, [safeEdges, setEdges, updateNodeConnectionCounts]) useEffect(() => { const handleEditIdea = (e: Event) => { @@ -431,11 +499,12 @@ export function FeatureIdeaCloud() { setTimeout(() => { setEdges((currentEdges) => { setSavedEdges(currentEdges) + updateNodeConnectionCounts(currentEdges) return currentEdges }) }, 100) }, - [onEdgesChange, setEdges, setSavedEdges] + [onEdgesChange, setEdges, setSavedEdges, updateNodeConnectionCounts] ) const validateAndRemoveConflicts = useCallback(( @@ -552,6 +621,7 @@ export function FeatureIdeaCloud() { }) setSavedEdges(updatedEdges) + updateNodeConnectionCounts(updatedEdges) if (removedCount > 0) { setTimeout(() => { @@ -568,7 +638,7 @@ export function FeatureIdeaCloud() { return updatedEdges }) }, - [setEdges, setSavedEdges, validateAndRemoveConflicts] + [setEdges, setSavedEdges, validateAndRemoveConflicts, updateNodeConnectionCounts] ) const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => { @@ -624,6 +694,7 @@ export function FeatureIdeaCloud() { }))) setSavedEdges(updatedEdges) + updateNodeConnectionCounts(updatedEdges) if (removedCount > 0) { setTimeout(() => { @@ -639,7 +710,7 @@ export function FeatureIdeaCloud() { return updatedEdges }) - }, [setEdges, setSavedEdges, validateAndRemoveConflicts]) + }, [setEdges, setSavedEdges, validateAndRemoveConflicts, updateNodeConnectionCounts]) const onReconnectEnd = useCallback((_: MouseEvent | TouchEvent, edge: Edge) => { if (!edgeReconnectSuccessful.current) { @@ -726,6 +797,7 @@ export function FeatureIdeaCloud() { const updatedEdges = edges.filter(e => e.source !== id && e.target !== id) setEdges(updatedEdges) setSavedEdges(updatedEdges) + updateNodeConnectionCounts(updatedEdges) setEditDialogOpen(false) setViewDialogOpen(false) @@ -797,6 +869,7 @@ export function FeatureIdeaCloud() { const updatedEdges = edges.filter(e => e.id !== edgeId) setEdges(updatedEdges) setSavedEdges(updatedEdges) + updateNodeConnectionCounts(updatedEdges) setEdgeDialogOpen(false) setSelectedEdge(null) toast.success('Connection removed') @@ -1010,12 +1083,18 @@ export function FeatureIdeaCloud() {
{safeIdeas.slice(0, 10).map((idea) => { const nodeEdges = edges.filter(e => e.source === idea.id || e.target === idea.id) - const leftHandles = edges.filter(e => e.target === idea.id && e.targetHandle === 'left') - const rightHandles = edges.filter(e => e.source === idea.id && e.sourceHandle === 'right') - const topHandles = edges.filter(e => e.target === idea.id && e.targetHandle === 'top') - const bottomHandles = edges.filter(e => e.source === idea.id && e.sourceHandle === 'bottom') + const leftHandles = edges.filter(e => e.target === idea.id && e.targetHandle?.startsWith('left')) + const rightHandles = edges.filter(e => e.source === idea.id && e.sourceHandle?.startsWith('right')) + const topHandles = edges.filter(e => e.target === idea.id && e.targetHandle?.startsWith('top')) + const bottomHandles = edges.filter(e => e.source === idea.id && e.sourceHandle?.startsWith('bottom')) - const hasViolation = leftHandles.length > 1 || rightHandles.length > 1 || topHandles.length > 1 || bottomHandles.length > 1 + const leftUnique = new Set(leftHandles.map(e => e.targetHandle)).size + const rightUnique = new Set(rightHandles.map(e => e.sourceHandle)).size + const topUnique = new Set(topHandles.map(e => e.targetHandle)).size + const bottomUnique = new Set(bottomHandles.map(e => e.sourceHandle)).size + + const hasViolation = leftHandles.length !== leftUnique || rightHandles.length !== rightUnique || + topHandles.length !== topUnique || bottomHandles.length !== bottomUnique return (
@@ -1025,32 +1104,32 @@ export function FeatureIdeaCloud() {
1 ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' : - leftHandles.length === 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted' + leftHandles.length !== leftUnique ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' : + leftHandles.length >= 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted' }`}> - ← {leftHandles.length > 0 ? `✓${leftHandles.length > 1 ? `(${leftHandles.length})` : ''}` : '○'} + ← {leftHandles.length}/{leftUnique} {leftHandles.length !== leftUnique ? '⚠️' : leftHandles.length > 0 ? '✓' : '○'}
1 ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' : - rightHandles.length === 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted' + rightHandles.length !== rightUnique ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' : + rightHandles.length >= 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted' }`}> - → {rightHandles.length > 0 ? `✓${rightHandles.length > 1 ? `(${rightHandles.length})` : ''}` : '○'} + → {rightHandles.length}/{rightUnique} {rightHandles.length !== rightUnique ? '⚠️' : rightHandles.length > 0 ? '✓' : '○'}
1 ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' : - topHandles.length === 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted' + topHandles.length !== topUnique ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' : + topHandles.length >= 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted' }`}> - ↑ {topHandles.length > 0 ? `✓${topHandles.length > 1 ? `(${topHandles.length})` : ''}` : '○'} + ↑ {topHandles.length}/{topUnique} {topHandles.length !== topUnique ? '⚠️' : topHandles.length > 0 ? '✓' : '○'}
1 ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' : - bottomHandles.length === 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted' + bottomHandles.length !== bottomUnique ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' : + bottomHandles.length >= 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted' }`}> - ↓ {bottomHandles.length > 0 ? `✓${bottomHandles.length > 1 ? `(${bottomHandles.length})` : ''}` : '○'} + ↓ {bottomHandles.length}/{bottomUnique} {bottomHandles.length !== bottomUnique ? '⚠️' : bottomHandles.length > 0 ? '✓' : '○'}
- Total: {nodeEdges.length} connection{nodeEdges.length !== 1 ? 's' : ''} + Total: {nodeEdges.length} connection{nodeEdges.length !== 1 ? 's' : ''}, Handles: L{leftUnique}|R{rightUnique}|T{topUnique}|B{bottomUnique}
) @@ -1067,15 +1146,20 @@ export function FeatureIdeaCloud() { {(() => { const violations: string[] = [] safeIdeas.forEach(idea => { - const leftHandles = edges.filter(e => e.target === idea.id && e.targetHandle === 'left') - const rightHandles = edges.filter(e => e.source === idea.id && e.sourceHandle === 'right') - const topHandles = edges.filter(e => e.target === idea.id && e.targetHandle === 'top') - const bottomHandles = edges.filter(e => e.source === idea.id && e.sourceHandle === 'bottom') + const leftHandles = edges.filter(e => e.target === idea.id && e.targetHandle?.startsWith('left')) + const rightHandles = edges.filter(e => e.source === idea.id && e.sourceHandle?.startsWith('right')) + const topHandles = edges.filter(e => e.target === idea.id && e.targetHandle?.startsWith('top')) + const bottomHandles = edges.filter(e => e.source === idea.id && e.sourceHandle?.startsWith('bottom')) - if (leftHandles.length > 1) violations.push(`${idea.title}: Left handle has ${leftHandles.length} connections`) - if (rightHandles.length > 1) violations.push(`${idea.title}: Right handle has ${rightHandles.length} connections`) - if (topHandles.length > 1) violations.push(`${idea.title}: Top handle has ${topHandles.length} connections`) - if (bottomHandles.length > 1) violations.push(`${idea.title}: Bottom handle has ${bottomHandles.length} connections`) + const leftUnique = new Set(leftHandles.map(e => e.targetHandle)).size + const rightUnique = new Set(rightHandles.map(e => e.sourceHandle)).size + const topUnique = new Set(topHandles.map(e => e.targetHandle)).size + const bottomUnique = new Set(bottomHandles.map(e => e.sourceHandle)).size + + if (leftHandles.length !== leftUnique) violations.push(`${idea.title}: Left side has duplicate handle usage (${leftHandles.length} connections on ${leftUnique} handles)`) + if (rightHandles.length !== rightUnique) violations.push(`${idea.title}: Right side has duplicate handle usage (${rightHandles.length} connections on ${rightUnique} handles)`) + if (topHandles.length !== topUnique) violations.push(`${idea.title}: Top side has duplicate handle usage (${topHandles.length} connections on ${topUnique} handles)`) + if (bottomHandles.length !== bottomUnique) violations.push(`${idea.title}: Bottom side has duplicate handle usage (${bottomHandles.length} connections on ${bottomUnique} handles)`) }) return violations.length > 0 ? ( @@ -1095,8 +1179,9 @@ export function FeatureIdeaCloud() { ✅ All Constraints Satisfied
-
• Each handle has at most 1 connection ✓
+
• Each handle has exactly 1 connection (1:1 mapping) ✓
• New connections automatically remove conflicts ✓
+
• Spare blank handles always available ✓
• Remapping preserves 1:1 constraint ✓
• Changes persist to database immediately ✓