Add QR codes to address pages with redesigned layout

- Installed qrcode npm package for QR generation
- Created reusable QRCode.svelte component
- Completely redesigned address page layout:
  - Prominent QR code display (200x200px)
  - Grid layout with QR + address info + balance stats
  - Copy address button with feedback
  - Stats grid showing transactions, UTXOs, sent, net activity
  - Enhanced UTXO table with clickable block heights
  - Better visual hierarchy and spacing
- QR code shows even if address index disabled
- White QR on dark background matching site theme
- Added proper meta descriptions for SEO

Address pages now have professional layout with scannable QR codes!
This commit is contained in:
Krystie
2026-03-27 17:34:37 -07:00
parent 4cb179f9fc
commit f881ad1f97
4 changed files with 456 additions and 24 deletions
+307 -1
View File
@@ -9,7 +9,8 @@
"version": "1.0.0",
"dependencies": {
"chart.js": "^4.4.0",
"leaflet": "^1.9.4"
"leaflet": "^1.9.4",
"qrcode": "^1.5.4"
},
"devDependencies": {
"@sveltejs/adapter-node": "^5.5.4",
@@ -1417,6 +1418,30 @@
"node": ">=0.4.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/aria-query": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
@@ -1437,6 +1462,15 @@
"node": ">= 0.4"
}
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/chart.js": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
@@ -1465,6 +1499,17 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -1475,6 +1520,24 @@
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -1510,6 +1573,15 @@
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@@ -1537,6 +1609,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/enhanced-resolve": {
"version": "5.20.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
@@ -1636,6 +1720,19 @@
}
}
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1661,6 +1758,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -1697,6 +1803,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
@@ -2008,6 +2123,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -2064,6 +2191,51 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@@ -2091,6 +2263,15 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/postcss": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
@@ -2120,6 +2301,23 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -2134,6 +2332,21 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -2213,6 +2426,12 @@
"node": ">=6"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/set-cookie-parser": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz",
@@ -2245,6 +2464,32 @@
"node": ">=0.10.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -2484,6 +2729,67 @@
}
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/zimmerframe": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
+2 -1
View File
@@ -23,6 +23,7 @@
},
"dependencies": {
"chart.js": "^4.4.0",
"leaflet": "^1.9.4"
"leaflet": "^1.9.4",
"qrcode": "^1.5.4"
}
}
+24
View File
@@ -0,0 +1,24 @@
<script lang="ts">
import { onMount } from 'svelte';
import QRCode from 'qrcode';
let { data, size = 200 }: { data: string; size?: number } = $props();
let canvas: HTMLCanvasElement;
onMount(() => {
if (canvas) {
QRCode.toCanvas(canvas, data, {
width: size,
margin: 2,
color: {
dark: '#FFFFFF', // White QR code
light: '#1a1a2e' // Dark background (tri-bg color)
}
}).catch(err => {
console.error('QR code generation failed:', err);
});
}
});
</script>
<canvas bind:this={canvas} class="rounded-lg border-2 border-tri-border"></canvas>
+123 -22
View File
@@ -1,36 +1,135 @@
<script lang="ts">
import TxTable from '$lib/components/TxTable.svelte';
import { formatAmount, truncateHash } from '$lib/utils';
import QRCode from '$lib/components/QRCode.svelte';
import { formatAmount, truncateHash, formatNumber } from '$lib/utils';
let { data } = $props();
// Copy address to clipboard
let copied = $state(false);
function copyAddress() {
navigator.clipboard.writeText(data.address).then(() => {
copied = true;
setTimeout(() => copied = false, 2000);
});
}
</script>
<svelte:head>
<title>Address {data.address} - Triangles Explorer</title>
<meta name="description" content="Triangles address {data.address} - balance, transactions, and QR code" />
</svelte:head>
<div class="mb-6">
<h1 class="text-2xl font-bold text-white">Address</h1>
<p class="text-tri-accent text-sm font-mono mt-1 break-all">{data.address}</p>
</div>
{#if data.indexError}
<div class="bg-tri-yellow/10 border border-tri-yellow/30 rounded-lg p-4 text-tri-yellow text-sm">
Address index is not enabled on this node. Start the daemon with <code class="bg-tri-surface px-1 rounded">-addressindex=1</code> to enable address lookups.
<div class="mb-6">
<h1 class="text-3xl font-bold text-white mb-3">Address Details</h1>
<div class="bg-tri-yellow/10 border border-tri-yellow/30 rounded-lg p-4 text-tri-yellow text-sm">
Address index is not enabled on this node. Start the daemon with <code class="bg-tri-surface px-1 rounded">-addressindex=1</code> to enable address lookups.
</div>
</div>
<!-- Still show address and QR even if index disabled -->
<div class="bg-tri-surface border border-tri-border rounded-lg p-6">
<div class="text-center">
<div class="inline-block mb-4">
<QRCode data={data.address} size={256} />
</div>
<div class="font-mono text-sm text-tri-text break-all mb-3">{data.address}</div>
<button
onclick={copyAddress}
class="px-4 py-2 bg-tri-accent hover:bg-tri-accent-light text-white rounded transition-colors text-sm font-medium"
>
{copied ? '✓ Copied!' : 'Copy Address'}
</button>
</div>
</div>
{:else}
<!-- Balance -->
{#if data.balance}
<div class="bg-tri-surface border border-tri-border rounded-lg p-6 mb-6">
<div class="text-tri-muted text-xs uppercase tracking-wider mb-1">Balance</div>
<div class="text-3xl font-bold text-white">
{formatAmount(data.balance.balance / 100000000)} <span class="text-tri-muted text-lg">TRI</span>
</div>
<div class="text-tri-muted text-sm mt-1">
Total received: {formatAmount(data.balance.received / 100000000)} TRI
<!-- Header -->
<div class="mb-6">
<h1 class="text-3xl font-bold text-white mb-2">Triangles Address</h1>
<p class="text-tri-muted text-sm">View balance, transactions, and QR code</p>
</div>
<!-- Main Grid: QR + Balance Info -->
<div class="grid md:grid-cols-3 gap-6 mb-6">
<!-- QR Code Card -->
<div class="bg-tri-surface border border-tri-border rounded-lg p-6">
<div class="text-center">
<h2 class="text-white font-semibold mb-4">QR Code</h2>
<div class="inline-block mb-4">
<QRCode data={data.address} size={200} />
</div>
<p class="text-tri-muted text-xs mb-3">Scan to send TRI to this address</p>
</div>
</div>
{/if}
<!-- Address & Balance Info -->
<div class="md:col-span-2 space-y-4">
<!-- Address Display -->
<div class="bg-tri-surface border border-tri-border rounded-lg p-6">
<div class="text-tri-muted text-xs uppercase tracking-wider mb-2">Address</div>
<div class="font-mono text-sm text-tri-text break-all mb-3">{data.address}</div>
<button
onclick={copyAddress}
class="px-4 py-2 bg-tri-accent hover:bg-tri-accent-light text-white rounded transition-colors text-sm font-medium"
>
{copied ? '✓ Copied!' : 'Copy Address'}
</button>
</div>
<!-- Balance Stats -->
{#if data.balance}
<div class="grid grid-cols-2 gap-4">
<div class="bg-tri-surface border border-tri-border rounded-lg p-4">
<div class="text-tri-muted text-xs uppercase tracking-wider mb-1">Balance</div>
<div class="text-2xl font-bold text-white">
{formatAmount(data.balance.balance / 100000000)}
</div>
<div class="text-tri-muted text-xs mt-0.5">TRI</div>
</div>
<div class="bg-tri-surface border border-tri-border rounded-lg p-4">
<div class="text-tri-muted text-xs uppercase tracking-wider mb-1">Received</div>
<div class="text-2xl font-bold text-tri-green">
{formatAmount(data.balance.received / 100000000)}
</div>
<div class="text-tri-muted text-xs mt-0.5">TRI</div>
</div>
</div>
{:else}
<div class="bg-tri-surface border border-tri-border rounded-lg p-6">
<div class="text-tri-muted text-center">Balance information unavailable</div>
</div>
{/if}
</div>
</div>
<!-- Additional Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="bg-tri-surface border border-tri-border rounded-lg p-4">
<div class="text-tri-muted text-xs uppercase tracking-wider mb-1">Transactions</div>
<div class="text-2xl font-bold text-white">{formatNumber(data.totalTxs)}</div>
</div>
<div class="bg-tri-surface border border-tri-border rounded-lg p-4">
<div class="text-tri-muted text-xs uppercase tracking-wider mb-1">Unspent Outputs</div>
<div class="text-2xl font-bold text-white">{formatNumber(data.utxos.length)}</div>
</div>
{#if data.balance}
<div class="bg-tri-surface border border-tri-border rounded-lg p-4">
<div class="text-tri-muted text-xs uppercase tracking-wider mb-1">Sent</div>
<div class="text-xl font-bold text-tri-yellow">
{formatAmount((data.balance.received - data.balance.balance) / 100000000)}
</div>
<div class="text-tri-muted text-xs mt-0.5">TRI</div>
</div>
<div class="bg-tri-surface border border-tri-border rounded-lg p-4">
<div class="text-tri-muted text-xs uppercase tracking-wider mb-1">Net Activity</div>
<div class="text-xl font-bold text-tri-accent">
{formatAmount(data.balance.balance / 100000000)}
</div>
<div class="text-tri-muted text-xs mt-0.5">TRI</div>
</div>
{/if}
</div>
<!-- UTXOs -->
{#if data.utxos.length > 0}
@@ -50,13 +149,15 @@
</thead>
<tbody>
{#each data.utxos as utxo}
<tr class="border-b border-tri-border/50">
<tr class="border-b border-tri-border/50 hover:bg-tri-surface/50 transition-colors">
<td class="py-2.5 px-3 font-mono text-xs">
<a href="/tx/{utxo.txid}" class="text-tri-accent hover:text-tri-accent-light">{truncateHash(utxo.txid, 10)}</a>
</td>
<td class="py-2.5 px-3 text-center">{utxo.outputIndex}</td>
<td class="py-2.5 px-3 text-right font-mono text-tri-green">{formatAmount(utxo.satoshis / 100000000)} TRI</td>
<td class="py-2.5 px-3 text-right">{utxo.height}</td>
<td class="py-2.5 px-3 text-right">
<a href="/blocks/{utxo.height}" class="text-tri-accent hover:text-tri-accent-light">{utxo.height}</a>
</td>
</tr>
{/each}
</tbody>
@@ -68,13 +169,13 @@
<!-- Transaction History -->
<div class="bg-tri-surface border border-tri-border rounded-lg overflow-hidden">
<div class="px-4 py-3 border-b border-tri-border">
<h2 class="text-white font-semibold">Transactions ({data.totalTxs})</h2>
<h2 class="text-white font-semibold">Transaction History ({formatNumber(data.totalTxs)})</h2>
</div>
{#if data.txs.length > 0}
<TxTable txs={data.txs} />
{#if data.totalTxs > 50}
<div class="px-4 py-3 text-center text-tri-muted text-sm border-t border-tri-border">
Showing 50 of {data.totalTxs} transactions
Showing 50 of {formatNumber(data.totalTxs)} transactions
</div>
{/if}
{:else}