Files
GL/interface/web/templates.templ
T
nfel 32e7b6b749
CI/CD Dev / dev (push) Failing after 44s
feat: add persistent dashboard dark mode
2026-08-15 00:53:58 +03:30

427 lines
28 KiB
Plaintext

package web
import (
"strconv"
"gl/application/explorer"
"gl/domain/ledger"
)
templ Page(title string, active string, locale Locale, englishURL string, persianURL string, content templ.Component) {
<!doctype html>
<html lang={ string(locale) } dir={ locale.Direction() }>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="description" content={ tr(locale, "general_ledger_explorer") }/>
<meta name="htmx-config" content='{"historyRestoreAsHxRequest":false,"selfRequestsOnly":true}'/>
<title>{ title } · { tr(locale, "site_name") }</title>
<link rel="icon" href="/favicon.ico" sizes="any"/>
<script src="/theme.js"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js" integrity="sha384-H5SrcfygHmAuTDZphMHqBJLc3FhssKjG7w/CeCpFReSfwBWDTKpkzPP8c+cLsK+V" crossorigin="anonymous"></script>
<style>
:root { color-scheme:light; --ink:#1f2a2c; --on-ink:#ffffff; --muted:rgba(31,42,44,.68); --paper:#F5F5F5; --surface:#DFF1F1; --card:#ffffff; --glass:rgba(255,255,255,.78); --hover:rgba(223,241,241,.72); --line:rgba(31,42,44,.15); --teal:#BBD5DA; --accent:#FF0000; --positive:#497f82; --teal-glow:rgba(187,213,218,.48); --accent-glow:rgba(255,0,0,.04); --shadow:0 18px 50px rgba(31,42,44,.10); }
html[data-theme="dark"] { color-scheme:dark; --ink:#F5F5F5; --on-ink:#1f2a2c; --muted:rgba(223,241,241,.68); --paper:#101719; --surface:#253639; --card:#182326; --glass:rgba(24,35,38,.86); --hover:rgba(37,54,57,.82); --line:rgba(187,213,218,.18); --teal:#BBD5DA; --accent:#FF0000; --positive:#9bc8cc; --teal-glow:rgba(187,213,218,.12); --accent-glow:rgba(255,0,0,.08); --shadow:0 18px 50px rgba(0,0,0,.28); }
* { box-sizing:border-box; }
html { background:var(--paper); color:var(--ink); font-family:Inter,Tahoma,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
body { margin:0; min-height:100vh; background:radial-gradient(circle at 78% 0%,var(--teal-glow),transparent 30rem),radial-gradient(circle at 5% 30%,var(--accent-glow),transparent 24rem),var(--paper); }
a { color:inherit; }
.shell { width:min(1180px,calc(100% - 40px)); margin:0 auto; }
.topbar { min-height:76px; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid var(--line); }
.brand { display:flex; align-items:center; gap:12px; text-decoration:none; font-weight:850; letter-spacing:-.03em; }
.mark { width:35px; height:35px; display:block; flex:none; object-fit:contain; }
.brand small { display:block; color:var(--muted); font-size:9px; letter-spacing:.2em; margin-top:2px; }
.nav { display:flex; gap:6px; align-items:center; }
.nav a { padding:10px 13px; border-radius:999px; text-decoration:none; color:var(--muted); font-size:13px; font-weight:700; }
.nav a:hover,.nav a.active { color:var(--ink); background:var(--surface); }
.live { display:flex; align-items:center; gap:8px; margin-inline-start:10px; padding:8px 11px; border:1px solid var(--line); border-radius:999px; font:700 11px/1 ui-monospace,SFMono-Regular,monospace; background:var(--glass); }
.dot { width:7px; height:7px; background:var(--teal); border-radius:50%; box-shadow:0 0 0 4px rgba(187,213,218,.35); }
.theme-toggle { width:34px; height:34px; flex:none; margin-inline-start:8px; border:1px solid var(--line); border-radius:50%; background:var(--glass); color:var(--ink); cursor:pointer; font-size:17px; line-height:1; }
.theme-toggle:hover { background:var(--surface); }
.language-switch { display:flex; padding:3px; margin-inline-start:8px; border:1px solid var(--line); border-radius:999px; background:var(--glass); direction:ltr; }
.language-switch a { padding:6px 8px; font:800 10px/1 ui-monospace,SFMono-Regular,monospace; }
.language-switch a.active { background:var(--ink); color:var(--on-ink); }
main { padding:62px 0 90px; }
.eyebrow { display:flex; align-items:center; gap:9px; color:var(--teal); text-transform:uppercase; letter-spacing:.14em; font:800 11px/1 ui-monospace,SFMono-Regular,monospace; }
.eyebrow:before { content:""; width:26px; height:2px; background:var(--accent); }
h1 { max-width:830px; margin:20px 0 14px; font-size:clamp(42px,7vw,82px); line-height:.94; letter-spacing:-.065em; font-weight:850; }
.lede { max-width:660px; margin:0; color:var(--muted); font-size:17px; line-height:1.65; }
.search { display:flex; gap:10px; margin:34px 0 46px; padding:9px; border:1px solid var(--line); border-radius:16px; background:var(--glass); box-shadow:var(--shadow); }
.search input { flex:1; min-width:0; border:0; outline:0; background:transparent; padding:9px 12px; color:var(--ink); font:500 14px/1.4 ui-monospace,SFMono-Regular,monospace; }
.search button,.button { border:0; border-radius:10px; background:var(--accent); color:white; padding:13px 18px; font-weight:800; cursor:pointer; }
.search button:hover,.button:hover { background:var(--ink); color:var(--on-ink); }
.stats { display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin-bottom:46px; }
.stat { min-height:136px; padding:22px; border:1px solid var(--line); border-radius:16px; background:var(--glass); }
.stat.accent { background:var(--accent); border-color:var(--accent); color:white; }
.stat.accent .stat-label { color:rgba(255,255,255,.78); }
.stat-label { color:var(--muted); text-transform:uppercase; letter-spacing:.13em; font:750 10px/1 ui-monospace,SFMono-Regular,monospace; }
.stat strong { display:block; margin-top:21px; font-size:32px; line-height:1; letter-spacing:-.05em; }
.stat time { display:block; margin-top:18px; font:650 12px/1.5 ui-monospace,SFMono-Regular,monospace; }
.section-head { display:flex; align-items:end; justify-content:space-between; margin:0 0 14px; }
.section-head h2 { margin:0; font-size:22px; letter-spacing:-.035em; }
.section-head span { color:var(--muted); font:600 11px/1 ui-monospace,SFMono-Regular,monospace; }
.dashboard-section { margin-top:46px; }
.panel { overflow:hidden; border:1px solid var(--line); border-radius:18px; background:var(--card); box-shadow:0 8px 25px rgba(28,35,29,.04); }
table { width:100%; border-collapse:collapse; }
th { padding:14px 18px; background:var(--surface); color:var(--ink); text-align:start; text-transform:uppercase; letter-spacing:.1em; font:750 9px/1 ui-monospace,SFMono-Regular,monospace; }
td { padding:17px 18px; border-top:1px solid var(--line); font-size:13px; vertical-align:middle; }
tr:first-child td { border-top:0; }
tr:hover td { background:var(--hover); }
.mono { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; direction:ltr; unicode-bidi:isolate; }
.hash-link { font-weight:750; text-decoration:none; border-bottom:1px solid var(--teal); }
.pill { display:inline-flex; padding:6px 9px; border-radius:999px; background:var(--surface); font:750 10px/1 ui-monospace,SFMono-Regular,monospace; text-transform:uppercase; }
.holder-id { max-width:330px; overflow-wrap:anywhere; font-weight:750; }
.holder-id small { display:block; margin-top:5px; color:var(--muted); font:650 10px/1 ui-monospace,SFMono-Regular,monospace; }
.empty { padding:45px 24px; text-align:center; color:var(--muted); }
.notice + .transaction-list { margin-top:46px; }
.pagination { display:flex; align-items:center; justify-content:center; gap:8px; margin-top:18px; direction:ltr; }
.pagination a,.pagination span { min-width:42px; padding:10px 13px; border:1px solid var(--line); border-radius:10px; text-align:center; text-decoration:none; font:750 11px/1 ui-monospace,SFMono-Regular,monospace; }
.pagination a { background:var(--card); }
.pagination a:hover { background:var(--surface); }
.pagination .current { background:var(--ink); color:var(--on-ink); border-color:var(--ink); }
.pagination .disabled { color:var(--muted); opacity:.45; }
.transaction-filters { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto auto; gap:10px; margin-bottom:14px; padding:14px; border:1px solid var(--line); border-radius:14px; background:var(--card); }
.transaction-filters .field input { height:40px; }
.transaction-filters .button { align-self:end; height:40px; padding-block:0; }
.transaction-filters .clear-filter { display:flex; align-items:center; align-self:end; height:40px; padding:0 12px; color:var(--muted); font-weight:750; text-decoration:none; }
.page-title h1 { font-size:clamp(42px,6vw,68px); }
.breadcrumb { margin-bottom:25px; color:var(--muted); font:650 11px/1 ui-monospace,SFMono-Regular,monospace; }
.breadcrumb a { text-decoration:none; }
.result-stack { display:grid; gap:18px; margin-top:34px; }
.journal { border:1px solid var(--line); border-radius:18px; background:var(--card); overflow:hidden; box-shadow:var(--shadow); }
.journal-head { padding:22px; display:flex; gap:20px; align-items:flex-start; justify-content:space-between; border-bottom:1px solid var(--line); }
.journal-head h2 { margin:8px 0 0; max-width:770px; overflow-wrap:anywhere; font:750 14px/1.5 ui-monospace,SFMono-Regular,monospace; }
.status { flex:none; padding:7px 10px; border-radius:999px; background:var(--teal); color:#1f2a2c; font:800 10px/1 ui-monospace,SFMono-Regular,monospace; }
.detail-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:1px; background:var(--line); border-bottom:1px solid var(--line); }
.detail { min-width:0; padding:18px 22px; background:var(--card); }
.detail label { display:block; margin-bottom:8px; color:var(--muted); text-transform:uppercase; letter-spacing:.1em; font:750 9px/1 ui-monospace,SFMono-Regular,monospace; }
.detail div { overflow-wrap:anywhere; font-size:13px; }
.entries { padding:18px 22px 22px; }
.entries h3 { margin:0 0 12px; font-size:13px; }
.entry { display:grid; grid-template-columns:46px minmax(0,1fr) 170px; gap:15px; align-items:center; padding:13px 0; border-top:1px solid var(--line); }
.entry:first-of-type { border-top:0; }
.entry-number { color:var(--muted); font:650 11px/1 ui-monospace,SFMono-Regular,monospace; }
.entry-account a { font-weight:700; text-decoration:none; }
.entry-account small { display:block; color:var(--muted); margin-top:4px; }
.amount { text-align:end; direction:ltr; font:800 13px/1 ui-monospace,SFMono-Regular,monospace; }
.amount.positive { color:var(--positive); }
.amount.negative { color:var(--accent); }
.notice { margin-top:28px; padding:20px; border:1px solid rgba(255,0,0,.32); border-radius:14px; background:rgba(255,0,0,.06); color:var(--ink); }
.notice strong { display:block; margin-bottom:5px; }
.account-form { display:grid; grid-template-columns:1.25fr 1fr 1.2fr .65fr auto; gap:10px; margin:34px 0; padding:14px; border:1px solid var(--line); border-radius:16px; background:var(--card); box-shadow:var(--shadow); }
.field label { display:block; margin:0 0 7px 3px; color:var(--muted); text-transform:uppercase; letter-spacing:.09em; font:750 9px/1 ui-monospace,SFMono-Regular,monospace; }
.field input,.field select { width:100%; height:43px; border:1px solid var(--line); border-radius:9px; outline:0; padding:0 11px; background:var(--card); color:var(--ink); }
.field input:focus,.field select:focus { border-color:var(--teal); box-shadow:0 0 0 3px rgba(187,213,218,.42); }
.account-form button { align-self:end; height:43px; }
.account-card { display:grid; grid-template-columns:1.35fr .65fr; gap:1px; background:var(--line); border:1px solid var(--line); border-radius:18px; overflow:hidden; margin-bottom:30px; }
.account-card > div { padding:25px; background:var(--card); }
.account-card h2 { margin:8px 0 0; font:800 16px/1.5 ui-monospace,SFMono-Regular,monospace; overflow-wrap:anywhere; }
.balance { font-size:34px; line-height:1; font-weight:850; letter-spacing:-.04em; overflow-wrap:anywhere; }
.htmx-request .search-label { display:none; }
.htmx-request button:after { content:" …"; }
footer { padding:24px 0 38px; border-top:1px solid var(--line); color:var(--muted); display:flex; justify-content:space-between; font-size:11px; }
html[dir="rtl"] body { letter-spacing:0; }
html[dir="rtl"] h1 { letter-spacing:-.035em; }
html[dir="rtl"] .search input { font-family:Tahoma,ui-sans-serif,system-ui,sans-serif; }
@media (max-width:850px) { .stats{grid-template-columns:repeat(2,1fr)} .account-form{grid-template-columns:1fr 1fr} .account-form button{grid-column:1/-1} .transaction-filters{grid-template-columns:1fr 1fr} .detail-grid{grid-template-columns:1fr 1fr} .nav>a{display:none} }
@media (max-width:620px) { .shell{width:min(100% - 24px,1180px)} .topbar{min-height:66px} .live{display:none} main{padding-top:42px} .stats{grid-template-columns:1fr 1fr} .stat{min-height:115px;padding:17px}.stat strong{font-size:25px} .search{display:grid}.search button{width:100%} .transaction-filters{grid-template-columns:1fr}.panel{overflow-x:auto} table{min-width:720px} .journal-head{display:block}.status{display:inline-flex;margin-top:14px}.detail-grid{grid-template-columns:1fr}.entry{grid-template-columns:32px minmax(0,1fr)}.amount{grid-column:2;text-align:start}.account-form{grid-template-columns:1fr}.account-form button{grid-column:auto}.account-card{grid-template-columns:1fr} footer{display:block;line-height:1.8} }
</style>
</head>
<body hx-boost="true" hx-target="#explorer-content" hx-select="#explorer-content" hx-swap="outerHTML show:top" hx-push-url="true">
<header class="shell topbar">
<a class="brand" href="/" hx-target="body" hx-select="body">
<img class="mark" src="/favicon.ico" alt="" width="35" height="35" aria-hidden="true"/>
<span>{ tr(locale, "site_name") } <small>{ tr(locale, "site_subtitle") }</small></span>
</a>
<nav class="nav" aria-label="Main navigation" hx-target="body" hx-select="body">
if active == "dashboard" {
<a class="active" href="/">{ tr(locale, "overview") }</a>
} else {
<a href="/">{ tr(locale, "overview") }</a>
}
if active == "transactions" {
<a class="active" href="/transactions">{ tr(locale, "transactions") }</a>
} else {
<a href="/transactions">{ tr(locale, "transactions") }</a>
}
if active == "assets" {
<a class="active" href="/assets">{ tr(locale, "assets") }</a>
} else {
<a href="/assets">{ tr(locale, "assets") }</a>
}
if active == "accounts" {
<a class="active" href="/accounts">{ tr(locale, "accounts") }</a>
} else {
<a href="/accounts">{ tr(locale, "accounts") }</a>
}
<span class="live"><span class="dot"></span> { tr(locale, "read_only") }</span>
<button class="theme-toggle" type="button" data-theme-toggle data-light-label={ tr(locale, "light_mode") } data-dark-label={ tr(locale, "dark_mode") } aria-label={ tr(locale, "dark_mode") } aria-pressed="false"><span data-theme-icon aria-hidden="true">☾</span></button>
<span class="language-switch" aria-label="Language">
if locale == LocaleEnglish {
<a class="active" href={ templ.URL(englishURL) } hx-boost="false" lang="en">EN</a>
} else {
<a href={ templ.URL(englishURL) } hx-boost="false" lang="en">EN</a>
}
if locale == LocalePersian {
<a class="active" href={ templ.URL(persianURL) } hx-boost="false" lang="fa">فا</a>
} else {
<a href={ templ.URL(persianURL) } hx-boost="false" lang="fa">فا</a>
}
</span>
</nav>
</header>
@content
<footer class="shell">
<span>{ tr(locale, "footer_description") }</span>
<span>{ tr(locale, "timezone_notice") }</span>
</footer>
</body>
</html>
}
templ TransactionSearch(value string, locale Locale) {
<form class="search" action="/transactions" method="get" hx-get="/transactions" hx-target="#explorer-content" hx-push-url="true">
<input name="q" value={ value } placeholder={ tr(locale, "transaction_placeholder") } aria-label={ tr(locale, "transaction_hash") } autocomplete="off" required/>
<button type="submit"><span class="search-label">{ tr(locale, "explore_transaction") }</span></button>
</form>
}
templ DashboardContent(data explorer.Dashboard, locale Locale) {
<main id="explorer-content" class="shell">
<section>
<div class="eyebrow">{ tr(locale, "general_ledger_explorer") }</div>
<h1>{ tr(locale, "hero_line_one") }<br/>{ tr(locale, "hero_line_two") }</h1>
<p class="lede">{ tr(locale, "hero_description") }</p>
@TransactionSearch("", locale)
</section>
<section class="stats" aria-label={ tr(locale, "general_ledger_explorer") }>
<div class="stat accent"><span class="stat-label">{ tr(locale, "committed_journals") }</span><strong>{ count(data.Stats.JournalCount) }</strong></div>
<div class="stat"><span class="stat-label">{ tr(locale, "ledger_entries") }</span><strong>{ count(data.Stats.EntryCount) }</strong></div>
<div class="stat"><span class="stat-label">{ tr(locale, "tracked_accounts") }</span><strong>{ count(data.Stats.AccountCount) }</strong></div>
<div class="stat"><span class="stat-label">{ tr(locale, "last_recorded") }</span><time>{ formatTime(data.Stats.LastRecordedAt) }</time></div>
</section>
<section>
<div class="section-head"><h2>{ tr(locale, "latest_activity") }</h2><span>{ tr(locale, "newest_first") }</span></div>
@JournalTable(data.Journals, locale)
</section>
</main>
}
templ AssetsContent(data explorer.Assets, locale Locale) {
<main id="explorer-content" class="shell">
<div class="breadcrumb"><a href="/">{ tr(locale, "network") }</a> / { tr(locale, "assets") }</div>
<section class="page-title">
<div class="eyebrow">{ tr(locale, "assets_explorer") }</div>
<h1>{ tr(locale, "track_asset_ownership") }</h1>
<p class="lede">{ tr(locale, "assets_description") }</p>
</section>
<section class="dashboard-section">
<div class="section-head"><h2>{ tr(locale, "top_asset_holders") }</h2><span>{ tr(locale, "holder_balance_scope") }</span></div>
@HolderTable(data.TopHolders, locale)
</section>
</main>
}
templ HolderTable(holders []explorer.Holder, locale Locale) {
<div class="panel">
if len(holders) == 0 {
<div class="empty">{ tr(locale, "no_asset_holders") }</div>
} else {
<table>
<thead><tr><th>{ tr(locale, "asset") }</th><th>{ tr(locale, "rank") }</th><th>{ tr(locale, "holder") }</th><th>{ tr(locale, "balance") }</th></tr></thead>
<tbody>
for _, holder := range holders {
<tr>
<td><span class="pill">{ tr(locale, "asset") } { strconv.FormatInt(holder.AssetID, 10) }</span></td>
<td class="mono">#{ strconv.FormatInt(holder.Rank, 10) }</td>
<td class="holder-id mono"><a class="hash-link" href={ templ.URL(holderURL(holder)) }>{ holder.OwnerID }</a><small>{ holder.OwnerType }</small></td>
<td class="amount positive">{ holder.Balance.String() }</td>
</tr>
}
</tbody>
</table>
}
</div>
}
templ HolderContent(data HolderPageData, locale Locale) {
<main id="explorer-content" class="shell">
<div class="breadcrumb"><a href="/">{ tr(locale, "network") }</a> / <a href="/assets">{ tr(locale, "assets") }</a> / { tr(locale, "holder") }</div>
<section class="page-title">
<div class="eyebrow">{ tr(locale, "holder_account") }</div>
<h1>{ tr(locale, "inspect_holder") }</h1>
<p class="lede">{ tr(locale, "holder_description") }</p>
</section>
if data.Error != "" {
<div class="notice"><strong>{ tr(locale, "account_unavailable") }</strong>{ data.Error }</div>
} else if data.Account != nil {
<div class="account-card result-stack">
<div><span class="stat-label">{ tr(locale, "holder") }</span><h2 class="mono">{ data.Account.Reference.OwnerID }</h2><p class="mono">{ data.Account.Reference.OwnerType } · { tr(locale, "asset") } { strconv.FormatInt(data.Account.Reference.AssetID, 10) }</p></div>
<div><span class="stat-label">{ tr(locale, "combined_balance") }</span><div class="balance">{ data.Account.Balance.String() }</div></div>
</div>
<section class="dashboard-section">
<div class="section-head"><h2>{ tr(locale, "account_activity") }</h2><span>{ tr(locale, "available_and_frozen") }</span></div>
@JournalTable(data.Account.Journals, locale)
</section>
}
</main>
}
templ JournalTable(journals []ledger.Journal, locale Locale) {
<div class="panel">
if len(journals) == 0 {
<div class="empty">{ tr(locale, "no_journals") }</div>
} else {
<table>
<thead><tr><th>{ tr(locale, "transaction") }</th><th>{ tr(locale, "effect") }</th><th>{ tr(locale, "source") }</th><th>{ tr(locale, "entries") }</th><th>{ tr(locale, "recorded") }</th></tr></thead>
<tbody>
for _, journal := range journals {
<tr>
<td class="mono">
<a class="hash-link" href={ templ.URL(transactionURL(transactionReference(journal))) }>{ transactionName(journal, locale) }</a>
</td>
<td><span class="pill">{ journal.EffectKind }</span></td>
<td>{ journal.SourceService }</td>
<td>{ strconv.Itoa(len(journal.Entries)) }</td>
<td class="mono">{ formatTime(journal.RecordedAt) }</td>
</tr>
}
</tbody>
</table>
}
</div>
}
templ TransactionContent(data TransactionPageData, locale Locale) {
<main id="explorer-content" class="shell">
<div class="breadcrumb"><a href="/">{ tr(locale, "network") }</a> / { tr(locale, "transactions") }</div>
<section class="page-title">
<div class="eyebrow">{ tr(locale, "transaction_explorer") }</div>
<h1>{ tr(locale, "trace_settlement") }</h1>
<p class="lede">{ tr(locale, "transaction_description") }</p>
@TransactionSearch(data.Query, locale)
</section>
if data.Error != "" {
<div class="notice"><strong>{ tr(locale, "transaction_not_found") }</strong>{ data.Error }</div>
}
if data.Query != "" && data.Error == "" {
<div class="result-stack">
for _, journal := range data.Journals {
@JournalCard(journal, locale)
}
</div>
}
if data.Listing != nil {
<section class="transaction-list">
<div class="section-head"><h2>{ tr(locale, "latest_transactions") }</h2><span>{ tr(locale, "page") } { strconv.Itoa(data.Listing.Filter.Page) }</span></div>
<form class="transaction-filters" action="/transactions" method="get" hx-get="/transactions" hx-target="#explorer-content" hx-push-url="true">
<div class="field"><label for="wallet">{ tr(locale, "user_wallet") }</label><input id="wallet" name="wallet" value={ data.Listing.Filter.Wallet } placeholder={ tr(locale, "user_wallet_placeholder") } autocomplete="off"/></div>
<div class="field"><label for="effect">{ tr(locale, "effect_type") }</label><input id="effect" name="effect" value={ data.Listing.Filter.EffectKind } placeholder={ tr(locale, "effect_type_placeholder") } autocomplete="off"/></div>
<button class="button" type="submit">{ tr(locale, "apply_filters") }</button>
<a class="clear-filter" href="/transactions">{ tr(locale, "clear_filters") }</a>
</form>
@JournalTable(data.Listing.Journals, locale)
@Pagination(*data.Listing, locale)
</section>
}
</main>
}
templ Pagination(listing explorer.TransactionListing, locale Locale) {
<nav class="pagination" aria-label={ tr(locale, "pagination") }>
if listing.HasPrevious {
<a href={ templ.URL(transactionPageURL(listing.Filter, listing.Filter.Page - 1)) } rel="prev">{ tr(locale, "previous") }</a>
} else {
<span class="disabled">{ tr(locale, "previous") }</span>
}
<span class="current" aria-current="page">{ strconv.Itoa(listing.Filter.Page) }</span>
if listing.HasNext {
<a href={ templ.URL(transactionPageURL(listing.Filter, listing.Filter.Page + 1)) } rel="next">{ tr(locale, "next") }</a>
} else {
<span class="disabled">{ tr(locale, "next") }</span>
}
</nav>
}
templ JournalCard(journal ledger.Journal, locale Locale) {
<article class="journal">
<header class="journal-head">
<div>
if journal.Blockchain.TransactionHash != "" {
<span class="stat-label">{ tr(locale, "transaction_hash") }</span>
} else {
<span class="stat-label">{ tr(locale, "journal_id") }</span>
}
<h2 class="mono">{ transactionReference(journal) }</h2>
</div>
<span class="status">{ tr(locale, "committed") }</span>
</header>
<div class="detail-grid">
<div class="detail"><label>{ tr(locale, "effect") }</label><div><span class="pill">{ journal.EffectKind }</span></div></div>
<div class="detail"><label>{ tr(locale, "journal_id") }</label><div class="mono">{ journal.ID }</div></div>
<div class="detail"><label>{ tr(locale, "recorded") }</label><div>{ formatTime(journal.RecordedAt) }</div></div>
<div class="detail"><label>{ tr(locale, "source") }</label><div>{ journal.SourceService } · { journal.SourceTransactionID }</div></div>
<div class="detail"><label>{ tr(locale, "network") }</label><div>{ journal.Blockchain.Network }</div></div>
<div class="detail"><label>{ tr(locale, "ledger_sequence") }</label><div class="mono">{ journal.Blockchain.LedgerSequence }</div></div>
</div>
<div class="entries">
<h3>{ tr(locale, "balanced_entries") }</h3>
for _, entry := range journal.Entries {
<div class="entry">
<span class="entry-number">#{ strconv.FormatUint(uint64(entry.LineNumber), 10) }</span>
<div class="entry-account"><a href={ templ.URL(accountURL(entry.Account)) }>{ accountName(entry.Account, locale) }</a><small>{ tr(locale, "asset") } { strconv.FormatInt(entry.Account.AssetID, 10) } · { entry.Account.OwnerType }</small></div>
if len(entry.Amount.String()) > 0 && entry.Amount.String()[0] == '-' {
<div class="amount negative">{ entry.Amount.String() }</div>
} else {
<div class="amount positive">+{ entry.Amount.String() }</div>
}
</div>
}
</div>
</article>
}
templ AccountContent(data AccountPageData, locale Locale) {
<main id="explorer-content" class="shell">
<div class="breadcrumb"><a href="/">{ tr(locale, "network") }</a> / { tr(locale, "accounts") }</div>
<section class="page-title">
<div class="eyebrow">{ tr(locale, "account_explorer") }</div>
<h1>{ tr(locale, "rebuild_account") }</h1>
<p class="lede">{ tr(locale, "account_description") }</p>
</section>
<form class="account-form" action="/accounts" method="get" hx-get="/accounts" hx-target="#explorer-content" hx-push-url="true">
<div class="field"><label for="class">{ tr(locale, "account_class") }</label><select id="class" name="class" required>
for _, class := range accountClasses() {
if string(class) == data.Input.Class {
<option value={ string(class) } selected>{ string(class) }</option>
} else {
<option value={ string(class) }>{ string(class) }</option>
}
}
</select></div>
<div class="field"><label for="owner_type">{ tr(locale, "owner_type") }</label><input id="owner_type" name="owner_type" value={ data.Input.OwnerType } placeholder="user"/></div>
<div class="field"><label for="owner_id">{ tr(locale, "owner_id") }</label><input id="owner_id" name="owner_id" value={ data.Input.OwnerID } placeholder={ tr(locale, "stable_owner_id") }/></div>
<div class="field"><label for="asset_id">{ tr(locale, "asset_id") }</label><input id="asset_id" name="asset_id" value={ data.Input.AssetID } inputmode="numeric" placeholder="1" required/></div>
<button class="button" type="submit">{ tr(locale, "explore") }</button>
</form>
if data.Error != "" {
<div class="notice"><strong>{ tr(locale, "account_unavailable") }</strong>{ data.Error }</div>
} else if data.Account != nil {
<div class="account-card">
<div><span class="stat-label">{ tr(locale, "ledger_identity") }</span><h2>{ accountName(data.Account.Reference, locale) }</h2><p class="mono">{ data.Account.Reference.OwnerType } · { tr(locale, "asset") } { strconv.FormatInt(data.Account.Reference.AssetID, 10) }</p></div>
<div><span class="stat-label">{ tr(locale, "current_balance") }</span><div class="balance">{ data.Account.Balance.String() }</div></div>
</div>
<div class="section-head"><h2>{ tr(locale, "account_activity") }</h2><span>{ strconv.Itoa(len(data.Account.Journals)) } { tr(locale, "journals") }</span></div>
@JournalTable(data.Account.Journals, locale)
} else {
<div class="panel empty">{ tr(locale, "choose_account") }</div>
}
</main>
}
templ FailureContent(title string, message string, locale Locale) {
<main id="explorer-content" class="shell">
<section class="page-title"><div class="eyebrow">{ tr(locale, "explorer_error") }</div><h1>{ title }</h1><p class="lede">{ message }</p></section>
</main>
}