mirror of
https://codeberg.org/likwid/likwid.git
synced 2026-02-10 05:23:09 +00:00
347 lines
10 KiB
Text
347 lines
10 KiB
Text
---
|
||
export const prerender = false;
|
||
import Layout from '../layouts/Layout.astro';
|
||
import { API_BASE as apiBase } from '../lib/api';
|
||
---
|
||
|
||
<Layout title="Communities">
|
||
<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>
|
||
|
||
<div class="communities-toolbar ui-toolbar ui-card ui-card-glass">
|
||
<div class="ui-toolbar-grid">
|
||
<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 (A–Z)</option>
|
||
<option value="name_desc">Name (Z–A)</option>
|
||
<option value="slug_asc">Slug (A–Z)</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 = [];
|
||
var currentQuery = '';
|
||
var currentSort = 'name_asc';
|
||
|
||
function escapeHtml(value) {
|
||
return String(value || '').replace(/[&<>\"']/g, function(ch) {
|
||
switch (ch) {
|
||
case '&': return '&';
|
||
case '<': return '<';
|
||
case '>': return '>';
|
||
case '"': return '"';
|
||
case "'": return ''';
|
||
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) {
|
||
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];
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
});
|
||
}
|
||
|
||
var sorted = sortCommunities(filtered);
|
||
updateKpis(allCommunities.length, sorted.length, currentQuery);
|
||
renderCommunities(sorted);
|
||
}
|
||
|
||
function loadCommunities() {
|
||
var container = document.getElementById('communities-list');
|
||
if (!container) return;
|
||
|
||
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 || []);
|
||
applyFilterSortAndRender();
|
||
})
|
||
.catch(function(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>';
|
||
});
|
||
}
|
||
|
||
// Initialize
|
||
loadCommunities();
|
||
|
||
// Setup search
|
||
var searchInput = document.getElementById('search-input');
|
||
if (searchInput) {
|
||
searchInput.addEventListener('input', function(e) {
|
||
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>
|
||
</div>
|
||
</section>
|
||
</Layout>
|
||
|
||
<style>
|
||
.ui-page {
|
||
--ui-state-card-pad: 2.5rem 1.5rem;
|
||
}
|
||
|
||
.communities-toolbar {
|
||
--ui-toolbar-pad: 1rem 1.25rem;
|
||
--ui-toolbar-mb: 1.25rem;
|
||
--ui-toolbar-cols: 1fr 220px;
|
||
--ui-toolbar-align: center;
|
||
}
|
||
|
||
.communities-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 1rem;
|
||
}
|
||
|
||
@media (max-width: 1024px) {
|
||
.communities-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.communities-toolbar .ui-toolbar-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.communities-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.community-tile:hover {
|
||
border-color: var(--color-border-hover);
|
||
transform: translateY(-2px);
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
|
||
.community-tile:hover::before {
|
||
opacity: 1;
|
||
}
|
||
|
||
.tile-top {
|
||
display: grid;
|
||
gap: 0.25rem;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.tile-title {
|
||
font-size: 1.05rem;
|
||
font-weight: 650;
|
||
letter-spacing: -0.01em;
|
||
margin: 0;
|
||
}
|
||
|
||
.tile-slug {
|
||
font-family: 'SF Mono', Monaco, 'Cascadia Code', ui-monospace, monospace;
|
||
font-size: 0.8125rem;
|
||
color: var(--color-primary);
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.tile-desc {
|
||
color: var(--color-text-muted);
|
||
font-size: 0.9375rem;
|
||
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);
|
||
}
|
||
|
||
.tile-meta {
|
||
margin-top: 1rem;
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
.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>
|