mirror of
https://codeberg.org/likwid/likwid.git
synced 2026-06-25 15:37:42 +00:00
672 lines
18 KiB
Text
672 lines
18 KiB
Text
|
|
---
|
||
|
|
export const prerender = false;
|
||
|
|
import Layout from '../../../layouts/Layout.astro';
|
||
|
|
import { API_BASE as apiBase } from '../../../lib/api';
|
||
|
|
const { slug } = Astro.params;
|
||
|
|
---
|
||
|
|
|
||
|
|
<Layout title="Community">
|
||
|
|
<section class="community-page">
|
||
|
|
<div id="community-content">
|
||
|
|
<p class="loading">Loading community...</p>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
</Layout>
|
||
|
|
|
||
|
|
<script define:vars={{ slug, apiBase }}>
|
||
|
|
const token = localStorage.getItem('token');
|
||
|
|
|
||
|
|
async function loadCommunity() {
|
||
|
|
const container = document.getElementById('community-content');
|
||
|
|
if (!container) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${apiBase}/api/communities`);
|
||
|
|
const communities = await res.json();
|
||
|
|
const community = communities.find(c => c.slug === slug);
|
||
|
|
|
||
|
|
if (!community) {
|
||
|
|
container.innerHTML = '<div class="error">Community not found</div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
container.innerHTML = `
|
||
|
|
<div class="community-header">
|
||
|
|
<h1>${community.name}</h1>
|
||
|
|
<span class="slug">/${community.slug}</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
${community.description ? `<p class="description">${community.description}</p>` : ''}
|
||
|
|
|
||
|
|
<div class="stats" id="stats">
|
||
|
|
<div class="stat">
|
||
|
|
<span class="stat-value" id="member-count">-</span>
|
||
|
|
<span class="stat-label">Members</span>
|
||
|
|
</div>
|
||
|
|
<div class="stat">
|
||
|
|
<span class="stat-value" id="proposal-count">-</span>
|
||
|
|
<span class="stat-label">Proposals</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="actions">
|
||
|
|
<a href="/communities/${slug}/proposals" class="btn-primary">View Proposals</a>
|
||
|
|
${token ? `<a href="/communities/${slug}/proposals/new" class="btn-secondary">Create Proposal</a>` : ''}
|
||
|
|
${token ? `<a href="/communities/${slug}/plugins" id="plugins-btn" class="btn-secondary" style="display:none;">Plugins</a>` : ''}
|
||
|
|
${token ? `<button id="join-btn" class="btn-join" data-id="${community.id}" style="display:none;">Join Community</button>` : ''}
|
||
|
|
${token ? `<button id="leave-btn" class="btn-leave" data-id="${community.id}" style="display:none;">Leave Community</button>` : ''}
|
||
|
|
${token ? `<button id="edit-community-btn" class="btn-edit" data-id="${community.id}" style="display:none;">Edit Community</button>` : ''}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="sections">
|
||
|
|
<div class="section">
|
||
|
|
<h2>Recent Proposals</h2>
|
||
|
|
<div id="recent-proposals" class="proposals-list">
|
||
|
|
<p class="loading-small">Loading...</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="section">
|
||
|
|
<h2>Members</h2>
|
||
|
|
<div id="members-list" class="members-list">
|
||
|
|
<p class="loading-small">Loading...</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="section full-width">
|
||
|
|
<h2>Moderation Log</h2>
|
||
|
|
<div id="mod-log" class="mod-log">
|
||
|
|
<p class="empty-small">No moderation actions yet</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
|
||
|
|
// Load data
|
||
|
|
loadDetails(community.id);
|
||
|
|
loadProposals(community.id);
|
||
|
|
loadMembers(community.id);
|
||
|
|
loadModerationLog(community.id);
|
||
|
|
if (token) checkMembership(community.id);
|
||
|
|
|
||
|
|
// Setup join button
|
||
|
|
document.getElementById('join-btn')?.addEventListener('click', async (e) => {
|
||
|
|
var btn = e.target;
|
||
|
|
const communityId = btn.dataset.id;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${apiBase}/api/communities/${communityId}/join`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Authorization': `Bearer ${token}` },
|
||
|
|
});
|
||
|
|
|
||
|
|
if (res.ok) {
|
||
|
|
location.reload();
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Failed to join', e);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Setup edit community button
|
||
|
|
document.getElementById('edit-community-btn')?.addEventListener('click', () => {
|
||
|
|
showEditCommunityModal(community);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Setup leave button
|
||
|
|
document.getElementById('leave-btn')?.addEventListener('click', async (e) => {
|
||
|
|
if (!confirm('Are you sure you want to leave this community?')) return;
|
||
|
|
|
||
|
|
var btn = e.target;
|
||
|
|
const communityId = btn.dataset.id;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${apiBase}/api/communities/${communityId}/leave`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Authorization': `Bearer ${token}` },
|
||
|
|
});
|
||
|
|
|
||
|
|
if (res.ok) {
|
||
|
|
location.reload();
|
||
|
|
} else {
|
||
|
|
const err = await res.text();
|
||
|
|
alert(err || 'Failed to leave community');
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Failed to leave', e);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
container.innerHTML = '<div class="error">Failed to load community</div>';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadDetails(communityId) {
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${apiBase}/api/communities/${communityId}/details`);
|
||
|
|
if (res.ok) {
|
||
|
|
const details = await res.json();
|
||
|
|
document.getElementById('member-count').textContent = details.member_count;
|
||
|
|
document.getElementById('proposal-count').textContent = details.proposal_count;
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Failed to load details', e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadProposals(communityId) {
|
||
|
|
const container = document.getElementById('recent-proposals');
|
||
|
|
if (!container) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${apiBase}/api/communities/${communityId}/proposals`);
|
||
|
|
const proposals = await res.json();
|
||
|
|
|
||
|
|
if (proposals.length === 0) {
|
||
|
|
container.innerHTML = '<p class="empty-small">No proposals yet</p>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
container.innerHTML = proposals.slice(0, 5).map(p => `
|
||
|
|
<a href="/proposals/${p.id}" class="proposal-item">
|
||
|
|
<span class="proposal-title">${p.title}</span>
|
||
|
|
<span class="proposal-status status-${p.status}">${p.status}</span>
|
||
|
|
</a>
|
||
|
|
`).join('');
|
||
|
|
|
||
|
|
} catch (e) {
|
||
|
|
container.innerHTML = '<p class="error-small">Failed to load</p>';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadMembers(communityId) {
|
||
|
|
const container = document.getElementById('members-list');
|
||
|
|
if (!container) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${apiBase}/api/communities/${communityId}/members`);
|
||
|
|
const members = await res.json();
|
||
|
|
|
||
|
|
if (members.length === 0) {
|
||
|
|
container.innerHTML = '<p class="empty-small">No members yet</p>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
container.innerHTML = members.slice(0, 10).map(m => `
|
||
|
|
<a href="/users/${m.username}" class="member-item">
|
||
|
|
<span class="member-name">${m.display_name || m.username}</span>
|
||
|
|
<span class="member-role role-${m.role}">${m.role}</span>
|
||
|
|
</a>
|
||
|
|
`).join('');
|
||
|
|
} catch (e) {
|
||
|
|
container.innerHTML = '<p class="error-small">Failed to load</p>';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function showEditCommunityModal(community) {
|
||
|
|
const modal = document.createElement('div');
|
||
|
|
modal.className = 'edit-modal';
|
||
|
|
modal.innerHTML = `
|
||
|
|
<div class="modal-content">
|
||
|
|
<h2>Edit Community</h2>
|
||
|
|
<form id="edit-community-form">
|
||
|
|
<div class="form-group">
|
||
|
|
<label for="edit-name">Name</label>
|
||
|
|
<input type="text" id="edit-name" value="${community.name}" required />
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label for="edit-description">Description</label>
|
||
|
|
<textarea id="edit-description" rows="4">${community.description || ''}</textarea>
|
||
|
|
</div>
|
||
|
|
<div class="modal-actions">
|
||
|
|
<button type="button" class="btn-cancel" id="cancel-edit">Cancel</button>
|
||
|
|
<button type="submit" class="btn-save">Save Changes</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
document.body.appendChild(modal);
|
||
|
|
|
||
|
|
document.getElementById('cancel-edit')?.addEventListener('click', () => modal.remove());
|
||
|
|
document.getElementById('edit-community-form')?.addEventListener('submit', async (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
var name = document.getElementById('edit-name').value;
|
||
|
|
var description = document.getElementById('edit-description').value;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${apiBase}/api/communities/${community.id}`, {
|
||
|
|
method: 'PUT',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'Authorization': `Bearer ${token}`,
|
||
|
|
},
|
||
|
|
body: JSON.stringify({ name, description: description || null }),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (res.ok) {
|
||
|
|
modal.remove();
|
||
|
|
location.reload();
|
||
|
|
} else {
|
||
|
|
const err = await res.text();
|
||
|
|
alert(err || 'Failed to update community');
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
alert('Error updating community');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async function checkMembership(communityId) {
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${apiBase}/api/communities/${communityId}/membership`, {
|
||
|
|
headers: { 'Authorization': `Bearer ${token}` },
|
||
|
|
});
|
||
|
|
if (res.ok) {
|
||
|
|
const data = await res.json();
|
||
|
|
const joinBtn = document.getElementById('join-btn');
|
||
|
|
const leaveBtn = document.getElementById('leave-btn');
|
||
|
|
const editBtn = document.getElementById('edit-community-btn');
|
||
|
|
const pluginsBtn = document.getElementById('plugins-btn');
|
||
|
|
|
||
|
|
if (data.is_member) {
|
||
|
|
if (joinBtn) joinBtn.style.display = 'none';
|
||
|
|
if (leaveBtn) leaveBtn.style.display = 'inline-block';
|
||
|
|
if (editBtn && data.role === 'admin') editBtn.style.display = 'inline-block';
|
||
|
|
if (pluginsBtn && (data.role === 'admin' || data.role === 'moderator')) pluginsBtn.style.display = 'inline-block';
|
||
|
|
} else {
|
||
|
|
if (joinBtn) joinBtn.style.display = 'inline-block';
|
||
|
|
if (leaveBtn) leaveBtn.style.display = 'none';
|
||
|
|
if (pluginsBtn) pluginsBtn.style.display = 'none';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Failed to check membership', e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadModerationLog(communityId) {
|
||
|
|
const container = document.getElementById('mod-log');
|
||
|
|
if (!container) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${apiBase}/api/communities/${communityId}/moderation`);
|
||
|
|
const entries = await res.json();
|
||
|
|
|
||
|
|
if (entries.length === 0) {
|
||
|
|
container.innerHTML = '<p class="empty-small">No moderation actions yet</p>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
container.innerHTML = entries.slice(0, 5).map(e => `
|
||
|
|
<div class="mod-entry">
|
||
|
|
<div class="mod-action">${e.action_type}</div>
|
||
|
|
<div class="mod-details">
|
||
|
|
<span class="mod-by">by ${e.moderator_username || 'System'}</span>
|
||
|
|
${e.target_username ? `<span class="mod-target">→ ${e.target_username}</span>` : ''}
|
||
|
|
</div>
|
||
|
|
<div class="mod-reason">${e.reason}</div>
|
||
|
|
<div class="mod-time">${new Date(e.created_at).toLocaleString()}</div>
|
||
|
|
</div>
|
||
|
|
`).join('');
|
||
|
|
} catch (e) {
|
||
|
|
container.innerHTML = '<p class="error-small">Failed to load</p>';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
loadCommunity();
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
.community-page {
|
||
|
|
padding: 2rem 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.community-header {
|
||
|
|
margin-bottom: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
h1 {
|
||
|
|
font-size: 2.5rem;
|
||
|
|
margin-bottom: 0.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.slug {
|
||
|
|
color: var(--color-primary);
|
||
|
|
font-family: monospace;
|
||
|
|
}
|
||
|
|
|
||
|
|
.description {
|
||
|
|
color: var(--color-text-muted);
|
||
|
|
font-size: 1.125rem;
|
||
|
|
margin-bottom: 2rem;
|
||
|
|
max-width: 600px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stats {
|
||
|
|
display: flex;
|
||
|
|
gap: 2rem;
|
||
|
|
margin-bottom: 2rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat {
|
||
|
|
background: var(--color-surface);
|
||
|
|
border: 1px solid var(--color-border);
|
||
|
|
border-radius: 8px;
|
||
|
|
padding: 1rem 1.5rem;
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-value {
|
||
|
|
display: block;
|
||
|
|
font-size: 2rem;
|
||
|
|
font-weight: 700;
|
||
|
|
color: var(--color-primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-label {
|
||
|
|
font-size: 0.875rem;
|
||
|
|
color: var(--color-text-muted);
|
||
|
|
}
|
||
|
|
|
||
|
|
.actions {
|
||
|
|
display: flex;
|
||
|
|
gap: 1rem;
|
||
|
|
margin-bottom: 3rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-primary, .btn-secondary {
|
||
|
|
padding: 0.75rem 1.5rem;
|
||
|
|
border-radius: 8px;
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-primary {
|
||
|
|
background: var(--color-primary);
|
||
|
|
color: var(--color-on-primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-primary:hover {
|
||
|
|
background: var(--color-primary-hover);
|
||
|
|
color: var(--color-on-primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-secondary {
|
||
|
|
background: transparent;
|
||
|
|
border: 1px solid var(--color-border);
|
||
|
|
color: var(--color-text);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-secondary:hover {
|
||
|
|
border-color: var(--color-primary);
|
||
|
|
color: var(--color-primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-join {
|
||
|
|
background: var(--color-success);
|
||
|
|
color: var(--color-on-primary);
|
||
|
|
border: none;
|
||
|
|
padding: 0.75rem 1.5rem;
|
||
|
|
border-radius: 8px;
|
||
|
|
font-weight: 600;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-join:hover {
|
||
|
|
background: var(--color-success-hover);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-join:disabled {
|
||
|
|
background: var(--color-neutral);
|
||
|
|
cursor: default;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-leave {
|
||
|
|
background: var(--color-error);
|
||
|
|
color: var(--color-on-primary);
|
||
|
|
border: none;
|
||
|
|
padding: 0.75rem 1.5rem;
|
||
|
|
border-radius: 8px;
|
||
|
|
font-weight: 600;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-leave:hover {
|
||
|
|
background: var(--color-error-hover);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-edit {
|
||
|
|
background: var(--color-info);
|
||
|
|
color: var(--color-on-primary);
|
||
|
|
border: none;
|
||
|
|
padding: 0.75rem 1.5rem;
|
||
|
|
border-radius: 8px;
|
||
|
|
font-weight: 600;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-edit:hover {
|
||
|
|
background: var(--color-info-hover);
|
||
|
|
}
|
||
|
|
|
||
|
|
.edit-modal {
|
||
|
|
position: fixed;
|
||
|
|
inset: 0;
|
||
|
|
background: var(--color-overlay);
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
z-index: 1000;
|
||
|
|
}
|
||
|
|
|
||
|
|
.modal-content {
|
||
|
|
background: var(--color-surface);
|
||
|
|
border: 1px solid var(--color-border);
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 2rem;
|
||
|
|
width: 100%;
|
||
|
|
max-width: 500px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.modal-content h2 {
|
||
|
|
margin-bottom: 1.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.modal-content .form-group {
|
||
|
|
margin-bottom: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.modal-content label {
|
||
|
|
display: block;
|
||
|
|
font-size: 0.875rem;
|
||
|
|
margin-bottom: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.modal-content input, .modal-content textarea {
|
||
|
|
width: 100%;
|
||
|
|
padding: 0.75rem;
|
||
|
|
border: 1px solid var(--color-border);
|
||
|
|
border-radius: 8px;
|
||
|
|
background: var(--color-bg);
|
||
|
|
color: var(--color-text);
|
||
|
|
font-family: inherit;
|
||
|
|
}
|
||
|
|
|
||
|
|
.modal-actions {
|
||
|
|
display: flex;
|
||
|
|
gap: 1rem;
|
||
|
|
justify-content: flex-end;
|
||
|
|
margin-top: 1.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-cancel {
|
||
|
|
background: transparent;
|
||
|
|
border: 1px solid var(--color-border);
|
||
|
|
color: var(--color-text);
|
||
|
|
padding: 0.75rem 1.5rem;
|
||
|
|
border-radius: 8px;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-save {
|
||
|
|
background: var(--color-primary);
|
||
|
|
color: var(--color-on-primary);
|
||
|
|
border: none;
|
||
|
|
padding: 0.75rem 1.5rem;
|
||
|
|
border-radius: 8px;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sections {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: 1fr 1fr;
|
||
|
|
gap: 2rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.sections {
|
||
|
|
grid-template-columns: 1fr;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.section {
|
||
|
|
background: var(--color-surface);
|
||
|
|
border: 1px solid var(--color-border);
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 1.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.section h2 {
|
||
|
|
font-size: 1.125rem;
|
||
|
|
margin-bottom: 1rem;
|
||
|
|
padding-bottom: 0.75rem;
|
||
|
|
border-bottom: 1px solid var(--color-border);
|
||
|
|
}
|
||
|
|
|
||
|
|
.proposals-list {
|
||
|
|
display: grid;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.proposal-item {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
padding: 0.75rem;
|
||
|
|
background: var(--color-bg);
|
||
|
|
border-radius: 6px;
|
||
|
|
color: var(--color-text);
|
||
|
|
}
|
||
|
|
|
||
|
|
.proposal-item:hover {
|
||
|
|
background: var(--color-border);
|
||
|
|
}
|
||
|
|
|
||
|
|
.proposal-title {
|
||
|
|
font-weight: 500;
|
||
|
|
}
|
||
|
|
|
||
|
|
.proposal-status {
|
||
|
|
font-size: 0.625rem;
|
||
|
|
padding: 0.125rem 0.5rem;
|
||
|
|
border-radius: 999px;
|
||
|
|
text-transform: uppercase;
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-draft { background: var(--color-neutral-muted); color: var(--color-on-primary); }
|
||
|
|
.status-discussion { background: var(--color-info); color: var(--color-on-primary); }
|
||
|
|
.status-voting { background: var(--color-success); color: var(--color-on-primary); }
|
||
|
|
.status-closed { background: var(--color-neutral); color: var(--color-on-primary); }
|
||
|
|
|
||
|
|
.loading, .error {
|
||
|
|
text-align: center;
|
||
|
|
padding: 3rem;
|
||
|
|
color: var(--color-text-muted);
|
||
|
|
}
|
||
|
|
|
||
|
|
.loading-small, .empty-small, .error-small {
|
||
|
|
color: var(--color-text-muted);
|
||
|
|
font-size: 0.875rem;
|
||
|
|
padding: 1rem;
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mod-log {
|
||
|
|
display: grid;
|
||
|
|
gap: 0.75rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mod-entry {
|
||
|
|
background: var(--color-bg);
|
||
|
|
border-radius: 6px;
|
||
|
|
padding: 0.75rem;
|
||
|
|
font-size: 0.875rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mod-action {
|
||
|
|
font-weight: 600;
|
||
|
|
color: var(--color-primary);
|
||
|
|
margin-bottom: 0.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mod-details {
|
||
|
|
color: var(--color-text-muted);
|
||
|
|
font-size: 0.75rem;
|
||
|
|
margin-bottom: 0.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mod-reason {
|
||
|
|
color: var(--color-text);
|
||
|
|
}
|
||
|
|
|
||
|
|
.mod-time {
|
||
|
|
color: var(--color-text-muted);
|
||
|
|
font-size: 0.625rem;
|
||
|
|
margin-top: 0.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.full-width {
|
||
|
|
grid-column: 1 / -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.members-list {
|
||
|
|
display: grid;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.member-item {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
padding: 0.5rem 0;
|
||
|
|
border-bottom: 1px solid var(--color-border);
|
||
|
|
text-decoration: none;
|
||
|
|
color: var(--color-text);
|
||
|
|
}
|
||
|
|
|
||
|
|
.member-item:hover .member-name {
|
||
|
|
color: var(--color-primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.member-item:last-child {
|
||
|
|
border-bottom: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.member-name {
|
||
|
|
font-weight: 500;
|
||
|
|
}
|
||
|
|
|
||
|
|
.member-role {
|
||
|
|
font-size: 0.625rem;
|
||
|
|
padding: 0.125rem 0.5rem;
|
||
|
|
border-radius: 999px;
|
||
|
|
text-transform: uppercase;
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.role-admin { background: var(--color-secondary); color: var(--color-on-primary); }
|
||
|
|
.role-moderator { background: var(--color-info); color: var(--color-on-primary); }
|
||
|
|
.role-member { background: var(--color-neutral-muted); color: var(--color-on-primary); }
|
||
|
|
</style>
|