Web3 — Transaction History (status · explorer link)
A glassy, dark-first Web3 transaction history list grouped by date, with type icons for send, receive, swap, approve and mint, monospace truncated counterparty addresses, plus or minus colored amounts, a per-row explorer link, and live status badges. Filter chips switch between all, sent, received and swaps, instant search matches hashes and addresses, rows expand for hash, block, fee and nonce, a pending swap flips to confirmed after a few seconds, and copy-hash fires a toast. Pure vanilla, no libraries.
MCP
Code
:root {
--bg: #0a0b0f;
--surface: #13151c;
--surface-2: #1b1e27;
--elevated: #23262f;
--text: #e9ecf2;
--muted: #8a90a2;
--line: rgba(255, 255, 255, 0.08);
--line-2: rgba(255, 255, 255, 0.16);
--accent: #7c5cff;
--accent-2: #00e0c6;
--accent-glow: rgba(124, 92, 255, 0.45);
--pos: #26d07c;
--neg: #ff4d6d;
--warn: #ffb347;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--r-pill: 999px;
--mono: "JetBrains Mono", ui-monospace, "SF Mono", monospace;
--sans: "Space Grotesk", system-ui, -apple-system, sans-serif;
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(1100px 540px at 12% -8%, rgba(124, 92, 255, 0.16), transparent 60%),
radial-gradient(900px 480px at 100% 0%, rgba(0, 224, 198, 0.1), transparent 55%),
var(--bg);
color: var(--text);
font-family: var(--sans);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.num,
.addr,
code,
kbd {
font-family: var(--mono);
font-variant-numeric: tabular-nums;
}
.app {
max-width: 760px;
margin: 0 auto;
padding: 32px 20px 56px;
}
/* ---------- header ---------- */
.app__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
margin-bottom: 22px;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.brand__mark {
display: grid;
place-items: center;
width: 46px;
height: 46px;
border-radius: var(--r-md);
font-size: 22px;
color: #fff;
background: linear-gradient(135deg, var(--accent), #b69bff);
box-shadow: 0 10px 28px -8px var(--accent-glow);
}
.brand__text h1 {
margin: 0;
font-size: 22px;
font-weight: 600;
letter-spacing: -0.02em;
}
.brand__sub {
margin: 2px 0 0;
font-size: 13px;
color: var(--muted);
display: flex;
align-items: center;
gap: 7px;
}
.brand__sub .addr {
color: var(--text);
font-size: 12px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.dot--live {
background: var(--accent-2);
box-shadow: 0 0 0 0 rgba(0, 224, 198, 0.6);
animation: ping 2s ease-out infinite;
}
@keyframes ping {
0% { box-shadow: 0 0 0 0 rgba(0, 224, 198, 0.55); }
70% { box-shadow: 0 0 0 7px rgba(0, 224, 198, 0); }
100% { box-shadow: 0 0 0 0 rgba(0, 224, 198, 0); }
}
.head__meta {
display: flex;
gap: 10px;
}
.stat {
display: flex;
flex-direction: column;
padding: 10px 16px;
border-radius: var(--r-md);
background: var(--surface);
border: 1px solid var(--line);
min-width: 92px;
}
.stat__k {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
}
.stat__v {
font-size: 18px;
font-weight: 700;
line-height: 1.2;
margin-top: 3px;
}
.stat__u {
font-size: 11px;
color: var(--muted);
}
/* ---------- toolbar ---------- */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
margin-bottom: 18px;
}
.chips {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.chip {
appearance: none;
cursor: pointer;
font-family: var(--sans);
font-size: 13px;
font-weight: 500;
color: var(--muted);
background: var(--surface);
border: 1px solid var(--line);
padding: 8px 14px;
border-radius: var(--r-pill);
display: inline-flex;
align-items: center;
gap: 7px;
transition: color 0.15s, background 0.15s, border-color 0.15s, transform 0.08s;
}
.chip:hover {
color: var(--text);
border-color: var(--line-2);
}
.chip:active {
transform: translateY(1px);
}
.chip.is-active {
color: #fff;
background: linear-gradient(135deg, rgba(124, 92, 255, 0.25), rgba(124, 92, 255, 0.12));
border-color: rgba(124, 92, 255, 0.6);
box-shadow: 0 0 0 1px rgba(124, 92, 255, 0.25), 0 8px 22px -14px var(--accent-glow);
}
.chip__n {
font-family: var(--mono);
font-size: 11px;
font-weight: 500;
color: var(--muted);
background: rgba(255, 255, 255, 0.06);
padding: 1px 7px;
border-radius: var(--r-pill);
}
.chip.is-active .chip__n {
color: #fff;
background: rgba(255, 255, 255, 0.14);
}
.search {
position: relative;
display: flex;
align-items: center;
flex: 1;
min-width: 200px;
}
.search__icon {
position: absolute;
left: 13px;
width: 16px;
height: 16px;
fill: none;
stroke: var(--muted);
stroke-width: 2;
stroke-linecap: round;
pointer-events: none;
}
.search__input {
width: 100%;
font-family: var(--mono);
font-size: 13px;
color: var(--text);
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-pill);
padding: 10px 40px 10px 38px;
transition: border-color 0.15s, box-shadow 0.15s;
}
.search__input::placeholder {
color: var(--muted);
font-family: var(--sans);
}
.search__input:focus {
outline: none;
border-color: rgba(124, 92, 255, 0.55);
box-shadow: 0 0 0 3px rgba(124, 92, 255, 0.18);
}
.search__kbd {
position: absolute;
right: 12px;
font-size: 11px;
color: var(--muted);
background: var(--elevated);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 1px 6px;
pointer-events: none;
}
/* ---------- list ---------- */
.list {
display: flex;
flex-direction: column;
gap: 22px;
}
.group__head {
display: flex;
align-items: center;
gap: 12px;
margin: 0 4px 10px;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--muted);
}
.group__head::after {
content: "";
flex: 1;
height: 1px;
background: var(--line);
}
.group__rows {
display: flex;
flex-direction: column;
gap: 8px;
}
.tx {
position: relative;
border-radius: var(--r-md);
background: linear-gradient(180deg, rgba(27, 30, 39, 0.7), rgba(19, 21, 28, 0.7));
border: 1px solid var(--line);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
overflow: hidden;
transition: border-color 0.15s, transform 0.08s, box-shadow 0.15s;
}
.tx:hover {
border-color: var(--line-2);
box-shadow: 0 12px 30px -20px rgba(0, 0, 0, 0.8);
}
.tx.is-open {
border-color: rgba(124, 92, 255, 0.45);
}
.tx__main {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 14px;
width: 100%;
text-align: left;
cursor: pointer;
appearance: none;
background: transparent;
border: 0;
color: inherit;
font-family: inherit;
padding: 14px 54px 14px 16px;
}
.tx__main:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
border-radius: var(--r-md);
}
.tx__icon {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: var(--r-sm);
font-size: 18px;
flex-shrink: 0;
background: var(--elevated);
border: 1px solid var(--line);
}
.tx__icon--send { color: var(--neg); }
.tx__icon--receive { color: var(--pos); }
.tx__icon--swap { color: var(--accent-2); }
.tx__icon--approve { color: var(--warn); }
.tx__icon--mint { color: var(--accent); }
.tx__body {
min-width: 0;
}
.tx__label {
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tx__sub {
display: flex;
align-items: center;
gap: 8px;
margin-top: 3px;
font-size: 12px;
color: var(--muted);
flex-wrap: wrap;
}
.tx__counter {
font-family: var(--mono);
font-size: 11.5px;
}
.tx__time {
white-space: nowrap;
}
.badge {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 500;
padding: 2px 9px;
border-radius: var(--r-pill);
border: 1px solid transparent;
}
.badge::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.badge--confirmed {
color: var(--pos);
background: rgba(38, 208, 124, 0.1);
border-color: rgba(38, 208, 124, 0.28);
}
.badge--failed {
color: var(--neg);
background: rgba(255, 77, 109, 0.1);
border-color: rgba(255, 77, 109, 0.28);
}
.badge--pending {
color: var(--warn);
background: rgba(255, 179, 71, 0.12);
border-color: rgba(255, 179, 71, 0.3);
}
.badge--pending::before {
animation: pulse 1.1s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.35; transform: scale(0.7); }
}
.tx__amounts {
text-align: right;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
flex-shrink: 0;
}
.tx__amount {
font-family: var(--mono);
font-size: 14px;
font-weight: 700;
white-space: nowrap;
}
.tx__amount--pos { color: var(--pos); }
.tx__amount--neg { color: var(--neg); }
.tx__caret {
font-size: 11px;
color: var(--muted);
transition: transform 0.2s;
}
.tx.is-open .tx__caret {
transform: rotate(180deg);
}
/* ---------- expand panel ---------- */
.tx__detail {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.28s ease;
}
.tx.is-open .tx__detail {
grid-template-rows: 1fr;
}
.tx__detail-inner {
overflow: hidden;
}
.detail {
border-top: 1px solid var(--line);
padding: 14px 16px;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px 18px;
}
.detail__row {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.detail__k {
font-size: 10.5px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
}
.detail__v {
font-family: var(--mono);
font-size: 12.5px;
color: var(--text);
word-break: break-all;
}
.detail__hash {
grid-column: 1 / -1;
}
.detail__actions {
grid-column: 1 / -1;
display: flex;
gap: 8px;
margin-top: 2px;
}
.btn {
appearance: none;
cursor: pointer;
font-family: var(--sans);
font-size: 12.5px;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 7px;
padding: 8px 13px;
border-radius: var(--r-sm);
border: 1px solid var(--line-2);
background: var(--elevated);
color: var(--text);
transition: background 0.15s, border-color 0.15s, transform 0.08s;
}
.btn:hover {
border-color: var(--accent);
background: var(--surface-2);
}
.btn:active {
transform: translateY(1px);
}
.btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.btn--primary {
border-color: rgba(124, 92, 255, 0.5);
background: linear-gradient(135deg, var(--accent), #6b4cf0);
color: #fff;
box-shadow: 0 8px 22px -12px var(--accent-glow);
}
.btn--primary:hover {
border-color: rgba(124, 92, 255, 0.8);
background: linear-gradient(135deg, #8a6dff, var(--accent));
}
.btn svg {
width: 14px;
height: 14px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.explore {
position: absolute;
top: 20px;
right: 13px;
display: inline-grid;
place-items: center;
width: 28px;
height: 28px;
border-radius: var(--r-sm);
border: 1px solid var(--line);
background: var(--surface);
color: var(--muted);
cursor: pointer;
flex-shrink: 0;
transition: color 0.15s, border-color 0.15s;
}
.explore:hover {
color: var(--accent-2);
border-color: rgba(0, 224, 198, 0.4);
}
.explore:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.explore svg {
width: 14px;
height: 14px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
/* ---------- empty / footer ---------- */
.empty {
text-align: center;
color: var(--muted);
font-size: 14px;
padding: 48px 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.empty__mark {
font-size: 30px;
opacity: 0.5;
}
.app__foot {
margin-top: 32px;
text-align: center;
font-size: 11.5px;
color: var(--muted);
letter-spacing: 0.03em;
}
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 18px);
background: var(--elevated);
color: var(--text);
border: 1px solid var(--line-2);
border-radius: var(--r-pill);
padding: 10px 18px;
font-size: 13px;
font-weight: 500;
box-shadow: 0 16px 40px -16px rgba(0, 0, 0, 0.85);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s, transform 0.22s;
z-index: 60;
display: flex;
align-items: center;
gap: 8px;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
.toast::before {
content: "✓";
color: var(--accent-2);
font-weight: 700;
}
/* ---------- responsive ---------- */
@media (max-width: 520px) {
.app {
padding: 22px 14px 44px;
}
.app__head {
align-items: center;
}
.head__meta {
width: 100%;
}
.stat {
flex: 1;
min-width: 0;
}
.toolbar {
flex-direction: column-reverse;
align-items: stretch;
}
.chips {
overflow-x: auto;
flex-wrap: nowrap;
padding-bottom: 2px;
scrollbar-width: none;
}
.chips::-webkit-scrollbar {
display: none;
}
.chip {
flex-shrink: 0;
}
.tx__main {
grid-template-columns: auto 1fr auto;
gap: 10px;
padding: 12px 46px 12px 13px;
}
.explore {
top: 17px;
right: 10px;
}
.tx__icon {
width: 36px;
height: 36px;
font-size: 16px;
}
.tx__amount {
font-size: 13px;
}
.detail {
grid-template-columns: 1fr;
}
.detail__actions {
flex-direction: column;
}
.btn {
justify-content: center;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}/* Web3 — Transaction History (UI-only simulation, mock data) */
(function () {
"use strict";
// ---- mock data ----------------------------------------------------------
// dir: "sent" | "received" | "swap" (used for filter chips)
// type: visual/icon category
const ICONS = {
send: "↑",
receive: "↓",
swap: "⇄",
approve: "✓",
mint: "✦",
};
const TXNS = [
{
id: "t1",
type: "swap",
dir: "swap",
label: "Swap NOVA → USDx",
counter: "0x5e21…9f0a",
counterFull: "0x5e21a4b9c7d3e8f1a2b6c4d5e9f0a1b29f0a",
amount: "−420.00 NOVA",
amountOut: "+619.84 USDx",
sign: "neg",
status: "pending",
day: "today",
time: "2 min ago",
hash: "0x9c4f1d7a2e8b6035c9a1f4d2e7b8c0a35d6e2f91a4b7c8d09e1f23a4b5c6d7e80",
block: "—",
fee: "0.00214 LUM",
nonce: "142",
},
{
id: "t2",
type: "receive",
dir: "received",
label: "Receive USDx",
counter: "0xa11c…3d77",
counterFull: "0xa11c5f9e2b7d4a0163e8c2f5b9d7a04e3d77",
amount: "+1,250.00 USDx",
sign: "pos",
status: "confirmed",
day: "today",
time: "1h ago",
hash: "0x3b8e2c9f1a6d4705e8b2c1f9d3a7e604b8c2d1f9a3e7b06c4d2f1a9e3b7c08d5f",
block: "21,408,332",
fee: "0.00098 LUM",
nonce: "141",
},
{
id: "t3",
type: "send",
dir: "sent",
label: "Send LUM",
counter: "0x7f44…b1e2",
counterFull: "0x7f44d9c2e6a8b0157f3c9e2d6a8b0157f4b1e2",
amount: "−3.5000 LUM",
sign: "neg",
status: "confirmed",
day: "today",
time: "4h ago",
hash: "0x6f1d3b8a2e9c4705d1b8e2c9f3a6705d1b8e2c9f3a6d4705e1b8c2f9a3e7b06d4",
block: "21,407,901",
fee: "0.00102 LUM",
nonce: "140",
},
{
id: "t4",
type: "approve",
dir: "sent",
label: "Approve ZephyrSwap",
counter: "0xd0c4…42e1",
counterFull: "0xd0c47b3e9f2a615c8d4b0e7a3f9c215842e1 (ZephyrSwap Router v2)",
amount: "± unlimited NOVA",
sign: "neg",
status: "confirmed",
day: "yesterday",
time: "Jun 8 · 19:42",
hash: "0x2a9e3b7c0d5f1a8e4b2c9f6d3a07e15b8c2d9f6a3e0b7c4d1f8a2e9b6c3d07f15",
block: "21,402,118",
fee: "0.00076 LUM",
nonce: "139",
},
{
id: "t5",
type: "swap",
dir: "swap",
label: "Swap USDx → NOVA",
counter: "0x5e21…9f0a",
counterFull: "0x5e21a4b9c7d3e8f1a2b6c4d5e9f0a1b29f0a",
amount: "−800.00 USDx",
amountOut: "+541.22 NOVA",
sign: "neg",
status: "confirmed",
day: "yesterday",
time: "Jun 8 · 14:05",
hash: "0x8c2d1f9a3e7b06c4d2f1a9e3b7c08d5f6e2c9f1a4b7d0e83a5c1f9b2e6d4a07c3",
block: "21,401,664",
fee: "0.00231 LUM",
nonce: "138",
},
{
id: "t6",
type: "mint",
dir: "received",
label: "Mint Glyph #0427",
counter: "0x4e8a…a55c",
counterFull: "0x4e8a2d7f1c9b3065e8a2d7f1c9b30659a55c (Mint Garden Collection)",
amount: "+1 GLYPH",
sign: "pos",
status: "confirmed",
day: "yesterday",
time: "Jun 8 · 09:18",
hash: "0x1f9a3e7b06c4d2f1a9e3b7c08d5f6e2c9c4f1d7a2e8b6035c9a1f4d2e7b8c0a35",
block: "21,399,802",
fee: "0.00418 LUM",
nonce: "137",
},
{
id: "t7",
type: "send",
dir: "sent",
label: "Send USDx",
counter: "0xc903…12ab",
counterFull: "0xc9035e7a1f8d2b609c4e3a1f8d2b609c712ab",
amount: "−500.00 USDx",
sign: "neg",
status: "failed",
day: "Jun 6",
time: "Jun 6 · 22:51",
hash: "0x4d2f1a9e3b7c08d5f6e2c9f1a4b7d0e83a5c1f9b2e6d4a07c8c2d1f9a3e7b06c4",
block: "reverted",
fee: "0.00057 LUM",
nonce: "136",
},
{
id: "t8",
type: "receive",
dir: "received",
label: "Receive NOVA",
counter: "0x0bf2…7d19",
counterFull: "0x0bf2c9e6a3d8014b7f2c9e6a3d8014b77d19",
amount: "+212.40 NOVA",
sign: "pos",
status: "confirmed",
day: "Jun 6",
time: "Jun 6 · 08:33",
hash: "0x7c08d5f6e2c9f1a4b7d0e83a5c1f9b2e6d4a07c3b8e2c9f1a6d4705e8b2c1f9d3",
block: "21,390,447",
fee: "0.00089 LUM",
nonce: "135",
},
];
// ---- dom refs -----------------------------------------------------------
const listEl = document.getElementById("list");
const emptyEl = document.getElementById("empty");
const searchEl = document.getElementById("q");
const chipEls = Array.prototype.slice.call(document.querySelectorAll(".chip"));
const toastEl = document.getElementById("toast");
let activeFilter = "all";
let query = "";
const openRows = new Set();
// ---- helpers ------------------------------------------------------------
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
let toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2200);
}
function copy(text, okMsg) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(
function () { toast(okMsg); },
function () { toast("Copy failed"); }
);
} else {
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
try { document.execCommand("copy"); toast(okMsg); }
catch (e) { toast("Copy failed"); }
document.body.removeChild(ta);
}
}
function matchesFilter(tx) {
return activeFilter === "all" || tx.dir === activeFilter;
}
function matchesQuery(tx) {
if (!query) return true;
const q = query.toLowerCase();
return (
tx.hash.toLowerCase().indexOf(q) !== -1 ||
tx.counter.toLowerCase().indexOf(q) !== -1 ||
tx.counterFull.toLowerCase().indexOf(q) !== -1 ||
tx.label.toLowerCase().indexOf(q) !== -1
);
}
function badge(status) {
const labels = { pending: "Pending", confirmed: "Confirmed", failed: "Failed" };
return (
'<span class="badge badge--' + status + '">' + labels[status] + "</span>"
);
}
function rowHtml(tx) {
const isOpen = openRows.has(tx.id);
const amountClass = tx.sign === "pos" ? "tx__amount--pos" : "tx__amount--neg";
const swapOut = tx.amountOut
? '<span class="tx__amount tx__amount--pos">' + escapeHtml(tx.amountOut) + "</span>"
: "";
return (
'<div class="tx' + (isOpen ? " is-open" : "") + '" data-id="' + tx.id + '">' +
'<button class="tx__main" type="button" aria-expanded="' + isOpen + '">' +
'<span class="tx__icon tx__icon--' + tx.type + '" aria-hidden="true">' + ICONS[tx.type] + "</span>" +
'<span class="tx__body">' +
'<span class="tx__label">' + escapeHtml(tx.label) + "</span>" +
'<span class="tx__sub">' +
'<span class="tx__counter">' + escapeHtml(tx.counter) + "</span>" +
badge(tx.status) +
'<span class="tx__time">' + escapeHtml(tx.time) + "</span>" +
"</span>" +
"</span>" +
'<span class="tx__amounts">' +
'<span class="tx__amount ' + amountClass + '">' + escapeHtml(tx.amount) + "</span>" +
swapOut +
'<span class="tx__caret" aria-hidden="true">▾</span>' +
"</span>" +
"</button>" +
'<button class="explore" type="button" data-explore="' + escapeHtml(tx.hash) + '" aria-label="View transaction on explorer">' +
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 4h6v6"></path><path d="M20 4 10 14"></path>' +
'<path d="M19 13v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h6"></path></svg>' +
"</button>" +
'<div class="tx__detail"><div class="tx__detail-inner">' +
'<div class="detail">' +
'<div class="detail__row detail__hash">' +
'<span class="detail__k">Transaction hash</span>' +
'<span class="detail__v">' + escapeHtml(tx.hash) + "</span>" +
"</div>" +
'<div class="detail__row"><span class="detail__k">Block</span>' +
'<span class="detail__v">' + escapeHtml(tx.block) + "</span></div>" +
'<div class="detail__row"><span class="detail__k">Network fee</span>' +
'<span class="detail__v">' + escapeHtml(tx.fee) + "</span></div>" +
'<div class="detail__row"><span class="detail__k">Nonce</span>' +
'<span class="detail__v">' + escapeHtml(tx.nonce) + "</span></div>" +
'<div class="detail__row"><span class="detail__k">Counterparty</span>' +
'<span class="detail__v">' + escapeHtml(tx.counterFull) + "</span></div>" +
'<div class="detail__actions">' +
'<button class="btn btn--primary" data-copy="' + escapeHtml(tx.hash) + '">' +
'<svg viewBox="0 0 24 24"><rect x="9" y="9" width="11" height="11" rx="2"></rect>' +
'<path d="M5 15V5a2 2 0 0 1 2-2h10"></path></svg>Copy hash</button>' +
'<button class="btn explore-btn" data-explore="' + escapeHtml(tx.hash) + '">' +
'<svg viewBox="0 0 24 24"><path d="M14 4h6v6"></path><path d="M20 4 10 14"></path>' +
'<path d="M19 13v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h6"></path></svg>' +
"View on explorer</button>" +
"</div>" +
"</div>" +
"</div></div>" +
"</div>"
);
}
function render() {
const visible = TXNS.filter(function (tx) {
return matchesFilter(tx) && matchesQuery(tx);
});
// counts for chips
const counts = { all: 0, sent: 0, received: 0, swap: 0 };
TXNS.forEach(function (tx) {
counts.all++;
if (tx.dir === "sent") counts.sent++;
else if (tx.dir === "received") counts.received++;
else if (tx.dir === "swap") counts.swap++;
});
Object.keys(counts).forEach(function (k) {
const el = document.querySelector('[data-count-for="' + k + '"]');
if (el) el.textContent = counts[k];
});
if (!visible.length) {
listEl.innerHTML = "";
emptyEl.hidden = false;
return;
}
emptyEl.hidden = true;
// group by day, preserving order
const order = [];
const groups = {};
visible.forEach(function (tx) {
if (!groups[tx.day]) {
groups[tx.day] = [];
order.push(tx.day);
}
groups[tx.day].push(tx);
});
const labelFor = { today: "Today", yesterday: "Yesterday" };
let html = "";
order.forEach(function (day) {
html +=
'<div class="group">' +
'<div class="group__head">' + (labelFor[day] || day) + "</div>" +
'<div class="group__rows">' +
groups[day].map(rowHtml).join("") +
"</div></div>";
});
listEl.innerHTML = html;
}
// ---- interactions -------------------------------------------------------
chipEls.forEach(function (chip) {
chip.addEventListener("click", function () {
chipEls.forEach(function (c) {
c.classList.remove("is-active");
c.setAttribute("aria-selected", "false");
});
chip.classList.add("is-active");
chip.setAttribute("aria-selected", "true");
activeFilter = chip.getAttribute("data-filter");
render();
});
});
let searchTimer;
searchEl.addEventListener("input", function () {
clearTimeout(searchTimer);
searchTimer = setTimeout(function () {
query = searchEl.value.trim();
render();
}, 90);
});
// "/" focuses search
document.addEventListener("keydown", function (e) {
if (e.key === "/" && document.activeElement !== searchEl) {
e.preventDefault();
searchEl.focus();
}
if (e.key === "Escape" && document.activeElement === searchEl) {
searchEl.value = "";
query = "";
render();
searchEl.blur();
}
});
// delegated clicks on list
listEl.addEventListener("click", function (e) {
const copyBtn = e.target.closest("[data-copy]");
if (copyBtn) {
e.stopPropagation();
copy(copyBtn.getAttribute("data-copy"), "Hash copied to clipboard");
return;
}
const expBtn = e.target.closest("[data-explore]");
if (expBtn) {
e.stopPropagation();
const h = expBtn.getAttribute("data-explore");
toast("Opening explorer · " + h.slice(0, 10) + "…");
return;
}
const main = e.target.closest(".tx__main");
if (main) {
const row = main.closest(".tx");
const id = row.getAttribute("data-id");
if (openRows.has(id)) openRows.delete(id);
else openRows.add(id);
row.classList.toggle("is-open");
main.setAttribute("aria-expanded", openRows.has(id) ? "true" : "false");
}
});
// ---- animated header numbers -------------------------------------------
function animateCounts() {
document.querySelectorAll(".num[data-count]").forEach(function (el) {
const target = parseFloat(el.getAttribute("data-count"));
const decimals = parseInt(el.getAttribute("data-decimals") || "0", 10);
const dur = 900;
const start = performance.now();
function step(now) {
const p = Math.min(1, (now - start) / dur);
const eased = 1 - Math.pow(1 - p, 3);
const val = target * eased;
el.textContent = decimals
? val.toFixed(decimals)
: Math.round(val).toLocaleString();
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
});
}
// ---- simulate the pending tx confirming --------------------------------
function confirmPending() {
const tx = TXNS.find(function (t) { return t.status === "pending"; });
if (!tx) return;
tx.status = "confirmed";
tx.block = "21,408,701";
tx.time = "just now";
render();
toast("Swap confirmed · " + tx.hash.slice(0, 10) + "…");
}
// ---- boot ---------------------------------------------------------------
render();
animateCounts();
setTimeout(confirmPending, 4200);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Web3 — Transaction History</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="app" role="main">
<header class="app__head">
<div class="brand">
<span class="brand__mark" aria-hidden="true">◈</span>
<div class="brand__text">
<h1>Transaction History</h1>
<p class="brand__sub">
<span class="dot dot--live" aria-hidden="true"></span>
Lumen Chain · <code class="addr">0x7a3f…c41d</code>
</p>
</div>
</div>
<div class="head__meta">
<div class="stat">
<span class="stat__k">This week</span>
<span class="stat__v num" data-count="18">0</span>
<span class="stat__u">txns</span>
</div>
<div class="stat">
<span class="stat__k">Gas spent</span>
<span class="stat__v num" data-count="0.0412" data-decimals="4">0</span>
<span class="stat__u">LUM</span>
</div>
</div>
</header>
<section class="toolbar" aria-label="Filter and search transactions">
<div class="chips" role="tablist" aria-label="Transaction type filter">
<button class="chip is-active" role="tab" aria-selected="true" data-filter="all">
All <span class="chip__n" data-count-for="all">0</span>
</button>
<button class="chip" role="tab" aria-selected="false" data-filter="sent">
Sent <span class="chip__n" data-count-for="sent">0</span>
</button>
<button class="chip" role="tab" aria-selected="false" data-filter="received">
Received <span class="chip__n" data-count-for="received">0</span>
</button>
<button class="chip" role="tab" aria-selected="false" data-filter="swap">
Swaps <span class="chip__n" data-count-for="swap">0</span>
</button>
</div>
<div class="search">
<svg class="search__icon" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="11" cy="11" r="7"></circle>
<line x1="21" y1="21" x2="16.5" y2="16.5"></line>
</svg>
<input
id="q"
type="search"
class="search__input"
placeholder="Search by hash or address…"
aria-label="Search transactions by hash or address"
autocomplete="off"
spellcheck="false"
/>
<kbd class="search__kbd">/</kbd>
</div>
</section>
<section class="list" id="list" aria-live="polite"></section>
<p class="empty" id="empty" hidden>
<span class="empty__mark" aria-hidden="true">⬡</span>
No transactions match your filters.
</p>
<footer class="app__foot">
UI-only simulation · mock data · fictional tokens
</footer>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Transaction History (status · explorer link)
A wallet-style activity feed for a fictional Lumen Chain account. Transactions are grouped by date (Today / Yesterday / older), and each row carries a typed icon (Send, Receive, Swap, Approve, Mint), a human label like “Swap NOVA → USDx”, the truncated counterparty address in monospace, and the amount tinted green for inflows or red for outflows. A status badge sits inline: Confirmed (green), Failed (red), or a softly pulsing Pending (amber). Animated header counters tally the week’s activity and gas spend on load.
The toolbar drives everything client-side. Filter chips toggle between All, Sent, Received and Swaps with live counts, while the search box matches against transaction hashes, labels and full or truncated addresses as you type. Press / to jump to search and Escape to clear it. Click any row to expand a detail panel with the full hash, block, network fee, nonce and counterparty — then copy the hash to your clipboard (with a toast) or fire the simulated explorer link.
To show off live status, one swap starts as Pending and automatically flips to Confirmed a few seconds after load, re-rendering its badge, block number and timestamp and surfacing a confirmation toast. Everything is glassy surfaces, neon-violet and teal accents, monospace numerics and tabular alignment — built with plain HTML, CSS and vanilla JavaScript, no frameworks or web3 libraries.
UI-only simulation — no real wallet, RPC, or on-chain calls. Mock data, fictional tokens.