/* ============================================================ 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).\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() { _container.innerHTML = `
`; _container.querySelector('#wiki-tab-bar').addEventListener('click', e => { const btn = e.target.closest('[data-tab]'); if (!btn) return; _tab = btn.dataset.tab; _container.querySelectorAll('.wiki-tab-btn').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); } // ---------------------------------------------------------- // 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: '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); // 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)`; } } function _breedCardHtml(r) { const photoHtml = r.foto_url ? `${_esc(r.name)}` : ''; const fallbackHtml = `
${UI.icon('dog')}
`; return `
${photoHtml} ${fallbackHtml}
${_esc(r.name)}
${_esc(r.gruppe || '—')}
${_groesseLabel(r.groesse)} ${_aktivLabel(r.aktivitaet)} ${_erfahrungLabel(r.erfahrung)}
`; } async function _openBreedDetail(slug) { let rasse; try { rasse = await _apiFetch(`/api/wiki/rassen/${slug}`); } catch { UI.toast.error('Rasse konnte nicht geladen werden.'); return; } const berichteHtml = _renderBerichteHtml(rasse.berichte || [], slug); // Temperament chips const chips = rasse.temperament ? rasse.temperament.split(',').map(t => `${_esc(t.trim())}`).join('') : ''; // Stats row 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 photoHtml = rasse.foto_url ? `${_esc(rasse.name)}` : ''; const body = ` ${photoHtml}
${_groesseLabel(rasse.groesse)} ${_aktivLabel(rasse.aktivitaet)} ${_erfahrungLabel(rasse.erfahrung)} ${rasse.gruppe ? `${_esc(rasse.gruppe)}` : ''}
${rasse.herkunft || rasse.bred_for ? `
${rasse.herkunft ? `
Herkunft

${_esc(rasse.herkunft)}

` : ''} ${rasse.bred_for ? `
Ursprüngliche Aufgabe

${_esc(rasse.bred_for)}

` : ''}
` : ''} ${chips ? `
Charakter
${chips}
` : ''}
Gewicht ${gewicht}
Lebenserwartung ${_esc(rasse.lebensdauer || '—')}
${UI.icon('house-line')} Wohnung: ${rasse.wohnung_geeignet ? UI.icon('check') : UI.icon('x')} ${UI.icon('users')} Kinder: ${rasse.kinder_geeignet ? UI.icon('check') : UI.icon('x')}
Community-Berichte
${berichteHtml}
${_appState.user ? `` : `

Anmelden, um einen Bericht zu schreiben.

` } `; UI.modal.open({ title: _esc(rasse.name), body }); document.getElementById('wiki-bericht-add-btn')?.addEventListener('click', () => { UI.modal.close(); setTimeout(() => _showBerichtForm(slug, rasse.name), 350); }); } function _renderBerichteHtml(berichte, slug) { if (!berichte || berichte.length === 0) { return `

Noch keine Community-Berichte für diese Rasse.

`; } return berichte.map(b => `
${_esc(b.autor)} ${_formatDate(b.created_at)} ${_appState.user && _appState.user.name === b.autor ? `` : ''}
${_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) => `
${UI.icon(s.icon)} ${_esc(s.titel)} ${UI.icon('caret-down')}
`).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) => `
${UI.icon('map-pin')} ${_esc(r.land)} ${UI.icon('caret-down')}
`).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: 'warning', title: 'Fehler', text: 'Ergebnis konnte nicht geladen werden.' }); return; } const cardsHtml = data.results.map(r => { const photoHtml = r.foto_url ? `${_esc(r.name)}` : `
${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('.wiki-tab-btn').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 _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, '"'); } // ---------------------------------------------------------- // PUBLIC // ---------------------------------------------------------- return { init, refresh }; })();