ux: improve communities states

This commit is contained in:
Marco Allegretti 2026-02-05 18:57:13 +01:00
parent 512ed13889
commit e084c0d235
2 changed files with 168 additions and 13 deletions

View file

@ -96,6 +96,56 @@ import { API_BASE as apiBase } from '../lib/api';
container.innerHTML = html; container.innerHTML = html;
} }
function renderErrorState(message) {
var container = document.getElementById('communities-list');
if (!container) return;
container.innerHTML =
'<div class="state-card ui-card">' +
'<p class="error">' + escapeHtml(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-communities">Retry</button>' +
'<a class="ui-btn ui-btn-secondary" href="/proposals">Browse proposals</a>' +
'</div>' +
'</div>';
var retryBtn = document.getElementById('retry-communities');
if (retryBtn) {
retryBtn.addEventListener('click', function() {
loadCommunities();
});
}
}
function renderEmptyState(isFiltered) {
var container = document.getElementById('communities-list');
if (!container) return;
var label = isFiltered ? 'No communities match your search.' : 'No communities found.';
var hint = isFiltered ? 'Try a different keyword or clear the search.' : 'Please try again later.';
container.innerHTML =
'<div class="state-card ui-card">' +
'<p class="empty">' + label + '</p>' +
'<p class="hint">' + hint + '</p>' +
'<div class="state-actions">' +
(isFiltered ? '<button type="button" class="ui-btn ui-btn-secondary" id="reset-community-search">Reset search</button>' : '') +
'<a class="ui-btn ui-btn-primary" href="/communities">Refresh</a>' +
'</div>' +
'</div>';
var resetBtn = document.getElementById('reset-community-search');
if (resetBtn) {
resetBtn.addEventListener('click', function() {
var searchInput = document.getElementById('search-input');
if (searchInput) searchInput.value = '';
currentQuery = '';
applyFilterSortAndRender();
});
}
}
function sortCommunities(list) { function sortCommunities(list) {
var sorted = list.slice(); var sorted = list.slice();
sorted.sort(function(a, b) { sorted.sort(function(a, b) {
@ -128,7 +178,7 @@ import { API_BASE as apiBase } from '../lib/api';
if (!container) return; if (!container) return;
if (!communities || communities.length === 0) { if (!communities || communities.length === 0) {
container.innerHTML = '<div class="state-card ui-card"><p>No communities found.</p></div>'; renderEmptyState(!!String(currentQuery || '').trim());
return; return;
} }
@ -189,7 +239,8 @@ import { API_BASE as apiBase } from '../lib/api';
}) })
.catch(function(error) { .catch(function(error) {
console.error('Failed to load communities:', error); console.error('Failed to load communities:', error);
container.innerHTML = '<div class="state-card ui-card"><p class="error">Failed to load communities.</p><p class="hint">Please try again later.</p></div>'; updateKpis('—', '—', '');
renderErrorState('Failed to load communities.');
}); });
} }

View file

@ -18,6 +18,24 @@ const { slug } = Astro.params;
<script define:vars={{ slug, apiBase }}> <script define:vars={{ slug, apiBase }}>
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
function renderCommunityErrorState(message) {
const container = document.getElementById('community-content');
if (!container) return;
container.innerHTML = `
<div class="state-card ui-card">
<p class="error">${escapeHtml(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-community">Retry</button>
<a class="ui-btn ui-btn-secondary" href="/communities">Back to communities</a>
</div>
</div>
`;
document.getElementById('retry-community')?.addEventListener('click', loadCommunity);
}
function escapeHtml(value) { function escapeHtml(value) {
return String(value || '').replace(/[&<>"']/g, function(ch) { return String(value || '').replace(/[&<>"']/g, function(ch) {
switch (ch) { switch (ch) {
@ -41,7 +59,15 @@ const { slug } = Astro.params;
const community = communities.find(c => c.slug === slug); const community = communities.find(c => c.slug === slug);
if (!community) { if (!community) {
container.innerHTML = '<div class="state-card ui-card"><p class="error">Community not found</p></div>'; container.innerHTML = `
<div class="state-card ui-card">
<p class="error">Community not found.</p>
<p class="hint">It may have been deleted, or the link is incorrect.</p>
<div class="state-actions">
<a class="ui-btn ui-btn-primary" href="/communities">Back to communities</a>
</div>
</div>
`;
return; return;
} }
@ -181,7 +207,7 @@ const { slug } = Astro.params;
}); });
} catch (error) { } catch (error) {
container.innerHTML = '<div class="error">Failed to load community</div>'; renderCommunityErrorState('Failed to load community.');
} }
} }
@ -207,7 +233,16 @@ const { slug } = Astro.params;
const proposals = await res.json(); const proposals = await res.json();
if (proposals.length === 0) { if (proposals.length === 0) {
container.innerHTML = '<p class="empty-small">No proposals yet</p>'; var communitySlug = encodeURIComponent(String(slug || ''));
container.innerHTML = `
<div class="state-card ui-card ui-card-soft">
<p class="empty">No proposals yet.</p>
<p class="hint">When someone creates one, itll show up here.</p>
<div class="state-actions">
<a class="ui-btn ui-btn-secondary" href="/communities/${communitySlug}/proposals">All proposals</a>
</div>
</div>
`;
return; return;
} }
@ -227,7 +262,17 @@ const { slug } = Astro.params;
}).join(''); }).join('');
} catch (e) { } catch (e) {
container.innerHTML = '<p class="error-small">Failed to load</p>'; container.innerHTML = `
<div class="state-card ui-card ui-card-soft">
<p class="error">Failed to load proposals.</p>
<p class="hint">Please try again.</p>
<div class="state-actions">
<button type="button" class="ui-btn ui-btn-secondary" id="retry-recent-proposals">Retry</button>
</div>
</div>
`;
document.getElementById('retry-recent-proposals')?.addEventListener('click', () => loadProposals(communityId));
} }
} }
@ -240,7 +285,12 @@ const { slug } = Astro.params;
const members = await res.json(); const members = await res.json();
if (members.length === 0) { if (members.length === 0) {
container.innerHTML = '<p class="empty-small">No members yet</p>'; container.innerHTML = `
<div class="state-card ui-card ui-card-soft">
<p class="empty">No members yet.</p>
<p class="hint">Once people join, theyll appear here.</p>
</div>
`;
return; return;
} }
@ -259,7 +309,17 @@ const { slug } = Astro.params;
`; `;
}).join(''); }).join('');
} catch (e) { } catch (e) {
container.innerHTML = '<p class="error-small">Failed to load</p>'; container.innerHTML = `
<div class="state-card ui-card ui-card-soft">
<p class="error">Failed to load members.</p>
<p class="hint">Please try again.</p>
<div class="state-actions">
<button type="button" class="ui-btn ui-btn-secondary" id="retry-members">Retry</button>
</div>
</div>
`;
document.getElementById('retry-members')?.addEventListener('click', () => loadMembers(communityId));
} }
} }
@ -350,7 +410,15 @@ const { slug } = Astro.params;
try { try {
if (!token) { if (!token) {
container.innerHTML = '<p class="empty-small"><a href="/login">Login</a> to view moderation history</p>'; container.innerHTML = `
<div class="state-card ui-card ui-card-soft">
<p class="empty">Login required.</p>
<p class="hint">Sign in to view moderation history.</p>
<div class="state-actions">
<a class="ui-btn ui-btn-primary" href="/login">Login</a>
</div>
</div>
`;
return; return;
} }
@ -359,19 +427,37 @@ const { slug } = Astro.params;
}); });
if (res.status === 401) { if (res.status === 401) {
container.innerHTML = '<p class="error-small"><a href="/login">Login</a> to view moderation history</p>'; container.innerHTML = `
<div class="state-card ui-card ui-card-soft">
<p class="error">Login required.</p>
<p class="hint">Sign in to view moderation history.</p>
<div class="state-actions">
<a class="ui-btn ui-btn-primary" href="/login">Login</a>
</div>
</div>
`;
return; return;
} }
if (res.status === 403) { if (res.status === 403) {
container.innerHTML = '<p class="error-small">You do not have permission to view moderation history</p>'; container.innerHTML = `
<div class="state-card ui-card ui-card-soft">
<p class="error">Not allowed.</p>
<p class="hint">You do not have permission to view moderation history.</p>
</div>
`;
return; return;
} }
const entries = await res.json(); const entries = await res.json();
if (entries.length === 0) { if (entries.length === 0) {
container.innerHTML = '<p class="empty-small">No moderation actions yet</p>'; container.innerHTML = `
<div class="state-card ui-card ui-card-soft">
<p class="empty">No moderation actions yet.</p>
<p class="hint">When something happens, itll show up here.</p>
</div>
`;
return; return;
} }
@ -395,7 +481,17 @@ const { slug } = Astro.params;
`; `;
}).join(''); }).join('');
} catch (e) { } catch (e) {
container.innerHTML = '<p class="error-small">Failed to load</p>'; container.innerHTML = `
<div class="state-card ui-card ui-card-soft">
<p class="error">Failed to load moderation history.</p>
<p class="hint">Please try again.</p>
<div class="state-actions">
<button type="button" class="ui-btn ui-btn-secondary" id="retry-mod-log">Retry</button>
</div>
</div>
`;
document.getElementById('retry-mod-log')?.addEventListener('click', () => loadModerationLog(communityId));
} }
} }
@ -407,6 +503,14 @@ const { slug } = Astro.params;
--ui-hero-mb: 1.25rem; --ui-hero-mb: 1.25rem;
} }
.state-actions {
margin-top: 1.25rem;
display: flex;
gap: 0.75rem;
justify-content: center;
flex-wrap: wrap;
}
.hero-slug { .hero-slug {
margin-top: 0.25rem; margin-top: 0.25rem;
font-family: 'SF Mono', Monaco, 'Cascadia Code', ui-monospace, monospace; font-family: 'SF Mono', Monaco, 'Cascadia Code', ui-monospace, monospace;