mirror of
https://codeberg.org/likwid/likwid.git
synced 2026-02-09 21:13:09 +00:00
ux: add delegations insights panel
This commit is contained in:
parent
22d932c4fc
commit
8d199d5eab
1 changed files with 141 additions and 0 deletions
|
|
@ -53,6 +53,20 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details class="panel ui-card" open>
|
||||||
|
<summary class="panel-summary">
|
||||||
|
<span class="panel-title">Insights</span>
|
||||||
|
<span class="panel-meta">
|
||||||
|
<span class="ui-badge" id="badge-delegation-insights">—</span>
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div id="delegation-insights" class="panel-content">
|
||||||
|
<p class="loading">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<div class="tabs ui-card">
|
<div class="tabs ui-card">
|
||||||
<button class="tab active" data-tab="outgoing" type="button">My Delegations</button>
|
<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="incoming" type="button">Delegated to Me</button>
|
||||||
|
|
@ -400,6 +414,26 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.insights-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-title {
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insights-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.1rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.delegate-card {
|
.delegate-card {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -417,6 +451,11 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
<script define:vars={{ apiBase }}>
|
<script define:vars={{ apiBase }}>
|
||||||
const API_URL = apiBase;
|
const API_URL = apiBase;
|
||||||
let token = localStorage.getItem('token');
|
let token = localStorage.getItem('token');
|
||||||
|
const delegationsState = {
|
||||||
|
outgoing: null,
|
||||||
|
incoming: null,
|
||||||
|
delegates: null,
|
||||||
|
};
|
||||||
|
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
return String(value || '').replace(/[&<>"']/g, function(ch) {
|
return String(value || '').replace(/[&<>"']/g, function(ch) {
|
||||||
|
|
@ -453,6 +492,100 @@ import DelegationGraph from '../components/voting/DelegationGraph.astro';
|
||||||
if (el) el.textContent = String(value);
|
if (el) el.textContent = String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setBadge(id, value) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (!el) return;
|
||||||
|
const str = String(value);
|
||||||
|
el.textContent = str;
|
||||||
|
el.setAttribute('aria-label', str);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pluralize(label, count) {
|
||||||
|
return count === 1 ? label : `${label}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDelegationInsights() {
|
||||||
|
const container = document.getElementById('delegation-insights');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const outgoing = delegationsState.outgoing;
|
||||||
|
const incoming = delegationsState.incoming;
|
||||||
|
const delegates = delegationsState.delegates;
|
||||||
|
|
||||||
|
if (!outgoing || !incoming || !delegates) {
|
||||||
|
container.innerHTML = '<p class="loading">Loading...</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const outgoingCount = outgoing.length;
|
||||||
|
const incomingCount = incoming.length;
|
||||||
|
const delegatesCount = delegates.length;
|
||||||
|
|
||||||
|
const bullets = [];
|
||||||
|
if (outgoingCount === 0) {
|
||||||
|
bullets.push('You are currently voting directly (no outgoing delegations).');
|
||||||
|
} else {
|
||||||
|
bullets.push(`You delegate to ${outgoingCount} ${pluralize('person', outgoingCount)}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (incomingCount === 0) {
|
||||||
|
bullets.push('No one has delegated to you yet.');
|
||||||
|
} else {
|
||||||
|
bullets.push(`${incomingCount} ${pluralize('person', incomingCount)} delegated votes to you—check if any are community-scoped.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delegatesCount === 0) {
|
||||||
|
bullets.push('No delegates are available yet.');
|
||||||
|
} else {
|
||||||
|
bullets.push(`${delegatesCount} available ${pluralize('delegate', delegatesCount)} to choose from.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outgoingCount > 0 && incomingCount > 0) {
|
||||||
|
bullets.push('You both delegate and receive delegation—double-check your responsibilities before voting.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = [];
|
||||||
|
if (outgoingCount === 0) {
|
||||||
|
actions.push({ action: 'new', label: 'Create Delegation', kind: 'primary' });
|
||||||
|
} else {
|
||||||
|
actions.push({ action: 'tab-outgoing', label: 'Review My Delegations', kind: 'secondary' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (incomingCount > 0) {
|
||||||
|
actions.push({ action: 'tab-incoming', label: 'Review Incoming', kind: 'secondary' });
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.push({ action: 'tab-delegates', label: 'Find Delegates', kind: 'secondary' });
|
||||||
|
|
||||||
|
setBadge('badge-delegation-insights', bullets.length);
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="insights-body">
|
||||||
|
<div class="insights-title">What stands out</div>
|
||||||
|
<ul class="insights-list">
|
||||||
|
${bullets.map((b) => `<li>${escapeHtml(b)}</li>`).join('')}
|
||||||
|
</ul>
|
||||||
|
<div class="panel-actions">
|
||||||
|
${actions.map((a) => `<button type="button" class="ui-btn ui-btn-${a.kind}" data-insight-action="${a.action}">${escapeHtml(a.label)}</button>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.querySelectorAll('[data-insight-action]').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const action = btn.getAttribute('data-insight-action') || '';
|
||||||
|
if (action === 'new') {
|
||||||
|
document.getElementById('new-delegation-btn-secondary')?.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action.startsWith('tab-')) {
|
||||||
|
const tab = action.replace('tab-', '');
|
||||||
|
document.querySelector(`.tab[data-tab="${tab}"]`)?.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
document.getElementById('auth-check').style.display = 'block';
|
document.getElementById('auth-check').style.display = 'block';
|
||||||
|
|
@ -462,6 +595,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';
|
||||||
|
|
||||||
|
renderDelegationInsights();
|
||||||
|
|
||||||
document.getElementById('new-delegation-btn')?.setAttribute('style', 'display:inline-flex;');
|
document.getElementById('new-delegation-btn')?.setAttribute('style', 'display:inline-flex;');
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|
@ -531,8 +666,10 @@ 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();
|
||||||
|
delegationsState.outgoing = delegations;
|
||||||
|
|
||||||
setText('kpi-outgoing', delegations.length);
|
setText('kpi-outgoing', delegations.length);
|
||||||
|
renderDelegationInsights();
|
||||||
|
|
||||||
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>';
|
||||||
|
|
@ -581,8 +718,10 @@ 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();
|
||||||
|
delegationsState.incoming = delegations;
|
||||||
|
|
||||||
setText('kpi-incoming', delegations.length);
|
setText('kpi-incoming', delegations.length);
|
||||||
|
renderDelegationInsights();
|
||||||
|
|
||||||
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>';
|
||||||
|
|
@ -618,8 +757,10 @@ 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();
|
||||||
|
delegationsState.delegates = delegates;
|
||||||
|
|
||||||
setText('kpi-delegates', delegates.length);
|
setText('kpi-delegates', delegates.length);
|
||||||
|
renderDelegationInsights();
|
||||||
|
|
||||||
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>';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue