ux: improve proposals states

This commit is contained in:
Marco Allegretti 2026-02-05 13:48:06 +01:00
parent a946fc6b85
commit 5b5a11a96d
2 changed files with 111 additions and 6 deletions

View file

@ -44,7 +44,7 @@ import { API_BASE as apiBase } from '../lib/api';
</div>
</div>
<div id="proposals-list" class="list">
<div id="proposals-list" class="list" aria-live="polite">
<div class="state-card ui-card"><p class="loading">Loading proposals...</p></div>
</div>
</div>
@ -78,6 +78,62 @@ import { API_BASE as apiBase } from '../lib/api';
container.innerHTML = html;
}
function renderErrorState(message) {
const container = document.getElementById('proposals-list');
if (!container) return;
container.innerHTML = `
<div class="state-card ui-card">
<p class="error">${message}</p>
<p class="hint">Check your connection and try again.</p>
<div class="state-actions">
<button type="button" class="ui-btn ui-btn-primary" id="retry-load">Retry</button>
<a class="ui-btn ui-btn-secondary" href="/communities">Browse communities</a>
</div>
</div>
`;
document.getElementById('retry-load')?.addEventListener('click', loadProposals);
}
function renderEmptyState(isFiltered) {
const container = document.getElementById('proposals-list');
if (!container) return;
container.innerHTML = `
<div class="state-card ui-card">
<p class="empty">${isFiltered ? 'No proposals match your filters.' : 'No proposals found.'}</p>
<p class="hint">Try adjusting search, status, or sort.</p>
<div class="state-actions">
<button type="button" class="ui-btn ui-btn-secondary" id="reset-filters">Reset filters</button>
<a class="ui-btn ui-btn-primary" href="/communities">Go to communities</a>
</div>
</div>
`;
document.getElementById('reset-filters')?.addEventListener('click', () => {
const searchInput = document.getElementById('search-input');
const statusFilter = document.getElementById('status-filter');
const sortFilter = document.getElementById('sort-filter');
if (searchInput) searchInput.value = '';
if (statusFilter) statusFilter.value = '';
if (sortFilter) sortFilter.value = 'newest';
filterProposals();
});
}
function hasActiveFilters() {
const searchInput = document.getElementById('search-input');
const statusFilter = document.getElementById('status-filter');
const sortFilter = document.getElementById('sort-filter');
const query = searchInput?.value.toLowerCase().trim() || '';
const status = statusFilter?.value || '';
const sort = sortFilter?.value || 'newest';
return Boolean(query || status || sort !== 'newest');
}
async function loadProposals() {
const container = document.getElementById('proposals-list');
if (!container) return;
@ -88,7 +144,7 @@ import { API_BASE as apiBase } from '../lib/api';
allProposals = await res.json();
renderProposals(allProposals);
} catch (error) {
container.innerHTML = '<div class="error"><p>Failed to load proposals.</p></div>';
renderErrorState('Failed to load proposals.');
}
}
@ -97,7 +153,7 @@ import { API_BASE as apiBase } from '../lib/api';
if (!container) return;
if (proposals.length === 0) {
container.innerHTML = '<div class="state-card ui-card"><p class="empty">No proposals found.</p></div>';
renderEmptyState(hasActiveFilters());
return;
}
@ -171,6 +227,14 @@ import { API_BASE as apiBase } from '../lib/api';
gap: 1rem;
}
.state-actions {
margin-top: 1.25rem;
display: flex;
gap: 0.75rem;
justify-content: center;
flex-wrap: wrap;
}
.proposal-card {
display: block;
padding: 1.1rem;

View file

@ -28,6 +28,24 @@ const proposalId = id ?? '';
let quadraticAllocations = {};
let starRatings = {};
function renderProposalErrorState(message) {
const container = document.getElementById('proposal-content');
if (!container) return;
container.innerHTML = `
<div class="state-card ui-card">
<p class="error">${escapeHtml(message)}</p>
<p class="hint">It may have been deleted, or you may not have access.</p>
<div class="state-actions">
<button type="button" class="ui-btn ui-btn-primary" id="retry-proposal">Retry</button>
<a class="ui-btn ui-btn-secondary" href="/proposals">Back to proposals</a>
</div>
</div>
`;
document.getElementById('retry-proposal')?.addEventListener('click', loadProposal);
}
function escapeHtml(value) {
return String(value || '').replace(/[&<>"']/g, function(ch) {
switch (ch) {
@ -407,7 +425,7 @@ const proposalId = id ?? '';
}
} catch (error) {
container.innerHTML = '<div class="error">Proposal not found</div>';
renderProposalErrorState('Proposal not found.');
}
}
@ -600,7 +618,12 @@ const proposalId = id ?? '';
const comments = await res.json();
if (comments.length === 0) {
container.innerHTML = '<p class="empty-comments">No comments yet. Be the first to share your thoughts!</p>';
container.innerHTML = `
<div class="state-card ui-card ui-card-soft">
<p class="empty">No comments yet.</p>
<p class="hint">Be the first to share your thoughts.</p>
</div>
`;
return;
}
@ -619,7 +642,17 @@ const proposalId = id ?? '';
`;
}).join('');
} catch (e) {
container.innerHTML = '<p class="error-small">Failed to load comments</p>';
container.innerHTML = `
<div class="state-card ui-card ui-card-soft">
<p class="error">Failed to load comments.</p>
<p class="hint">Try again in a moment.</p>
<div class="state-actions">
<button type="button" class="ui-btn ui-btn-secondary" id="retry-comments">Retry</button>
</div>
</div>
`;
document.getElementById('retry-comments')?.addEventListener('click', loadComments);
}
}
@ -659,6 +692,14 @@ const proposalId = id ?? '';
max-width: 880px;
}
.state-actions {
margin-top: 1.25rem;
display: flex;
gap: 0.75rem;
justify-content: center;
flex-wrap: wrap;
}
.results-chart-host {
display: none;
}