2026-01-27 16:21:58 +00:00
|
|
|
---
|
2026-01-29 10:38:43 +00:00
|
|
|
export const prerender = false;
|
2026-01-27 16:21:58 +00:00
|
|
|
import Layout from '../layouts/Layout.astro';
|
|
|
|
|
import { API_BASE as apiBase } from '../lib/api';
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
<Layout title="All Proposals">
|
2026-01-29 17:13:32 +00:00
|
|
|
<section class="ui-page">
|
|
|
|
|
<div class="ui-container">
|
2026-01-29 20:40:25 +00:00
|
|
|
<div class="ui-hero ui-card ui-card-glass">
|
|
|
|
|
<div class="ui-hero-top">
|
2026-01-29 17:13:32 +00:00
|
|
|
<div>
|
2026-01-29 20:40:25 +00:00
|
|
|
<h1 class="ui-hero-title">All Proposals</h1>
|
|
|
|
|
<p class="ui-hero-subtitle">Browse proposals across all communities</p>
|
2026-01-29 17:13:32 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-27 16:21:58 +00:00
|
|
|
</div>
|
|
|
|
|
|
2026-01-29 17:13:32 +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-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">
|
|
|
|
|
<div class="state-card ui-card"><p class="loading">Loading proposals...</p></div>
|
|
|
|
|
</div>
|
2026-01-27 16:21:58 +00:00
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</Layout>
|
|
|
|
|
|
|
|
|
|
<script define:vars={{ apiBase }}>
|
|
|
|
|
let allProposals = [];
|
|
|
|
|
|
|
|
|
|
async function loadProposals() {
|
|
|
|
|
const container = document.getElementById('proposals-list');
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${apiBase}/api/proposals`);
|
|
|
|
|
allProposals = await res.json();
|
|
|
|
|
renderProposals(allProposals);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
container.innerHTML = '<div class="error"><p>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 17:13:32 +00:00
|
|
|
container.innerHTML = '<div class="state-card ui-card"><p class="empty">No proposals found.</p></div>';
|
2026-01-27 16:21:58 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
container.innerHTML = proposals.map(p => `
|
2026-01-29 17:13:32 +00:00
|
|
|
<a href="/proposals/${p.id}" class="proposal-card ui-card">
|
2026-01-27 16:21:58 +00:00
|
|
|
<div class="proposal-header">
|
|
|
|
|
<h3>${p.title}</h3>
|
2026-01-29 16:45:49 +00:00
|
|
|
<span class="ui-pill status-${p.status}">${p.status}</span>
|
2026-01-27 16:21:58 +00:00
|
|
|
</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>
|
2026-01-29 17:13:32 +00:00
|
|
|
.toolbar {
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
margin-bottom: 1rem;
|
2026-01-27 16:21:58 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-29 17:13:32 +00:00
|
|
|
.toolbar-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1.5fr 1fr 1fr;
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
align-items: end;
|
2026-01-27 16:21:58 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-29 17:13:32 +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-27 16:21:58 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-29 17:13:32 +00:00
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.toolbar-grid {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 16:21:58 +00:00
|
|
|
.list {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.proposal-card {
|
|
|
|
|
display: block;
|
2026-01-29 17:13:32 +00:00
|
|
|
padding: 1.1rem;
|
2026-01-27 16:21:58 +00:00
|
|
|
color: var(--color-text);
|
2026-01-29 17:13:32 +00:00
|
|
|
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);
|
2026-01-27 16:21:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.proposal-card:hover {
|
|
|
|
|
border-color: var(--color-border-hover);
|
2026-01-29 17:13:32 +00:00
|
|
|
transform: translateY(-2px);
|
2026-01-27 16:21:58 +00:00
|
|
|
box-shadow: var(--shadow-md);
|
2026-01-29 17:13:32 +00:00
|
|
|
background: rgba(255, 255, 255, 0.03);
|
2026-01-27 16:21:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.proposal-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 0.25rem;
|
2026-01-29 17:13:32 +00:00
|
|
|
gap: 0.75rem;
|
2026-01-27 16:21:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.proposal-card h3 {
|
|
|
|
|
font-size: 1.25rem;
|
2026-01-29 17:13:32 +00:00
|
|
|
letter-spacing: -0.01em;
|
|
|
|
|
margin: 0;
|
2026-01-27 16:21:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.proposal-stats {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
margin-top: 0.75rem;
|
|
|
|
|
padding-top: 0.75rem;
|
|
|
|
|
border-top: 1px solid var(--color-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.proposal-stats .stat {
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
}
|
|
|
|
|
</style>
|