likwid/frontend/src/pages/communities.astro

372 lines
11 KiB
Text
Raw Normal View History

---
2026-01-29 10:38:43 +00:00
export const prerender = false;
import Layout from '../layouts/Layout.astro';
import { API_BASE as apiBase } from '../lib/api';
---
<Layout title="Communities">
2026-01-29 11:11:17 +00:00
<section class="ui-page">
<div class="ui-container">
<div class="ui-page-header">
<div class="ui-page-title">
<h1>Communities</h1>
<p class="ui-subtitle">Browse organizations and communities</p>
</div>
<div class="ui-actions">
<a href="/communities/new" class="ui-btn ui-btn-primary" id="create-btn" style="display: none;">Create Community</a>
</div>
</div>
2026-01-30 07:50:16 +00:00
<div class="communities-toolbar ui-toolbar ui-card ui-card-glass">
<div class="ui-toolbar-grid">
2026-01-29 11:11:17 +00:00
<div class="toolbar-search">
<input type="text" id="search-input" class="ui-input" placeholder="Search communities…" autocomplete="off" />
</div>
<div class="toolbar-sort">
<select id="sort-select" class="ui-select" aria-label="Sort communities">
<option value="name_asc">Name (AZ)</option>
<option value="name_desc">Name (ZA)</option>
<option value="slug_asc">Slug (AZ)</option>
</select>
</div>
</div>
<div class="ui-kpis" aria-label="Communities summary">
<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-showing">—</div>
<div class="ui-kpi-label">Showing</div>
</div>
<div class="ui-kpi">
<div class="ui-kpi-value" id="kpi-query">All</div>
<div class="ui-kpi-label">Filter</div>
</div>
</div>
</div>
<div id="communities-list" class="communities-grid" aria-live="polite">
<div class="state-card ui-card"><p class="loading">Loading communities…</p></div>
</div>
<script is:inline define:vars={{ apiBase }}>
(function() {
var API_BASE = apiBase;
var allCommunities = [];
2026-01-29 11:11:17 +00:00
var currentQuery = '';
var currentSort = 'name_asc';
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 updateKpis(total, showing, query) {
var totalEl = document.getElementById('kpi-total');
var showingEl = document.getElementById('kpi-showing');
var queryEl = document.getElementById('kpi-query');
if (totalEl) totalEl.textContent = String(total);
if (showingEl) showingEl.textContent = String(showing);
if (queryEl) queryEl.textContent = query ? '“' + query + '”' : 'All';
}
function renderSkeleton(count) {
var container = document.getElementById('communities-list');
if (!container) return;
var html = '';
for (var i = 0; i < count; i++) {
html += '<div class="community-tile ui-card is-skeleton" aria-hidden="true">' +
'<div class="tile-top">' +
'<div class="skel skel-title"></div>' +
'<div class="skel skel-slug"></div>' +
'</div>' +
'<div class="skel skel-desc"></div>' +
'<div class="skel skel-desc short"></div>' +
'</div>';
}
container.innerHTML = html;
}
function sortCommunities(list) {
var sorted = list.slice();
sorted.sort(function(a, b) {
var an = (a && a.name ? String(a.name) : '').toLowerCase();
var bn = (b && b.name ? String(b.name) : '').toLowerCase();
var as = (a && a.slug ? String(a.slug) : '').toLowerCase();
var bs = (b && b.slug ? String(b.slug) : '').toLowerCase();
if (currentSort === 'name_desc') {
if (an < bn) return 1;
if (an > bn) return -1;
return 0;
}
if (currentSort === 'slug_asc') {
if (as < bs) return -1;
if (as > bs) return 1;
return 0;
}
if (an < bn) return -1;
if (an > bn) return 1;
return 0;
});
return sorted;
}
function renderCommunities(communities) {
var container = document.getElementById('communities-list');
if (!container) return;
if (!communities || communities.length === 0) {
2026-01-29 11:11:17 +00:00
container.innerHTML = '<div class="state-card ui-card"><p>No communities found.</p></div>';
return;
}
var html = '';
for (var i = 0; i < communities.length; i++) {
var c = communities[i];
2026-01-29 11:11:17 +00:00
var name = escapeHtml(c && c.name ? c.name : 'Community');
var slug = escapeHtml(c && c.slug ? c.slug : '');
var desc = escapeHtml(c && c.description ? c.description : 'No description');
html += '<a href="/communities/' + slug + '" class="community-tile ui-card">' +
'<div class="tile-top">' +
'<h3 class="tile-title">' + name + '</h3>' +
'<div class="tile-slug">/' + slug + '</div>' +
'</div>' +
'<p class="tile-desc">' + desc + '</p>' +
'<div class="tile-meta">' +
'<span class="meta-pill">Open community</span>' +
'</div>' +
'</a>';
}
container.innerHTML = html;
}
2026-01-29 11:11:17 +00:00
function applyFilterSortAndRender() {
var q = String(currentQuery || '').toLowerCase().trim();
var filtered = allCommunities;
if (q) {
filtered = allCommunities.filter(function(c) {
var name = (c && c.name ? String(c.name) : '').toLowerCase();
var slug = (c && c.slug ? String(c.slug) : '').toLowerCase();
var desc = (c && c.description ? String(c.description) : '').toLowerCase();
return name.indexOf(q) >= 0 || slug.indexOf(q) >= 0 || desc.indexOf(q) >= 0;
});
}
2026-01-29 11:11:17 +00:00
var sorted = sortCommunities(filtered);
updateKpis(allCommunities.length, sorted.length, currentQuery);
renderCommunities(sorted);
}
function loadCommunities() {
var container = document.getElementById('communities-list');
if (!container) return;
2026-01-29 11:11:17 +00:00
renderSkeleton(8);
updateKpis('—', '—', '');
fetch(API_BASE + '/api/communities')
.then(function(res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
})
.then(function(data) {
allCommunities = Array.isArray(data) ? data : (data.value || data.communities || []);
2026-01-29 11:11:17 +00:00
applyFilterSortAndRender();
})
.catch(function(error) {
console.error('Failed to load communities:', error);
2026-01-29 11:11:17 +00:00
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>';
});
}
// Initialize
loadCommunities();
// Setup search
var searchInput = document.getElementById('search-input');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
2026-01-29 11:11:17 +00:00
currentQuery = e.target.value;
applyFilterSortAndRender();
});
}
var sortSelect = document.getElementById('sort-select');
if (sortSelect) {
sortSelect.addEventListener('change', function(e) {
currentSort = e.target.value || 'name_asc';
applyFilterSortAndRender();
});
}
// Show create button if logged in
var token = localStorage.getItem('token');
var createBtn = document.getElementById('create-btn');
if (token && createBtn) {
createBtn.style.display = 'block';
}
})();
</script>
2026-01-29 11:11:17 +00:00
</div>
</section>
</Layout>
<style>
2026-01-29 20:06:00 +00:00
.ui-page {
--ui-state-card-pad: 2.5rem 1.5rem;
}
2026-01-29 11:11:17 +00:00
.communities-toolbar {
2026-01-30 07:50:16 +00:00
--ui-toolbar-pad: 1rem 1.25rem;
--ui-toolbar-mb: 1.25rem;
--ui-toolbar-cols: 1fr 220px;
--ui-toolbar-align: center;
}
2026-01-29 11:11:17 +00:00
.communities-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
2026-01-29 11:11:17 +00:00
@media (max-width: 1024px) {
.communities-grid {
grid-template-columns: repeat(2, 1fr);
}
}
2026-01-29 11:11:17 +00:00
@media (max-width: 640px) {
2026-01-30 07:50:16 +00:00
.communities-toolbar .ui-toolbar-grid {
2026-01-29 11:11:17 +00:00
grid-template-columns: 1fr;
}
2026-01-29 11:11:17 +00:00
.communities-grid {
grid-template-columns: 1fr;
}
}
2026-01-29 11:11:17 +00:00
.community-tile {
display: flex;
flex-direction: column;
padding: 1.25rem 1.25rem 1rem;
color: var(--color-text);
position: relative;
overflow: hidden;
transition: transform var(--motion-normal) var(--easing-standard), box-shadow var(--motion-normal) var(--easing-standard), border-color var(--motion-fast) var(--easing-standard);
}
2026-02-04 00:06:08 +00:00
@media (max-width: 640px) {
.community-tile {
padding: 1rem;
}
}
2026-01-29 11:11:17 +00:00
.community-tile::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(700px 220px at 15% 0%, rgba(129, 140, 248, 0.12), transparent 60%);
opacity: 0;
transition: opacity var(--motion-normal) var(--easing-standard);
pointer-events: none;
}
2026-01-29 11:11:17 +00:00
.community-tile:hover {
border-color: var(--color-border-hover);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
2026-01-29 11:11:17 +00:00
.community-tile:hover::before {
opacity: 1;
}
2026-01-29 11:11:17 +00:00
.tile-top {
display: grid;
2026-01-29 11:11:17 +00:00
gap: 0.25rem;
margin-bottom: 0.75rem;
}
2026-01-29 11:11:17 +00:00
.tile-title {
font-size: 1.05rem;
font-weight: 650;
letter-spacing: -0.01em;
margin: 0;
}
2026-02-04 00:06:08 +00:00
@media (max-width: 640px) {
.tile-title {
font-size: 1rem;
}
}
2026-01-29 11:11:17 +00:00
.tile-slug {
font-family: 'SF Mono', Monaco, 'Cascadia Code', ui-monospace, monospace;
font-size: 0.8125rem;
2026-01-29 11:11:17 +00:00
color: var(--color-primary);
opacity: 0.9;
}
2026-01-29 11:11:17 +00:00
.tile-desc {
color: var(--color-text-muted);
font-size: 0.9375rem;
2026-01-29 11:11:17 +00:00
line-height: 1.55;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
margin: 0;
min-height: calc(0.9375rem * 1.55 * 2);
}
2026-02-04 00:06:08 +00:00
@media (max-width: 640px) {
.tile-desc {
font-size: 0.9rem;
}
}
2026-01-29 11:11:17 +00:00
.tile-meta {
margin-top: 1rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
2026-02-04 00:06:08 +00:00
@media (max-width: 640px) {
.tile-meta {
margin-top: 0.75rem;
}
}
2026-01-29 11:11:17 +00:00
.meta-pill {
display: inline-flex;
align-items: center;
gap: 0.375rem;
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.10);
background: rgba(255, 255, 255, 0.04);
color: rgba(255, 255, 255, 0.78);
}
2026-01-29 11:11:17 +00:00
.skel-title { height: 18px; width: 68%; }
.skel-slug { height: 12px; width: 36%; }
.skel-desc { height: 12px; width: 100%; margin-top: 0.75rem; }
.skel-desc.short { width: 78%; margin-top: 0.5rem; }
</style>