Admin: Moderation-Tab für Züchter + Wiki-Fotos (SW by-v280)
This commit is contained in:
parent
180de32e57
commit
2b442ebd98
3 changed files with 145 additions and 16 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '267'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '268'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
|
|
||||||
const App = (() => {
|
const App = (() => {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ window.Page_admin = (() => {
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ id: 'uebersicht', label: 'Übersicht', icon: 'house-line' },
|
{ id: 'uebersicht', label: 'Übersicht', icon: 'house-line' },
|
||||||
{ id: 'nutzer', label: 'Nutzer', icon: 'users' },
|
{ id: 'nutzer', label: 'Nutzer', icon: 'users' },
|
||||||
|
{ id: 'moderation', label: 'Moderation', icon: 'shield-check' },
|
||||||
{ id: 'forum', label: 'Forum & Meldungen', icon: 'chat-circle-dots' },
|
{ id: 'forum', label: 'Forum & Meldungen', icon: 'chat-circle-dots' },
|
||||||
{ id: 'analytics', label: 'Analytics', icon: 'target' },
|
{ id: 'analytics', label: 'Analytics', icon: 'target' },
|
||||||
{ id: 'system', label: 'System', icon: 'gear' },
|
{ id: 'system', label: 'System', icon: 'gear' },
|
||||||
|
|
@ -76,6 +77,7 @@ window.Page_admin = (() => {
|
||||||
switch (_tab) {
|
switch (_tab) {
|
||||||
case 'uebersicht': await _renderStats(el); break;
|
case 'uebersicht': await _renderStats(el); break;
|
||||||
case 'nutzer': await _renderUsers(el); break;
|
case 'nutzer': await _renderUsers(el); break;
|
||||||
|
case 'moderation': await _renderModeration(el); break;
|
||||||
case 'forum': await _renderForum(el); break;
|
case 'forum': await _renderForum(el); break;
|
||||||
case 'analytics': await _renderAnalytics(el); break;
|
case 'analytics': await _renderAnalytics(el); break;
|
||||||
case 'system': await _renderSystem(el); break;
|
case 'system': await _renderSystem(el); break;
|
||||||
|
|
@ -737,6 +739,133 @@ window.Page_admin = (() => {
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// TAB: JOBS
|
// TAB: JOBS
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// TAB: MODERATION
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
async function _renderModeration(el) {
|
||||||
|
el.innerHTML = `
|
||||||
|
<div style="display:flex;justify-content:flex-end;margin-bottom:var(--space-3)">
|
||||||
|
<button class="btn btn-ghost btn-sm" id="adm-mod-refresh">${UI.icon('arrows-clockwise')} Aktualisieren</button>
|
||||||
|
</div>
|
||||||
|
<div id="adm-mod-content">Lade…</div>
|
||||||
|
`;
|
||||||
|
el.querySelector('#adm-mod-refresh').addEventListener('click', () => _loadModeration(el.querySelector('#adm-mod-content')));
|
||||||
|
await _loadModeration(el.querySelector('#adm-mod-content'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _loadModeration(el) {
|
||||||
|
el.innerHTML = `<div style="padding:var(--space-4);text-align:center;color:var(--c-text-muted)">Lade…</div>`;
|
||||||
|
|
||||||
|
const [zuchter, fotos] = await Promise.all([
|
||||||
|
API.get('/wiki/zuchter/pending').catch(() => []),
|
||||||
|
API.get('/wiki/foto-submissions').catch(() => []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// --- Züchter-Einreichungen ---
|
||||||
|
html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||||
|
color:var(--c-text-muted);text-transform:uppercase;letter-spacing:.06em;
|
||||||
|
margin-bottom:var(--space-3)">
|
||||||
|
Züchter-Einreichungen
|
||||||
|
<span style="background:var(--c-primary);color:#fff;border-radius:999px;
|
||||||
|
padding:1px 8px;font-size:var(--text-xs);margin-left:6px">${zuchter.length}</span>
|
||||||
|
</h3>`;
|
||||||
|
|
||||||
|
if (!zuchter.length) {
|
||||||
|
html += `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-bottom:var(--space-6)">Keine ausstehenden Einreichungen.</p>`;
|
||||||
|
} else {
|
||||||
|
html += `<div class="card adm-table-card" style="margin-bottom:var(--space-6)"><div class="adm-table-scroll"><table class="adm-table">
|
||||||
|
<thead><tr style="background:var(--c-surface-2);text-align:left">
|
||||||
|
<th class="adm-th">Rasse</th><th class="adm-th">Name / Zwingername</th>
|
||||||
|
<th class="adm-th">Ort</th><th class="adm-th">VDH</th><th class="adm-th">Website</th><th class="adm-th"></th>
|
||||||
|
</tr></thead><tbody>
|
||||||
|
${zuchter.map((z, i) => `
|
||||||
|
<tr style="${i%2===1?'background:var(--c-surface-2)':''}">
|
||||||
|
<td class="adm-td" style="font-weight:var(--weight-semibold)">${_esc(z.rasse_slug)}</td>
|
||||||
|
<td class="adm-td">${_esc(z.name)}${z.zwingername ? `<br><span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(z.zwingername)}</span>` : ''}</td>
|
||||||
|
<td class="adm-td">${_esc([z.plz, z.ort, z.bundesland].filter(Boolean).join(' '))}</td>
|
||||||
|
<td class="adm-td">${z.vdh_mitglied ? '<span style="color:var(--c-success)">✓ VDH</span>' : '—'}</td>
|
||||||
|
<td class="adm-td">${z.website ? `<a href="${_esc(z.website)}" target="_blank" style="color:var(--c-primary);font-size:var(--text-xs)">Link</a>` : '—'}</td>
|
||||||
|
<td class="adm-td" style="text-align:right;white-space:nowrap">
|
||||||
|
<button class="btn btn-sm btn-primary adm-zuchter-approve" data-id="${z.id}" style="margin-right:4px">✓ Freigeben</button>
|
||||||
|
<button class="btn btn-sm btn-ghost adm-zuchter-delete" data-id="${z.id}" style="color:var(--c-danger)">✗</button>
|
||||||
|
</td>
|
||||||
|
</tr>`).join('')}
|
||||||
|
</tbody></table></div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Wiki-Foto-Einreichungen ---
|
||||||
|
html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||||
|
color:var(--c-text-muted);text-transform:uppercase;letter-spacing:.06em;
|
||||||
|
margin-bottom:var(--space-3)">
|
||||||
|
Wiki-Foto-Einreichungen
|
||||||
|
<span style="background:var(--c-primary);color:#fff;border-radius:999px;
|
||||||
|
padding:1px 8px;font-size:var(--text-xs);margin-left:6px">${fotos.length}</span>
|
||||||
|
</h3>`;
|
||||||
|
|
||||||
|
if (!fotos.length) {
|
||||||
|
html += `<p style="font-size:var(--text-sm);color:var(--c-text-muted)">Keine ausstehenden Foto-Einreichungen.</p>`;
|
||||||
|
} else {
|
||||||
|
html += `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:var(--space-4)">
|
||||||
|
${fotos.map(f => `
|
||||||
|
<div class="card" style="padding:var(--space-4)">
|
||||||
|
<img src="${_esc(f.foto_url)}" alt=""
|
||||||
|
style="width:100%;height:140px;object-fit:cover;border-radius:var(--radius-md);margin-bottom:var(--space-3)">
|
||||||
|
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">${_esc(f.rasse_name)}</div>
|
||||||
|
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-3)">von ${_esc(f.user_name)}</div>
|
||||||
|
${f.aktuell_foto ? `<img src="${_esc(f.aktuell_foto)}" alt="Aktuell"
|
||||||
|
style="width:100%;height:80px;object-fit:cover;border-radius:var(--radius-sm);
|
||||||
|
opacity:.5;margin-bottom:var(--space-2)">
|
||||||
|
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-3)">↑ aktuelles Foto</div>` : ''}
|
||||||
|
<div style="display:flex;gap:var(--space-2)">
|
||||||
|
<button class="btn btn-sm btn-primary adm-foto-approve" data-id="${f.id}" style="flex:1">✓</button>
|
||||||
|
<button class="btn btn-sm btn-ghost adm-foto-reject" data-id="${f.id}" style="color:var(--c-danger)">✗</button>
|
||||||
|
</div>
|
||||||
|
</div>`).join('')}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.innerHTML = html;
|
||||||
|
|
||||||
|
// Züchter freigeben
|
||||||
|
el.querySelectorAll('.adm-zuchter-approve').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
btn.disabled = true;
|
||||||
|
await API.patch(`/wiki/zuchter/${btn.dataset.id}/verify`, {});
|
||||||
|
await _loadModeration(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Züchter löschen
|
||||||
|
el.querySelectorAll('.adm-zuchter-delete').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
if (!window.confirm('Eintrag löschen?')) return;
|
||||||
|
btn.disabled = true;
|
||||||
|
await API.delete(`/admin/wiki/zuchter/${btn.dataset.id}`);
|
||||||
|
await _loadModeration(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Foto freigeben
|
||||||
|
el.querySelectorAll('.adm-foto-approve').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
btn.disabled = true;
|
||||||
|
await API.patch(`/wiki/foto-submissions/${btn.dataset.id}`, {action: 'approve'});
|
||||||
|
await _loadModeration(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Foto ablehnen
|
||||||
|
el.querySelectorAll('.adm-foto-reject').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
btn.disabled = true;
|
||||||
|
await API.patch(`/wiki/foto-submissions/${btn.dataset.id}`, {action: 'reject', reject_reason: 'Nicht geeignet.'});
|
||||||
|
await _loadModeration(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
async function _renderJobs(el) {
|
async function _renderJobs(el) {
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v279';
|
const CACHE_VERSION = 'by-v280';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue