mirror of
https://codeberg.org/likwid/likwid.git
synced 2026-02-09 21:13:09 +00:00
ui: refresh delegations page
This commit is contained in:
parent
5f52ddb94f
commit
365e205be7
2 changed files with 637 additions and 229 deletions
|
|
@ -37,6 +37,14 @@ const { userId, communityId, compact = false } = Astro.props;
|
||||||
|
|
||||||
<div id="delegation-tree" class="tree-view" style="display: none;"></div>
|
<div id="delegation-tree" class="tree-view" style="display: none;"></div>
|
||||||
<div id="delegation-list" class="list-view" style="display: none;"></div>
|
<div id="delegation-list" class="list-view" style="display: none;"></div>
|
||||||
|
|
||||||
|
<div class="inspector" style="display: none;">
|
||||||
|
<div class="inspector-header">
|
||||||
|
<div class="inspector-title">Selection</div>
|
||||||
|
<button type="button" class="inspector-close" aria-label="Close selection">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="inspector-body"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="graph-legend">
|
<div class="graph-legend">
|
||||||
|
|
@ -65,6 +73,9 @@ const { userId, communityId, compact = false } = Astro.props;
|
||||||
this.outgoing = [];
|
this.outgoing = [];
|
||||||
this.incoming = [];
|
this.incoming = [];
|
||||||
this.currentView = 'tree';
|
this.currentView = 'tree';
|
||||||
|
this.inspector = this.container.querySelector('.inspector');
|
||||||
|
this.inspectorBody = this.container.querySelector('.inspector-body');
|
||||||
|
this.treeClickHandler = null;
|
||||||
this.setupControls();
|
this.setupControls();
|
||||||
this.loadData();
|
this.loadData();
|
||||||
|
|
||||||
|
|
@ -82,6 +93,11 @@ const { userId, communityId, compact = false } = Astro.props;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const closeBtn = this.container.querySelector('.inspector-close');
|
||||||
|
closeBtn?.addEventListener('click', () => {
|
||||||
|
this.hideInspector();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
switchView(view) {
|
switchView(view) {
|
||||||
|
|
@ -125,6 +141,31 @@ const { userId, communityId, compact = false } = Astro.props;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
escapeHtml(value) {
|
||||||
|
return String(value || '').replace(/[&<>"']/g, function(ch) {
|
||||||
|
switch (ch) {
|
||||||
|
case '&': return '&';
|
||||||
|
case '<': return '<';
|
||||||
|
case '>': return '>';
|
||||||
|
case '"': return '"';
|
||||||
|
case "'": return ''';
|
||||||
|
default: return ch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showInspector(html) {
|
||||||
|
if (!this.inspector || !this.inspectorBody) return;
|
||||||
|
this.inspectorBody.innerHTML = html;
|
||||||
|
this.inspector.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
hideInspector() {
|
||||||
|
if (!this.inspector || !this.inspectorBody) return;
|
||||||
|
this.inspectorBody.innerHTML = '';
|
||||||
|
this.inspector.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
showError(message) {
|
showError(message) {
|
||||||
const loading = this.container.querySelector('#delegation-loading');
|
const loading = this.container.querySelector('#delegation-loading');
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|
@ -136,6 +177,8 @@ const { userId, communityId, compact = false } = Astro.props;
|
||||||
const loading = this.container.querySelector('#delegation-loading');
|
const loading = this.container.querySelector('#delegation-loading');
|
||||||
if (loading) loading.style.display = 'none';
|
if (loading) loading.style.display = 'none';
|
||||||
|
|
||||||
|
this.hideInspector();
|
||||||
|
|
||||||
this.renderTreeView();
|
this.renderTreeView();
|
||||||
this.renderListView();
|
this.renderListView();
|
||||||
this.switchView(this.currentView);
|
this.switchView(this.currentView);
|
||||||
|
|
@ -154,7 +197,7 @@ const { userId, communityId, compact = false } = Astro.props;
|
||||||
<svg class="icon icon-xl"><use href="#icon-delegation"/></svg>
|
<svg class="icon icon-xl"><use href="#icon-delegation"/></svg>
|
||||||
<h4>No Active Delegations</h4>
|
<h4>No Active Delegations</h4>
|
||||||
<p>You haven't delegated your vote to anyone, and no one has delegated to you.</p>
|
<p>You haven't delegated your vote to anyone, and no one has delegated to you.</p>
|
||||||
<a href="/delegations" class="btn-primary">Explore Delegates</a>
|
<a href="/delegations" class="btn-primary ui-btn ui-btn-primary">Explore Delegates</a>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
|
|
@ -171,7 +214,7 @@ const { userId, communityId, compact = false } = Astro.props;
|
||||||
<span>${this.incoming.length} delegate${this.incoming.length > 1 ? 's' : ''} to you</span>
|
<span>${this.incoming.length} delegate${this.incoming.length > 1 ? 's' : ''} to you</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="delegation-nodes">
|
<div class="delegation-nodes">
|
||||||
${this.incoming.map(d => this.renderNode(d, 'incoming')).join('')}
|
${this.incoming.map((d, idx) => this.renderNode(d, 'incoming', idx)).join('')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -198,7 +241,7 @@ const { userId, communityId, compact = false } = Astro.props;
|
||||||
<span>You delegate to ${this.outgoing.length}</span>
|
<span>You delegate to ${this.outgoing.length}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="delegation-nodes">
|
<div class="delegation-nodes">
|
||||||
${this.outgoing.map(d => this.renderNode(d, 'outgoing')).join('')}
|
${this.outgoing.map((d, idx) => this.renderNode(d, 'outgoing', idx)).join('')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -206,16 +249,83 @@ const { userId, communityId, compact = false } = Astro.props;
|
||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
treeView.innerHTML = html;
|
treeView.innerHTML = html;
|
||||||
|
|
||||||
|
if (this.treeClickHandler) {
|
||||||
|
treeView.removeEventListener('click', this.treeClickHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.treeClickHandler = (e) => {
|
||||||
|
const target = e.target;
|
||||||
|
if (!(target instanceof Element)) return;
|
||||||
|
const node = target.closest('.delegation-node');
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
const direction = node.getAttribute('data-direction') || '';
|
||||||
|
const idxStr = node.getAttribute('data-index') || '';
|
||||||
|
const idx = Number(idxStr);
|
||||||
|
if (Number.isNaN(idx)) return;
|
||||||
|
|
||||||
|
const source = direction === 'incoming' ? this.incoming : this.outgoing;
|
||||||
|
const d = source[idx];
|
||||||
|
if (!d) return;
|
||||||
|
|
||||||
|
const name = this.escapeHtml(d.delegate_username || d.delegator_username || 'Unknown');
|
||||||
|
const scope = this.escapeHtml(this.getScopeLabel(d.scope));
|
||||||
|
const weightPct = Math.round((Number(d.weight) || 1) * 100);
|
||||||
|
const status = d.is_active ? 'Active' : 'Inactive';
|
||||||
|
const createdAt = d.created_at ? new Date(d.created_at).toLocaleString() : '';
|
||||||
|
|
||||||
|
this.showInspector(`
|
||||||
|
<div class="inspector-grid">
|
||||||
|
<div class="inspector-kv">
|
||||||
|
<div class="k">User</div>
|
||||||
|
<div class="v">${name}</div>
|
||||||
|
</div>
|
||||||
|
<div class="inspector-kv">
|
||||||
|
<div class="k">Direction</div>
|
||||||
|
<div class="v">${this.escapeHtml(direction)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="inspector-kv">
|
||||||
|
<div class="k">Scope</div>
|
||||||
|
<div class="v">${scope}</div>
|
||||||
|
</div>
|
||||||
|
<div class="inspector-kv">
|
||||||
|
<div class="k">Weight</div>
|
||||||
|
<div class="v">${weightPct}%</div>
|
||||||
|
</div>
|
||||||
|
<div class="inspector-kv">
|
||||||
|
<div class="k">Status</div>
|
||||||
|
<div class="v">${this.escapeHtml(status)}</div>
|
||||||
|
</div>
|
||||||
|
${createdAt ? `
|
||||||
|
<div class="inspector-kv">
|
||||||
|
<div class="k">Created</div>
|
||||||
|
<div class="v">${this.escapeHtml(createdAt)}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
treeView.addEventListener('click', this.treeClickHandler);
|
||||||
|
|
||||||
|
treeView.querySelectorAll('.delegation-node[tabindex="0"]').forEach((el) => {
|
||||||
|
el.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||||
|
e.preventDefault();
|
||||||
|
el.click();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNode(delegation, type) {
|
renderNode(delegation, type, index) {
|
||||||
const name = delegation.delegate_username || delegation.delegator_username || 'Unknown';
|
const name = this.escapeHtml(delegation.delegate_username || delegation.delegator_username || 'Unknown');
|
||||||
const scope = this.getScopeLabel(delegation.scope);
|
const scope = this.escapeHtml(this.getScopeLabel(delegation.scope));
|
||||||
const weight = delegation.weight || 1;
|
const weight = Number(delegation.weight) || 1;
|
||||||
const isActive = delegation.is_active;
|
const isActive = Boolean(delegation.is_active);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="delegation-node ${type} ${isActive ? '' : 'inactive'}">
|
<div class="delegation-node ${type} ${isActive ? '' : 'inactive'}" data-direction="${this.escapeHtml(type)}" data-index="${this.escapeHtml(index)}" tabindex="0" role="button" aria-label="Inspect delegation">
|
||||||
<div class="node-avatar">
|
<div class="node-avatar">
|
||||||
<svg class="icon"><use href="#icon-user"/></svg>
|
<svg class="icon"><use href="#icon-user"/></svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -258,23 +368,33 @@ const { userId, communityId, compact = false } = Astro.props;
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${allDelegations.map(d => `
|
${allDelegations.map(d => {
|
||||||
<tr class="${d.is_active ? '' : 'inactive'}">
|
const direction = String(d.direction || '').toLowerCase();
|
||||||
<td>
|
const safeDirection = direction === 'outgoing' ? 'outgoing' : 'incoming';
|
||||||
<span class="direction-badge ${d.direction}">
|
const user = this.escapeHtml(d.delegate_username || d.delegator_username || 'Unknown');
|
||||||
${d.direction === 'outgoing' ? '→ To' : '← From'}
|
const scope = this.escapeHtml(this.getScopeLabel(d.scope));
|
||||||
</span>
|
const weightPct = ((Number(d.weight) || 1) * 100).toFixed(0);
|
||||||
</td>
|
const statusLabel = d.is_active ? 'Active' : 'Revoked';
|
||||||
<td>${d.delegate_username || d.delegator_username || 'Unknown'}</td>
|
const dateLabel = d.created_at ? new Date(d.created_at).toLocaleDateString() : '';
|
||||||
<td>${this.getScopeLabel(d.scope)}</td>
|
|
||||||
<td>${((d.weight || 1) * 100).toFixed(0)}%</td>
|
return `
|
||||||
<td>
|
<tr class="${d.is_active ? '' : 'inactive'}">
|
||||||
<span class="status-dot ${d.is_active ? 'active' : 'inactive'}"></span>
|
<td>
|
||||||
${d.is_active ? 'Active' : 'Revoked'}
|
<span class="direction-badge ${safeDirection}">
|
||||||
</td>
|
${safeDirection === 'outgoing' ? '→ To' : '← From'}
|
||||||
<td>${new Date(d.created_at).toLocaleDateString()}</td>
|
</span>
|
||||||
</tr>
|
</td>
|
||||||
`).join('')}
|
<td>${user}</td>
|
||||||
|
<td>${scope}</td>
|
||||||
|
<td>${this.escapeHtml(weightPct)}%</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-dot ${d.is_active ? 'active' : 'inactive'}"></span>
|
||||||
|
${this.escapeHtml(statusLabel)}
|
||||||
|
</td>
|
||||||
|
<td>${this.escapeHtml(dateLabel)}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
`;
|
`;
|
||||||
|
|
@ -304,6 +424,87 @@ const { userId, communityId, compact = false } = Astro.props;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.graph-content {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
bottom: 0.75rem;
|
||||||
|
width: min(360px, calc(100% - 1.5rem));
|
||||||
|
background: rgba(15, 23, 42, 0.92);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 0.85rem;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-title {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-close {
|
||||||
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-close:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.28);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-kv {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0.55rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-kv .k {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
margin-bottom: 0.15rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-kv .v {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.graph-header {
|
.graph-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
|
||||||
|
|
@ -5,143 +5,226 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Delegations - Likwid">
|
<Layout title="Delegations - Likwid">
|
||||||
<div class="container">
|
<section class="ui-page">
|
||||||
<header class="page-header">
|
<div class="ui-container">
|
||||||
<h1>Vote Delegations</h1>
|
<div class="hero ui-card ui-card-glass">
|
||||||
<p class="subtitle">Delegate your voting power to trusted members</p>
|
<div class="hero-top">
|
||||||
</header>
|
<div>
|
||||||
|
<h1 class="hero-title">Vote Delegations</h1>
|
||||||
<div class="auth-required" id="auth-check">
|
<p class="hero-subtitle">Delegate your voting power to trusted members</p>
|
||||||
<p>Please <a href="/login">log in</a> to manage delegations.</p>
|
</div>
|
||||||
</div>
|
<div class="hero-actions">
|
||||||
|
<a href="/communities" class="ui-btn ui-btn-secondary">Browse</a>
|
||||||
<div class="delegations-content" id="delegations-content" style="display: none;">
|
<button class="ui-btn ui-btn-primary" id="new-delegation-btn" type="button" style="display:none;">New Delegation</button>
|
||||||
<section class="graph-section">
|
</div>
|
||||||
<DelegationGraph />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="tabs">
|
|
||||||
<button class="tab active" data-tab="outgoing">My Delegations</button>
|
|
||||||
<button class="tab" data-tab="incoming">Delegated to Me</button>
|
|
||||||
<button class="tab" data-tab="delegates">Find Delegates</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- My Delegations Tab -->
|
|
||||||
<div class="tab-content active" id="tab-outgoing">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>Active Delegations</h2>
|
|
||||||
<button class="btn btn-primary" id="new-delegation-btn">New Delegation</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="delegations-list" id="my-delegations">
|
|
||||||
<p class="loading">Loading delegations...</p>
|
<div class="hero-kpis">
|
||||||
|
<div class="hero-kpis-label">At a glance</div>
|
||||||
|
<div class="ui-kpis">
|
||||||
|
<div class="ui-kpi">
|
||||||
|
<div class="ui-kpi-value" id="kpi-outgoing">—</div>
|
||||||
|
<div class="ui-kpi-label">My delegations</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui-kpi">
|
||||||
|
<div class="ui-kpi-value" id="kpi-incoming">—</div>
|
||||||
|
<div class="ui-kpi-label">Delegated to me</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui-kpi">
|
||||||
|
<div class="ui-kpi-value" id="kpi-delegates">—</div>
|
||||||
|
<div class="ui-kpi-label">Available delegates</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delegated to Me Tab -->
|
<div class="ui-card auth-required" id="auth-check">
|
||||||
<div class="tab-content" id="tab-incoming">
|
<p>Please <a href="/login">log in</a> to manage delegations.</p>
|
||||||
<h2>Votes Delegated to Me</h2>
|
</div>
|
||||||
<div class="delegations-list" id="incoming-delegations">
|
|
||||||
<p class="loading">Loading...</p>
|
<div class="delegations-content" id="delegations-content" style="display: none;">
|
||||||
|
<details class="panel ui-card" open>
|
||||||
|
<summary class="panel-summary">
|
||||||
|
<span class="panel-title">Delegation network</span>
|
||||||
|
<span class="panel-meta">Explorable view</span>
|
||||||
|
</summary>
|
||||||
|
<div class="panel-body">
|
||||||
|
<DelegationGraph />
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div class="tabs ui-card">
|
||||||
|
<button class="tab active" data-tab="outgoing" type="button">My Delegations</button>
|
||||||
|
<button class="tab" data-tab="incoming" type="button">Delegated to Me</button>
|
||||||
|
<button class="tab" data-tab="delegates" type="button">Find Delegates</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content active" id="tab-outgoing">
|
||||||
|
<div class="section-header ui-card">
|
||||||
|
<div>
|
||||||
|
<h2>Active Delegations</h2>
|
||||||
|
<p class="help-text">Your current outgoing delegations (what you delegate to others).</p>
|
||||||
|
</div>
|
||||||
|
<button class="ui-btn ui-btn-primary" id="new-delegation-btn-secondary" type="button">New Delegation</button>
|
||||||
|
</div>
|
||||||
|
<div class="delegations-list" id="my-delegations">
|
||||||
|
<p class="loading">Loading delegations...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tab-incoming">
|
||||||
|
<div class="section-header ui-card">
|
||||||
|
<div>
|
||||||
|
<h2>Votes Delegated to Me</h2>
|
||||||
|
<p class="help-text">Incoming delegations (people who trust you to vote on their behalf).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="delegations-list" id="incoming-delegations">
|
||||||
|
<p class="loading">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tab-delegates">
|
||||||
|
<div class="section-header ui-card">
|
||||||
|
<div>
|
||||||
|
<h2>Available Delegates</h2>
|
||||||
|
<p class="help-text">Find trusted community members to delegate your votes to.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="delegates-list" id="delegates-list">
|
||||||
|
<p class="loading">Loading delegates...</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Find Delegates Tab -->
|
<div class="modal" id="delegation-modal">
|
||||||
<div class="tab-content" id="tab-delegates">
|
<div class="modal-content ui-card">
|
||||||
<h2>Available Delegates</h2>
|
<div class="modal-header">
|
||||||
<p class="help-text">Find trusted community members to delegate your votes to.</p>
|
<h2>Create Delegation</h2>
|
||||||
<div class="delegates-list" id="delegates-list">
|
<button type="button" class="ui-btn ui-btn-secondary" id="cancel-delegation">Close</button>
|
||||||
<p class="loading">Loading delegates...</p>
|
</div>
|
||||||
|
<form id="delegation-form">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="delegate-select">Delegate to</label>
|
||||||
|
<select id="delegate-select" required class="ui-select">
|
||||||
|
<option value="">Select a delegate...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="scope-select">Scope</label>
|
||||||
|
<select id="scope-select" required class="ui-select">
|
||||||
|
<option value="global">Global (all votes)</option>
|
||||||
|
<option value="community">Specific Community</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="community-select-group" style="display: none;">
|
||||||
|
<label for="community-select">Community</label>
|
||||||
|
<select id="community-select" class="ui-select">
|
||||||
|
<option value="">Select community...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="weight-input">Delegation weight</label>
|
||||||
|
<input id="weight-input" type="range" min="0.1" max="1" step="0.1" value="1" />
|
||||||
|
<div class="help-text" id="weight-label">100% of your voting power</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="ui-btn ui-btn-primary">Create Delegation</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
<!-- New Delegation Modal -->
|
|
||||||
<div class="modal" id="delegation-modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<h2>Create Delegation</h2>
|
|
||||||
<form id="delegation-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="delegate-select">Delegate to</label>
|
|
||||||
<select id="delegate-select" required>
|
|
||||||
<option value="">Select a delegate...</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="scope-select">Scope</label>
|
|
||||||
<select id="scope-select" required>
|
|
||||||
<option value="global">Global (all votes)</option>
|
|
||||||
<option value="community">Specific Community</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" id="community-select-group" style="display: none;">
|
|
||||||
<label for="community-select">Community</label>
|
|
||||||
<select id="community-select">
|
|
||||||
<option value="">Select community...</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="weight-input">Delegation weight</label>
|
|
||||||
<input id="weight-input" type="range" min="0.1" max="1" step="0.1" value="1" />
|
|
||||||
<div class="help-text" id="weight-label">100% of your voting power</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="button" class="btn btn-secondary" id="cancel-delegation">Cancel</button>
|
|
||||||
<button type="submit" class="btn btn-primary">Create Delegation</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.hero {
|
||||||
max-width: 900px;
|
padding: 1.25rem;
|
||||||
margin: 0 auto;
|
margin-bottom: 1rem;
|
||||||
padding: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.hero-top {
|
||||||
margin-bottom: 2rem;
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header h1 {
|
.hero-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--text-primary);
|
font-size: 2.125rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.hero-subtitle {
|
||||||
color: var(--text-secondary);
|
margin: 0.25rem 0 0;
|
||||||
margin-top: 0.5rem;
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-kpis {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-kpis-label {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-required {
|
||||||
|
padding: 1rem 1.1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-required a {
|
||||||
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-bottom: 1.5rem;
|
margin: 1rem 0;
|
||||||
border-bottom: 1px solid var(--border-color);
|
padding: 0.5rem;
|
||||||
padding-bottom: 0.5rem;
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
padding: 0.75rem 1.5rem;
|
flex: 1;
|
||||||
|
padding: 0.65rem 0.9rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: 1px solid transparent;
|
||||||
color: var(--text-secondary);
|
color: var(--color-text-muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 0.5rem 0.5rem 0 0;
|
border-radius: 12px;
|
||||||
transition: all 0.2s;
|
transition: transform 140ms ease, background 140ms ease, color 140ms ease, border-color 140ms ease;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab:hover {
|
.tab:hover {
|
||||||
color: var(--text-primary);
|
color: var(--color-text);
|
||||||
background: var(--bg-hover);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab.active {
|
.tab.active {
|
||||||
color: var(--accent-color);
|
color: var(--color-text);
|
||||||
background: var(--bg-secondary);
|
background: rgba(255, 255, 255, 0.07);
|
||||||
font-weight: 600;
|
border-color: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
|
|
@ -155,8 +238,16 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
.section-header {
|
.section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delegations-list, .delegates-list {
|
.delegations-list, .delegates-list {
|
||||||
|
|
@ -166,10 +257,17 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
}
|
}
|
||||||
|
|
||||||
.delegation-card, .delegate-card {
|
.delegation-card, .delegate-card {
|
||||||
background: var(--bg-secondary);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 0.75rem;
|
border-radius: 14px;
|
||||||
padding: 1.25rem;
|
padding: 1.1rem;
|
||||||
|
transition: transform 140ms ease, border-color 140ms ease, background 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delegation-card:hover, .delegate-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: var(--color-border-hover);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.delegation-card header {
|
.delegation-card header {
|
||||||
|
|
@ -181,26 +279,23 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
|
|
||||||
.delegation-card .delegate-name {
|
.delegation-card .delegate-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.delegation-card .scope-badge {
|
.delegation-card .scope-badge {
|
||||||
padding: 0.25rem 0.75rem;
|
padding: 0.25rem 0.75rem;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
background: var(--accent-color);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
color: white;
|
border: 1px solid var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.delegation-card .meta {
|
.delegation-card .meta {
|
||||||
color: var(--text-secondary);
|
color: var(--color-text-muted);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-section {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delegation-card .actions {
|
.delegation-card .actions {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -215,51 +310,21 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
|
|
||||||
.delegate-info h3 {
|
.delegate-info h3 {
|
||||||
margin: 0 0 0.25rem 0;
|
margin: 0 0 0.25rem 0;
|
||||||
color: var(--text-primary);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.delegate-info .bio {
|
.delegate-info .bio {
|
||||||
color: var(--text-secondary);
|
color: var(--color-text-muted);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delegate-stats {
|
.delegate-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
color: var(--text-secondary);
|
color: var(--color-text-muted);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: var(--accent-color);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: #dc3545;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
@ -268,6 +333,7 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal.active {
|
.modal.active {
|
||||||
|
|
@ -275,13 +341,26 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: var(--bg-primary);
|
padding: 1.25rem;
|
||||||
padding: 2rem;
|
border-radius: 16px;
|
||||||
border-radius: 1rem;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
@ -292,29 +371,25 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group select, .form-group input {
|
.form-grid {
|
||||||
width: 100%;
|
display: grid;
|
||||||
padding: 0.75rem;
|
grid-template-columns: 1fr;
|
||||||
border: 1px solid var(--border-color);
|
gap: 0.75rem;
|
||||||
border-radius: 0.5rem;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-actions {
|
.form-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: 1.5rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.help-text {
|
.help-text {
|
||||||
color: var(--text-secondary);
|
color: var(--color-text-muted);
|
||||||
margin-bottom: 1rem;
|
margin: 0.35rem 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
color: var(--text-secondary);
|
color: var(--color-text-muted);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
@ -322,7 +397,55 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 3rem;
|
padding: 3rem;
|
||||||
color: var(--text-secondary);
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-summary {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 1rem 1.1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-meta {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-body {
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
padding: 1rem 1.1rem 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.delegate-card {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delegate-stats {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
@ -330,6 +453,41 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
const API_URL = apiBase;
|
const API_URL = apiBase;
|
||||||
let token = localStorage.getItem('token');
|
let token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value || '').replace(/[&<>"']/g, function(ch) {
|
||||||
|
switch (ch) {
|
||||||
|
case '&': return '&';
|
||||||
|
case '<': return '<';
|
||||||
|
case '>': return '>';
|
||||||
|
case '"': return '"';
|
||||||
|
case "'": return ''';
|
||||||
|
default: return ch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeScope(scope) {
|
||||||
|
const s = String(scope || '').toLowerCase();
|
||||||
|
if (s === 'global' || s === 'community' || s === 'topic' || s === 'proposal') return s;
|
||||||
|
return 'global';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScopeLabel(scope) {
|
||||||
|
const s = normalizeScope(scope);
|
||||||
|
const labels = {
|
||||||
|
global: '🌐 Global',
|
||||||
|
community: '🏘️ Community',
|
||||||
|
topic: '📁 Topic',
|
||||||
|
proposal: '📄 Proposal'
|
||||||
|
};
|
||||||
|
return labels[s] || s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setText(id, value) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.textContent = String(value);
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
document.getElementById('auth-check').style.display = 'block';
|
document.getElementById('auth-check').style.display = 'block';
|
||||||
|
|
@ -339,6 +497,8 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
document.getElementById('auth-check').style.display = 'none';
|
document.getElementById('auth-check').style.display = 'none';
|
||||||
document.getElementById('delegations-content').style.display = 'block';
|
document.getElementById('delegations-content').style.display = 'block';
|
||||||
|
|
||||||
|
document.getElementById('new-delegation-btn')?.setAttribute('style', 'display:inline-flex;');
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadMyDelegations(),
|
loadMyDelegations(),
|
||||||
loadIncomingDelegations(),
|
loadIncomingDelegations(),
|
||||||
|
|
@ -363,6 +523,7 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
function setupModal() {
|
function setupModal() {
|
||||||
const modal = document.getElementById('delegation-modal');
|
const modal = document.getElementById('delegation-modal');
|
||||||
const newBtn = document.getElementById('new-delegation-btn');
|
const newBtn = document.getElementById('new-delegation-btn');
|
||||||
|
const newBtnSecondary = document.getElementById('new-delegation-btn-secondary');
|
||||||
const cancelBtn = document.getElementById('cancel-delegation');
|
const cancelBtn = document.getElementById('cancel-delegation');
|
||||||
const form = document.getElementById('delegation-form');
|
const form = document.getElementById('delegation-form');
|
||||||
const scopeSelect = document.getElementById('scope-select');
|
const scopeSelect = document.getElementById('scope-select');
|
||||||
|
|
@ -370,11 +531,14 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
const weightInput = document.getElementById('weight-input');
|
const weightInput = document.getElementById('weight-input');
|
||||||
const weightLabel = document.getElementById('weight-label');
|
const weightLabel = document.getElementById('weight-label');
|
||||||
|
|
||||||
newBtn?.addEventListener('click', async () => {
|
const openModal = async () => {
|
||||||
await loadDelegatesForSelect();
|
await loadDelegatesForSelect();
|
||||||
await loadCommunitiesForSelect();
|
await loadCommunitiesForSelect();
|
||||||
modal.classList.add('active');
|
modal.classList.add('active');
|
||||||
});
|
};
|
||||||
|
|
||||||
|
newBtn?.addEventListener('click', openModal);
|
||||||
|
newBtnSecondary?.addEventListener('click', openModal);
|
||||||
|
|
||||||
cancelBtn?.addEventListener('click', () => {
|
cancelBtn?.addEventListener('click', () => {
|
||||||
modal.classList.remove('active');
|
modal.classList.remove('active');
|
||||||
|
|
@ -402,27 +566,44 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
const delegations = await res.json();
|
const delegations = await res.json();
|
||||||
|
|
||||||
|
setText('kpi-outgoing', delegations.length);
|
||||||
|
|
||||||
if (delegations.length === 0) {
|
if (delegations.length === 0) {
|
||||||
container.innerHTML = '<div class="empty-state">No active delegations. Create one to delegate your voting power.</div>';
|
container.innerHTML = '<div class="empty-state">No active delegations. Create one to delegate your voting power.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = delegations.map(d => `
|
container.innerHTML = delegations.map(d => {
|
||||||
<div class="delegation-card">
|
const safeName = escapeHtml(d.delegate_username || 'Unknown');
|
||||||
<header>
|
const scopeLabel = escapeHtml(getScopeLabel(d.scope));
|
||||||
<span class="delegate-name">→ ${d.delegate_username || 'Unknown'}</span>
|
const weightPct = Math.round((Number(d.weight) || 1) * 100);
|
||||||
<span class="scope-badge">${d.scope}</span>
|
const createdAt = d.created_at ? new Date(d.created_at).toLocaleDateString() : '';
|
||||||
</header>
|
const safeId = escapeHtml(d.id);
|
||||||
<div class="meta">
|
|
||||||
Weight: ${Math.round((d.weight || 1) * 100)}%<br/>
|
return `
|
||||||
Created: ${new Date(d.created_at).toLocaleDateString()}
|
<div class="delegation-card">
|
||||||
|
<header>
|
||||||
|
<span class="delegate-name">→ ${safeName}</span>
|
||||||
|
<span class="scope-badge">${scopeLabel}</span>
|
||||||
|
</header>
|
||||||
|
<div class="meta">
|
||||||
|
Weight: ${weightPct}%<br/>
|
||||||
|
${createdAt ? `Created: ${escapeHtml(createdAt)}` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="ui-btn ui-btn-danger" type="button" data-revoke-id="${safeId}">Revoke</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
`;
|
||||||
<button class="btn btn-danger btn-sm" onclick="revokeDelegation('${d.id}')">Revoke</button>
|
}).join('');
|
||||||
</div>
|
|
||||||
</div>
|
container.querySelectorAll('[data-revoke-id]').forEach((btn) => {
|
||||||
`).join('');
|
btn.addEventListener('click', async () => {
|
||||||
|
const id = btn.getAttribute('data-revoke-id') || '';
|
||||||
|
await window.revokeDelegation(id);
|
||||||
|
});
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
container.innerHTML = '<div class="error">Failed to load delegations</div>';
|
container.innerHTML = '<div class="error">Failed to load delegations</div>';
|
||||||
}
|
}
|
||||||
|
|
@ -435,24 +616,33 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
const delegations = await res.json();
|
const delegations = await res.json();
|
||||||
|
|
||||||
|
setText('kpi-incoming', delegations.length);
|
||||||
|
|
||||||
if (delegations.length === 0) {
|
if (delegations.length === 0) {
|
||||||
container.innerHTML = '<div class="empty-state">No one has delegated votes to you yet.</div>';
|
container.innerHTML = '<div class="empty-state">No one has delegated votes to you yet.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = delegations.map(d => `
|
container.innerHTML = delegations.map(d => {
|
||||||
<div class="delegation-card">
|
const safeName = escapeHtml(d.delegator_username || d.delegate_username || 'Unknown');
|
||||||
<header>
|
const scopeLabel = escapeHtml(getScopeLabel(d.scope));
|
||||||
<span class="delegate-name">← ${d.delegate_username || 'Unknown'}</span>
|
const weightPct = Math.round((Number(d.weight) || 1) * 100);
|
||||||
<span class="scope-badge">${d.scope}</span>
|
const createdAt = d.created_at ? new Date(d.created_at).toLocaleDateString() : '';
|
||||||
</header>
|
|
||||||
<div class="meta">
|
return `
|
||||||
Weight: ${Math.round((d.weight || 1) * 100)}%<br/>
|
<div class="delegation-card">
|
||||||
Since: ${new Date(d.created_at).toLocaleDateString()}
|
<header>
|
||||||
|
<span class="delegate-name">← ${safeName}</span>
|
||||||
|
<span class="scope-badge">${scopeLabel}</span>
|
||||||
|
</header>
|
||||||
|
<div class="meta">
|
||||||
|
Weight: ${weightPct}%<br/>
|
||||||
|
${createdAt ? `Since: ${escapeHtml(createdAt)}` : ''}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
`;
|
||||||
`).join('');
|
}).join('');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
container.innerHTML = '<div class="error">Failed to load incoming delegations</div>';
|
container.innerHTML = '<div class="error">Failed to load incoming delegations</div>';
|
||||||
}
|
}
|
||||||
|
|
@ -463,24 +653,33 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/delegates`);
|
const res = await fetch(`${API_URL}/api/delegates`);
|
||||||
const delegates = await res.json();
|
const delegates = await res.json();
|
||||||
|
|
||||||
|
setText('kpi-delegates', delegates.length);
|
||||||
|
|
||||||
if (delegates.length === 0) {
|
if (delegates.length === 0) {
|
||||||
container.innerHTML = '<div class="empty-state">No delegates available yet.</div>';
|
container.innerHTML = '<div class="empty-state">No delegates available yet.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = delegates.map(d => `
|
container.innerHTML = delegates.map(d => {
|
||||||
<div class="delegate-card">
|
const safeName = escapeHtml(d.display_name || d.username || 'Unknown');
|
||||||
<div class="delegate-info">
|
const safeBio = escapeHtml(d.bio || 'No bio provided');
|
||||||
<h3>${d.display_name || d.username}</h3>
|
const delegators = Number(d.total_delegators) || 0;
|
||||||
<p class="bio">${d.bio || 'No bio provided'}</p>
|
const votes = Number(d.total_votes_cast) || 0;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="delegate-card">
|
||||||
|
<div class="delegate-info">
|
||||||
|
<h3>${safeName}</h3>
|
||||||
|
<p class="bio">${safeBio}</p>
|
||||||
|
</div>
|
||||||
|
<div class="delegate-stats">
|
||||||
|
<span>${delegators} delegators</span>
|
||||||
|
<span>${votes} votes cast</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="delegate-stats">
|
`;
|
||||||
<span>${d.total_delegators} delegators</span>
|
}).join('');
|
||||||
<span>${d.total_votes_cast} votes cast</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
container.innerHTML = '<div class="error">Failed to load delegates</div>';
|
container.innerHTML = '<div class="error">Failed to load delegates</div>';
|
||||||
}
|
}
|
||||||
|
|
@ -492,7 +691,11 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
const res = await fetch(`${API_URL}/api/delegates`);
|
const res = await fetch(`${API_URL}/api/delegates`);
|
||||||
const delegates = await res.json();
|
const delegates = await res.json();
|
||||||
select.innerHTML = '<option value="">Select a delegate...</option>' +
|
select.innerHTML = '<option value="">Select a delegate...</option>' +
|
||||||
delegates.map(d => `<option value="${d.user_id}">${d.display_name || d.username}</option>`).join('');
|
delegates.map(d => {
|
||||||
|
const value = escapeHtml(d.user_id);
|
||||||
|
const label = escapeHtml(d.display_name || d.username || 'Unknown');
|
||||||
|
return `<option value="${value}">${label}</option>`;
|
||||||
|
}).join('');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load delegates for select');
|
console.error('Failed to load delegates for select');
|
||||||
}
|
}
|
||||||
|
|
@ -504,7 +707,11 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
const res = await fetch(`${API_URL}/api/communities`);
|
const res = await fetch(`${API_URL}/api/communities`);
|
||||||
const communities = await res.json();
|
const communities = await res.json();
|
||||||
select.innerHTML = '<option value="">Select community...</option>' +
|
select.innerHTML = '<option value="">Select community...</option>' +
|
||||||
communities.map(c => `<option value="${c.id}">${c.name}</option>`).join('');
|
communities.map(c => {
|
||||||
|
const value = escapeHtml(c.id);
|
||||||
|
const label = escapeHtml(c.name);
|
||||||
|
return `<option value="${value}">${label}</option>`;
|
||||||
|
}).join('');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load communities');
|
console.error('Failed to load communities');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue