/* ============================================================ BAN YARO — Forum (Sprint 11) Kategorien, Threads, Antworten, Likes, Reports, Foto-Upload, Mitgliederkarte, Moderations-Panel ============================================================ */ window.Page_forum = (() => { let _container = null; let _appState = null; let _threads = []; let _aktivKat = 'alle'; let _offset = 0; let _searchTimer = null; let _searching = false; let _mapLoaded = false; let _leafletLoaded = false; let _map = null; let _clusterGroup = null; let _activeSection = 'list'; // 'list' | 'map' const LIMIT = 30; const KATEGORIEN = [ { key: 'alle', label: 'Alle' }, { key: 'allgemein', label: 'Allgemein' }, { key: 'rasse', label: 'Rasse' }, { key: 'region', label: 'Region' }, { key: 'gesundheit', label: 'Gesundheit' }, { key: 'erziehung', label: 'Erziehung' }, { key: 'spaziergang', label: 'Spaziergang' }, { key: 'ausflug', label: 'Ausflug' }, { key: 'training', label: 'Training & Lektionen' }, { key: 'ernaehrung', label: 'Ernährung & Rezepte' }, { key: 'probleme', label: 'Probleme' }, { key: 'tauschboerse', label: 'Tauschbörse' }, ]; // ---------------------------------------------------------- // Helpers // ---------------------------------------------------------- function _esc(s) { return String(s || '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function _fmtDate(iso) { if (!iso) return '—'; const d = new Date(iso); const now = new Date(); const diff = (now - d) / 1000; if (diff < 60) return 'gerade eben'; if (diff < 3600) return `vor ${Math.floor(diff / 60)} Min`; if (diff < 86400) return `vor ${Math.floor(diff / 3600)} Std`; return d.toLocaleDateString('de-DE'); } function _initial(name) { return (name || '?').charAt(0).toUpperCase(); } // ---------------------------------------------------------- // INIT // ---------------------------------------------------------- async function init(container, appState) { _container = container; _appState = appState; _render(); _loadThreads(true); } function refresh() { _loadThreads(true); } function onDogChange() {} // ---------------------------------------------------------- // RENDER — Grundstruktur // ---------------------------------------------------------- function _render() { const isMod = !!_appState.user?.is_moderator; _container.innerHTML = `

Forum

${isMod ? `` : ''}
${KATEGORIEN.map(k => ` `).join('')}

Lädt…

`; // Tab-Klicks document.getElementById('forum-tabs').addEventListener('click', e => { const btn = e.target.closest('[data-kat], [data-section]'); if (!btn) return; if (btn.dataset.section === 'map') { _aktivKat = 'alle'; _activeSection = 'map'; document.querySelectorAll('#forum-tabs .by-tab').forEach(b => b.classList.remove('active')); btn.classList.add('active'); _renderMembersMap(); return; } _aktivKat = btn.dataset.kat; _activeSection = 'list'; document.querySelectorAll('#forum-tabs .by-tab').forEach(b => b.classList.remove('active')); btn.classList.add('active'); _offset = 0; _threads = []; _loadThreads(true); }); // Suche document.getElementById('forum-search').addEventListener('input', e => { clearTimeout(_searchTimer); const q = e.target.value.trim(); if (!q) { _searching = false; _renderList(); return; } _searchTimer = setTimeout(() => _doSearch(q), 400); }); // Neues Thema document.getElementById('forum-new-btn').addEventListener('click', () => { if (!_appState.user) { UI.toast.info('Bitte erst anmelden.'); return; } _showCreateForm(); }); // Moderations-Panel document.getElementById('forum-mod-btn')?.addEventListener('click', _showModPanel); // Regeln & Netiquette document.getElementById('forum-rules-btn').addEventListener('click', _showRules); } // ---------------------------------------------------------- // Threads laden // ---------------------------------------------------------- async function _loadThreads(reset = false) { if (reset) { _offset = 0; _threads = []; } const mainEl = document.getElementById('forum-main'); if (mainEl && reset) { mainEl.innerHTML = `

Lädt…

`; } try { const params = { limit: LIMIT, offset: _offset }; if (_aktivKat !== 'alle') params.kategorie = _aktivKat; const rows = await API.forum.threads(params); _threads = reset ? rows : [..._threads, ...rows]; _offset += rows.length; _renderList(rows.length < LIMIT); } catch (err) { UI.toast.error(err.message || 'Fehler beim Laden.'); } } // ---------------------------------------------------------- // Thread-Liste rendern // ---------------------------------------------------------- function _renderList(noMore = false) { if (_searching) return; const el = document.getElementById('forum-main'); if (!el) return; if (!_threads.length) { el.innerHTML = `
${UI.icon('chat-circle-dots')}

Noch keine Beiträge in dieser Kategorie.

`; document.getElementById('forum-first-btn')?.addEventListener('click', () => { if (!_appState.user) { UI.toast.info('Bitte erst anmelden.'); return; } _showCreateForm(); }); return; } el.innerHTML = `
${_threads.map(_threadCardHTML).join('')}
${!noMore ? `
` : ''} `; el.querySelectorAll('.forum-thread-card').forEach(card => { card.addEventListener('click', () => _openThread(parseInt(card.dataset.id))); }); document.getElementById('forum-loadmore')?.addEventListener('click', () => { _loadThreads(false); }); } function _threadCardHTML(t) { const preview = t.text_preview ? _esc(t.text_preview.slice(0, 120)) + (t.text_preview.length >= 120 ? '…' : '') : ''; const pinBadge = t.is_pinned ? `${UI.icon('push-pin')}` : ''; const lockBadge = t.is_locked ? `${UI.icon('lock')}` : ''; const fotoHtml = t.foto_preview ? /\.(mp4|mov|webm|m4v|avi)$/i.test(t.foto_preview) ? `
${UI.icon('video-camera')}
` : `` : ''; return `
${_esc(t.kategorie)} ${pinBadge}${lockBadge}
${_esc(t.titel)}
${preview ? `
${preview}
` : ''}
${UI.icon('user')} ${_esc(t.autor_name || 'Unbekannt')} ${UI.icon('calendar-dots')} ${_fmtDate(t.created_at)} ${UI.icon('chat-circle-dots')} ${t.antworten || 0} ${UI.icon('heart')} ${t.likes || 0}
${fotoHtml}
`; } // ---------------------------------------------------------- // Suche // ---------------------------------------------------------- async function _doSearch(q) { _searching = true; const el = document.getElementById('forum-main'); if (el) el.innerHTML = `

Suche…

`; try { const results = await API.forum.search(q); if (!document.getElementById('forum-main')) return; if (!results.length) { document.getElementById('forum-main').innerHTML = `
${UI.icon('magnifying-glass')}

Keine Ergebnisse für „${_esc(q)}"

`; return; } document.getElementById('forum-main').innerHTML = `
${results.map(t => _threadCardHTML({ ...t, foto_preview: null, is_pinned: 0, is_locked: 0, user_liked: false })).join('')}
`; document.getElementById('forum-main').querySelectorAll('.forum-thread-card').forEach(card => { card.addEventListener('click', () => _openThread(parseInt(card.dataset.id))); }); } catch (err) { UI.toast.error(err.message || 'Suchfehler.'); } } // ---------------------------------------------------------- // Thread-Detail-Modal // ---------------------------------------------------------- async function _openThread(threadId) { let thread; try { thread = await API.forum.thread(threadId); } catch (err) { UI.toast.error(err.message); return; } const uid = _appState.user?.id; const isMod = !!_appState.user?.is_moderator; const isOwn = uid && uid === thread.user_id; const modToolbar = (isMod) ? `
` : ''; const _forumMediaHtml = (u) => { if (u.endsWith('.pdf')) return ` ${UI.icon('file-text')} ${_esc(u.split('/').pop())}`; if (/\.(mp4|mov|webm|m4v|avi)$/i.test(u)) return ``; return ``; }; const fotoGallery = (thread.foto_urls?.length) ? `
${thread.foto_urls.map(_forumMediaHtml).join('')}
` : ''; const likeClass = thread.user_liked ? 'forum-like-btn active' : 'forum-like-btn'; const postsHtml = (thread.posts?.length) ? thread.posts.map(p => _postHTML(p, uid, isMod)).join('') : `

Noch keine Antworten.

`; const replySection = _appState.user && !thread.is_locked ? `
` : (!_appState.user ? `

Bitte anmelden um zu antworten.

` : `

${UI.icon('lock')} Dieser Thread ist gesperrt.

` ); const body = `
${modToolbar}
${_esc(thread.kategorie)} ${_fmtDate(thread.created_at)} ${thread.is_pinned ? `${UI.icon('push-pin')}` : ''} ${thread.is_locked ? `${UI.icon('lock')}` : ''}

${_esc(thread.text)}

${fotoGallery}
${_esc(_initial(thread.autor_name))}
${_esc(thread.autor_name || 'Unbekannt')}
${_appState.user && !isOwn ? `` : ''}
${thread.antworten || 0} Antworten
${postsHtml}
${replySection}
`; const footer = _appState.user ? `
${(isOwn || isMod) ? `` : ''} ${isOwn ? `` : ''}
${(!thread.is_locked && _appState.user) ? `` : ''}
` : ``; UI.modal.open({ title: `${UI.icon('chat-circle-dots')} ${_esc(thread.titel)}`, body, footer }); // Close document.getElementById('ft-close')?.addEventListener('click', UI.modal.close); // Like thread document.getElementById('thread-like-btn')?.addEventListener('click', async () => { if (!_appState.user) { UI.toast.info('Bitte erst anmelden.'); return; } const btn = document.getElementById('thread-like-btn'); try { const res = await API.forum.like('thread', thread.id); thread.user_liked = res.liked; thread.likes = res.count; btn.classList.toggle('active', res.liked); const countEl = document.getElementById('thread-like-count'); if (countEl) countEl.textContent = res.count; // Update local state const idx = _threads.findIndex(t => t.id === thread.id); if (idx !== -1) { _threads[idx].likes = res.count; _threads[idx].user_liked = res.liked; } } catch (err) { UI.toast.error(err.message); } }); // Report thread document.getElementById('thread-report-btn')?.addEventListener('click', () => { _showReportForm('thread', thread.id); }); // Delete thread document.getElementById('ft-delete-thread')?.addEventListener('click', async () => { const ok = await UI.modal.confirm({ title: 'Thread löschen?', message: 'Der Thread wird unwiderruflich entfernt.', confirmText: 'Löschen', danger: true, }); if (!ok) return; try { await API.forum.deleteThread(thread.id); _threads = _threads.filter(t => t.id !== thread.id); UI.modal.close(); _renderList(true); UI.toast.success('Thread gelöscht.'); } catch (err) { UI.toast.error(err.message); } }); // Edit thread (owner) document.getElementById('ft-edit-thread')?.addEventListener('click', () => { _showEditThreadModal(thread); }); // Moderator: pin/lock/delete document.querySelector('.forum-mod-pin')?.addEventListener('click', async () => { try { await API.forum.patchThread(thread.id, { is_pinned: thread.is_pinned ? 0 : 1 }); UI.toast.success('Gespeichert.'); UI.modal.close(); _loadThreads(true); } catch (err) { UI.toast.error(err.message); } }); document.querySelector('.forum-mod-lock')?.addEventListener('click', async () => { try { await API.forum.patchThread(thread.id, { is_locked: thread.is_locked ? 0 : 1 }); UI.toast.success('Gespeichert.'); UI.modal.close(); _loadThreads(true); } catch (err) { UI.toast.error(err.message); } }); document.querySelector('.forum-mod-delete-thread')?.addEventListener('click', async () => { const ok = await UI.modal.confirm({ title: 'Thread löschen?', message: 'Moderator-Löschung.', confirmText: 'Löschen', danger: true }); if (!ok) return; try { await API.forum.deleteThread(thread.id); _threads = _threads.filter(t => t.id !== thread.id); UI.modal.close(); _renderList(true); UI.toast.success('Thread gelöscht.'); } catch (err) { UI.toast.error(err.message); } }); // Foto-Vollbild document.getElementById('modal-container')?.querySelectorAll('.forum-foto-img').forEach(img => { img.addEventListener('click', () => _showLightbox(img.dataset.src || img.src)); }); // Reply file preview const replyFileInput = document.getElementById('forum-reply-file'); replyFileInput?.addEventListener('change', () => { const previews = document.getElementById('forum-reply-previews'); if (!previews) return; previews.innerHTML = ''; const files = Array.from(replyFileInput.files || []); files.slice(0, 5).forEach(file => { const url = URL.createObjectURL(file); const img = document.createElement('img'); img.src = url; img.className = 'forum-upload-thumb'; previews.appendChild(img); }); }); // Post-Löschen-Buttons binden const postsListEl = document.getElementById('forum-posts-list'); if (postsListEl) _bindPostActions(postsListEl, thread.id, uid, isMod); // Reply abschicken document.getElementById('ft-reply')?.addEventListener('click', async () => { const btn = document.getElementById('ft-reply'); const text = document.getElementById('forum-reply-text')?.value?.trim(); if (!text) { UI.toast.warning('Bitte Text eingeben.'); return; } await UI.asyncButton(btn, async () => { const post = await API.forum.addPost(thread.id, { text }); // Foto hochladen falls vorhanden const files = Array.from(document.getElementById('forum-reply-file')?.files || []); for (const file of files.slice(0, 5)) { try { await API.forum.uploadPostFoto(post.id, file); } catch (e) { /* Foto-Upload-Fehler ignorieren */ } } thread.antworten = (thread.antworten || 0) + 1; const idx = _threads.findIndex(t => t.id === thread.id); if (idx !== -1) _threads[idx].antworten = thread.antworten; const listEl = document.getElementById('forum-posts-list'); if (listEl) { const placeholder = listEl.querySelector('p[style*="italic"]'); if (placeholder) listEl.innerHTML = ''; listEl.insertAdjacentHTML('beforeend', _postHTML(post, uid, isMod)); _bindPostActions(listEl, thread.id, uid, isMod); listEl.lastElementChild?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } document.getElementById('forum-reply-text').value = ''; const previews = document.getElementById('forum-reply-previews'); if (previews) previews.innerHTML = ''; UI.toast.success('Antwort gesendet.'); }); }); } // ---------------------------------------------------------- // Post HTML // ---------------------------------------------------------- function _postHTML(p, uid, isMod) { if (p.is_deleted) { return `
Beitrag wurde entfernt
`; } const isOwn = uid && uid === p.user_id; const fotoHtml = (p.foto_urls?.length) ? `
${p.foto_urls.map(u => `` ).join('')}
` : ''; const likeClass = p.user_liked ? 'forum-like-btn active' : 'forum-like-btn'; const canDelete = isOwn || isMod; return `
${_esc(_initial(p.autor_name))}
${_esc(p.text)}
${fotoHtml}
${(!isOwn && uid) ? `` : ''}
${isOwn ? `` : ''} ${canDelete ? `` : ''}
`; } // ---------------------------------------------------------- // Post-Aktionen binden // ---------------------------------------------------------- function _bindPostActions(container, threadId, uid, isMod) { // Like container.querySelectorAll('.forum-post-like:not([data-bound])').forEach(btn => { btn.dataset.bound = '1'; btn.addEventListener('click', async () => { if (!uid) { UI.toast.info('Bitte erst anmelden.'); return; } const postId = parseInt(btn.dataset.postId); try { const res = await API.forum.like('post', postId); btn.classList.toggle('active', res.liked); const countEl = btn.querySelector('.forum-post-like-count'); if (countEl) countEl.textContent = res.count; } catch (err) { UI.toast.error(err.message); } }); }); // Report container.querySelectorAll('.forum-post-report:not([data-bound])').forEach(btn => { btn.dataset.bound = '1'; btn.addEventListener('click', () => { _showReportForm('post', parseInt(btn.dataset.postId)); }); }); // Edit (owner) container.querySelectorAll('.forum-post-edit:not([data-bound])').forEach(btn => { btn.dataset.bound = '1'; btn.addEventListener('click', () => { const postId = parseInt(btn.dataset.postId); const currentText = btn.dataset.text || ''; _showEditPostModal(postId, currentText, container, threadId, uid, isMod); }); }); // Delete container.querySelectorAll('.forum-post-delete:not([data-bound])').forEach(btn => { btn.dataset.bound = '1'; btn.addEventListener('click', async () => { const postId = parseInt(btn.dataset.postId); const postEl = container.querySelector(`[data-post-id="${postId}"]`); const ok = await UI.modal.confirm({ title: 'Antwort löschen?', message: 'Dieser Beitrag wird entfernt.', confirmText: 'Löschen', danger: true, }); if (!ok) return; try { await API.forum.deletePost(postId); if (postEl) { postEl.innerHTML = 'Beitrag wurde entfernt'; postEl.className = 'forum-post forum-post--deleted'; } const idx = _threads.findIndex(t => t.id === threadId); if (idx !== -1 && _threads[idx].antworten > 0) _threads[idx].antworten--; UI.toast.success('Beitrag gelöscht.'); } catch (err) { UI.toast.error(err.message); } }); }); // Foto-Fullscreen container.querySelectorAll('.forum-foto-img:not([data-bound])').forEach(img => { img.dataset.bound = '1'; img.addEventListener('click', () => _showLightbox(img.dataset.src || img.src)); }); } // ---------------------------------------------------------- // Report-Formular // ---------------------------------------------------------- function _showReportForm(targetType, targetId) { const body = `
`; const footer = ` `; UI.modal.open({ title: `${UI.icon('flag')} Inhalt melden`, body, footer }); document.getElementById('rep-cancel')?.addEventListener('click', UI.modal.close); document.getElementById('forum-report-form')?.addEventListener('submit', async e => { e.preventDefault(); const btn = document.querySelector('[form="forum-report-form"][type="submit"]'); const fd = UI.formData(e.target); await UI.asyncButton(btn, async () => { await API.forum.report(targetType, targetId, fd.grund); UI.modal.close(); UI.toast.success('Gemeldet. Danke!'); }); }); } // ---------------------------------------------------------- // Neues Thema // ---------------------------------------------------------- // Regeln & Netiquette // ---------------------------------------------------------- function _showRules() { UI.modal.open({ title: `${UI.icon('info')} Regeln & Netiquette`, body: `

Das Ban-Yaro-Forum ist ein Ort für Hundehalter — freundlich, hilfsbereit und respektvoll. Bitte halte diese Grundregeln ein, damit es für alle schön bleibt.

${UI.icon('chat-circle-dots')} Ton & Umgang
${UI.icon('files')} Inhalte
${UI.icon('syringe')} Gesundheit & Notfälle
${UI.icon('flag')} Moderation

Wer wiederholt gegen die Regeln verstößt, kann vorübergehend gesperrt werden. Das Ziel ist ein freundliches Forum — nicht Kontrolle um der Kontrolle willen. 🐾

`, footer: ``, }); } // ---------------------------------------------------------- function _showCreateForm() { const katOptions = KATEGORIEN.filter(k => k.key !== 'alle').map(k => `` ).join(''); const body = `

${UI.icon('info')} Bitte die beachten.

`; const footer = ` `; UI.modal.open({ title: '+ Neues Thema', body, footer }); let _picker = null; setTimeout(() => { _picker = UI.locationPicker({ containerId: 'forum-location-picker' }); }, 50); document.getElementById('ff-cancel')?.addEventListener('click', UI.modal.close); document.getElementById('ff-rules-link')?.addEventListener('click', _showRules); // Foto-Vorschau — eigenes Array damit iOS-Mehrfachauswahl akkumuliert let _threadFiles = []; const _renderThreadPreviews = () => { const previews = document.getElementById('forum-thread-previews'); if (!previews) return; previews.innerHTML = ''; _threadFiles.forEach((file, i) => { const wrap = document.createElement('div'); wrap.style.cssText = 'position:relative;display:inline-block'; let thumb; if (file.type === 'application/pdf' || file.name.endsWith('.pdf')) { thumb = document.createElement('div'); thumb.className = 'forum-upload-thumb'; thumb.style.cssText = 'display:flex;align-items:center;justify-content:center;background:var(--c-surface-2);font-size:11px;color:var(--c-text-secondary);text-align:center;padding:4px'; thumb.textContent = '📄 PDF'; } else if (file.type.startsWith('video/')) { thumb = document.createElement('video'); thumb.src = URL.createObjectURL(file); thumb.className = 'forum-upload-thumb'; thumb.muted = true; } else { thumb = document.createElement('img'); thumb.src = URL.createObjectURL(file); thumb.className = 'forum-upload-thumb'; } const del = document.createElement('button'); del.type = 'button'; del.textContent = '×'; del.style.cssText = 'position:absolute;top:-4px;right:-4px;width:18px;height:18px;border-radius:50%;' + 'background:var(--c-danger);color:#fff;border:none;font-size:12px;line-height:1;cursor:pointer;' + 'display:flex;align-items:center;justify-content:center;padding:0'; del.addEventListener('click', () => { _threadFiles.splice(i, 1); _renderThreadPreviews(); }); wrap.appendChild(thumb); wrap.appendChild(del); previews.appendChild(wrap); }); }; document.getElementById('forum-thread-files')?.addEventListener('change', e => { const neu = Array.from(e.target.files || []); neu.forEach(f => { if (_threadFiles.length < 5) _threadFiles.push(f); }); e.target.value = ''; // reset damit dieselbe Datei nochmal wählbar ist _renderThreadPreviews(); }); document.getElementById('forum-thread-form')?.addEventListener('submit', async e => { e.preventDefault(); const btn = document.querySelector('[form="forum-thread-form"][type="submit"]'); const fd = UI.formData(e.target); if ((fd.text || '').trim().length < 20) { UI.toast.warning('Text muss mindestens 20 Zeichen lang sein.'); return; } await UI.asyncButton(btn, async () => { const loc = _picker ? _picker.getValue() : { lat: null, lon: null, name: null }; const created = await API.forum.create({ kategorie: fd.kategorie, titel: (fd.titel || '').trim(), text: (fd.text || '').trim(), thread_lat: loc.lat ?? null, thread_lon: loc.lon ?? null, thread_ort: loc.name ?? null, }); // Fotos hochladen for (const file of _threadFiles.slice(0, 5)) { try { await API.forum.uploadThreadFoto(created.id, file); } catch (e) { /* ignorieren */ } } _threads.unshift({ ...created, text_preview: created.text?.slice(0, 120) || '', foto_preview: null, }); UI.modal.close(); _renderList(); UI.toast.success('Beitrag erstellt!'); document.getElementById('forum-thread-list')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); }); } // ---------------------------------------------------------- // Mitgliederkarte // ---------------------------------------------------------- async function _renderMembersMap() { const el = document.getElementById('forum-main'); if (!el) return; el.innerHTML = `
Mitglieder auf der Karte ${_appState.user ? ` ` : ''}
`; // Location-Toggle document.getElementById('forum-loc-toggle')?.addEventListener('change', async e => { const show = e.target.checked; if (show) { try { const pos = await API.getLocation(); // Ortsmitte via Nominatim: erst Ort (zoom=10), dann Nominatim-Suche nach Ortsname let lat = pos.lat, lon = pos.lon; try { const rev = await fetch( `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&zoom=10&addressdetails=1&accept-language=de`, { cache: 'no-store' } ); const d = await rev.json(); const a = d.address || {}; const ort = a.city || a.town || a.village || a.municipality || ''; if (ort) { // Ortsname vorwärts-geocodieren um echte Ortsmitte zu bekommen const fwd = await fetch( `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(ort)}&format=json&limit=1&accept-language=de`, { cache: 'no-store' } ); const results = await fwd.json(); if (results[0]?.lat && results[0]?.lon) { lat = parseFloat(results[0].lat); lon = parseFloat(results[0].lon); } } } catch {} await API.forum.setLocation(lat, lon, true); UI.toast.success('Ortsmitte geteilt — dein genauer Standort bleibt privat.'); _loadMembersOnMap(); } catch (err) { e.target.checked = false; UI.toast.error('Standort konnte nicht ermittelt werden.'); } } else { try { await API.forum.setLocation(null, null, false); UI.toast.success('Standort versteckt.'); _loadMembersOnMap(); } catch (err) { UI.toast.error(err.message); } } }); await _loadLeaflet(); const mapEl = document.getElementById('forum-map'); if (!mapEl) return; _map = L.map(mapEl, { zoomControl: true }).setView([51.0, 10.0], 6); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap', maxZoom: 18, }).addTo(_map); _loadMembersOnMap(); } async function _loadMembersOnMap() { if (!_map) return; try { // MarkerCluster laden falls nicht vorhanden if (!window.L.markerClusterGroup) { await Promise.all([ new Promise((res, rej) => { if (document.querySelector('link[href*="MarkerCluster"]')) { res(); return; } const l1 = document.createElement('link'); l1.rel='stylesheet'; l1.href='/css/MarkerCluster.css'; l1.onload=res; l1.onerror=rej; document.head.appendChild(l1); }), new Promise((res, rej) => { const s = document.createElement('script'); s.src='/js/leaflet.markercluster.js'; s.onload=res; s.onerror=rej; document.head.appendChild(s); }), ]); } const members = await API.forum.membersMap(); // Alte Cluster-Gruppe sauber entfernen if (_clusterGroup) { _map.removeLayer(_clusterGroup); _clusterGroup = null; } _clusterGroup = L.markerClusterGroup({ maxClusterRadius: 60 }); members.forEach(m => { const icon = L.divIcon({ className: '', html: `
${_esc((m.vorname||'?')[0].toUpperCase())}
`, iconSize: [32, 32], iconAnchor: [16, 16], }); _clusterGroup.addLayer( L.marker([m.lat, m.lon], { icon }) .bindPopup(`${_esc(m.vorname || '?')}`) ); }); _map.addLayer(_clusterGroup); } catch (err) { console.error('Mitgliederkarte Fehler:', err); } } async function _loadLeaflet() { if (_leafletLoaded || window.L) { _leafletLoaded = true; return; } // CSS if (!document.querySelector('link[href*="leaflet.css"]')) { const lCss = document.createElement('link'); lCss.rel = 'stylesheet'; lCss.href = '/css/leaflet.css'; document.head.appendChild(lCss); } // JS await new Promise((resolve, reject) => { if (window.L) { resolve(); return; } const s = document.createElement('script'); s.src = '/js/leaflet.js'; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); _leafletLoaded = true; } // ---------------------------------------------------------- // Moderations-Panel // ---------------------------------------------------------- async function _showModPanel() { let reports; try { reports = await API.forum.reports(); } catch (err) { UI.toast.error(err.message); return; } const body = reports.length ? `
${reports.map(r => `
${_esc(r.target_type)} #${r.target_id} — ${_esc(r.grund)}
von ${_esc(r.melder_name || '?')} · ${_fmtDate(r.created_at)}
`).join('')}
` : `

Keine offenen Berichte.

`; const footer = ``; UI.modal.open({ title: `${UI.icon('scales')} Moderationsberichte`, body, footer }); document.getElementById('mod-close')?.addEventListener('click', UI.modal.close); document.querySelectorAll('.forum-resolve-btn').forEach(btn => { btn.addEventListener('click', async () => { const id = parseInt(btn.dataset.id); try { await API.forum.resolveReport(id); btn.closest('.forum-mod-report-item')?.remove(); UI.toast.success('Erledigt.'); } catch (err) { UI.toast.error(err.message); } }); }); } // ---------------------------------------------------------- // Post bearbeiten (Besitzer) // ---------------------------------------------------------- function _showEditPostModal(postId, currentText, container, threadId, uid, isMod) { const id = 'forum-edit-post-form'; UI.modal.open({ title: 'Antwort bearbeiten', body: `
`, footer: ` `, }); document.getElementById(id)?.addEventListener('submit', async e => { e.preventDefault(); const text = new FormData(e.target).get('text')?.trim(); if (!text) return; const btn = document.querySelector(`[form="${id}"][type="submit"]`); await UI.asyncButton(btn, async () => { await API.forum.updatePost(postId, { text }); UI.modal.close(); // Post-Text im DOM aktualisieren if (container) { const postEl = container.querySelector(`[data-post-id="${postId}"]`); const textEl = postEl?.querySelector('.forum-post-text'); if (textEl) textEl.textContent = text; // data-text auf Edit-Button aktualisieren const editBtn = postEl?.querySelector('.forum-post-edit'); if (editBtn) editBtn.dataset.text = text; } UI.toast.success('Antwort aktualisiert.'); }); }); } // ---------------------------------------------------------- // Thread bearbeiten (Besitzer) // ---------------------------------------------------------- function _showEditThreadModal(thread) { const id = 'forum-edit-thread-form'; UI.modal.open({ title: 'Beitrag bearbeiten', body: `
`, footer: ` `, }); let _picker = null; setTimeout(() => { _picker = UI.locationPicker({ containerId: 'forum-edit-location-picker' }); if (thread.thread_lat && thread.thread_lon) { _picker.setValue(thread.thread_lat, thread.thread_lon, thread.thread_ort || null); } }, 50); document.getElementById(id)?.addEventListener('submit', async e => { e.preventDefault(); const fd = new FormData(e.target); const loc = _picker ? _picker.getValue() : { lat: null, lon: null, name: null }; const btn = document.querySelector(`[form="${id}"][type="submit"]`); await UI.asyncButton(btn, async () => { await API.forum.updateThread(thread.id, { titel: fd.get('titel'), text: fd.get('text'), thread_lat: loc.lat ?? null, thread_lon: loc.lon ?? null, thread_ort: loc.name ?? null, }); UI.modal.close(); _loadThreads(true); UI.toast.success('Beitrag aktualisiert.'); }); }); } function openNew() { if (!_appState?.user) { UI.toast.info('Bitte erst anmelden.'); return; } _showCreateForm(); } function _showLightbox(src) { const lb = document.createElement('div'); lb.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.92);z-index:9999;display:flex;align-items:center;justify-content:center;cursor:zoom-out'; lb.innerHTML = ` `; lb.addEventListener('click', () => lb.remove()); document.body.appendChild(lb); } return { init, refresh, onDogChange, openNew, openThread: _openThread }; })();