likwid/frontend/src/pages/proposals.astro
Marco Allegretti 888cdb37cd frontend: modify 2 files
Verified changes:
- modify frontend/src/pages/proposals.astro
- modify frontend/src/pages/proposals/[id].astro

Diffstat:
- 2 files changed, 111 insertions(+), 6 deletions(-)
2026-02-05 13:48:06 +01:00

333 lines
10 KiB
Text

---
export const prerender = false;
import Layout from '../layouts/Layout.astro';
import { API_BASE as apiBase } from '../lib/api';
---
<Layout title="All Proposals">
<section class="ui-page">
<div class="ui-container">
<div class="ui-hero ui-card ui-card-glass">
<div class="ui-hero-top">
<div>
<h1 class="ui-hero-title">All Proposals</h1>
<p class="ui-hero-subtitle">Browse proposals across all communities</p>
</div>
</div>
</div>
<div class="ui-toolbar ui-card">
<div class="ui-toolbar-grid">
<div>
<label class="ui-toolbar-label" for="search-input">Search</label>
<input type="text" id="search-input" class="ui-input" placeholder="Search proposals…" />
</div>
<div>
<label class="ui-toolbar-label" for="status-filter">Status</label>
<select id="status-filter" class="ui-select">
<option value="">All statuses</option>
<option value="draft">Draft</option>
<option value="discussion">Discussion</option>
<option value="voting">Voting</option>
<option value="closed">Closed</option>
</select>
</div>
<div>
<label class="ui-toolbar-label" for="sort-filter">Sort</label>
<select id="sort-filter" class="ui-select">
<option value="newest">Newest first</option>
<option value="oldest">Oldest first</option>
<option value="most-votes">Most votes</option>
<option value="most-comments">Most comments</option>
</select>
</div>
</div>
</div>
<div id="proposals-list" class="list" aria-live="polite">
<div class="state-card ui-card"><p class="loading">Loading proposals...</p></div>
</div>
</div>
</section>
</Layout>
<script define:vars={{ apiBase }}>
let allProposals = [];
function renderSkeleton(count) {
const container = document.getElementById('proposals-list');
if (!container) return;
let html = '';
for (let i = 0; i < count; i++) {
html += `
<div class="proposal-card ui-card is-skeleton" aria-hidden="true">
<div class="proposal-header">
<div class="skel skel-title"></div>
<div class="skel skel-pill"></div>
</div>
<div class="skel skel-community"></div>
<div class="skel skel-desc"></div>
<div class="skel skel-desc short"></div>
<div class="proposal-stats">
<div class="skel skel-stat"></div>
<div class="skel skel-stat"></div>
</div>
</div>
`;
}
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;
try {
renderSkeleton(8);
const res = await fetch(`${apiBase}/api/proposals`);
allProposals = await res.json();
renderProposals(allProposals);
} catch (error) {
renderErrorState('Failed to load proposals.');
}
}
function renderProposals(proposals) {
const container = document.getElementById('proposals-list');
if (!container) return;
if (proposals.length === 0) {
renderEmptyState(hasActiveFilters());
return;
}
container.innerHTML = proposals.map(p => `
<a href="/proposals/${p.id}" class="proposal-card ui-card">
<div class="proposal-header">
<h3>${p.title}</h3>
<span class="ui-pill status-${p.status}">${p.status}</span>
</div>
<p class="community">in <span>${p.community_name}</span></p>
<p class="description">${p.description.substring(0, 120)}${p.description.length > 120 ? '...' : ''}</p>
<div class="proposal-stats">
<span class="stat">🗳️ ${p.vote_count} votes</span>
<span class="stat">💬 ${p.comment_count} comments</span>
</div>
</a>
`).join('');
}
function filterProposals() {
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';
let filtered = [...allProposals];
if (query) {
filtered = filtered.filter(p =>
p.title.toLowerCase().includes(query) ||
p.description.toLowerCase().includes(query) ||
p.community_name.toLowerCase().includes(query)
);
}
if (status) {
filtered = filtered.filter(p => p.status === status);
}
// Sort
switch (sort) {
case 'oldest':
filtered.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
break;
case 'most-votes':
filtered.sort((a, b) => b.vote_count - a.vote_count);
break;
case 'most-comments':
filtered.sort((a, b) => b.comment_count - a.comment_count);
break;
default: // newest
filtered.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
}
renderProposals(filtered);
}
loadProposals();
document.getElementById('search-input')?.addEventListener('input', filterProposals);
document.getElementById('status-filter')?.addEventListener('change', filterProposals);
document.getElementById('sort-filter')?.addEventListener('change', filterProposals);
</script>
<style>
.list {
display: grid;
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;
color: var(--color-text);
text-decoration: none;
transition: transform var(--motion-fast) var(--easing-standard), box-shadow var(--motion-fast) var(--easing-standard), border-color var(--motion-fast) var(--easing-standard), background var(--motion-fast) var(--easing-standard);
}
@media (max-width: 640px) {
.proposal-card {
padding: 1rem;
}
}
.proposal-card:hover {
border-color: var(--color-border-hover);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
background: rgba(255, 255, 255, 0.03);
}
.proposal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.25rem;
gap: 0.75rem;
}
@media (max-width: 640px) {
.proposal-header {
flex-direction: column;
align-items: flex-start;
}
}
.proposal-card h3 {
font-size: 1.25rem;
letter-spacing: -0.01em;
margin: 0;
line-height: 1.25;
}
@media (max-width: 640px) {
.proposal-card h3 {
font-size: 1.125rem;
}
}
.community {
font-size: 0.75rem;
color: var(--color-text-muted);
margin-bottom: 0.5rem;
}
.community span {
color: var(--color-primary);
}
.description {
color: var(--color-text-muted);
font-size: 0.875rem;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.proposal-stats {
display: flex;
gap: 1rem;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--color-border);
}
@media (max-width: 640px) {
.proposal-stats {
flex-wrap: wrap;
gap: 0.5rem 1rem;
}
}
.proposal-stats .stat {
font-size: 0.75rem;
color: var(--color-text-muted);
}
.skel-title { height: 18px; width: 68%; }
.skel-pill { height: 14px; width: 88px; border-radius: 999px; }
.skel-community { height: 10px; width: 42%; margin-top: 0.5rem; }
.skel-desc { height: 12px; width: 100%; margin-top: 0.75rem; }
.skel-desc.short { width: 78%; margin-top: 0.5rem; }
.skel-stat { height: 10px; width: 120px; }
</style>