"use client" import { useEffect, useRef, useState } from "react" import Link from "next/link" import type { FlowFundingNetwork, Allocation } from "@/lib/tbff/types" import { renderNetwork } from "@/lib/tbff/rendering" import { sampleNetworks, networkOptions, getSampleNetwork } from "@/lib/tbff/sample-networks" import { formatCurrency, getStatusColorClass, normalizeAllocations, calculateNetworkTotals, updateAccountComputedProperties } from "@/lib/tbff/utils" import { initialDistribution, getDistributionSummary } from "@/lib/tbff/algorithms" type Tool = 'select' | 'create-allocation' export default function TBFFPage() { const canvasRef = useRef(null) const [network, setNetwork] = useState(sampleNetworks.statesDemo) const [selectedAccountId, setSelectedAccountId] = useState(null) const [selectedAllocationId, setSelectedAllocationId] = useState(null) const [selectedNetworkKey, setSelectedNetworkKey] = useState('statesDemo') const [tool, setTool] = useState('select') const [allocationSourceId, setAllocationSourceId] = useState(null) const [isDragging, setIsDragging] = useState(false) const [draggedAccountId, setDraggedAccountId] = useState(null) const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }) const [mouseDownPos, setMouseDownPos] = useState<{ x: number; y: number } | null>(null) const [fundingAmount, setFundingAmount] = useState(1000) const [lastDistribution, setLastDistribution] = useState<{ totalDistributed: number accountsChanged: number changes: Array<{ accountId: string; name: string; before: number; after: number; delta: number }> } | null>(null) // Render canvas whenever network changes useEffect(() => { const canvas = canvasRef.current if (!canvas) return const ctx = canvas.getContext("2d") if (!ctx) return // Set canvas size canvas.width = canvas.offsetWidth canvas.height = canvas.offsetHeight // Render the network renderNetwork(ctx, network, canvas.width, canvas.height, selectedAccountId, selectedAllocationId) }, [network, selectedAccountId, selectedAllocationId]) // Handle mouse down - record position for all interactions const handleMouseDown = (e: React.MouseEvent) => { const canvas = canvasRef.current if (!canvas) return const rect = canvas.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top // Always record mouse down position setMouseDownPos({ x, y }) // Find clicked account const clickedAccount = network.accounts.find( (acc) => x >= acc.x && x <= acc.x + acc.width && y >= acc.y && y <= acc.y + acc.height ) // Prepare for potential drag (only in select mode) if (tool === 'select' && clickedAccount) { setDraggedAccountId(clickedAccount.id) setDragOffset({ x: x - clickedAccount.x, y: y - clickedAccount.y, }) } } // Handle mouse move - start drag if threshold exceeded (select mode only) const handleMouseMove = (e: React.MouseEvent) => { const canvas = canvasRef.current if (!canvas) return const rect = canvas.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top // Only handle dragging in select mode if (tool === 'select' && mouseDownPos && draggedAccountId && !isDragging) { const dx = x - mouseDownPos.x const dy = y - mouseDownPos.y const distance = Math.sqrt(dx * dx + dy * dy) // Start drag if moved more than 5 pixels if (distance > 5) { setIsDragging(true) } } // If dragging, update position if (isDragging && draggedAccountId) { const updatedNetwork = calculateNetworkTotals({ ...network, accounts: network.accounts.map((acc) => acc.id === draggedAccountId ? updateAccountComputedProperties({ ...acc, x: x - dragOffset.x, y: y - dragOffset.y, }) : acc ), }) setNetwork(updatedNetwork) } } // Handle mouse up - end dragging or handle click const handleMouseUp = (e: React.MouseEvent) => { const canvas = canvasRef.current if (!canvas) return const rect = canvas.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top // If was dragging, just end drag if (isDragging) { setIsDragging(false) setDraggedAccountId(null) setMouseDownPos(null) return } // Clear drag-related state setDraggedAccountId(null) setMouseDownPos(null) // Find what was clicked const clickedAccount = network.accounts.find( (acc) => x >= acc.x && x <= acc.x + acc.width && y >= acc.y && y <= acc.y + acc.height ) // Handle based on current tool if (tool === 'select') { if (clickedAccount) { setSelectedAccountId(clickedAccount.id) setSelectedAllocationId(null) } else { // Check if clicked on an allocation arrow const clickedAllocation = findAllocationAtPoint(x, y) if (clickedAllocation) { setSelectedAllocationId(clickedAllocation.id) setSelectedAccountId(null) } else { setSelectedAccountId(null) setSelectedAllocationId(null) } } } else if (tool === 'create-allocation') { if (clickedAccount) { if (!allocationSourceId) { // First click - set source setAllocationSourceId(clickedAccount.id) } else { // Second click - create allocation if (clickedAccount.id !== allocationSourceId) { createAllocation(allocationSourceId, clickedAccount.id) } setAllocationSourceId(null) } } } } // Handle mouse leave - only cancel drag, don't deselect const handleMouseLeave = () => { if (isDragging) { setIsDragging(false) } setDraggedAccountId(null) setMouseDownPos(null) } // Find allocation at point (simple distance check) const findAllocationAtPoint = (x: number, y: number): Allocation | null => { const tolerance = 15 for (const allocation of network.allocations) { const source = network.accounts.find(a => a.id === allocation.sourceAccountId) const target = network.accounts.find(a => a.id === allocation.targetAccountId) if (!source || !target) continue const sourceCenter = { x: source.x + source.width / 2, y: source.y + source.height / 2 } const targetCenter = { x: target.x + target.width / 2, y: target.y + target.height / 2 } const distance = pointToLineDistance(x, y, sourceCenter.x, sourceCenter.y, targetCenter.x, targetCenter.y) if (distance < tolerance) { return allocation } } return null } // Point to line distance calculation const pointToLineDistance = ( px: number, py: number, x1: number, y1: number, x2: number, y2: number ): number => { const A = px - x1 const B = py - y1 const C = x2 - x1 const D = y2 - y1 const dot = A * C + B * D const lenSq = C * C + D * D let param = -1 if (lenSq !== 0) { param = dot / lenSq } let xx, yy if (param < 0) { xx = x1 yy = y1 } else if (param > 1) { xx = x2 yy = y2 } else { xx = x1 + param * C yy = y1 + param * D } const dx = px - xx const dy = py - yy return Math.sqrt(dx * dx + dy * dy) } // Create new allocation const createAllocation = (sourceId: string, targetId: string) => { const newAllocation: Allocation = { id: `alloc_${Date.now()}`, sourceAccountId: sourceId, targetAccountId: targetId, percentage: 0.5, // Default 50% } // Add allocation and normalize const updatedAllocations = [...network.allocations, newAllocation] const sourceAllocations = updatedAllocations.filter(a => a.sourceAccountId === sourceId) const normalized = normalizeAllocations(sourceAllocations) // Replace source allocations with normalized ones const finalAllocations = updatedAllocations.map(a => { const normalizedVersion = normalized.find(n => n.id === a.id) return normalizedVersion || a }) const updatedNetwork = calculateNetworkTotals({ ...network, allocations: finalAllocations, }) setNetwork(updatedNetwork) setSelectedAllocationId(newAllocation.id) } // Update allocation percentage const updateAllocationPercentage = (allocationId: string, newPercentage: number) => { const allocation = network.allocations.find(a => a.id === allocationId) if (!allocation) return const updatedAllocations = network.allocations.map(a => a.id === allocationId ? { ...a, percentage: Math.max(0, Math.min(1, newPercentage)) } : a ) // Normalize all allocations from the same source const sourceAllocations = updatedAllocations.filter( a => a.sourceAccountId === allocation.sourceAccountId ) const normalized = normalizeAllocations(sourceAllocations) // Replace source allocations with normalized ones const finalAllocations = updatedAllocations.map(a => { const normalizedVersion = normalized.find(n => n.id === a.id) return normalizedVersion || a }) const updatedNetwork = calculateNetworkTotals({ ...network, allocations: finalAllocations, }) setNetwork(updatedNetwork) } // Delete allocation const deleteAllocation = (allocationId: string) => { const allocation = network.allocations.find(a => a.id === allocationId) if (!allocation) return const updatedAllocations = network.allocations.filter(a => a.id !== allocationId) // Normalize remaining allocations from the same source const sourceAllocations = updatedAllocations.filter( a => a.sourceAccountId === allocation.sourceAccountId ) const normalized = normalizeAllocations(sourceAllocations) // Replace source allocations with normalized ones const finalAllocations = updatedAllocations.map(a => { const normalizedVersion = normalized.find(n => n.id === a.id) return normalizedVersion || a }) const updatedNetwork = calculateNetworkTotals({ ...network, allocations: finalAllocations, }) setNetwork(updatedNetwork) setSelectedAllocationId(null) } // Load different network const handleLoadNetwork = (key: string) => { setSelectedNetworkKey(key) const newNetwork = getSampleNetwork(key as keyof typeof sampleNetworks) setNetwork(newNetwork) setSelectedAccountId(null) setSelectedAllocationId(null) setAllocationSourceId(null) setTool('select') } // Add funding to network const handleAddFunding = () => { if (fundingAmount <= 0) { console.warn('āš ļø Funding amount must be positive') return } const beforeNetwork = network const afterNetwork = initialDistribution(network, fundingAmount) const summary = getDistributionSummary(beforeNetwork, afterNetwork) setNetwork(afterNetwork) setLastDistribution(summary) console.log(`\nāœ… Distribution Complete`) console.log(`Total distributed: ${summary.totalDistributed.toFixed(0)}`) console.log(`Accounts changed: ${summary.accountsChanged}`) } // Get selected account/allocation details const selectedAccount = selectedAccountId ? network.accounts.find((a) => a.id === selectedAccountId) : null const selectedAllocation = selectedAllocationId ? network.allocations.find((a) => a.id === selectedAllocationId) : null // Get allocations from selected account const outgoingAllocations = selectedAccount ? network.allocations.filter(a => a.sourceAccountId === selectedAccount.id) : [] // Get allocations from selected allocation's source (for checking if single) const selectedAllocationSiblings = selectedAllocation ? network.allocations.filter(a => a.sourceAccountId === selectedAllocation.sourceAccountId) : [] // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { setTool('select') setAllocationSourceId(null) setSelectedAccountId(null) setSelectedAllocationId(null) } else if (e.key === 'Delete' && selectedAllocationId) { deleteAllocation(selectedAllocationId) } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [selectedAllocationId]) return (
{/* Header */}

