diff --git a/frontend/src/components/voting/DelegationGraph.astro b/frontend/src/components/voting/DelegationGraph.astro index ebdc55c..cf78aae 100644 --- a/frontend/src/components/voting/DelegationGraph.astro +++ b/frontend/src/components/voting/DelegationGraph.astro @@ -37,6 +37,14 @@ const { userId, communityId, compact = false } = Astro.props; + +
@@ -65,6 +73,9 @@ const { userId, communityId, compact = false } = Astro.props; this.outgoing = []; this.incoming = []; this.currentView = 'tree'; + this.inspector = this.container.querySelector('.inspector'); + this.inspectorBody = this.container.querySelector('.inspector-body'); + this.treeClickHandler = null; this.setupControls(); 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) { @@ -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) { const loading = this.container.querySelector('#delegation-loading'); if (loading) { @@ -136,6 +177,8 @@ const { userId, communityId, compact = false } = Astro.props; const loading = this.container.querySelector('#delegation-loading'); if (loading) loading.style.display = 'none'; + this.hideInspector(); + this.renderTreeView(); this.renderListView(); this.switchView(this.currentView); @@ -154,7 +197,7 @@ const { userId, communityId, compact = false } = Astro.props;

No Active Delegations

You haven't delegated your vote to anyone, and no one has delegated to you.

- Explore Delegates + Explore Delegates
`; return; @@ -171,7 +214,7 @@ const { userId, communityId, compact = false } = Astro.props; ${this.incoming.length} delegate${this.incoming.length > 1 ? 's' : ''} to you
- ${this.incoming.map(d => this.renderNode(d, 'incoming')).join('')} + ${this.incoming.map((d, idx) => this.renderNode(d, 'incoming', idx)).join('')}
`; @@ -198,7 +241,7 @@ const { userId, communityId, compact = false } = Astro.props; You delegate to ${this.outgoing.length}
- ${this.outgoing.map(d => this.renderNode(d, 'outgoing')).join('')} + ${this.outgoing.map((d, idx) => this.renderNode(d, 'outgoing', idx)).join('')}
`; @@ -206,16 +249,83 @@ const { userId, communityId, compact = false } = Astro.props; 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(` +
+
+
User
+
${name}
+
+
+
Direction
+
${this.escapeHtml(direction)}
+
+
+
Scope
+
${scope}
+
+
+
Weight
+
${weightPct}%
+
+
+
Status
+
${this.escapeHtml(status)}
+
+ ${createdAt ? ` +
+
Created
+
${this.escapeHtml(createdAt)}
+
+ ` : ''} +
+ `); + }; + + 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) { - const name = delegation.delegate_username || delegation.delegator_username || 'Unknown'; - const scope = this.getScopeLabel(delegation.scope); - const weight = delegation.weight || 1; - const isActive = delegation.is_active; + renderNode(delegation, type, index) { + const name = this.escapeHtml(delegation.delegate_username || delegation.delegator_username || 'Unknown'); + const scope = this.escapeHtml(this.getScopeLabel(delegation.scope)); + const weight = Number(delegation.weight) || 1; + const isActive = Boolean(delegation.is_active); return ` -
+
@@ -258,23 +368,33 @@ const { userId, communityId, compact = false } = Astro.props; - ${allDelegations.map(d => ` - - - - ${d.direction === 'outgoing' ? '→ To' : '← From'} - - - ${d.delegate_username || d.delegator_username || 'Unknown'} - ${this.getScopeLabel(d.scope)} - ${((d.weight || 1) * 100).toFixed(0)}% - - - ${d.is_active ? 'Active' : 'Revoked'} - - ${new Date(d.created_at).toLocaleDateString()} - - `).join('')} + ${allDelegations.map(d => { + const direction = String(d.direction || '').toLowerCase(); + const safeDirection = direction === 'outgoing' ? 'outgoing' : 'incoming'; + const user = this.escapeHtml(d.delegate_username || d.delegator_username || 'Unknown'); + const scope = this.escapeHtml(this.getScopeLabel(d.scope)); + const weightPct = ((Number(d.weight) || 1) * 100).toFixed(0); + const statusLabel = d.is_active ? 'Active' : 'Revoked'; + const dateLabel = d.created_at ? new Date(d.created_at).toLocaleDateString() : ''; + + return ` + + + + ${safeDirection === 'outgoing' ? '→ To' : '← From'} + + + ${user} + ${scope} + ${this.escapeHtml(weightPct)}% + + + ${this.escapeHtml(statusLabel)} + + ${this.escapeHtml(dateLabel)} + + `; + }).join('')} `; @@ -304,6 +424,87 @@ const { userId, communityId, compact = false } = Astro.props; 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 { display: flex; justify-content: space-between; diff --git a/frontend/src/pages/delegations.astro b/frontend/src/pages/delegations.astro index ec68f65..412106f 100644 --- a/frontend/src/pages/delegations.astro +++ b/frontend/src/pages/delegations.astro @@ -5,143 +5,226 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro'; --- -
- - -
-

Please log in to manage delegations.

-
- -