likwid/frontend/src/pages/communities/[slug]/proposals/index.astro

357 lines
10 KiB
Text
Raw Normal View History

---
export const prerender = false;
import Layout from '../../../../layouts/Layout.astro';
import { API_BASE as apiBase } from '../../../../lib/api';
const { slug } = Astro.params;
---
<Layout title="Proposals">
2026-01-29 12:13:46 +00:00
<section class="ui-page">
<div class="ui-container">
<div class="hero ui-card ui-card-glass">
<div class="hero-top">
<div>
<h1 class="hero-title">Proposals</h1>
<p class="hero-subtitle">Proposals and decisions</p>
</div>
<a href={`/communities/${slug}/proposals/new`} class="ui-btn ui-btn-primary" id="create-btn" style="display: none;">+ New Proposal</a>
</div>
<div class="hero-kpis">
<div class="hero-kpis-label">At a glance</div>
<div class="ui-kpis" id="kpis">
<div class="ui-kpi">
<div class="ui-kpi-value" id="kpi-total">—</div>
<div class="ui-kpi-label">Total</div>
</div>
<div class="ui-kpi">
<div class="ui-kpi-value" id="kpi-voting">—</div>
<div class="ui-kpi-label">Voting</div>
</div>
<div class="ui-kpi">
<div class="ui-kpi-value" id="kpi-discussion">—</div>
<div class="ui-kpi-label">Discussion</div>
</div>
<div class="ui-kpi">
<div class="ui-kpi-value" id="kpi-draft">—</div>
<div class="ui-kpi-label">Drafts</div>
</div>
<div class="ui-kpi">
<div class="ui-kpi-value" id="kpi-closed">—</div>
<div class="ui-kpi-label">Closed</div>
</div>
</div>
</div>
</div>
2026-01-29 12:13:46 +00:00
<div class="toolbar ui-card">
<div class="toolbar-grid">
<div>
<label class="toolbar-label" for="search-input">Search</label>
<input type="text" id="search-input" class="ui-input" placeholder="Search proposals…" />
</div>
<div>
<label class="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="toolbar-label" for="sort-select">Sort</label>
<select id="sort-select" class="ui-select">
<option value="newest">Newest</option>
<option value="oldest">Oldest</option>
<option value="status">Status</option>
<option value="title">Title</option>
</select>
</div>
</div>
</div>
<div id="proposals-list" class="list">
<div class="list-skeleton">
<div class="skeleton-card"></div>
<div class="skeleton-card"></div>
<div class="skeleton-card"></div>
<div class="skeleton-card"></div>
</div>
</div>
</div>
</section>
</Layout>
<script define:vars={{ slug, apiBase }}>
let allProposals = [];
2026-01-29 12:13:46 +00:00
function escapeHtml(value) {
return String(value || '').replace(/[&<>"']/g, function(ch) {
switch (ch) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '"': return '&quot;';
case "'": return '&#39;';
default: return ch;
}
});
}
function normalizeStatus(status) {
const s = String(status || '').toLowerCase();
if (s === 'draft' || s === 'discussion' || s === 'voting' || s === 'closed') return s;
return 'draft';
}
function updateKpis(proposals) {
const total = proposals.length;
const byStatus = { draft: 0, discussion: 0, voting: 0, closed: 0 };
proposals.forEach((p) => {
const key = normalizeStatus(p.status);
byStatus[key] = (byStatus[key] || 0) + 1;
});
const setText = (id, value) => {
const el = document.getElementById(id);
if (el) el.textContent = String(value);
};
setText('kpi-total', total);
setText('kpi-draft', byStatus.draft);
setText('kpi-discussion', byStatus.discussion);
setText('kpi-voting', byStatus.voting);
setText('kpi-closed', byStatus.closed);
}
async function loadProposals() {
const container = document.getElementById('proposals-list');
if (!container) return;
try {
const commRes = await fetch(`${apiBase}/api/communities`);
const communities = await commRes.json();
const community = communities.find(c => c.slug === slug);
if (!community) {
container.innerHTML = '<div class="error">Community not found</div>';
return;
}
const res = await fetch(`${apiBase}/api/communities/${community.id}/proposals`);
allProposals = await res.json();
2026-01-29 12:13:46 +00:00
updateKpis(allProposals);
filterProposals();
} catch (error) {
2026-01-29 12:13:46 +00:00
container.innerHTML = '<div class="ui-card"><p class="error">Failed to load proposals.</p></div>';
}
}
function renderProposals(proposals) {
const container = document.getElementById('proposals-list');
if (!container) return;
if (proposals.length === 0) {
2026-01-29 12:13:46 +00:00
container.innerHTML = '<div class="ui-card"><p class="empty">No proposals found.</p></div>';
return;
}
2026-01-29 12:13:46 +00:00
container.innerHTML = proposals.map(p => {
const statusKey = normalizeStatus(p.status);
const safeTitle = escapeHtml(p.title);
const desc = String(p.description || '');
const safeDesc = escapeHtml(desc.substring(0, 160));
const suffix = desc.length > 160 ? '…' : '';
const createdAt = p.created_at ? new Date(p.created_at).toLocaleDateString() : '';
return `
<a href="/proposals/${encodeURIComponent(String(p.id))}" class="proposal-card ui-card">
<div class="proposal-header">
<h3>${safeTitle}</h3>
2026-01-29 16:45:49 +00:00
<span class="ui-pill status-${statusKey}">${escapeHtml(statusKey)}</span>
2026-01-29 12:13:46 +00:00
</div>
${createdAt ? `<div class="proposal-meta">Created ${escapeHtml(createdAt)}</div>` : ''}
<p class="description">${safeDesc}${suffix}</p>
</a>
`;
}).join('');
}
function filterProposals() {
const searchInput = document.getElementById('search-input');
const statusFilter = document.getElementById('status-filter');
2026-01-29 12:13:46 +00:00
const sortSelect = document.getElementById('sort-select');
const query = searchInput?.value.toLowerCase().trim() || '';
const status = statusFilter?.value || '';
2026-01-29 12:13:46 +00:00
const sortKey = sortSelect?.value || 'newest';
let filtered = allProposals;
if (query) {
filtered = filtered.filter(p =>
p.title.toLowerCase().includes(query) ||
p.description.toLowerCase().includes(query)
);
}
if (status) {
filtered = filtered.filter(p => p.status === status);
}
2026-01-29 12:13:46 +00:00
const getTime = (p) => {
const d = p.created_at ? Date.parse(p.created_at) : NaN;
if (!Number.isNaN(d)) return d;
const asNum = Number(p.id);
if (!Number.isNaN(asNum)) return asNum;
return 0;
};
filtered = [...filtered];
if (sortKey === 'newest') {
filtered.sort((a, b) => getTime(b) - getTime(a));
} else if (sortKey === 'oldest') {
filtered.sort((a, b) => getTime(a) - getTime(b));
} else if (sortKey === 'title') {
filtered.sort((a, b) => String(a.title || '').localeCompare(String(b.title || '')));
} else if (sortKey === 'status') {
const order = { voting: 0, discussion: 1, draft: 2, closed: 3 };
filtered.sort((a, b) => (order[normalizeStatus(a.status)] ?? 99) - (order[normalizeStatus(b.status)] ?? 99));
}
renderProposals(filtered);
}
loadProposals();
document.getElementById('search-input')?.addEventListener('input', filterProposals);
document.getElementById('status-filter')?.addEventListener('change', filterProposals);
2026-01-29 12:13:46 +00:00
document.getElementById('sort-select')?.addEventListener('change', filterProposals);
const token = localStorage.getItem('token');
const createBtn = document.getElementById('create-btn');
if (token && createBtn) {
2026-01-29 12:13:46 +00:00
createBtn.style.display = 'inline-flex';
}
</script>
<style>
2026-01-29 12:13:46 +00:00
.hero {
padding: 1.25rem;
margin-bottom: 1rem;
}
2026-01-29 12:13:46 +00:00
.hero-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
2026-01-29 12:13:46 +00:00
gap: 1rem;
flex-wrap: wrap;
}
2026-01-29 12:13:46 +00:00
.hero-title {
margin: 0;
font-size: 2.125rem;
letter-spacing: -0.02em;
}
2026-01-29 12:13:46 +00:00
.hero-subtitle {
margin: 0.25rem 0 0;
color: var(--color-text-muted);
}
2026-01-29 12:13:46 +00:00
.hero-kpis {
margin-top: 1rem;
}
2026-01-29 12:13:46 +00:00
.hero-kpis-label {
color: var(--color-text-muted);
font-size: 0.8125rem;
margin-bottom: 0.5rem;
letter-spacing: 0.02em;
text-transform: uppercase;
}
2026-01-29 12:13:46 +00:00
.toolbar {
padding: 1rem;
margin-bottom: 1rem;
}
2026-01-29 12:13:46 +00:00
.toolbar-grid {
display: grid;
grid-template-columns: 1.5fr 1fr 1fr;
gap: 0.75rem;
align-items: end;
}
2026-01-29 12:13:46 +00:00
.toolbar-label {
display: block;
font-size: 0.75rem;
letter-spacing: 0.02em;
text-transform: uppercase;
color: var(--color-text-muted);
margin-bottom: 0.35rem;
}
2026-01-29 12:13:46 +00:00
@media (max-width: 768px) {
.toolbar-grid {
grid-template-columns: 1fr;
}
}
.list {
display: grid;
gap: 1rem;
}
.proposal-card {
display: block;
2026-01-29 12:13:46 +00:00
padding: 1.1rem;
color: var(--color-text);
2026-01-29 12:13:46 +00:00
transition: transform 140ms ease, border-color 140ms ease, background 140ms ease;
}
.proposal-card:hover {
2026-01-29 12:13:46 +00:00
border-color: var(--color-border-hover);
transform: translateY(-2px);
background: rgba(255, 255, 255, 0.03);
}
.proposal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
2026-01-29 12:13:46 +00:00
gap: 0.75rem;
}
.proposal-card h3 {
font-size: 1.25rem;
2026-01-29 12:13:46 +00:00
letter-spacing: -0.01em;
}
.proposal-meta {
color: var(--color-text-muted);
font-size: 0.8125rem;
margin-bottom: 0.5rem;
}
.description {
color: var(--color-text-muted);
font-size: 0.875rem;
2026-01-29 12:13:46 +00:00
line-height: 1.5;
}
2026-01-29 12:13:46 +00:00
.empty, .error {
text-align: center;
2026-01-29 12:13:46 +00:00
padding: 2rem;
color: var(--color-text-muted);
}
2026-01-29 16:39:03 +00:00
.list-skeleton .skeleton-card {
--ui-skeleton-card-h: 92px;
}
</style>