|
1 | 1 | 'use client' |
2 | 2 |
|
3 | 3 | import { AnimatePresence, motion } from 'framer-motion' |
| 4 | +import { Loader2 } from 'lucide-react' |
4 | 5 | import { useLayoutEffect, useRef } from 'react' |
5 | | -import { BLOCK_STATE_CONFIG } from '@/constants/block-state' |
6 | 6 | import type { Block } from '@/types/block' |
7 | 7 | import { BlockCard } from './block-card' |
8 | 8 |
|
9 | 9 | interface BlockchainProps { |
10 | | - finalizedBlocks: Block[] |
11 | | - verifiedBlocks: Block[] |
| 10 | + blocks: Block[] |
12 | 11 | } |
13 | 12 |
|
14 | 13 | /** |
15 | | - * Horizontal blockchain visualization showing finalized and verified blocks. |
16 | | - * Auto-scrolls to show the newest blocks on the right. |
| 14 | + * Horizontal blockchain visualization. |
| 15 | + * Blocks are added from the right and stay in place as their state changes. |
| 16 | + * Auto-scrolls to show the newest blocks. |
17 | 17 | */ |
18 | | -export function Blockchain({ |
19 | | - finalizedBlocks, |
20 | | - verifiedBlocks, |
21 | | -}: BlockchainProps) { |
| 18 | +export function Blockchain({ blocks }: BlockchainProps) { |
22 | 19 | const scrollContainerRef = useRef<HTMLDivElement>(null) |
23 | 20 | const prevBlockCountRef = useRef(0) |
24 | 21 |
|
25 | | - // Combine and sort blocks: lower IDs on left, higher IDs on right |
26 | | - const chainBlocks = [...verifiedBlocks, ...finalizedBlocks].sort( |
27 | | - (a, b) => a.id - b.id, |
28 | | - ) |
| 22 | + // Sort blocks by ID (oldest on left, newest on right) |
| 23 | + const sortedBlocks = [...blocks].sort((a, b) => a.id - b.id) |
29 | 24 |
|
30 | | - // Scroll BEFORE paint so layout animation target is visible |
| 25 | + // Auto-scroll to the right when new blocks are added |
31 | 26 | useLayoutEffect(() => { |
32 | 27 | const container = scrollContainerRef.current |
33 | 28 | if (!container) return |
34 | 29 |
|
35 | | - const currentCount = chainBlocks.length |
| 30 | + const currentCount = blocks.length |
36 | 31 | const prevCount = prevBlockCountRef.current |
37 | 32 |
|
38 | 33 | if (currentCount > prevCount && currentCount > 0) { |
39 | | - container.scrollLeft = container.scrollWidth |
| 34 | + // Small delay to let the new block render first |
| 35 | + requestAnimationFrame(() => { |
| 36 | + container.scrollTo({ |
| 37 | + left: container.scrollWidth, |
| 38 | + behavior: 'smooth', |
| 39 | + }) |
| 40 | + }) |
40 | 41 | } |
41 | 42 |
|
42 | 43 | prevBlockCountRef.current = currentCount |
43 | | - }, [chainBlocks.length]) |
| 44 | + }, [blocks.length]) |
44 | 45 |
|
45 | 46 | return ( |
46 | 47 | <div className="flex flex-col bg-[#16162a]/80 rounded-xl border border-[#2a2a4a]/50"> |
47 | | - <div className="px-4 py-3 border-b border-[#2a2a4a]/50 flex flex-col sm:flex-row sm:items-center justify-between gap-2"> |
| 48 | + <div className="px-4 py-3 border-b border-[#2a2a4a]/50"> |
48 | 49 | <h3 className="text-sm font-semibold uppercase tracking-wider text-[#8888a0]"> |
49 | 50 | Blockchain |
50 | 51 | </h3> |
51 | | - {/* Legend */} |
52 | | - <div className="flex items-center gap-4 text-xs text-[#6a6a7a]"> |
53 | | - <div className="flex items-center gap-1.5"> |
54 | | - <div |
55 | | - className="w-3 h-3 rounded" |
56 | | - style={{ background: BLOCK_STATE_CONFIG.finalized.gradient }} |
57 | | - /> |
58 | | - <span>Finalized</span> |
59 | | - </div> |
60 | | - <div className="flex items-center gap-1.5"> |
61 | | - <div |
62 | | - className="w-3 h-3 rounded" |
63 | | - style={{ background: BLOCK_STATE_CONFIG.verified.gradient }} |
64 | | - /> |
65 | | - <span>Verified</span> |
66 | | - </div> |
67 | | - </div> |
68 | 52 | </div> |
69 | 53 | <div |
70 | 54 | ref={scrollContainerRef} |
71 | 55 | className="flex-1 p-4 overflow-x-auto overflow-y-hidden scrollbar-none" |
72 | 56 | > |
73 | | - <div className="flex items-center min-h-[100px] sm:min-h-[120px] w-fit"> |
74 | | - <AnimatePresence mode="popLayout"> |
75 | | - {chainBlocks.map((block, index) => ( |
76 | | - <motion.div |
77 | | - key={block.id} |
78 | | - className="flex items-center shrink-0" |
79 | | - layout |
80 | | - transition={{ |
81 | | - layout: { type: 'spring', stiffness: 300, damping: 30 }, |
82 | | - }} |
83 | | - > |
84 | | - {/* Chain connector */} |
85 | | - {index > 0 && ( |
86 | | - <div className="w-4 h-0.5 bg-[#3a3a5a] sm:w-6 shrink-0" /> |
87 | | - )} |
88 | | - <BlockCard block={block} showLabel compact /> |
89 | | - </motion.div> |
90 | | - ))} |
91 | | - </AnimatePresence> |
92 | | - </div> |
| 57 | + {sortedBlocks.length === 0 ? ( |
| 58 | + <div className="flex flex-col items-center justify-center gap-3 w-full py-8"> |
| 59 | + <Loader2 className="text-[#6a6a7a] animate-spin size-12" /> |
| 60 | + <p className="text-[#6a6a7a] text-sm">Waiting for blocks...</p> |
| 61 | + </div> |
| 62 | + ) : ( |
| 63 | + <motion.div |
| 64 | + className="flex items-center gap-2 min-h-[120px] sm:min-h-[140px] w-fit" |
| 65 | + layout |
| 66 | + transition={{ layout: { duration: 0.3, ease: 'easeInOut' } }} |
| 67 | + > |
| 68 | + <AnimatePresence mode="sync"> |
| 69 | + {sortedBlocks.map((block, index) => ( |
| 70 | + <motion.div |
| 71 | + key={block.id} |
| 72 | + className="flex items-center shrink-0" |
| 73 | + layout |
| 74 | + initial={{ opacity: 0, x: 20 }} |
| 75 | + animate={{ opacity: 1, x: 0 }} |
| 76 | + transition={{ duration: 0.3, ease: 'easeOut' }} |
| 77 | + > |
| 78 | + {/* Chain connector */} |
| 79 | + {index > 0 && ( |
| 80 | + <motion.div |
| 81 | + className="w-3 h-1 bg-[#3a3a5a] rounded-full mr-2 sm:w-4 shrink-0" |
| 82 | + initial={{ scaleX: 0 }} |
| 83 | + animate={{ scaleX: 1 }} |
| 84 | + exit={{ |
| 85 | + scaleX: 0, |
| 86 | + opacity: 0, |
| 87 | + transition: { duration: 0.2 }, |
| 88 | + }} |
| 89 | + transition={{ duration: 0.2, delay: 0.1 }} |
| 90 | + /> |
| 91 | + )} |
| 92 | + <BlockCard block={block} /> |
| 93 | + </motion.div> |
| 94 | + ))} |
| 95 | + </AnimatePresence> |
| 96 | + </motion.div> |
| 97 | + )} |
93 | 98 | </div> |
94 | 99 | </div> |
95 | 100 | ) |
|
0 commit comments