Sprint 11b: Wiki-Foto-Einreichungen + Wikipedia-Foto-Scraper
- User können Fotos für Rassen vorschlagen (Upload-Modal in Rassen-Detail) - Mod/Admin-Review-Tab im Wiki mit Freischalten/Ablehnen + Push-Notification - wikipedia_photos.py: holt Fotos über Wikidata-QID → Wikipedia-API - Foto-Status: 578 lokal, 186 extern, 238 ohne Foto - DB: wiki_foto_submissions Tabelle - SW by-v90
This commit is contained in:
parent
097295c628
commit
32d630d5a1
6 changed files with 598 additions and 3 deletions
|
|
@ -94,12 +94,15 @@ window.Page_wiki = (() => {
|
|||
// RENDER
|
||||
// ----------------------------------------------------------
|
||||
async function _render() {
|
||||
const isMod = _appState.user && (_appState.user.is_moderator || _appState.user.rolle === 'admin');
|
||||
|
||||
_container.innerHTML = `
|
||||
<div class="wiki-tab-bar" id="wiki-tab-bar">
|
||||
<button class="wiki-tab-btn${_tab === 'rassen' ? ' active' : ''}" data-tab="rassen">${UI.icon('dog')} Rassen</button>
|
||||
<button class="wiki-tab-btn${_tab === 'gesundheit'? ' active' : ''}" data-tab="gesundheit">${UI.icon('syringe')} Gesundheit</button>
|
||||
<button class="wiki-tab-btn${_tab === 'recht' ? ' active' : ''}" data-tab="recht">${UI.icon('handshake')} Recht</button>
|
||||
<button class="wiki-tab-btn${_tab === 'quiz' ? ' active' : ''}" data-tab="quiz">${UI.icon('star')} Quiz</button>
|
||||
${isMod ? `<button class="wiki-tab-btn${_tab === 'fotos' ? ' active' : ''}" data-tab="fotos" id="wiki-fotos-tab">${UI.icon('camera')} Fotos <span id="wiki-fotos-badge" style="display:none" class="badge badge-sm">0</span></button>` : ''}
|
||||
</div>
|
||||
<div id="wiki-content"></div>
|
||||
`;
|
||||
|
|
@ -122,6 +125,97 @@ window.Page_wiki = (() => {
|
|||
else if (_tab === 'gesundheit') _renderGesundheit(content);
|
||||
else if (_tab === 'recht') _renderRecht(content);
|
||||
else if (_tab === 'quiz') _renderQuiz(content);
|
||||
else if (_tab === 'fotos') await _renderFotoSubmissions(content);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// TAB: Foto-Einreichungen (Mod/Admin)
|
||||
// ----------------------------------------------------------
|
||||
async function _renderFotoSubmissions(el) {
|
||||
el.innerHTML = `<div style="padding:var(--space-4)">${UI.skeleton(3)}</div>`;
|
||||
let subs;
|
||||
try {
|
||||
subs = await _apiFetch('/api/wiki/foto-submissions');
|
||||
} catch (e) {
|
||||
el.innerHTML = `<div class="empty-state"><p>${_esc(e.message)}</p></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Badge updaten
|
||||
const badge = document.getElementById('wiki-fotos-badge');
|
||||
if (badge) { badge.textContent = subs.length; badge.style.display = subs.length ? '' : 'none'; }
|
||||
|
||||
if (!subs.length) {
|
||||
el.innerHTML = `
|
||||
<div class="empty-state" style="padding:var(--space-10)">
|
||||
${UI.icon('check')}
|
||||
<p style="margin-top:var(--space-3);color:var(--c-text-muted)">Keine ausstehenden Foto-Einreichungen.</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="padding:var(--space-4)">
|
||||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);margin-bottom:var(--space-4)">
|
||||
Ausstehende Fotos (${subs.length})
|
||||
</h3>
|
||||
<div id="wiki-subs-list">
|
||||
${subs.map(s => `
|
||||
<div class="card" style="margin-bottom:var(--space-3);padding:var(--space-3)" id="wiki-sub-${s.id}">
|
||||
<div style="display:flex;gap:var(--space-3);align-items:flex-start">
|
||||
<img src="${_esc(s.foto_url)}" alt=""
|
||||
style="width:100px;height:80px;object-fit:cover;border-radius:var(--radius-md);flex-shrink:0;background:var(--c-surface-2)">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:var(--weight-semibold)">${_esc(s.rasse_name)}</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">
|
||||
von ${_esc(s.user_name)} · ${_formatDate(s.created_at)}
|
||||
</div>
|
||||
${s.aktuell_foto
|
||||
? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:4px">
|
||||
Aktuelles Foto: <img src="${_esc(s.aktuell_foto)}" style="height:20px;vertical-align:middle;border-radius:2px">
|
||||
</div>`
|
||||
: `<div style="font-size:var(--text-xs);color:var(--c-warning,#e8a000);margin-top:4px">Kein Foto vorhanden</div>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3)">
|
||||
<button class="btn btn-primary btn-sm flex-1"
|
||||
onclick="Page_wiki._approveSubmission(${s.id})">
|
||||
${UI.icon('check')} Freischalten
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm flex-1"
|
||||
onclick="Page_wiki._rejectSubmission(${s.id})">
|
||||
${UI.icon('x')} Ablehnen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function _approveSubmission(id) {
|
||||
try {
|
||||
await _apiPatch(`/api/wiki/foto-submissions/${id}`, { action: 'approve' });
|
||||
document.getElementById(`wiki-sub-${id}`)?.remove();
|
||||
UI.toast('Foto freigeschaltet!', 'success');
|
||||
const badge = document.getElementById('wiki-fotos-badge');
|
||||
if (badge) {
|
||||
const n = Math.max(0, parseInt(badge.textContent || '0') - 1);
|
||||
badge.textContent = n; badge.style.display = n ? '' : 'none';
|
||||
}
|
||||
} catch (e) { UI.toast(e.message, 'danger'); }
|
||||
}
|
||||
|
||||
async function _rejectSubmission(id) {
|
||||
const reason = prompt('Ablehnungsgrund (optional):') ?? null;
|
||||
if (reason === null) return; // Abbrechen
|
||||
try {
|
||||
await _apiPatch(`/api/wiki/foto-submissions/${id}`, { action: 'reject', reject_reason: reason });
|
||||
document.getElementById(`wiki-sub-${id}`)?.remove();
|
||||
UI.toast('Einreichung abgelehnt.', 'info');
|
||||
} catch (e) { UI.toast(e.message, 'danger'); }
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -366,6 +460,12 @@ window.Page_wiki = (() => {
|
|||
<a href="#settings" style="color:var(--c-primary)">Anmelden</a>, um einen Bericht zu schreiben.
|
||||
</p>`
|
||||
}
|
||||
${_appState.user ? `
|
||||
<div style="margin-top:var(--space-4);padding-top:var(--space-4);border-top:1px solid var(--c-border-light)">
|
||||
<button class="btn btn-ghost w-full" id="wiki-foto-submit-btn" style="font-size:var(--text-sm)">
|
||||
${UI.icon('camera')} ${rasse.foto_url ? 'Besseres Foto vorschlagen' : 'Foto hinzufügen'}
|
||||
</button>
|
||||
</div>` : ''}
|
||||
`;
|
||||
|
||||
UI.modal.open({ title: _esc(rasse.name), body });
|
||||
|
|
@ -374,6 +474,87 @@ window.Page_wiki = (() => {
|
|||
UI.modal.close();
|
||||
setTimeout(() => _showBerichtForm(slug, rasse.name), 350);
|
||||
});
|
||||
|
||||
document.getElementById('wiki-foto-submit-btn')?.addEventListener('click', () => {
|
||||
UI.modal.close();
|
||||
setTimeout(() => _showFotoSubmitForm(slug, rasse.name), 350);
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Foto vorschlagen
|
||||
// ----------------------------------------------------------
|
||||
function _showFotoSubmitForm(slug, rasseName) {
|
||||
const body = `
|
||||
<p style="color:var(--c-text-secondary);font-size:var(--text-sm);margin-bottom:var(--space-4)">
|
||||
Dein Foto wird nach einer kurzen Prüfung freigeschaltet und als Hauptbild im Wiki verwendet.
|
||||
</p>
|
||||
<form id="wiki-foto-form" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Foto von <strong>${_esc(rasseName)}</strong></label>
|
||||
<input class="form-control" type="file" id="wiki-foto-input"
|
||||
accept="image/jpeg,image/png,image/webp" required>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:4px">
|
||||
JPG, PNG oder WebP · max. 8 MB · möglichst hochauflösend
|
||||
</div>
|
||||
</div>
|
||||
<div id="wiki-foto-preview" style="margin-top:var(--space-3);display:none">
|
||||
<img id="wiki-foto-preview-img" style="max-width:100%;max-height:200px;border-radius:var(--radius-md);object-fit:contain">
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
const footer = `
|
||||
<button type="button" class="btn btn-secondary flex-1" id="wiki-foto-cancel">Abbrechen</button>
|
||||
<button type="submit" form="wiki-foto-form" class="btn btn-primary flex-1" id="wiki-foto-submit">
|
||||
${UI.icon('paper-plane-tilt')} Einreichen
|
||||
</button>
|
||||
`;
|
||||
|
||||
UI.modal.open({ title: 'Foto vorschlagen', body, footer });
|
||||
document.getElementById('wiki-foto-cancel')?.addEventListener('click', UI.modal.close);
|
||||
|
||||
document.getElementById('wiki-foto-input')?.addEventListener('change', e => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const preview = document.getElementById('wiki-foto-preview');
|
||||
const img = document.getElementById('wiki-foto-preview-img');
|
||||
const url = URL.createObjectURL(file);
|
||||
img.src = url;
|
||||
preview.style.display = '';
|
||||
});
|
||||
|
||||
document.getElementById('wiki-foto-form')?.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const input = document.getElementById('wiki-foto-input');
|
||||
const file = input?.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const btn = document.getElementById('wiki-foto-submit');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Wird hochgeladen…';
|
||||
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
const token = localStorage.getItem('by_token');
|
||||
const resp = await fetch(`/api/wiki/rassen/${encodeURIComponent(slug)}/foto`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
||||
body: fd,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Upload fehlgeschlagen');
|
||||
}
|
||||
UI.modal.close();
|
||||
UI.toast('Danke! Dein Foto wird geprüft und dann veröffentlicht.', 'success');
|
||||
} catch (err) {
|
||||
UI.toast(err.message, 'danger');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = `${UI.icon('paper-plane-tilt')} Einreichen`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _renderBerichteHtml(berichte, slug) {
|
||||
|
|
@ -637,6 +818,21 @@ window.Page_wiki = (() => {
|
|||
return resp.json();
|
||||
}
|
||||
|
||||
async function _apiPatch(url, body) {
|
||||
const token = localStorage.getItem('by_token');
|
||||
const resp = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json', ...(token ? { 'Authorization': `Bearer ${token}` } : {}) },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.detail || `HTTP ${resp.status}`);
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function _apiPost(url, body) {
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
|
|
@ -682,6 +878,6 @@ window.Page_wiki = (() => {
|
|||
// ----------------------------------------------------------
|
||||
// PUBLIC
|
||||
// ----------------------------------------------------------
|
||||
return { init, refresh };
|
||||
return { init, refresh, _approveSubmission, _rejectSubmission };
|
||||
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue