PHASE 1 — Sofort-Cleanup ohne Risiko: - Neue Datei utilities.css mit ~25 Klassen für häufige Kombinationen: * text-xs-muted, text-xs-secondary, text-sm-muted, text-sm-secondary * flex-gap-2/3, flex-col-gap-2/3/4, flex-center-gap-1/2/3 * flex-between, flex-1-min, mb-1/3, mt-1/3 * icon-xs/sm/md/lg, label-block, caption - index.html bindet utilities.css ein - mb-3/mt-3 ergänzt (waren in design-system.css unvollständig) PHASE 2 — .by-tab Modifier für Vereinheitlichung: - .by-tabs.grid (mit --tab-cols Variable für Admin/Health/etc.) - .by-tabs.sticky (Desktop vertikale Tabs für Admin) - .by-tabs.wrap (Zuchthunde, flex-wrap statt scroll) - .by-tabs.separated (Sitting, mit eigenem Hintergrund + Border) PHASE 3 — Inline-Style → Klassen-Migration (Python-Script): - 948 Inline-Styles entfernt (5101 → 4153, -18%) - 962 Migrationen über 47 Page-Dateien - Top-Treffer: admin.js (180), health.js (67), dog-profile.js (67), litters.js (62), settings.js (61), zuchthunde.js (51) - Patterns: text-muted, text-secondary, text-danger, text-xs-muted, text-sm-muted, grid-2 (Duplikat-Bug behoben!), flex-col-gap-3, p-3/4, mb-2/3/4, hidden, w-full, flex-1, ... - Bewahrt bestehende class-Attribute (mergt korrekt) Alle 19 Tests grün. Kein visueller Diff erwartet (gleiche Property-Werte).
1473 lines
64 KiB
JavaScript
1473 lines
64 KiB
JavaScript
/* ============================================================
|
||
BAN YARO — Hunde-Wiki
|
||
Rassen-Datenbank, Gesundheit, Recht, Quiz
|
||
============================================================ */
|
||
|
||
window.Page_wiki = (() => {
|
||
|
||
// ----------------------------------------------------------
|
||
// MODUL-STATE
|
||
// ----------------------------------------------------------
|
||
let _container = null;
|
||
let _appState = null;
|
||
let _tab = 'rassen';
|
||
let _rassen = [];
|
||
let _gruppen = [];
|
||
let _totalBreeds = 0;
|
||
let _currentOffset = 0;
|
||
const PAGE_SIZE = 30;
|
||
let _currentSearch = '';
|
||
let _currentGruppe = '';
|
||
let _quizAnswers = {};
|
||
let _quizStep = 0;
|
||
|
||
// ----------------------------------------------------------
|
||
// HARDCODED: Gesundheits-Inhalte
|
||
// ----------------------------------------------------------
|
||
const GESUNDHEIT = [
|
||
{
|
||
titel: 'Zecken & FSME',
|
||
icon: 'skull',
|
||
text: 'Zecken sind von März bis November aktiv (Spitze April–Juni, September–Oktober). Täglich nach Gassi auf Zecken untersuchen — besonders Ohren, Achseln, Leiste.\n\nZecke entfernen: Zeckenzange ansetzen, nicht drehen, gerade herausziehen. KEINE Öle/Vaseline.\n\nFSME: Impfung für Menschen empfohlen in Risikogebieten (RKI-Karte: rki.de/fsme). Hunde können Borreliose bekommen — Impfung empfohlen.',
|
||
},
|
||
{
|
||
titel: 'Vergiftungen — Sofortmaßnahmen',
|
||
icon: 'skull',
|
||
text: 'Verdacht auf Vergiftung: Sofort Tierarzt oder Tiergiftzentrale (Berlin: 030 19240, München: 089 19240, Wien: 01 4064343).\n\nNICHT versuchen den Hund zum Erbrechen zu bringen ohne tierärztlichen Rat.\n\nHäufige Giftquellen: Schokolade, Trauben/Rosinen, Zwiebeln, Xylitol (Süßungsmittel), Ibuprofen.',
|
||
},
|
||
{
|
||
titel: 'Hitzschlag',
|
||
icon: 'warning',
|
||
text: 'Symptome: Starkes Hecheln, Speichelfluss, taumeln, Kollaps.\n\nSofortmaßnahme: In den Schatten, mit lauwarmem (nicht kaltem!) Wasser abkühlen, sofort zum Tierarzt.\n\nHunde NIEMALS im Auto lassen.',
|
||
},
|
||
{
|
||
titel: 'Erste Hilfe Grundlagen',
|
||
icon: 'first-aid',
|
||
text: 'Bewusstloser Hund: Atemwege frei? Atemkontrolle.\n\nHerzdruckmassage: 100–120/min, 1/3 Brusttiefe.\n\nBeatmung: Maul zu, in Nase blasen.\n\nBlutung: Druckverband.\n\nKnochenbruch: Immobilisieren, tragen.',
|
||
},
|
||
];
|
||
|
||
// ----------------------------------------------------------
|
||
// HARDCODED: Recht & Regeln
|
||
// ----------------------------------------------------------
|
||
const RECHT = [
|
||
{ land: 'Bayern', leine: 'Anleinpflicht im Wald und in Ortschaften', rasse: 'Keine allgemeine Rasseliste (Gefährlichkeitsfeststellung individuell)', steuer: '~100€/Jahr (variiert nach Gemeinde)' },
|
||
{ land: 'Baden-Württemberg', leine: 'Leinenpflicht in Ortschaften und Parks', rasse: 'American Pitbull Terrier, American Staffordshire Terrier u.a.', steuer: '~100–150€/Jahr' },
|
||
{ land: 'Berlin', leine: 'Allgemeine Leinenpflicht in öffentlichen Anlagen', rasse: 'Pitbull, Staffordshire, Rottweiler (bedingt)', steuer: '~120€/Jahr (ab 2. Hund: 180€)' },
|
||
{ land: 'Brandenburg', leine: 'Leinenpflicht in Ortschaften und Wäldern April–Juli', rasse: 'Pitbull, American Staffordshire u.a.', steuer: '~60–100€/Jahr' },
|
||
{ land: 'Hamburg', leine: 'Allgemeine Leinenpflicht', rasse: 'Pitbull, Rottweiler, Staffordshire u.a.', steuer: '~90€/Jahr (Kampfhund: 900€)' },
|
||
{ land: 'Hessen', leine: 'Anleinpflicht in Ortschaften', rasse: 'Pitbull u.a. (Liste)', steuer: '~75–120€/Jahr' },
|
||
{ land: 'NRW', leine: 'Leinenpflicht in bebauten Gebieten', rasse: 'Pitbull, American Staffordshire, Staffordshire Bull Terrier u.a.', steuer: '~100–160€/Jahr' },
|
||
{ land: 'Niedersachsen', leine: 'Anleinpflicht in Ortschaften', rasse: 'Pitbull u.a.', steuer: '~60–100€/Jahr' },
|
||
{ land: 'Sachsen', leine: 'Leinenpflicht in Ortschaften und öffentl. Anlagen', rasse: 'Keine staatliche Liste (kommunal)', steuer: '~50–100€/Jahr' },
|
||
{ land: 'Thüringen', leine: 'Anleinpflicht in Wäldern und Ortschaften', rasse: 'Pitbull u.a.', steuer: '~60–100€/Jahr' },
|
||
];
|
||
|
||
// ----------------------------------------------------------
|
||
// QUIZ: Fragen
|
||
// ----------------------------------------------------------
|
||
const QUIZ_FRAGEN = [
|
||
{ key: 'groesse', frage: 'Welche Größe passt zu dir?', optionen: [{val:'klein', label:'Klein (unter 10 kg)'}, {val:'mittel', label:'Mittel (10–30 kg)'}, {val:'gross', label:'Groß (über 30 kg)'}] },
|
||
{ key: 'aktivitaet', frage: 'Wie aktiv bist du?', optionen: [{val:'niedrig', label:'Eher gemütlich'}, {val:'mittel', label:'Regelmäßige Spaziergänge'}, {val:'hoch', label:'Sehr sportlich'}] },
|
||
{ key: 'erfahrung', frage: 'Wie viel Hundeerfahrung hast du?', optionen: [{val:'anfaenger', label:'Ersthundehalter'}, {val:'fortgeschritten', label:'Erfahren'}, {val:'experte', label:'Profi'}] },
|
||
{ key: 'kinder', frage: 'Lebst du mit Kindern zusammen?', optionen: [{val:'true', label:'Ja'}, {val:'false', label:'Nein'}] },
|
||
{ key: 'wohnung', frage: 'Wo wohnst du?', optionen: [{val:'true', label:'Wohnung (ohne Garten)'}, {val:'false', label:'Haus mit Garten'}] },
|
||
];
|
||
|
||
// ----------------------------------------------------------
|
||
// INIT
|
||
// ----------------------------------------------------------
|
||
async function init(container, appState) {
|
||
_container = container;
|
||
_appState = appState;
|
||
await _render();
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// REFRESH
|
||
// ----------------------------------------------------------
|
||
async function refresh() {
|
||
// Wiki ist nicht hunde-spezifisch, kein Reload nötig
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// RENDER
|
||
// ----------------------------------------------------------
|
||
async function _render() {
|
||
const isMod = _appState.user && (_appState.user.is_moderator || _appState.user.rolle === 'admin');
|
||
|
||
_container.innerHTML = `
|
||
<div class="by-tabs" id="wiki-tab-bar">
|
||
<button class="by-tab${_tab === 'rassen' ? ' active' : ''}" data-tab="rassen">${UI.icon('dog')} Rassen</button>
|
||
<button class="by-tab${_tab === 'gesundheit'? ' active' : ''}" data-tab="gesundheit">${UI.icon('syringe')} Gesundheit</button>
|
||
<button class="by-tab${_tab === 'recht' ? ' active' : ''}" data-tab="recht">${UI.icon('handshake')} Recht</button>
|
||
<button class="by-tab${_tab === 'quiz' ? ' active' : ''}" data-tab="quiz">${UI.icon('star')} Quiz</button>
|
||
${isMod ? `<button class="by-tab${_tab === 'fotos' ? ' active' : ''}" data-tab="fotos" id="wiki-fotos-tab">${UI.icon('camera')} Fotos <span id="wiki-fotos-badge" class="badge badge-sm hidden">0</span></button>` : ''}
|
||
</div>
|
||
<div id="wiki-content"></div>
|
||
`;
|
||
|
||
_container.querySelector('#wiki-tab-bar').addEventListener('click', e => {
|
||
const btn = e.target.closest('[data-tab]');
|
||
if (!btn) return;
|
||
_tab = btn.dataset.tab;
|
||
_container.querySelectorAll('.by-tab').forEach(b => b.classList.toggle('active', b.dataset.tab === _tab));
|
||
_renderTab();
|
||
});
|
||
|
||
await _renderTab();
|
||
}
|
||
|
||
async function _renderTab() {
|
||
const content = _container.querySelector('#wiki-content');
|
||
if (!content) return;
|
||
if (_tab === 'rassen') await _renderRassen(content);
|
||
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 class="p-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 class="p-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 class="flex-1-min">
|
||
<div style="font-weight:var(--weight-semibold)">${_esc(s.rasse_name)}</div>
|
||
<div class="text-xs-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);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'); }
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// TAB: Rassen
|
||
// ----------------------------------------------------------
|
||
async function _renderRassen(el) {
|
||
// Check seeding state first
|
||
let stats;
|
||
try {
|
||
stats = await _apiFetch('/api/wiki/stats');
|
||
} catch {
|
||
el.innerHTML = UI.emptyState({ icon: UI.icon('warning'), title: 'Fehler', text: 'Wiki konnte nicht geladen werden.' });
|
||
return;
|
||
}
|
||
|
||
if (!stats.seeded) {
|
||
el.innerHTML = `
|
||
<div class="wiki-loading-state">
|
||
<div class="wiki-loading-spinner"></div>
|
||
<p class="wiki-loading-text">Rassen-Datenbank wird geladen… ${UI.icon('dog')}</p>
|
||
<p class="wiki-loading-hint">Beim ersten Start werden ~170 Rassen von TheDogAPI abgerufen.</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// Reset state when re-rendering the tab fresh
|
||
_rassen = [];
|
||
_currentOffset = 0;
|
||
_currentSearch = '';
|
||
_currentGruppe = '';
|
||
|
||
el.innerHTML = `
|
||
<div class="wiki-filter-bar">
|
||
<input class="form-control wiki-search-input" type="search" id="wiki-rassen-search" placeholder="Rasse suchen…">
|
||
<select class="form-control wiki-gruppe-select" id="wiki-gruppe-select">
|
||
<option value="">Alle Gruppen</option>
|
||
</select>
|
||
</div>
|
||
<div style="padding:0 0 var(--space-3)">
|
||
<button class="btn btn-secondary w-full" id="wiki-rasse-erkennen-btn"
|
||
class="text-sm">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>
|
||
Welche Rasse ist das? — Foto analysieren
|
||
</button>
|
||
<input type="file" accept="image/jpeg,image/png,image/webp"
|
||
id="wiki-rasse-foto-input" class="hidden">
|
||
</div>
|
||
<div class="wiki-breed-grid" id="wiki-breed-grid"></div>
|
||
<div id="wiki-mehr-wrap" style="text-align:center;padding:var(--space-4) 0;display:none">
|
||
<button class="btn btn-secondary" id="wiki-mehr-btn">Mehr laden</button>
|
||
</div>
|
||
`;
|
||
|
||
// Load initial batch (also populates gruppen)
|
||
await _loadBreeds(el, true);
|
||
|
||
// Rassen-Erkennung per KI
|
||
_bindWikiRasseErkennung(el);
|
||
|
||
// Search handler with debounce
|
||
let _searchTimer;
|
||
el.querySelector('#wiki-rassen-search').addEventListener('input', e => {
|
||
clearTimeout(_searchTimer);
|
||
_searchTimer = setTimeout(() => {
|
||
_currentSearch = e.target.value;
|
||
_rassen = [];
|
||
_currentOffset = 0;
|
||
_loadBreeds(el, true);
|
||
}, 300);
|
||
});
|
||
|
||
// Gruppe filter handler
|
||
el.querySelector('#wiki-gruppe-select').addEventListener('change', e => {
|
||
_currentGruppe = e.target.value;
|
||
_rassen = [];
|
||
_currentOffset = 0;
|
||
_loadBreeds(el, true);
|
||
});
|
||
|
||
// "Mehr laden" button
|
||
el.querySelector('#wiki-mehr-btn').addEventListener('click', () => {
|
||
_loadBreeds(el, false);
|
||
});
|
||
}
|
||
|
||
async function _loadBreeds(el, reset) {
|
||
const grid = el.querySelector('#wiki-breed-grid');
|
||
const mehrWrap = el.querySelector('#wiki-mehr-wrap');
|
||
const mehrBtn = el.querySelector('#wiki-mehr-btn');
|
||
if (!grid) return;
|
||
|
||
if (reset) {
|
||
grid.innerHTML = `<div class="wiki-breeds-loading">Lade Rassen…</div>`;
|
||
}
|
||
|
||
const params = new URLSearchParams({
|
||
search: _currentSearch,
|
||
gruppe: _currentGruppe,
|
||
limit: PAGE_SIZE,
|
||
offset: _currentOffset,
|
||
});
|
||
|
||
let data;
|
||
try {
|
||
data = await _apiFetch(`/api/wiki/rassen?${params}`);
|
||
} catch {
|
||
grid.innerHTML = `<p style="color:var(--c-danger);padding:var(--space-4)">Rassen konnten nicht geladen werden.</p>`;
|
||
return;
|
||
}
|
||
|
||
// Populate Gruppen dropdown (only on first load)
|
||
if (reset && data.gruppen && data.gruppen.length) {
|
||
_gruppen = data.gruppen;
|
||
const sel = el.querySelector('#wiki-gruppe-select');
|
||
if (sel) {
|
||
// Preserve current selection
|
||
const cur = _currentGruppe;
|
||
sel.innerHTML = `<option value="">Alle Gruppen</option>` +
|
||
_gruppen.map(g => `<option value="${_esc(g)}"${g === cur ? ' selected' : ''}>${_esc(g)}</option>`).join('');
|
||
}
|
||
}
|
||
|
||
if (reset) {
|
||
_rassen = data.breeds;
|
||
_totalBreeds = data.total;
|
||
grid.innerHTML = '';
|
||
} else {
|
||
_rassen = _rassen.concat(data.breeds);
|
||
_currentOffset += data.breeds.length;
|
||
}
|
||
|
||
if (reset) {
|
||
_currentOffset = data.breeds.length;
|
||
}
|
||
|
||
if (reset && _rassen.length === 0) {
|
||
grid.innerHTML = `<p style="color:var(--c-text-secondary);padding:var(--space-4)">Keine Rassen gefunden.</p>`;
|
||
if (mehrWrap) mehrWrap.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// Render cards
|
||
const newCards = data.breeds.map(r => _breedCardHtml(r)).join('');
|
||
if (reset) {
|
||
grid.innerHTML = newCards;
|
||
} else {
|
||
grid.insertAdjacentHTML('beforeend', newCards);
|
||
}
|
||
|
||
// Attach click handlers to newly added cards
|
||
grid.querySelectorAll('.wiki-breed-card:not([data-bound])').forEach(card => {
|
||
card.dataset.bound = '1';
|
||
card.addEventListener('click', () => _openBreedDetail(card.dataset.slug));
|
||
});
|
||
|
||
// Show/hide "Mehr laden"
|
||
if (mehrWrap) {
|
||
const shown = _rassen.length;
|
||
mehrWrap.style.display = shown < _totalBreeds ? 'block' : 'none';
|
||
if (mehrBtn) mehrBtn.textContent = `Mehr laden (${_totalBreeds - shown} weitere)`;
|
||
}
|
||
}
|
||
|
||
const _DOG_SILHOUETTE = `<svg class="ph-icon" aria-hidden="true" style="width:2.5rem;height:2.5rem"><use href="/icons/phosphor.svg#dog"></use></svg>`;
|
||
|
||
function _breedCardHtml(r) {
|
||
const fotoUrl = r.foto_url || r.user_foto || '';
|
||
// Für lokale Bilder: _preview.webp zuerst, bei Fehler Original nachladen
|
||
const srcUrl = fotoUrl.startsWith('/media/')
|
||
? fotoUrl.replace(/\.(jpe?g|png|gif|webp)$/i, '_preview.webp')
|
||
: fotoUrl;
|
||
const photoHtml = fotoUrl
|
||
? `<img class="wiki-breed-photo" src="${_esc(srcUrl)}" loading="lazy" alt="${_esc(r.name)}"
|
||
onerror="if(this.src.includes('_preview')){this.src='${_esc(fotoUrl)}'}else{this.style.display='none';this.nextElementSibling.style.display='flex'}">`
|
||
: '';
|
||
const fallbackHtml = `<div class="wiki-breed-photo-fallback" style="${fotoUrl ? 'display:none' : ''}">${_DOG_SILHOUETTE}</div>`;
|
||
|
||
return `
|
||
<div class="wiki-breed-card" data-slug="${_esc(r.slug)}">
|
||
<div class="wiki-breed-photo-wrap">
|
||
${photoHtml}
|
||
${fallbackHtml}
|
||
</div>
|
||
<div class="wiki-breed-card-body">
|
||
<div class="wiki-breed-card-name">${_esc(r.name)}</div>
|
||
<div class="wiki-breed-card-gruppe">${_esc(r.gruppe || '—')}</div>
|
||
<div class="wiki-breed-badges">
|
||
<span class="wiki-badge-groesse wiki-badge-groesse--${_esc(r.groesse)}">${_groesseLabel(r.groesse)}</span>
|
||
<span class="wiki-badge-aktivitaet wiki-badge-aktivitaet--${_esc(r.aktivitaet)}">${_aktivLabel(r.aktivitaet)}</span>
|
||
<span class="wiki-badge-erfahrung wiki-badge-erfahrung--${_esc(r.erfahrung)}">${_erfahrungLabel(r.erfahrung)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// API-Funktionen: Interesse / Stats / Züchter
|
||
// ----------------------------------------------------------
|
||
async function _fetchStats(slug) {
|
||
const r = await fetch(`/api/wiki/rassen/${encodeURIComponent(slug)}/stats`, { credentials: 'include' });
|
||
return r.ok ? r.json() : null;
|
||
}
|
||
|
||
async function _setInteresse(slug, typ) {
|
||
const r = await fetch(`/api/wiki/rassen/${encodeURIComponent(slug)}/interesse`, {
|
||
method: 'POST', credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ typ }),
|
||
});
|
||
return r.ok ? r.json() : null;
|
||
}
|
||
|
||
async function _deleteInteresse(slug) {
|
||
const r = await fetch(`/api/wiki/rassen/${encodeURIComponent(slug)}/interesse`, {
|
||
method: 'DELETE', credentials: 'include',
|
||
});
|
||
return r.ok ? r.json() : null;
|
||
}
|
||
|
||
async function _fetchZuchter(slug) {
|
||
const r = await fetch(`/api/wiki/rassen/${encodeURIComponent(slug)}/zuchter`);
|
||
return r.ok ? r.json() : [];
|
||
}
|
||
|
||
async function _submitZuchter(data) {
|
||
const r = await fetch('/api/wiki/zuchter', {
|
||
method: 'POST', credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data),
|
||
});
|
||
return r.ok;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// Render-Helfer: Steckbrief-Grid
|
||
// ----------------------------------------------------------
|
||
function _renderSteckbriefGrid(rasse) {
|
||
const gewicht = (rasse.gewicht_min_kg && rasse.gewicht_max_kg)
|
||
? `${rasse.gewicht_min_kg}–${rasse.gewicht_max_kg} kg`
|
||
: (rasse.gewicht_max_kg ? `bis ${rasse.gewicht_max_kg} kg` : '—');
|
||
|
||
const kinderLabel = rasse.kinder_geeignet === true
|
||
? `<span class="text-success">✓ Ja</span>`
|
||
: rasse.kinder_geeignet === false
|
||
? `<span style="color:var(--c-warning)">⚡ Bedingt</span>`
|
||
: '—';
|
||
|
||
const wohnungLabel = rasse.wohnung_geeignet
|
||
? `<span class="text-success">✓ Ja</span>`
|
||
: `<span class="text-secondary">✗ Besser Garten</span>`;
|
||
|
||
const rows = [
|
||
['Größe', _groesseLabel(rasse.groesse) || '—'],
|
||
['Gewicht', gewicht],
|
||
['Lebensdauer', _esc(rasse.lebensdauer) || '—'],
|
||
['Aktivität', _aktivLabel(rasse.aktivitaet) || '—'],
|
||
['Eignung', _erfahrungLabel(rasse.erfahrung) || '—'],
|
||
['Kinder', kinderLabel],
|
||
['Wohnung', wohnungLabel],
|
||
['FCI-Gruppe', _esc(rasse.gruppe) || '—'],
|
||
];
|
||
|
||
return `
|
||
<div class="wiki-steckbrief-grid">
|
||
${rows.map(([label, val]) => `
|
||
<div class="wiki-steckbrief-item">
|
||
<span class="wiki-steckbrief-label">${label}</span>
|
||
<span class="wiki-steckbrief-value">${val}</span>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// Render-Helfer: Interesse-Section (Social)
|
||
// ----------------------------------------------------------
|
||
function _renderInteresseSection(stats, slug) {
|
||
const hatCount = stats?.dogs_count ?? '–';
|
||
const willCount = stats?.will_count ?? '–';
|
||
const interest = stats?.user_interest ?? null;
|
||
const isLoggedIn = !!_appState.user;
|
||
|
||
const hatActive = interest === 'hat';
|
||
const willActive = interest === 'will';
|
||
|
||
const hatStyle = hatActive ? `background:var(--c-primary);color:#fff;border-color:var(--c-primary)` : '';
|
||
const willStyle = willActive ? `background:var(--c-primary);color:#fff;border-color:var(--c-primary)` : '';
|
||
|
||
return `
|
||
<div class="wiki-detail-section wiki-interesse-section" id="wiki-interesse-section">
|
||
<div class="wiki-detail-label">In der Community</div>
|
||
<div class="wiki-interesse-counts" style="display:flex;gap:var(--space-4);margin-bottom:var(--space-3);font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||
<span id="wiki-hat-count">🐕 <strong>${hatCount}</strong> haben diesen Hund</span>
|
||
<span id="wiki-will-count">❤️ <strong>${willCount}</strong> möchten ihn</span>
|
||
</div>
|
||
<div class="flex-gap-2">
|
||
<button class="btn btn-sm wiki-interesse-btn" id="wiki-btn-hat"
|
||
style="flex:1;${hatStyle}"
|
||
data-slug="${_esc(slug)}" data-typ="hat">
|
||
${isLoggedIn ? '' : '🔒 '}Ich hab einen
|
||
</button>
|
||
<button class="btn btn-sm wiki-interesse-btn" id="wiki-btn-will"
|
||
style="flex:1;${willStyle}"
|
||
data-slug="${_esc(slug)}" data-typ="will">
|
||
${isLoggedIn ? '' : '🔒 '}Ich will einen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function _bindInteresseButtons(slug) {
|
||
document.querySelectorAll('.wiki-interesse-btn').forEach(btn => {
|
||
btn.addEventListener('click', async () => {
|
||
if (!_appState.user) {
|
||
UI.toast('Bitte melde dich an, um diese Funktion zu nutzen.', 'info');
|
||
return;
|
||
}
|
||
const typ = btn.dataset.typ;
|
||
const hatBtn = document.getElementById('wiki-btn-hat');
|
||
const willBtn = document.getElementById('wiki-btn-will');
|
||
|
||
// Determine current state
|
||
const isActive = btn.style.background.includes('var(--c-primary)') || btn.style.backgroundColor;
|
||
const currentActive = (hatBtn?.style.background || '').includes('var(--c-primary)') ? 'hat'
|
||
: (willBtn?.style.background || '').includes('var(--c-primary)') ? 'will' : null;
|
||
|
||
// Optimistic disable
|
||
btn.disabled = true;
|
||
try {
|
||
if (currentActive === typ) {
|
||
await _deleteInteresse(slug);
|
||
} else {
|
||
await _setInteresse(slug, typ);
|
||
}
|
||
// Reload stats and re-render counts + button states
|
||
const stats = await _fetchStats(slug);
|
||
if (stats) {
|
||
const hatCount = stats.dogs_count ?? '–';
|
||
const willCount = stats.will_count ?? '–';
|
||
const interest = stats.user_interest ?? null;
|
||
|
||
const hatEl = document.getElementById('wiki-hat-count');
|
||
const willEl = document.getElementById('wiki-will-count');
|
||
if (hatEl) hatEl.innerHTML = `🐕 <strong>${hatCount}</strong> haben diesen Hund`;
|
||
if (willEl) willEl.innerHTML = `❤️ <strong>${willCount}</strong> möchten ihn`;
|
||
|
||
const activeStyle = `background:var(--c-primary);color:#fff;border-color:var(--c-primary)`;
|
||
if (hatBtn) { hatBtn.removeAttribute('style'); if (interest === 'hat') hatBtn.style.cssText = activeStyle; }
|
||
if (willBtn) { willBtn.removeAttribute('style'); if (interest === 'will') willBtn.style.cssText = activeStyle; }
|
||
}
|
||
} catch {
|
||
UI.toast.error('Aktion fehlgeschlagen.');
|
||
}
|
||
btn.disabled = false;
|
||
});
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// Render-Helfer: Züchter-Sektion
|
||
// ----------------------------------------------------------
|
||
function _renderZuchterSection(zuchter, slug) {
|
||
const DE_BUNDESLAENDER = [
|
||
'Baden-Württemberg','Bayern','Berlin','Brandenburg','Bremen','Hamburg',
|
||
'Hessen','Mecklenburg-Vorpommern','Niedersachsen','Nordrhein-Westfalen',
|
||
'Rheinland-Pfalz','Saarland','Sachsen','Sachsen-Anhalt',
|
||
'Schleswig-Holstein','Thüringen',
|
||
];
|
||
|
||
const listHtml = zuchter.length === 0
|
||
? `<p style="color:var(--c-text-secondary);font-size:var(--text-sm)">Noch keine Züchter eingetragen.</p>`
|
||
: zuchter.map(z => `
|
||
<div class="wiki-zuchter-card" style="padding:var(--space-3);border-radius:var(--radius-md);background:var(--c-surface-2);margin-bottom:var(--space-2)">
|
||
<div style="font-weight:var(--weight-semibold)">${_esc(z.name)}
|
||
${z.zwingername ? `<em style="font-weight:normal;color:var(--c-text-secondary)"> „${_esc(z.zwingername)}“</em>` : ''}
|
||
${z.vdh_mitglied ? `<span class="badge badge-sm" style="margin-left:var(--space-1);background:var(--c-primary);color:#fff">VDH</span>` : ''}
|
||
</div>
|
||
${(z.ort || z.bundesland) ? `<div class="text-sm-secondary">${[z.ort, z.bundesland].filter(Boolean).map(_esc).join(', ')}</div>` : ''}
|
||
${z.beschreibung ? `<p style="font-size:var(--text-sm);margin-top:var(--space-1)">${_esc(z.beschreibung)}</p>` : ''}
|
||
${z.website ? `<a href="${_esc(z.website)}" target="_blank" rel="noopener" style="font-size:var(--text-sm);color:var(--c-primary)">${_esc(z.website)}</a>` : ''}
|
||
</div>
|
||
`).join('');
|
||
|
||
const formHtml = _appState.user ? `
|
||
<div id="wiki-zuchter-form-wrap" style="display:none;margin-top:var(--space-3)">
|
||
<form id="wiki-zuchter-form" autocomplete="off">
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2)">
|
||
<div class="form-group" style="grid-column:1/-1">
|
||
<label class="form-label">Name *</label>
|
||
<input class="form-control" name="name" required maxlength="100">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Zwingername</label>
|
||
<input class="form-control" name="zwingername" maxlength="100">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Ort</label>
|
||
<input class="form-control" name="ort" maxlength="80">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">PLZ</label>
|
||
<input class="form-control" name="plz" maxlength="10">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Bundesland</label>
|
||
<select class="form-control" name="bundesland">
|
||
<option value="">— bitte wählen —</option>
|
||
${DE_BUNDESLAENDER.map(bl => `<option value="${_esc(bl)}">${_esc(bl)}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="form-group" style="grid-column:1/-1">
|
||
<label class="form-label">Website</label>
|
||
<input class="form-control" name="website" type="url" maxlength="200" placeholder="https://…">
|
||
</div>
|
||
<div class="form-group" style="grid-column:1/-1">
|
||
<label class="form-label">Telefon</label>
|
||
<input class="form-control" name="telefon" type="tel" maxlength="30">
|
||
</div>
|
||
<div class="form-group" style="grid-column:1/-1">
|
||
<label class="form-label">Kurzbeschreibung</label>
|
||
<textarea class="form-control" name="beschreibung" rows="3" maxlength="500"></textarea>
|
||
</div>
|
||
<div class="form-group" style="grid-column:1/-1;display:flex;align-items:center;gap:var(--space-2)">
|
||
<input type="checkbox" id="wiki-zuchter-vdh" name="vdh_mitglied" value="1" style="width:auto">
|
||
<label for="wiki-zuchter-vdh" style="margin:0;font-size:var(--text-sm)">VDH-Mitglied</label>
|
||
</div>
|
||
</div>
|
||
<div id="wiki-zuchter-success" style="display:none;color:var(--c-success);padding:var(--space-3);text-align:center;font-size:var(--text-sm)">
|
||
Vielen Dank! Dein Eintrag wird geprüft.
|
||
</div>
|
||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2)">
|
||
<button type="button" class="btn btn-ghost flex-1" id="wiki-zuchter-cancel">Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary flex-1" id="wiki-zuchter-submit">Eintragen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<button class="btn btn-secondary btn-sm" id="wiki-zuchter-add-btn" class="mt-3">
|
||
+ Züchter eintragen
|
||
</button>
|
||
` : '';
|
||
|
||
return `
|
||
<div class="wiki-detail-section" id="wiki-zuchter-section">
|
||
<div class="wiki-detail-label">Züchter</div>
|
||
<div id="wiki-zuchter-list">${listHtml}</div>
|
||
${formHtml}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function _bindZuchterForm(slug) {
|
||
const addBtn = document.getElementById('wiki-zuchter-add-btn');
|
||
const cancelBtn = document.getElementById('wiki-zuchter-cancel');
|
||
const formWrap = document.getElementById('wiki-zuchter-form-wrap');
|
||
const form = document.getElementById('wiki-zuchter-form');
|
||
|
||
addBtn?.addEventListener('click', () => {
|
||
formWrap.style.display = '';
|
||
addBtn.style.display = 'none';
|
||
});
|
||
|
||
cancelBtn?.addEventListener('click', () => {
|
||
formWrap.style.display = 'none';
|
||
addBtn.style.display = '';
|
||
});
|
||
|
||
form?.addEventListener('submit', async e => {
|
||
e.preventDefault();
|
||
const submitBtn = document.getElementById('wiki-zuchter-submit');
|
||
const fd = new FormData(form);
|
||
const data = {
|
||
rasse_slug: slug,
|
||
name: fd.get('name'),
|
||
zwingername: fd.get('zwingername') || null,
|
||
ort: fd.get('ort') || null,
|
||
plz: fd.get('plz') || null,
|
||
bundesland: fd.get('bundesland') || null,
|
||
vdh_mitglied: fd.get('vdh_mitglied') === '1',
|
||
website: fd.get('website') || null,
|
||
telefon: fd.get('telefon') || null,
|
||
beschreibung: fd.get('beschreibung') || null,
|
||
};
|
||
|
||
submitBtn.disabled = true;
|
||
submitBtn.textContent = 'Wird gesendet…';
|
||
|
||
const ok = await _submitZuchter(data);
|
||
if (ok) {
|
||
form.reset();
|
||
document.getElementById('wiki-zuchter-success').style.display = '';
|
||
// Hide submit row
|
||
submitBtn.closest('div[style*="flex"]').style.display = 'none';
|
||
} else {
|
||
UI.toast.error('Fehler beim Einsenden. Bitte versuche es erneut.');
|
||
submitBtn.disabled = false;
|
||
submitBtn.textContent = 'Eintragen';
|
||
}
|
||
});
|
||
}
|
||
|
||
async function _openBreedDetail(slug) {
|
||
let rasse;
|
||
try {
|
||
rasse = await _apiFetch(`/api/wiki/rassen/${slug}`);
|
||
} catch {
|
||
UI.toast.error('Rasse konnte nicht geladen werden.');
|
||
return;
|
||
}
|
||
|
||
// Temperament chips
|
||
const chips = rasse.temperament
|
||
? rasse.temperament.split(',').map(t => `<span class="wiki-trait-chip">${_esc(t.trim())}</span>`).join('')
|
||
: '';
|
||
|
||
const _dogSvgLg = _DOG_SILHOUETTE.replace('width="48" height="48"', 'width="56" height="56"');
|
||
// Alle Fotos: Hauptbild zuerst, dann Community-Fotos
|
||
const allFotos = [];
|
||
if (rasse.foto_url) allFotos.push({ foto_url: rasse.foto_url, user_name: null });
|
||
(rasse.user_fotos || []).forEach(f => allFotos.push(f));
|
||
|
||
const photoHtml = allFotos.length
|
||
? `<div class="wiki-gallery-wrap">
|
||
<img class="wiki-detail-photo wiki-gallery-main" id="wiki-main-photo"
|
||
src="${_esc(allFotos[0].foto_url)}" alt="${_esc(rasse.name)}"
|
||
onerror="this.style.display='none';document.getElementById('wiki-photo-fallback').style.display='flex'">
|
||
<div id="wiki-photo-fallback" class="wiki-detail-photo-placeholder hidden">${_dogSvgLg}<span>Kein Foto verfügbar</span></div>
|
||
${allFotos.length > 1 ? `
|
||
<div class="wiki-gallery-strip" id="wiki-gallery-strip">
|
||
${allFotos.map((f, i) => `
|
||
<button class="wiki-gallery-thumb${i === 0 ? ' active' : ''}" data-idx="${i}"
|
||
aria-label="Foto ${i + 1}">
|
||
<img src="${_esc(f.foto_url.startsWith('/media/') ? f.foto_url.replace(/\.(jpe?g|png|gif|webp)$/i,'_preview.webp') : f.foto_url)}"
|
||
alt="" loading="lazy"
|
||
onerror="if(this.src.includes('_preview')){this.src='${_esc(f.foto_url)}'}else{this.style.display='none'}">
|
||
${f.user_name ? `<span class="wiki-gallery-thumb-label">von ${_esc(f.user_name)}</span>` : ''}
|
||
</button>`).join('')}
|
||
</div>` : ''}
|
||
<button class="wiki-gallery-expand" id="wiki-gallery-expand" aria-label="Vollbild">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#arrows-out"></use></svg>
|
||
</button>
|
||
</div>`
|
||
: `<div class="wiki-detail-photo-placeholder">${_dogSvgLg}<span>Kein Foto verfügbar</span></div>`;
|
||
|
||
const berichteHtml = _renderBerichteHtml(rasse.berichte || [], slug);
|
||
const userFotosHtml = '';
|
||
|
||
const body = `
|
||
${/* 1. Hero */ ''}
|
||
<div class="wiki-detail-hero" style="text-align:center;margin-bottom:var(--space-4)">
|
||
${photoHtml}
|
||
${userFotosHtml}
|
||
<h1 style="font-size:var(--text-xl);font-weight:var(--weight-bold);margin:var(--space-2) 0 var(--space-1)">${_esc(rasse.name)}</h1>
|
||
${rasse.herkunft ? `<div class="text-sm-secondary">${UI.icon('map-pin')} ${_esc(rasse.herkunft)}</div>` : ''}
|
||
${rasse.gruppe ? `<div style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:2px">${_esc(rasse.gruppe)}</div>` : ''}
|
||
</div>
|
||
|
||
${/* 2. Charakter-Badges */ chips ? `
|
||
<div class="wiki-detail-section">
|
||
<div class="wiki-detail-label">Charakter</div>
|
||
<div class="wiki-trait-chips">${chips}</div>
|
||
</div>` : ''}
|
||
|
||
${/* 3. Beschreibung */ rasse.beschreibung ? `
|
||
<div class="wiki-detail-section">
|
||
<div class="wiki-detail-label">Beschreibung</div>
|
||
<p style="line-height:1.6;color:var(--c-text)">${_esc(rasse.beschreibung)}</p>
|
||
</div>` : (rasse.bred_for ? `
|
||
<div class="wiki-detail-section">
|
||
<div class="wiki-detail-label">Ursprüngliche Aufgabe</div>
|
||
<p style="line-height:1.6;color:var(--c-text)">${_esc(rasse.bred_for)}</p>
|
||
</div>` : '')}
|
||
|
||
${/* 4. Steckbrief */ _renderSteckbriefGrid(rasse)}
|
||
|
||
${/* 5. Vorkommen */ rasse.vorkommen_de ? `
|
||
<div class="wiki-detail-section">
|
||
<div class="wiki-detail-label">Vorkommen in Deutschland</div>
|
||
<p style="line-height:1.6;color:var(--c-text)">${_esc(rasse.vorkommen_de)}</p>
|
||
</div>` : ''}
|
||
|
||
${/* 6. Interesse — wird async befüllt */ `
|
||
<div id="wiki-interesse-placeholder">
|
||
<div class="wiki-detail-section" style="opacity:0.5">
|
||
<div class="wiki-detail-label">In der Community</div>
|
||
<div style="height:60px;background:var(--c-surface-2);border-radius:var(--radius-md)"></div>
|
||
</div>
|
||
</div>`}
|
||
|
||
${/* 7. Züchter — wird async befüllt */ `
|
||
<div id="wiki-zuchter-placeholder">
|
||
<div class="wiki-detail-section" style="opacity:0.5">
|
||
<div class="wiki-detail-label">Züchter</div>
|
||
<div style="height:40px;background:var(--c-surface-2);border-radius:var(--radius-md)"></div>
|
||
</div>
|
||
</div>`}
|
||
|
||
${/* 8. Community-Berichte */ `
|
||
<div class="wiki-detail-section" id="wiki-berichte-section">
|
||
<div class="wiki-detail-label">Community-Berichte</div>
|
||
${berichteHtml}
|
||
</div>
|
||
${_appState.user
|
||
? `<button class="btn btn-secondary w-full" id="wiki-bericht-add-btn" class="mt-3">+ Eigenen Bericht hinzufügen</button>`
|
||
: `<p style="color:var(--c-text-secondary);font-size:var(--text-sm);margin-top:var(--space-3)">
|
||
<a href="#settings" class="text-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" class="text-sm">
|
||
${UI.icon('camera')} ${rasse.foto_url ? 'Besseres Foto vorschlagen' : 'Foto hinzufügen'}
|
||
</button>
|
||
</div>` : ''}`}
|
||
`;
|
||
|
||
UI.modal.open({ title: _esc(rasse.name), body });
|
||
|
||
// Async: load stats + züchter in parallel
|
||
Promise.all([_fetchStats(slug), _fetchZuchter(slug)]).then(([stats, zuchter]) => {
|
||
const interessePlaceholder = document.getElementById('wiki-interesse-placeholder');
|
||
if (interessePlaceholder) {
|
||
interessePlaceholder.outerHTML = _renderInteresseSection(stats, slug);
|
||
_bindInteresseButtons(slug);
|
||
}
|
||
|
||
const zuchterPlaceholder = document.getElementById('wiki-zuchter-placeholder');
|
||
if (zuchterPlaceholder) {
|
||
zuchterPlaceholder.outerHTML = _renderZuchterSection(zuchter || [], slug);
|
||
_bindZuchterForm(slug);
|
||
}
|
||
}).catch(() => {
|
||
// Silently remove placeholders on error
|
||
document.getElementById('wiki-interesse-placeholder')?.remove();
|
||
document.getElementById('wiki-zuchter-placeholder')?.remove();
|
||
});
|
||
|
||
// Gallery-Thumbnails + Lightbox
|
||
const mainImg = document.getElementById('wiki-main-photo');
|
||
const strip = document.getElementById('wiki-gallery-strip');
|
||
if (strip && mainImg) {
|
||
strip.querySelectorAll('.wiki-gallery-thumb').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const idx = parseInt(btn.dataset.idx);
|
||
mainImg.src = allFotos[idx].foto_url;
|
||
mainImg.style.display = '';
|
||
document.getElementById('wiki-photo-fallback').style.display = 'none';
|
||
strip.querySelectorAll('.wiki-gallery-thumb').forEach(b => b.classList.toggle('active', b === btn));
|
||
});
|
||
});
|
||
}
|
||
|
||
document.getElementById('wiki-gallery-expand')?.addEventListener('click', () => {
|
||
const src = mainImg?.src || allFotos[0]?.foto_url;
|
||
if (!src) return;
|
||
let curIdx = allFotos.findIndex(f => f.foto_url && src.endsWith(f.foto_url.split('/').pop()));
|
||
if (curIdx < 0) curIdx = 0;
|
||
|
||
function _lbOpen(idx) {
|
||
const f = allFotos[idx];
|
||
const lb = document.getElementById('wiki-lightbox');
|
||
lb.querySelector('.wlb-img').src = f.foto_url;
|
||
lb.querySelector('.wlb-caption').textContent = f.user_name ? `Foto von ${f.user_name}` : rasse.name;
|
||
lb.querySelector('.wlb-counter').textContent = `${idx + 1} / ${allFotos.length}`;
|
||
lb.querySelector('.wlb-prev').style.display = allFotos.length > 1 ? '' : 'none';
|
||
lb.querySelector('.wlb-next').style.display = allFotos.length > 1 ? '' : 'none';
|
||
curIdx = idx;
|
||
}
|
||
|
||
const lb = document.createElement('div');
|
||
lb.id = 'wiki-lightbox';
|
||
lb.innerHTML = `
|
||
<div class="wlb-backdrop"></div>
|
||
<div class="wlb-content">
|
||
<button class="wlb-close" aria-label="Schließen"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg></button>
|
||
<button class="wlb-prev" aria-label="Zurück"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#caret-left"></use></svg></button>
|
||
<img class="wlb-img" src="" alt="">
|
||
<button class="wlb-next" aria-label="Weiter"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#caret-right"></use></svg></button>
|
||
<div class="wlb-caption"></div>
|
||
<div class="wlb-counter"></div>
|
||
</div>`;
|
||
document.body.appendChild(lb);
|
||
_lbOpen(curIdx);
|
||
|
||
const close = () => lb.remove();
|
||
lb.querySelector('.wlb-close').addEventListener('click', close);
|
||
lb.querySelector('.wlb-backdrop').addEventListener('click', close);
|
||
lb.querySelector('.wlb-prev').addEventListener('click', () => _lbOpen((curIdx - 1 + allFotos.length) % allFotos.length));
|
||
lb.querySelector('.wlb-next').addEventListener('click', () => _lbOpen((curIdx + 1) % allFotos.length));
|
||
document.addEventListener('keydown', function onKey(e) {
|
||
if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); }
|
||
if (e.key === 'ArrowLeft') lb.querySelector('.wlb-prev').click();
|
||
if (e.key === 'ArrowRight') lb.querySelector('.wlb-next').click();
|
||
});
|
||
});
|
||
|
||
document.getElementById('wiki-bericht-add-btn')?.addEventListener('click', () => {
|
||
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>
|
||
<div style="margin-top:var(--space-4);padding:var(--space-3);
|
||
background:var(--c-surface-2);border-radius:var(--radius-md);
|
||
border-left:3px solid var(--c-warning)">
|
||
<label style="display:flex;gap:var(--space-3);align-items:flex-start;cursor:pointer">
|
||
<input type="checkbox" id="wiki-foto-rights" style="margin-top:3px;flex-shrink:0">
|
||
<span style="font-size:var(--text-sm);line-height:1.5">
|
||
Ich bestätige, dass ich die <strong>uneingeschränkten Bildrechte</strong> an diesem Foto besitze
|
||
und banyaro.app das dauerhaft gültige, kostenlose Recht einräume, es zu veröffentlichen und zu
|
||
verwenden. Fotos aus dem Internet oder von Dritten dürfen nicht eingereicht werden.
|
||
</span>
|
||
</label>
|
||
</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" disabled>
|
||
${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-rights')?.addEventListener('change', e => {
|
||
document.getElementById('wiki-foto-submit').disabled = !e.target.checked;
|
||
});
|
||
|
||
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;
|
||
if (!document.getElementById('wiki-foto-rights')?.checked) {
|
||
UI.toast('Bitte Bildrechte bestätigen.', 'danger');
|
||
return;
|
||
}
|
||
|
||
const btn = document.getElementById('wiki-foto-submit');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Wird hochgeladen…';
|
||
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('file', file);
|
||
fd.append('rights_confirmed', '1');
|
||
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) {
|
||
if (!berichte || berichte.length === 0) {
|
||
return `<p style="color:var(--c-text-secondary);font-size:var(--text-sm)">Noch keine Community-Berichte für diese Rasse.</p>`;
|
||
}
|
||
return berichte.map(b => `
|
||
<div class="wiki-bericht-item" data-id="${b.id}">
|
||
<div class="wiki-bericht-header">
|
||
<span class="wiki-bericht-autor">${_esc(b.autor)}</span>
|
||
<span class="wiki-bericht-date">${_formatDate(b.created_at)}</span>
|
||
${_appState.user && _appState.user.name === b.autor
|
||
? `<button class="btn btn-danger btn-xs wiki-bericht-del" data-id="${b.id}" data-slug="${_esc(slug)}" style="margin-left:auto;padding:2px 8px;font-size:0.7rem">Löschen</button>`
|
||
: ''}
|
||
</div>
|
||
<div class="wiki-bericht-titel">${_esc(b.titel)}</div>
|
||
<p class="wiki-bericht-text">${_esc(b.text)}</p>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function _showBerichtForm(slug, rasseName) {
|
||
const body = `
|
||
<form id="wiki-bericht-form" autocomplete="off">
|
||
<div class="form-group">
|
||
<label class="form-label">Rasse</label>
|
||
<input class="form-control" type="text" value="${_esc(rasseName)}" disabled>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Titel</label>
|
||
<input class="form-control" type="text" name="titel" maxlength="120" placeholder="z.B. Mein Erfahrungsbericht" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Bericht</label>
|
||
<textarea class="form-control" name="text" rows="6" placeholder="Deine Erfahrungen mit dieser Rasse…" required></textarea>
|
||
</div>
|
||
</form>
|
||
`;
|
||
const footer = `
|
||
<button type="button" class="btn btn-secondary flex-1" id="wiki-bericht-cancel">Abbrechen</button>
|
||
<button type="submit" form="wiki-bericht-form" class="btn btn-primary flex-1">Veröffentlichen</button>
|
||
`;
|
||
|
||
UI.modal.open({ title: 'Bericht schreiben', body, footer });
|
||
document.getElementById('wiki-bericht-cancel')?.addEventListener('click', UI.modal.close);
|
||
|
||
const form = document.getElementById('wiki-bericht-form');
|
||
setTimeout(() => form?.querySelector('[name="titel"]')?.focus(), 150);
|
||
|
||
form.addEventListener('submit', async e => {
|
||
e.preventDefault();
|
||
const submitBtn = document.querySelector('[form="wiki-bericht-form"][type="submit"]');
|
||
const fd = UI.formData(form);
|
||
|
||
await UI.asyncButton(submitBtn, async () => {
|
||
try {
|
||
await _apiPost('/api/wiki/berichte', { rasse: slug, titel: fd.titel, text: fd.text });
|
||
UI.modal.close();
|
||
UI.toast.success('Bericht veröffentlicht!');
|
||
} catch (err) {
|
||
UI.toast.error(err.message || 'Fehler beim Veröffentlichen.');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// TAB: Gesundheit
|
||
// ----------------------------------------------------------
|
||
function _renderGesundheit(el) {
|
||
const items = GESUNDHEIT.map((s, i) => `
|
||
<div class="wiki-section" data-idx="${i}">
|
||
<div class="wiki-section-header">
|
||
<span class="wiki-section-icon">${UI.icon(s.icon)}</span>
|
||
<span class="wiki-section-titel">${_esc(s.titel)}</span>
|
||
<span class="wiki-section-arrow">${UI.icon('caret-down')}</span>
|
||
</div>
|
||
<div class="wiki-section-body hidden">
|
||
<p style="white-space:pre-wrap;line-height:1.6;color:var(--c-text)">${_esc(s.text)}</p>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
el.innerHTML = `<div class="wiki-accordion">${items}</div>`;
|
||
|
||
el.querySelectorAll('.wiki-section').forEach(sec => {
|
||
sec.querySelector('.wiki-section-header').addEventListener('click', () => {
|
||
const body = sec.querySelector('.wiki-section-body');
|
||
const arrow = sec.querySelector('.wiki-section-arrow');
|
||
const open = body.style.display !== 'none';
|
||
body.style.display = open ? 'none' : 'block';
|
||
arrow.innerHTML = open ? UI.icon('caret-down') : UI.icon('caret-up');
|
||
sec.classList.toggle('open', !open);
|
||
});
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// TAB: Recht & Regeln
|
||
// ----------------------------------------------------------
|
||
function _renderRecht(el) {
|
||
const items = RECHT.map((r, i) => `
|
||
<div class="wiki-section" data-idx="${i}">
|
||
<div class="wiki-section-header">
|
||
<span class="wiki-section-icon">${UI.icon('map-pin')}</span>
|
||
<span class="wiki-section-titel">${_esc(r.land)}</span>
|
||
<span class="wiki-section-arrow">${UI.icon('caret-down')}</span>
|
||
</div>
|
||
<div class="wiki-section-body hidden">
|
||
<div class="wiki-recht-row"><span class="wiki-recht-label">Leinenpflicht</span><span>${_esc(r.leine)}</span></div>
|
||
<div class="wiki-recht-row"><span class="wiki-recht-label">Rasseliste</span><span>${_esc(r.rasse)}</span></div>
|
||
<div class="wiki-recht-row"><span class="wiki-recht-label">Hundesteuer</span><span>${_esc(r.steuer)}</span></div>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
el.innerHTML = `
|
||
<div class="wiki-accordion">${items}</div>
|
||
<p class="wiki-disclaimer">Angaben ohne Gewähr — Regelungen ändern sich. Bitte beim zuständigen Ordnungsamt prüfen.</p>
|
||
`;
|
||
|
||
el.querySelectorAll('.wiki-section').forEach(sec => {
|
||
sec.querySelector('.wiki-section-header').addEventListener('click', () => {
|
||
const body = sec.querySelector('.wiki-section-body');
|
||
const arrow = sec.querySelector('.wiki-section-arrow');
|
||
const open = body.style.display !== 'none';
|
||
body.style.display = open ? 'none' : 'block';
|
||
arrow.innerHTML = open ? UI.icon('caret-down') : UI.icon('caret-up');
|
||
sec.classList.toggle('open', !open);
|
||
});
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// TAB: Quiz
|
||
// ----------------------------------------------------------
|
||
function _renderQuiz(el) {
|
||
_quizAnswers = {};
|
||
_quizStep = 0;
|
||
_renderQuizStep(el);
|
||
}
|
||
|
||
function _renderQuizStep(el) {
|
||
if (_quizStep >= QUIZ_FRAGEN.length) {
|
||
_loadQuizResult(el);
|
||
return;
|
||
}
|
||
|
||
const frage = QUIZ_FRAGEN[_quizStep];
|
||
const progress = Math.round((_quizStep / QUIZ_FRAGEN.length) * 100);
|
||
|
||
const optionsHtml = frage.optionen.map(o => `
|
||
<button class="wiki-quiz-option${_quizAnswers[frage.key] === o.val ? ' selected' : ''}"
|
||
data-key="${_esc(frage.key)}" data-val="${_esc(o.val)}">
|
||
${_esc(o.label)}
|
||
</button>
|
||
`).join('');
|
||
|
||
el.innerHTML = `
|
||
<div class="wiki-quiz-wrap">
|
||
<div class="wiki-quiz-progress-bar">
|
||
<div class="wiki-quiz-progress" style="width:${progress}%"></div>
|
||
</div>
|
||
<p class="wiki-quiz-step-info">Frage ${_quizStep + 1} von ${QUIZ_FRAGEN.length}</p>
|
||
<p class="wiki-quiz-frage">${_esc(frage.frage)}</p>
|
||
<div class="wiki-quiz-options">${optionsHtml}</div>
|
||
<div class="wiki-quiz-nav">
|
||
${_quizStep > 0
|
||
? `<button class="btn btn-secondary" id="quiz-back">Zurück</button>`
|
||
: '<span></span>'}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
el.querySelectorAll('.wiki-quiz-option').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
_quizAnswers[btn.dataset.key] = btn.dataset.val;
|
||
_quizStep++;
|
||
_renderQuizStep(el);
|
||
});
|
||
});
|
||
|
||
el.querySelector('#quiz-back')?.addEventListener('click', () => {
|
||
_quizStep--;
|
||
const prevKey = QUIZ_FRAGEN[_quizStep].key;
|
||
delete _quizAnswers[prevKey];
|
||
_renderQuizStep(el);
|
||
});
|
||
}
|
||
|
||
async function _loadQuizResult(el) {
|
||
el.innerHTML = `<div style="text-align:center;padding:var(--space-8)">Berechne Ergebnis…</div>`;
|
||
|
||
const params = new URLSearchParams(_quizAnswers).toString();
|
||
let data;
|
||
try {
|
||
data = await _apiFetch(`/api/wiki/quiz/result?${params}`);
|
||
} catch {
|
||
el.innerHTML = UI.emptyState({ icon: UI.icon('warning'), title: 'Fehler', text: 'Ergebnis konnte nicht geladen werden.' });
|
||
return;
|
||
}
|
||
|
||
const cardsHtml = data.results.map(r => {
|
||
const photoHtml = r.foto_url
|
||
? `<img class="wiki-quiz-result-photo" src="${_esc(r.foto_url)}" loading="lazy" alt="${_esc(r.name)}" onerror="this.style.display='none'">`
|
||
: `<div class="wiki-quiz-result-photo-fallback">${UI.icon('dog')}</div>`;
|
||
return `
|
||
<div class="wiki-quiz-result-card">
|
||
<div class="wiki-quiz-result-photo-wrap">${photoHtml}</div>
|
||
<div class="wiki-quiz-result-card-body">
|
||
<div class="wiki-quiz-result-name">${_esc(r.name)}</div>
|
||
<div class="wiki-quiz-result-gruppe">${_esc(r.gruppe || '')}</div>
|
||
<div class="wiki-breed-badges" style="margin:var(--space-2) 0">
|
||
<span class="wiki-badge-groesse wiki-badge-groesse--${_esc(r.groesse)}">${_groesseLabel(r.groesse)}</span>
|
||
<span class="wiki-badge-aktivitaet wiki-badge-aktivitaet--${_esc(r.aktivitaet)}">${_aktivLabel(r.aktivitaet)}</span>
|
||
</div>
|
||
${r.temperament ? `<p class="wiki-quiz-result-char">${_esc(r.temperament.split(',').slice(0,4).join(', '))}</p>` : ''}
|
||
<div class="wiki-fit-row" style="font-size:var(--text-xs);margin-top:var(--space-1)">
|
||
<span>${UI.icon('house-line')} ${r.wohnung_geeignet ? 'Wohnung' : 'Haus'}</span>
|
||
<span>${UI.icon('users')} ${r.kinder_geeignet ? 'Kinderfreundlich' : 'Erfahrung nötig'}</span>
|
||
</div>
|
||
<button class="btn btn-secondary btn-sm wiki-quiz-mehr" data-slug="${_esc(r.slug)}" class="mt-2">Mehr erfahren</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
el.innerHTML = `
|
||
<div class="wiki-quiz-wrap">
|
||
<div class="wiki-quiz-progress-bar">
|
||
<div class="wiki-quiz-progress w-full"></div>
|
||
</div>
|
||
<h3 style="margin:var(--space-4) 0 var(--space-2);text-align:center">Deine Top 3 Rassen</h3>
|
||
<div class="wiki-quiz-results">${cardsHtml}</div>
|
||
<button class="btn btn-secondary w-full" id="quiz-restart" class="mt-4">Quiz neu starten</button>
|
||
</div>
|
||
`;
|
||
|
||
el.querySelectorAll('.wiki-quiz-mehr').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
_tab = 'rassen';
|
||
_container.querySelectorAll('.by-tab').forEach(b => b.classList.toggle('active', b.dataset.tab === 'rassen'));
|
||
_openBreedDetail(btn.dataset.slug);
|
||
});
|
||
});
|
||
|
||
el.querySelector('#quiz-restart')?.addEventListener('click', () => {
|
||
_renderQuiz(el);
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// HELPER: API-Fetch
|
||
// ----------------------------------------------------------
|
||
async function _apiFetch(url) {
|
||
const resp = await fetch(url, { credentials: 'include' });
|
||
if (!resp.ok) {
|
||
const err = await resp.json().catch(() => ({}));
|
||
throw new Error(err.detail || `HTTP ${resp.status}`);
|
||
}
|
||
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',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
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();
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// HELPER: Labels
|
||
// ----------------------------------------------------------
|
||
function _groesseLabel(g) {
|
||
return { klein: 'Klein', mittel: 'Mittel', gross: 'Groß', sehr_gross: 'Sehr groß' }[g] || g;
|
||
}
|
||
|
||
function _aktivLabel(a) {
|
||
return { niedrig: 'Ruhig', mittel: 'Aktiv', hoch: 'Sportlich', sehr_hoch: 'Sehr aktiv' }[a] || a;
|
||
}
|
||
|
||
function _erfahrungLabel(e) {
|
||
return { anfaenger: 'Anfänger', fortgeschritten: 'Erfahren', experte: 'Experte' }[e] || e;
|
||
}
|
||
|
||
function _formatDate(iso) {
|
||
if (!iso) return '';
|
||
try {
|
||
return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||
} catch { return iso; }
|
||
}
|
||
|
||
function _esc(str) {
|
||
if (!str) return '';
|
||
return String(str).replace(/&/g, '&').replace(/</g, '<')
|
||
.replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// RASSEN-ERKENNUNG PER KI (Wiki-Tab)
|
||
// ----------------------------------------------------------
|
||
function _bindWikiRasseErkennung(el) {
|
||
const btn = el.querySelector('#wiki-rasse-erkennen-btn');
|
||
const fileInput = el.querySelector('#wiki-rasse-foto-input');
|
||
if (!btn || !fileInput) return;
|
||
|
||
btn.addEventListener('click', () => {
|
||
if (!_appState.user) {
|
||
UI.toast('Bitte melde dich an, um diese Funktion zu nutzen.', 'info');
|
||
return;
|
||
}
|
||
fileInput.value = '';
|
||
fileInput.click();
|
||
});
|
||
|
||
fileInput.addEventListener('change', async () => {
|
||
const file = fileInput.files[0];
|
||
if (!file) return;
|
||
|
||
if (file.size > 5 * 1024 * 1024) {
|
||
UI.toast('Bild zu groß (max. 5 MB).', 'danger');
|
||
return;
|
||
}
|
||
|
||
const origHtml = btn.innerHTML;
|
||
btn.disabled = true;
|
||
btn.innerHTML = `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#spinner"></use></svg> KI analysiert das Bild…`;
|
||
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('file', file);
|
||
const token = localStorage.getItem('by_token');
|
||
const resp = await fetch('/api/ki/rasse-erkennung', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
||
body: fd,
|
||
});
|
||
const data = await resp.json();
|
||
if (!resp.ok) throw new Error(data.detail || 'Fehler bei der Erkennung.');
|
||
|
||
btn.disabled = false;
|
||
btn.innerHTML = origHtml;
|
||
_showWikiRasseErgebnis(data);
|
||
} catch (e) {
|
||
btn.disabled = false;
|
||
btn.innerHTML = origHtml;
|
||
UI.toast(e.message || 'Fehler bei der Rassen-Erkennung.', 'danger');
|
||
}
|
||
});
|
||
}
|
||
|
||
function _showWikiRasseErgebnis(data) {
|
||
if (!data.ist_hund) {
|
||
UI.modal.open({
|
||
title: 'Kein Hund erkannt',
|
||
body: `<div style="text-align:center;padding:var(--space-6) var(--space-2)">
|
||
<div style="font-size:3rem;margin-bottom:var(--space-3)">🐾</div>
|
||
<p class="text-secondary">
|
||
Auf diesem Foto konnte kein Hund erkannt werden.<br>
|
||
Bitte lade ein deutlicheres Foto hoch.
|
||
</p>
|
||
${data.hinweis ? `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:var(--space-3)">${_esc(data.hinweis)}</p>` : ''}
|
||
</div>`,
|
||
footer: `<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>`,
|
||
});
|
||
return;
|
||
}
|
||
|
||
const rassen = data.rassen || [];
|
||
const cardsHtml = rassen.map((r, i) => {
|
||
const isTop = i === 0;
|
||
return `
|
||
<div class="rasse-result-card${isTop ? ' rasse-result-card--top' : ''}">
|
||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||
<div class="rasse-result-name">${isTop ? '🐕 ' : ''}${_esc(r.name)}</div>
|
||
<span class="rasse-result-pct${isTop ? '' : ' rasse-result-pct--dim'}">${r.sicherheit}%</span>
|
||
</div>
|
||
<div class="rasse-result-bar-wrap">
|
||
<div class="rasse-result-bar${isTop ? '' : ' rasse-result-bar--dim'}"
|
||
style="width:${r.sicherheit}%"></div>
|
||
</div>
|
||
${r.beschreibung ? `<div class="rasse-result-desc">${_esc(r.beschreibung)}</div>` : ''}
|
||
${r.wiki_slug ? `
|
||
<div class="mt-3">
|
||
<button class="btn btn-${isTop ? 'primary' : 'secondary'} btn-sm w-full"
|
||
data-action="wiki" data-slug="${_esc(r.wiki_slug)}">
|
||
Im Wiki nachschlagen
|
||
</button>
|
||
</div>` : ''}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
UI.modal.open({
|
||
title: 'Erkannte Rasse',
|
||
body: `
|
||
<div style="padding-bottom:var(--space-2)">
|
||
${data.hinweis ? `<div style="background:var(--c-surface-2);border-radius:var(--radius-md);
|
||
padding:var(--space-3);margin-bottom:var(--space-3);font-size:var(--text-sm);
|
||
color:var(--c-text-secondary)">ℹ️ ${_esc(data.hinweis)}</div>` : ''}
|
||
${cardsHtml}
|
||
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-2);
|
||
text-align:center">
|
||
Noch ${data.verbleibende_anfragen} Erkennung${data.verbleibende_anfragen !== 1 ? 'en' : ''} heute verfügbar
|
||
</p>
|
||
</div>
|
||
`,
|
||
footer: `<button class="btn btn-secondary" id="wiki-rasse-modal-schliessen">Schließen</button>`,
|
||
});
|
||
|
||
document.getElementById('wiki-rasse-modal-schliessen')
|
||
?.addEventListener('click', UI.modal.close);
|
||
|
||
document.querySelectorAll('[data-action="wiki"]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
UI.modal.close();
|
||
setTimeout(() => _openBreedDetail(btn.dataset.slug), 300);
|
||
});
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// PUBLIC
|
||
// ----------------------------------------------------------
|
||
return { init, refresh, _approveSubmission, _rejectSubmission };
|
||
|
||
})();
|