Redesign transaction list and add transaction detail view
All checks were successful
check / check (push) Successful in 13s

Transaction list entries are now two lines with more spacing:
- Line 1: humanized age (hover for ISO datetime) + direction (Sent/Received)
- Line 2: counterparty address + amount with symbol
- Clickable rows navigate to transaction detail view

Transaction detail view (placeholder) shows:
- Status, time, amount, from, to, transaction hash
- Back button returns to address detail

Also added "transaction" to VIEWS list in helpers.
This commit is contained in:
2026-02-26 02:20:13 +07:00
parent a15fb1a761
commit bf9ae4919d
3 changed files with 117 additions and 20 deletions

View File

@@ -519,6 +519,41 @@
</div> </div>
</div> </div>
<!-- ============ TRANSACTION DETAIL ============ -->
<div id="view-transaction" class="view hidden">
<button
id="btn-tx-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mb-2"
>
&lt; Back
</button>
<h2 class="font-bold mb-2">Transaction</h2>
<div class="mb-2">
<div class="text-xs text-muted">Status</div>
<div id="tx-detail-status" class="text-xs"></div>
</div>
<div class="mb-2">
<div class="text-xs text-muted">Time</div>
<div id="tx-detail-time" class="text-xs"></div>
</div>
<div class="mb-2">
<div class="text-xs text-muted">Amount</div>
<div id="tx-detail-value" class="text-xs font-bold"></div>
</div>
<div class="mb-2">
<div class="text-xs text-muted">From</div>
<div id="tx-detail-from" class="text-xs break-all"></div>
</div>
<div class="mb-2">
<div class="text-xs text-muted">To</div>
<div id="tx-detail-to" class="text-xs break-all"></div>
</div>
<div class="mb-2">
<div class="text-xs text-muted">Transaction hash</div>
<div id="tx-detail-hash" class="text-xs break-all"></div>
</div>
</div>
<!-- ============ APPROVAL ============ --> <!-- ============ APPROVAL ============ -->
<div id="view-approve" class="view hidden"> <div id="view-approve" class="view hidden">
<h2 class="font-bold mb-2">A website is requesting access</h2> <h2 class="font-bold mb-2">A website is requesting access</h2>

View File

