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, 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
Код
: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 {
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 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(--text); }
.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 {
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 13px;
}
.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: "0xZeph…r0ut",
counterFull: "0xZephyrSwapRouterV2…7c41dabc12r0ut",
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: "0xMint…a55c",
counterFull: "0xMintGardenLumenCollection…9a55c",
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>" +
'<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 neutral 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.