/* ============================================================
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 = `
${isMod ? `` : ''}
`;
_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 = `${UI.skeleton(3)}
`;
let subs;
try {
subs = await _apiFetch('/api/wiki/foto-submissions');
} catch (e) {
el.innerHTML = ``;
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 = `
${UI.icon('check')}
Keine ausstehenden Foto-Einreichungen.
`;
return;
}
el.innerHTML = `
Ausstehende Fotos (${subs.length})
${subs.map(s => `
${_esc(s.rasse_name)}
von ${_esc(s.user_name)} · ${_formatDate(s.created_at)}
${s.aktuell_foto
? `
Aktuelles Foto:
`
: `
Kein Foto vorhanden
`
}
`).join('')}
`;
}
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 = `
Rassen-Datenbank wird geladen… ${UI.icon('dog')}
Beim ersten Start werden ~170 Rassen von TheDogAPI abgerufen.
`;
return;
}
// Reset state when re-rendering the tab fresh
_rassen = [];
_currentOffset = 0;
_currentSearch = '';
_currentGruppe = '';
el.innerHTML = `
`;
// 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 = `Lade Rassen…
`;
}
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 = `Rassen konnten nicht geladen werden.
`;
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 = `` +
_gruppen.map(g => ``).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 = `Keine Rassen gefunden.
`;
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 = ``;
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
? `
`
: '';
const fallbackHtml = `${_DOG_SILHOUETTE}
`;
return `
${photoHtml}
${fallbackHtml}
${_esc(r.name)}
${_esc(r.gruppe || '—')}
${_groesseLabel(r.groesse)}
${_aktivLabel(r.aktivitaet)}
${_erfahrungLabel(r.erfahrung)}
`;
}
// ----------------------------------------------------------
// 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
? `✓ Ja`
: rasse.kinder_geeignet === false
? `⚡ Bedingt`
: '—';
const wohnungLabel = rasse.wohnung_geeignet
? `✓ Ja`
: `✗ Besser Garten`;
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 `
${rows.map(([label, val]) => `
${label}
${val}
`).join('')}
`;
}
// ----------------------------------------------------------
// 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 `
In der Community
🐕 ${hatCount} haben diesen Hund
❤️ ${willCount} möchten ihn
`;
}
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 = `🐕 ${hatCount} haben diesen Hund`;
if (willEl) willEl.innerHTML = `❤️ ${willCount} 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
? `Noch keine Züchter eingetragen.
`
: zuchter.map(z => `
${_esc(z.name)}
${z.zwingername ? ` „${_esc(z.zwingername)}“` : ''}
${z.vdh_mitglied ? `VDH` : ''}
${(z.ort || z.bundesland) ? `
${[z.ort, z.bundesland].filter(Boolean).map(_esc).join(', ')}
` : ''}
${z.beschreibung ? `
${_esc(z.beschreibung)}
` : ''}
${z.website ? `
${_esc(z.website)}` : ''}
`).join('');
const formHtml = _appState.user ? `
` : '';
return `
Züchter
${listHtml}
${formHtml}
`;
}
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 => `${_esc(t.trim())}`).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
? `
${_dogSvgLg}Kein Foto verfügbar
${allFotos.length > 1 ? `
${allFotos.map((f, i) => `
`).join('')}
` : ''}
`
: `${_dogSvgLg}Kein Foto verfügbar
`;
const berichteHtml = _renderBerichteHtml(rasse.berichte || [], slug);
const userFotosHtml = '';
const body = `
${/* 1. Hero */ ''}
${photoHtml}
${userFotosHtml}
${_esc(rasse.name)}
${rasse.herkunft ? `
${UI.icon('map-pin')} ${_esc(rasse.herkunft)}
` : ''}
${rasse.gruppe ? `
${_esc(rasse.gruppe)}
` : ''}
${/* 2. Charakter-Badges */ chips ? `
` : ''}
${/* 3. Beschreibung */ rasse.beschreibung ? `
Beschreibung
${_esc(rasse.beschreibung)}
` : (rasse.bred_for ? `
Ursprüngliche Aufgabe
${_esc(rasse.bred_for)}
` : '')}
${/* 4. Steckbrief */ _renderSteckbriefGrid(rasse)}
${/* 5. Vorkommen */ rasse.vorkommen_de ? `
Vorkommen in Deutschland
${_esc(rasse.vorkommen_de)}
` : ''}
${/* 6. Interesse — wird async befüllt */ `
`}
${/* 7. Züchter — wird async befüllt */ `
`}
${/* 8. Community-Berichte */ `
Community-Berichte
${berichteHtml}
${_appState.user
? ``
: `
Anmelden, um einen Bericht zu schreiben.
`
}
${_appState.user ? `
` : ''}`}
`;
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 = `
`;
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 = `
Dein Foto wird nach einer kurzen Prüfung freigeschaltet und als Hauptbild im Wiki verwendet.
`;
const footer = `
`;
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 `Noch keine Community-Berichte für diese Rasse.
`;
}
return berichte.map(b => `
${_esc(b.titel)}
${_esc(b.text)}
`).join('');
}
function _showBerichtForm(slug, rasseName) {
const body = `
`;
const footer = `
`;
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) => `
`).join('');
el.innerHTML = `${items}
`;
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) => `
Leinenpflicht${_esc(r.leine)}
Rasseliste${_esc(r.rasse)}
Hundesteuer${_esc(r.steuer)}
`).join('');
el.innerHTML = `
${items}
Angaben ohne Gewähr — Regelungen ändern sich. Bitte beim zuständigen Ordnungsamt prüfen.
`;
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 => `
`).join('');
el.innerHTML = `
Frage ${_quizStep + 1} von ${QUIZ_FRAGEN.length}
${_esc(frage.frage)}
${optionsHtml}
${_quizStep > 0
? ``
: ''}
`;
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 = `Berechne Ergebnis…
`;
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
? `
`
: `${UI.icon('dog')}
`;
return `
${photoHtml}
${_esc(r.name)}
${_esc(r.gruppe || '')}
${_groesseLabel(r.groesse)}
${_aktivLabel(r.aktivitaet)}
${r.temperament ? `
${_esc(r.temperament.split(',').slice(0,4).join(', '))}
` : ''}
${UI.icon('house-line')} ${r.wohnung_geeignet ? 'Wohnung' : 'Haus'}
${UI.icon('users')} ${r.kinder_geeignet ? 'Kinderfreundlich' : 'Erfahrung nötig'}
`;
}).join('');
el.innerHTML = `
Deine Top 3 Rassen
${cardsHtml}
`;
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, '"');
}
// ----------------------------------------------------------
// 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 = ` 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: `
🐾
Auf diesem Foto konnte kein Hund erkannt werden.
Bitte lade ein deutlicheres Foto hoch.
${data.hinweis ? `
${_esc(data.hinweis)}
` : ''}
`,
footer: ``,
});
return;
}
const rassen = data.rassen || [];
const cardsHtml = rassen.map((r, i) => {
const isTop = i === 0;
return `
${isTop ? '🐕 ' : ''}${_esc(r.name)}
${r.sicherheit}%
${r.beschreibung ? `
${_esc(r.beschreibung)}
` : ''}
${r.wiki_slug ? `
` : ''}
`;
}).join('');
UI.modal.open({
title: 'Erkannte Rasse',
body: `
${data.hinweis ? `
ℹ️ ${_esc(data.hinweis)}
` : ''}
${cardsHtml}
Noch ${data.verbleibende_anfragen} Erkennung${data.verbleibende_anfragen !== 1 ? 'en' : ''} heute verfügbar
`,
footer: ``,
});
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 };
})();