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
+42 -6
View File
@@ -1,13 +1,49 @@
// Cross-component selection state. Currently just tracks the user
// whose detail modal is open; App renders the modal off this store
// so any component in the tree can open it by setting `user`.
// Simple hash-based routing for user pages. The URL hash is the
// source of truth:
// #/ -> dashboard
// #/user/<address> -> per-user page
//
// selectUser / clearSelection just mutate the hash; a hashchange
// listener reads it back into reactive state so any component in the
// tree re-renders when the route changes.
export const selection = $state<{ user: string | null }>({ user: null });
const USER_PREFIX = "#/user/";
export const selection = $state<{ user: string | null }>({
user: readHash(),
});
function readHash(): string | null {
if (typeof window === "undefined") return null;
const h = window.location.hash;
if (h.startsWith(USER_PREFIX)) {
const addr = decodeURIComponent(h.slice(USER_PREFIX.length));
return addr || null;
}
return null;
}
function syncFromHash(): void {
selection.user = readHash();
}
if (typeof window !== "undefined") {
window.addEventListener("hashchange", syncFromHash);
}
export function selectUser(address: string): void {
selection.user = address;
// Set the hash; the hashchange listener updates selection.user so
// routing-style state stays in one place.
window.location.hash = USER_PREFIX + encodeURIComponent(address);
}
export function clearSelection(): void {
selection.user = null;
// Prefer history.back when we arrived here via selectUser so the
// browser back arrow does the obvious thing; otherwise just drop
// the hash so a fresh page load starts on the dashboard.
if (window.history.length > 1 && window.location.hash.startsWith(USER_PREFIX)) {
window.history.back();
} else {
window.location.hash = "";
}
}