- Updated branding from "SolaceScanScout" to "Solace" across various files including deployment scripts, API responses, and documentation. - Changed default base URL for Playwright tests and updated security headers to reflect the new branding. - Enhanced README and API documentation to include new authentication endpoints and product access details. This refactor aligns the project branding and improves clarity in the API documentation.
140 lines
4.9 KiB
TypeScript
140 lines
4.9 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useState } from 'react'
|
|
import { useRouter } from 'next/router'
|
|
import { blocksApi, Block } from '@/services/api/blocks'
|
|
import { Card, Address } from '@/libs/frontend-ui-primitives'
|
|
import Link from 'next/link'
|
|
import { DetailRow } from '@/components/common/DetailRow'
|
|
import PageIntro from '@/components/common/PageIntro'
|
|
import { formatTimestamp } from '@/utils/format'
|
|
|
|
export default function BlockDetailPage() {
|
|
const router = useRouter()
|
|
const rawNumber = typeof router.query.number === 'string' ? router.query.number : ''
|
|
const blockNumber = parseInt(rawNumber, 10)
|
|
const isValidBlock = rawNumber !== '' && !Number.isNaN(blockNumber) && blockNumber >= 0
|
|
const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '138')
|
|
|
|
const [block, setBlock] = useState<Block | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
const loadBlock = useCallback(async () => {
|
|
setLoading(true)
|
|
try {
|
|
const response = await blocksApi.getByNumber(chainId, blockNumber)
|
|
setBlock(response.data)
|
|
} catch (error) {
|
|
console.error('Failed to load block:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [chainId, blockNumber])
|
|
|
|
useEffect(() => {
|
|
if (!router.isReady) {
|
|
return
|
|
}
|
|
if (!isValidBlock) {
|
|
setLoading(false)
|
|
setBlock(null)
|
|
return
|
|
}
|
|
loadBlock()
|
|
}, [isValidBlock, loadBlock, router.isReady])
|
|
|
|
const gasUtilization = block && block.gas_limit > 0
|
|
? Math.round((block.gas_used / block.gas_limit) * 100)
|
|
: null
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-6 sm:py-8">
|
|
<PageIntro
|
|
eyebrow="Block Detail"
|
|
title={block ? `Block #${block.number}` : 'Block'}
|
|
description="Inspect a single Chain 138 block, then move into its related miner address, adjacent block numbers, or broader explorer search flows."
|
|
actions={[
|
|
{ href: '/blocks', label: 'All blocks' },
|
|
{ href: '/transactions', label: 'Recent transactions' },
|
|
{ href: '/search', label: 'Search explorer' },
|
|
]}
|
|
/>
|
|
|
|
<div className="mb-6 flex flex-wrap gap-3 text-sm">
|
|
<Link href="/blocks" className="text-primary-600 hover:underline">
|
|
Back to blocks
|
|
</Link>
|
|
{block && block.number > 0 ? (
|
|
<Link href={`/blocks/${block.number - 1}`} className="text-primary-600 hover:underline">
|
|
Previous block
|
|
</Link>
|
|
) : null}
|
|
{block && (
|
|
<Link href={`/blocks/${block.number + 1}`} className="text-primary-600 hover:underline">
|
|
Next block
|
|
</Link>
|
|
)}
|
|
</div>
|
|
|
|
{!router.isReady || loading ? (
|
|
<Card>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">Loading block...</p>
|
|
</Card>
|
|
) : !isValidBlock ? (
|
|
<Card>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">Invalid block number. Please use a valid block number from the URL.</p>
|
|
<div className="mt-4 flex flex-wrap gap-3 text-sm">
|
|
<Link href="/blocks" className="text-primary-600 hover:underline">
|
|
Back to blocks →
|
|
</Link>
|
|
<Link href="/search" className="text-primary-600 hover:underline">
|
|
Search by block number →
|
|
</Link>
|
|
</div>
|
|
</Card>
|
|
) : !block ? (
|
|
<Card>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">Block not found.</p>
|
|
<div className="mt-4 flex flex-wrap gap-3 text-sm">
|
|
<Link href="/blocks" className="text-primary-600 hover:underline">
|
|
Browse recent blocks →
|
|
</Link>
|
|
<Link href="/search" className="text-primary-600 hover:underline">
|
|
Search the explorer →
|
|
</Link>
|
|
</div>
|
|
</Card>
|
|
) : (
|
|
<Card title="Block Information">
|
|
<dl className="space-y-4">
|
|
<DetailRow label="Hash">
|
|
<Address address={block.hash} />
|
|
</DetailRow>
|
|
<DetailRow label="Timestamp">
|
|
{formatTimestamp(block.timestamp)}
|
|
</DetailRow>
|
|
<DetailRow label="Miner">
|
|
<Link href={`/addresses/${block.miner}`} className="text-primary-600 hover:underline">
|
|
<Address address={block.miner} truncate showCopy={false} />
|
|
</Link>
|
|
</DetailRow>
|
|
<DetailRow label="Transactions">
|
|
<Link href={`/search?q=${block.number}`} className="text-primary-600 hover:underline">
|
|
{block.transaction_count}
|
|
</Link>
|
|
</DetailRow>
|
|
<DetailRow label="Gas Used">
|
|
{block.gas_used.toLocaleString()} / {block.gas_limit.toLocaleString()}
|
|
</DetailRow>
|
|
{gasUtilization != null && (
|
|
<DetailRow label="Gas Utilization">
|
|
{gasUtilization}%
|
|
</DetailRow>
|
|
)}
|
|
</dl>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|