Threshold-Based Flow Funding

Milestone 3: Initial Distribution

← Back to Home
{/* Main Content */}
{/* Canvas */}
{/* Tool indicator */} {allocationSourceId && (
Click target account to create allocation
)}
{/* Sidebar */}
{/* Tools */}

Tools

{/* Network Selector */}

Select Network

{/* Network Info */}

{network.name}

Accounts: {network.accounts.length}
Allocations: {network.allocations.length}
Total Funds: {formatCurrency(network.totalFunds)}
Shortfall: {formatCurrency(network.totalShortfall)}
Capacity: {formatCurrency(network.totalCapacity)}
Overflow: {formatCurrency(network.totalOverflow)}
{/* Funding Controls */}

šŸ’° Add Funding

setFundingAmount(parseFloat(e.target.value) || 0)} className="w-full px-3 py-2 bg-slate-700 rounded text-sm" min="0" step="100" />
{/* Distribution Summary */} {lastDistribution && (
Distributed: {formatCurrency(lastDistribution.totalDistributed)}
Accounts Changed: {lastDistribution.accountsChanged}
{lastDistribution.changes.length > 0 && (
Changes:
{lastDistribution.changes.map((change) => (
{change.name} +{formatCurrency(change.delta)}
))}
)}
)}
{/* Selected Allocation Editor */} {selectedAllocation && (

Edit Allocation

From: {network.accounts.find(a => a.id === selectedAllocation.sourceAccountId)?.name}
To: {network.accounts.find(a => a.id === selectedAllocation.targetAccountId)?.name}
{selectedAllocationSiblings.length === 1 ? (
Single allocation must be 100%. Create additional allocations to split overflow.
) : ( <> updateAllocationPercentage( selectedAllocation.id, parseFloat(e.target.value) / 100 ) } className="w-full" />
Note: Percentages auto-normalize with other allocations from same source
)}
)} {/* Selected Account Details */} {selectedAccount && (

Account Details

Name: {selectedAccount.name}
Status: {selectedAccount.status.toUpperCase()}
Balance: {formatCurrency(selectedAccount.balance)}
Min Threshold: {formatCurrency(selectedAccount.minThreshold)}
Max Threshold: {formatCurrency(selectedAccount.maxThreshold)}
{/* Outgoing Allocations */} {outgoingAllocations.length > 0 && (
Outgoing Allocations:
{outgoingAllocations.map((alloc) => { const target = network.accounts.find(a => a.id === alloc.targetAccountId) return (
setSelectedAllocationId(alloc.id)} > → {target?.name} {Math.round(alloc.percentage * 100)}%
) })}
)}
)} {/* Account List */}

All Accounts

{network.accounts.map((acc) => ( ))}
{/* Legend */}

Legend

Deficit - Below minimum threshold
Minimum - At minimum threshold
Healthy - Between thresholds
Overflow - Above maximum threshold
{/* Instructions */}

Milestone 3: Initial Distribution

  • Add funding to distribute across accounts
  • Drag accounts to reposition them
  • Use Create Arrow tool to draw allocations
  • Click arrow to edit percentage
  • Press Delete to remove allocation
  • Check console for distribution logs
) }