synor/apps/explorer-web/src/components/TransactionFlowDiagram.tsx
Gulshan Yadav 48949ebb3f Initial commit: Synor blockchain monorepo
A complete blockchain implementation featuring:
- synord: Full node with GHOSTDAG consensus
- explorer-web: Modern React blockchain explorer with 3D DAG visualization
- CLI wallet and tools
- Smart contract SDK and example contracts (DEX, NFT, token)
- WASM crypto library for browser/mobile
2026-01-08 05:22:17 +05:30

262 lines
9.3 KiB
TypeScript

/**
* Visual flow diagram showing transaction inputs and outputs.
* Creates a Sankey-style visualization of fund flow.
*/
import { Link } from 'react-router-dom';
import { ArrowRight, Coins, Wallet, Gift } from 'lucide-react';
import { truncateHash, formatSynor } from '../lib/utils';
import type { ExplorerInput, ExplorerOutput } from '../lib/types';
interface TransactionFlowDiagramProps {
inputs: ExplorerInput[];
outputs: ExplorerOutput[];
isCoinbase: boolean;
totalInput: number;
totalOutput: number;
fee: number;
}
export default function TransactionFlowDiagram({
inputs,
outputs,
isCoinbase,
totalInput,
totalOutput,
fee,
}: TransactionFlowDiagramProps) {
// Calculate percentages for visual sizing
const maxValue = Math.max(totalInput, totalOutput);
return (
<div className="relative overflow-hidden rounded-2xl border border-gray-700/50 bg-gray-900/40 backdrop-blur-xl p-6">
{/* Background gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 via-transparent to-red-500/5" />
<div className="relative">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2">
<Coins size={18} className="text-synor-400" />
<h3 className="font-semibold">Transaction Flow</h3>
</div>
{!isCoinbase && fee > 0 && (
<div className="text-sm text-gray-400">
Fee: <span className="text-amber-400 font-mono">{formatSynor(fee, 4)}</span>
</div>
)}
</div>
<div className="grid grid-cols-[1fr_auto_1fr] gap-4 items-center">
{/* Inputs Column */}
<div className="space-y-2">
<div className="text-xs text-gray-500 uppercase tracking-wider mb-3 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-red-500" />
Inputs
</div>
{isCoinbase ? (
<CoinbaseInput />
) : (
<div className="space-y-2 max-h-64 overflow-y-auto scrollbar-thin pr-2">
{inputs.map((input, i) => (
<InputNode
key={i}
input={input}
percentage={input.value ? (input.value / maxValue) * 100 : 50}
/>
))}
</div>
)}
{/* Total input */}
{!isCoinbase && (
<div className="pt-2 border-t border-gray-700/50 mt-3">
<div className="text-xs text-gray-500">Total Input</div>
<div className="text-lg font-bold text-red-400 font-mono">
{formatSynor(totalInput, 4)}
</div>
</div>
)}
</div>
{/* Center Flow Arrow */}
<div className="flex flex-col items-center justify-center px-4">
{/* Animated flow lines */}
<div className="relative h-48 w-16 flex items-center justify-center">
{/* Background track */}
<div className="absolute inset-y-8 left-1/2 -translate-x-1/2 w-1 bg-gray-700 rounded-full" />
{/* Animated particles */}
<div className="absolute inset-y-8 left-1/2 -translate-x-1/2 w-1 overflow-hidden rounded-full">
<div className="absolute w-full h-4 bg-gradient-to-b from-transparent via-synor-500 to-transparent animate-flow" />
</div>
{/* Central transaction icon */}
<div className="relative z-10 p-3 rounded-full bg-gray-800 border-2 border-synor-500 shadow-[0_0_20px_rgba(124,58,237,0.4)]">
<ArrowRight size={20} className="text-synor-400" />
</div>
{/* Top fade */}
<div className="absolute top-0 inset-x-0 h-8 bg-gradient-to-b from-gray-900/40 to-transparent" />
{/* Bottom fade */}
<div className="absolute bottom-0 inset-x-0 h-8 bg-gradient-to-t from-gray-900/40 to-transparent" />
</div>
</div>
{/* Outputs Column */}
<div className="space-y-2">
<div className="text-xs text-gray-500 uppercase tracking-wider mb-3 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-green-500" />
Outputs
</div>
<div className="space-y-2 max-h-64 overflow-y-auto scrollbar-thin pr-2">
{outputs.map((output, i) => (
<OutputNode
key={i}
output={output}
index={i}
percentage={(output.value / maxValue) * 100}
/>
))}
</div>
{/* Total output */}
<div className="pt-2 border-t border-gray-700/50 mt-3">
<div className="text-xs text-gray-500">Total Output</div>
<div className="text-lg font-bold text-green-400 font-mono">
{formatSynor(totalOutput, 4)}
</div>
</div>
</div>
</div>
{/* Flow summary bar */}
<div className="mt-6 pt-4 border-t border-gray-700/50">
<div className="flex items-center gap-2 h-3">
{/* Input portion */}
<div className="flex-1 h-full bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-red-600 to-red-500 transition-all duration-500"
style={{ width: `${(totalInput / maxValue) * 100}%` }}
/>
</div>
<ArrowRight size={14} className="text-gray-600 flex-shrink-0" />
{/* Output portion */}
<div className="flex-1 h-full bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-green-600 to-green-500 transition-all duration-500"
style={{ width: `${(totalOutput / maxValue) * 100}%` }}
/>
</div>
</div>
</div>
</div>
{/* CSS for flow animation */}
<style>{`
@keyframes flow {
0% { transform: translateY(-100%); }
100% { transform: translateY(calc(100% + 12rem)); }
}
.animate-flow {
animation: flow 2s linear infinite;
}
`}</style>
</div>
);
}
function CoinbaseInput() {
return (
<div className="flex items-center gap-3 px-4 py-3 rounded-lg bg-amber-900/20 border border-amber-700/50">
<div className="p-2 rounded-lg bg-amber-500/20">
<Gift size={18} className="text-amber-400" />
</div>
<div>
<div className="text-sm font-medium text-amber-300">Block Reward</div>
<div className="text-xs text-gray-500">Coinbase Transaction</div>
</div>
</div>
);
}
interface InputNodeProps {
input: ExplorerInput;
percentage: number;
}
function InputNode({ input, percentage }: InputNodeProps) {
const barWidth = Math.max(20, Math.min(100, percentage));
return (
<div className="relative group">
{/* Value bar background */}
<div
className="absolute inset-0 bg-red-500/10 rounded-lg transition-all duration-300"
style={{ width: `${barWidth}%` }}
/>
<div className="relative flex items-center gap-3 px-3 py-2.5 rounded-lg border border-gray-700/50 group-hover:border-gray-600 transition-colors">
<Wallet size={14} className="text-gray-500 flex-shrink-0" />
<div className="min-w-0 flex-1">
{input.address ? (
<Link
to={`/address/${input.address}`}
className="font-mono text-xs text-gray-300 hover:text-synor-400 transition-colors truncate block"
>
{truncateHash(input.address, 8, 8)}
</Link>
) : (
<span className="font-mono text-xs text-gray-500">Unknown</span>
)}
</div>
{input.value !== undefined && (
<span className="text-xs font-mono text-red-400 flex-shrink-0">
-{formatSynor(input.value, 2)}
</span>
)}
</div>
</div>
);
}
interface OutputNodeProps {
output: ExplorerOutput;
index: number;
percentage: number;
}
function OutputNode({ output, percentage }: OutputNodeProps) {
const barWidth = Math.max(20, Math.min(100, percentage));
return (
<div className="relative group">
{/* Value bar background */}
<div
className="absolute inset-0 bg-green-500/10 rounded-lg transition-all duration-300"
style={{ width: `${barWidth}%` }}
/>
<div className="relative flex items-center gap-3 px-3 py-2.5 rounded-lg border border-gray-700/50 group-hover:border-gray-600 transition-colors">
<Wallet size={14} className="text-gray-500 flex-shrink-0" />
<div className="min-w-0 flex-1">
{output.address ? (
<Link
to={`/address/${output.address}`}
className="font-mono text-xs text-gray-300 hover:text-synor-400 transition-colors truncate block"
>
{truncateHash(output.address, 8, 8)}
</Link>
) : (
<span className="font-mono text-xs text-gray-500">{output.scriptType}</span>
)}
</div>
<span className="text-xs font-mono text-green-400 flex-shrink-0">
+{formatSynor(output.value, 2)}
</span>
</div>
</div>
);
}