/* ============================================================ 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 = `

${_esc(e.message)}

`; 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); // 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)}
`; } // ---------------------------------------------------------- // 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 photoHtml = rasse.foto_url ? `
${_esc(rasse.name)}
` : ''; const berichteHtml = _renderBerichteHtml(rasse.berichte || [], slug); const body = ` ${/* 1. Hero */ ''}
${photoHtml}

${_esc(rasse.name)}

${rasse.herkunft ? `
${UI.icon('map-pin')} ${_esc(rasse.herkunft)}
` : ''} ${rasse.gruppe ? `
${_esc(rasse.gruppe)}
` : ''}
${/* 2. Charakter-Badges */ chips ? `
Charakter
${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 */ `
In der Community
`} ${/* 7. Züchter — wird async befüllt */ `
Züchter
`} ${/* 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(); }); 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.

JPG, PNG oder WebP · max. 8 MB · möglichst hochauflösend
`; const footer = ` `; UI.modal.open({ title: 'Foto vorschlagen', body, footer }); document.getElementById('wiki-foto-cancel')?.addEventListener('click', UI.modal.close); document.getElementById('wiki-foto-input')?.addEventListener('change', e => { const file = e.target.files?.[0]; if (!file) return; const preview = document.getElementById('wiki-foto-preview'); const img = document.getElementById('wiki-foto-preview-img'); const url = URL.createObjectURL(file); img.src = url; preview.style.display = ''; }); document.getElementById('wiki-foto-form')?.addEventListener('submit', async e => { e.preventDefault(); const input = document.getElementById('wiki-foto-input'); const file = input?.files?.[0]; if (!file) return; const btn = document.getElementById('wiki-foto-submit'); btn.disabled = true; btn.textContent = 'Wird hochgeladen…'; try { const fd = new FormData(); fd.append('file', file); const token = localStorage.getItem('by_token'); const resp = await fetch(`/api/wiki/rassen/${encodeURIComponent(slug)}/foto`, { method: 'POST', credentials: 'include', headers: token ? { 'Authorization': `Bearer ${token}` } : {}, body: fd, }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.detail || 'Upload fehlgeschlagen'); } UI.modal.close(); UI.toast('Danke! Dein Foto wird geprüft und dann veröffentlicht.', 'success'); } catch (err) { UI.toast(err.message, 'danger'); btn.disabled = false; btn.innerHTML = `${UI.icon('paper-plane-tilt')} Einreichen`; } }); } function _renderBerichteHtml(berichte, slug) { 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: UI.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('.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, '"'); } // ---------------------------------------------------------- // PUBLIC // ---------------------------------------------------------- return { init, refresh, _approveSubmission, _rejectSubmission }; })();