User page: hash-routed view, full block hash, aligned rewards

The per-user view was an overlay modal, but users asked for an
actual page — focused, scrollable, addressable. Replace the modal
with a hash-routed page: selectUser(addr) sets window.location.hash
to #/user/<addr> and a hashchange listener syncs the selection
state back, so browser back/forward and direct-link refresh all
work without a routing library. App renders the dashboard or the
UserDetailPage based on selection.user.

Make the clickable BTC addresses in the Miners table obviously
interactive: accent colour, always-on dashed underline, brighter on
hover/focus. The previous styling rendered them as dim static text
with a hover underline, so it wasn't obvious they were links.

BlocksTable: show the full 64-char block hash as a link to
mempool.space (auto-picks testnet4/signet based on snap.chain).
word-break: break-all keeps the hash from blowing out the column
width. Reward column is split into a tabular-num number span and a
dim 0.75em "BTC" unit span, matching the Block reward tile in the
overview, so numbers line up cleanly across rows.
This commit is contained in:
satoshi
2026-04-24 01:02:11 +03:00
parent c104c1eefa
commit 49c951f94f
6 changed files with 397 additions and 337 deletions
+79 -21
View File
@@ -4,9 +4,17 @@
const blocks = $derived.by(() => {
const b = snap.data?.recent_blocks ?? [];
// Show newest first.
return [...b].reverse();
});
// mempool.space mirror picked per network. If chain isn't known, the
// mainnet link still works as a best effort.
const explorerBase = $derived.by(() => {
const chain = snap.data?.chain?.chain ?? "main";
if (chain === "test" || chain === "testnet4") return "https://mempool.space/testnet4";
if (chain === "signet") return "https://mempool.space/signet";
return "https://mempool.space";
});
</script>
<section class="card">
@@ -14,28 +22,49 @@
{#if blocks.length === 0}
<div class="empty">No blocks found yet.</div>
{:else}
<table>
<thead>
<tr>
<th>Height</th>
<th>Hash</th>
<th class="num">Reward</th>
<th class="num">Winning share</th>
<th class="num">When</th>
</tr>
</thead>
<tbody>
{#each blocks as b (b.height + "-" + b.found_at)}
<div class="table-wrap">
<table>
<thead>
<tr>
<td class="mono">{b.height}</td>
<td class="hash mono">{b.hash ? b.hash.slice(0, 16) + "…" : "—"}</td>
<td class="num">{b.reward_btc ? b.reward_btc.toFixed(4) + " BTC" : "—"}</td>
<td class="num">{b.share_diff ? formatDifficulty(b.share_diff) : "—"}</td>
<td class="num">{formatAgo(new Date(b.found_at).getTime() / 1000)}</td>
<th>Height</th>
<th>Hash</th>
<th class="num">Reward</th>
<th class="num">Winning share</th>
<th class="num">When</th>
</tr>
{/each}
</tbody>
</table>
</thead>
<tbody>
{#each blocks as b (b.height + "-" + b.found_at)}
<tr>
<td class="mono">{b.height}</td>
<td class="hash-cell">
{#if b.hash}
<a
class="hash mono"
href="{explorerBase}/block/{b.hash}"
target="_blank"
rel="noopener noreferrer"
title="Open block on mempool.space"
>{b.hash}</a>
{:else}
<span class="hash mono">&mdash;</span>
{/if}
</td>
<td class="num">
{#if b.reward_btc}
<span class="reward-num">{b.reward_btc.toFixed(4)}</span>
<span class="unit">BTC</span>
{:else}
<span class="reward-num">&mdash;</span>
{/if}
</td>
<td class="num">{b.share_diff ? formatDifficulty(b.share_diff) : "—"}</td>
<td class="num">{formatAgo(new Date(b.found_at).getTime() / 1000)}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</section>
@@ -49,7 +78,36 @@
color: var(--fg-dim);
padding: 0.5rem 0;
}
.table-wrap {
overflow-x: auto;
}
.hash-cell {
/* Let the 64-char hash wrap inside the cell instead of stretching
* the whole table. */
max-width: 620px;
word-break: break-all;
}
.hash {
color: var(--fg-dim);
font-size: 0.85em;
line-height: 1.4;
}
a.hash {
color: var(--fg-dim);
text-decoration: none;
border-bottom: 1px dashed transparent;
}
a.hash:hover {
color: var(--accent);
border-bottom-color: var(--accent);
}
.reward-num {
font-variant-numeric: tabular-nums;
}
.unit {
color: var(--fg-dim);
font-size: 0.75em;
font-weight: 400;
margin-left: 0.25em;
}
</style>