@@ -30,7 +30,7 @@ function show() {
loadTransactions(addr.address); loadTransactions(addr.address);
} }
function formatDate(timestamp) { function isoDate(timestamp) {
const d = new Date(timestamp * 1000); const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0"); const pad = (n) => String(n).padStart(2, "0");
return ( return (
@@ -42,20 +42,42 @@ function formatDate(timestamp) {
" " + " " +
pad(d.getHours()) + pad(d.getHours()) +
":" + ":" +
pad(d.getMinutes()) pad(d.getMinutes()) +
":" +
pad(d.getSeconds())
); );
} }
function timeAgo(timestamp) {
const seconds = Math.floor(Date.now() / 1000 - timestamp);
if (seconds < 60) return seconds + " seconds ago";
const minutes = Math.floor(seconds / 60);
if (minutes < 60)
return minutes + " minute" + (minutes !== 1 ? "s" : "") + " ago";
const hours = Math.floor(minutes / 60);
if (hours < 24) return hours + " hour" + (hours !== 1 ? "s" : "") + " ago";
const days = Math.floor(hours / 24);
if (days < 30) return days + " day" + (days !== 1 ? "s" : "") + " ago";
const months = Math.floor(days / 30);
if (months < 12)
return months + " month" + (months !== 1 ? "s" : "") + " ago";
const years = Math.floor(days / 365);
return years + " year" + (years !== 1 ? "s" : "") + " ago";
}
function escapeHtml(s) { function escapeHtml(s) {
const div = document.createElement("div"); const div = document.createElement("div");
div.textContent = s; div.textContent = s;
return div.innerHTML; return div.innerHTML;
} }
let loadedTxs = [];
async function loadTransactions(address) { async function loadTransactions(address) {
try { try {
const txs = await fetchRecentTransactions(address, state.blockscoutUrl); const txs = await fetchRecentTransactions(address, state.blockscoutUrl);
renderTransactions(txs, address); loadedTxs = txs;
renderTransactions(txs);
} catch (e) { } catch (e) {
log.errorf("loadTransactions failed:", e.message); log.errorf("loadTransactions failed:", e.message);
$("tx-list").innerHTML = $("tx-list").innerHTML =
@@ -63,30 +85,65 @@ async function loadTransactions(address) {
} }
} }
function renderTransactions(txs, address) { function renderTransactions(txs) {
const list = $("tx-list"); const list = $("tx-list");
if (txs.length === 0) { if (txs.length === 0) {
list.innerHTML = list.innerHTML =
'<div class="text-muted text-xs py-1">No transactions found.</div>'; '<div class="text-muted text-xs py-1">No transactions found.</div>';
return; return;
} }
const addrLower = address.toLowerCase(); list.innerHTML = "";
let html = ""; txs.forEach((tx, i) => {
for (const tx of txs) {
const arrow = tx.direction === "sent" ? "\u2192" : "\u2190";
const counterparty = tx.direction === "sent" ? tx.to : tx.from; const counterparty = tx.direction === "sent" ? tx.to : tx.from;
const label = tx.direction === "sent" ? "to" : "from"; const dirLabel = tx.direction === "sent" ? "Sent" : "Received";
const errorClass = tx.isError ? ' style="opacity:0.5"' : ""; const errorStyle = tx.isError ? " opacity:0.5" : "";
const errorTag = tx.isError
? ' <span class="text-muted">[failed]</span>' const row = document.createElement("div");
: ""; row.className =
html += `<div class="py-1 border-b border-border-light text-xs"${errorClass}>`; "py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover";
html += `<div>${formatDate(tx.timestamp)} ${arrow} ${escapeHtml(tx.value)} ${escapeHtml(tx.symbol)}${errorTag}</div>`; if (errorStyle) row.style.cssText = errorStyle;
html += `<div class="text-muted break-all">${label}: ${escapeHtml(counterparty)}</div>`;
html += `<div class="break-all"><a href="https://etherscan.io/tx/${escapeHtml(tx.hash)}" target="_blank" class="underline decoration-dashed">${escapeHtml(tx.hash)}</a></div>`; const line1 = document.createElement("div");
html += `</div>`; line1.className = "flex justify-between";
} const age = document.createElement("span");
list.innerHTML = html; age.className = "text-muted";
age.textContent = timeAgo(tx.timestamp);
age.title = isoDate(tx.timestamp);
const dir = document.createElement("span");
dir.className = tx.isError ? "text-muted" : "";
dir.textContent = dirLabel + (tx.isError ? " (failed)" : "");
line1.appendChild(age);
line1.appendChild(dir);
const line2 = document.createElement("div");
line2.className = "flex justify-between";
const addr = document.createElement("span");
addr.className = "break-all pr-2";
addr.textContent = counterparty;
const amount = document.createElement("span");
amount.className = "shrink-0";
amount.textContent = tx.value + " " + tx.symbol;
line2.appendChild(addr);
line2.appendChild(amount);
row.appendChild(line1);
row.appendChild(line2);
row.addEventListener("click", () => {
state.selectedTx = i;
showTxDetail(tx);
});
list.appendChild(row);
});
}
function showTxDetail(tx) {
$("tx-detail-hash").textContent = tx.hash;
$("tx-detail-from").textContent = tx.from;
$("tx-detail-to").textContent = tx.to;
$("tx-detail-value").textContent = tx.value + " " + tx.symbol;
$("tx-detail-time").textContent = isoDate(tx.timestamp);
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
showView("transaction");
} }
function renderSendTokenSelect(addr) { function renderSendTokenSelect(addr) {
@@ -144,6 +201,10 @@ function init(ctx) {
}); });
$("btn-add-token").addEventListener("click", ctx.showAddTokenView); $("btn-add-token").addEventListener("click", ctx.showAddTokenView);
$("btn-tx-back").addEventListener("click", () => {
show();
});
} }
module.exports = { init, show }; module.exports = { init, show };

View File

@@ -18,6 +18,7 @@ const VIEWS = [
"receive", "receive",
"add-token", "add-token",
"settings", "settings",
"transaction",
"approve", "approve",
]; ];