/* ============================================================ BAN YARO — Playdate-Matching Spielkameraden in der Nähe finden, Inserate verwalten, Anfragen ============================================================ */ window.Page_playdate = (() => { let _container = null; let _appState = null; let _activeTab = 'nearby'; // 'nearby' | 'listings' | 'requests' let _userPos = null; let _radius = 10; let _dogs = []; // ------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------ function _esc(s) { return String(s || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function _fmtDate(iso) { if (!iso) return ''; const d = new Date(iso.replace(' ', 'T')); return d.toLocaleDateString('de-DE'); } function _dogAvatar(foto_url, name, size = 48) { const initials = _esc((name || '?').charAt(0).toUpperCase()); if (foto_url) { return `${initials}`; } return `
${initials}
`; } function _statusBadge(status) { const map = { pending: ['warning', 'Ausstehend'], accepted: ['success', 'Angenommen'], declined: ['danger', 'Abgelehnt'], }; const [type, label] = map[status] || ['default', status]; const colors = { warning: 'var(--c-warning, #f59e0b)', success: 'var(--c-success, #10b981)', danger: 'var(--c-danger, #ef4444)', default: 'var(--c-text-muted)', }; return `${label}`; } // ------------------------------------------------------------------ // INIT // ------------------------------------------------------------------ async function init(container, appState) { _container = container; _appState = appState; _dogs = appState.dogs?.filter(d => !d.is_guest) || []; _render(); _switchTab(_activeTab); } function refresh() { _dogs = _appState?.dogs?.filter(d => !d.is_guest) || []; _switchTab(_activeTab); } function onDogChange() { _dogs = _appState?.dogs?.filter(d => !d.is_guest) || []; if (_activeTab === 'listings') _loadListings(); } // ------------------------------------------------------------------ // RENDER — Grundstruktur mit Tabs // ------------------------------------------------------------------ function _render() { _container.innerHTML = `
`; document.getElementById('playdate-tabs').addEventListener('click', e => { const btn = e.target.closest('.by-tab'); if (!btn) return; _switchTab(btn.dataset.tab); }); } function _switchTab(tab) { _activeTab = tab; document.querySelectorAll('#playdate-tabs .by-tab').forEach(b => { b.classList.toggle('active', b.dataset.tab === tab); }); const content = document.getElementById('playdate-content'); if (!content) return; if (tab === 'nearby') _renderNearby(content); if (tab === 'listings') _renderListings(content); if (tab === 'requests') _renderRequests(content); } // ------------------------------------------------------------------ // TAB: IN DER NÄHE // ------------------------------------------------------------------ async function _renderNearby(el) { el.innerHTML = `
${UI.icon('map-pin')} ${_userPos ? 'Standort bekannt' : 'Kein Standort'}
${UI.icon('info')} Nur Hunde von Nutzern die sich ebenfalls mit einem Inserat eingetragen haben. Dein genauer Standort bleibt privat — es wird nur der Ortsname und die ungefähre Entfernung angezeigt.

Standort wird ermittelt…

`; document.getElementById('nearby-radius').addEventListener('change', e => { _radius = parseInt(e.target.value, 10); _loadNearby(); }); document.getElementById('nearby-locate-btn').addEventListener('click', async () => { const btn = document.getElementById('nearby-locate-btn'); UI.setLoading(btn, true); try { _userPos = await API.getLocation(); const label = document.getElementById('nearby-location-label'); if (label) label.textContent = 'Standort aktualisiert'; await _loadNearby(); } catch { UI.toast.error('Standort konnte nicht ermittelt werden.'); } finally { UI.setLoading(btn, false); } }); if (!_userPos) { try { _userPos = await API.getLocation(); const label = document.getElementById('nearby-location-label'); if (label) label.textContent = 'Standort bekannt'; } catch { document.getElementById('nearby-results').innerHTML = `
${UI.icon('map-pin')}

Standort konnte nicht automatisch ermittelt werden.
Klicke auf "Standort aktualisieren".

`; return; } } await _loadNearby(); } async function _loadNearby() { if (!_userPos) return; const resultsEl = document.getElementById('nearby-results'); if (!resultsEl) return; resultsEl.innerHTML = `

${UI.icon('spinner')} Suche…

`; try { const data = await API.get(`/playdate/nearby?lat=${_userPos.lat}&lon=${_userPos.lon}&radius=${_radius}`); if (!data || data.length === 0) { resultsEl.innerHTML = UI.emptyState({ icon: UI.icon('paw-print'), title: 'Niemand in der Nähe', text: `Aktuell hat sich noch niemand in ${_radius} km Umkreis eingetragen. Trage deinen Hund unter "Meine Inserate" ein!`, }); return; } resultsEl.innerHTML = `
${data.map(d => _nearbyCard(d)).join('')}
`; resultsEl.querySelectorAll('.playdate-anfrage-btn').forEach(btn => { btn.addEventListener('click', () => { const toDogId = parseInt(btn.dataset.dogId, 10); const dogName = btn.dataset.dogName; _showRequestModal(toDogId, dogName); }); }); } catch (err) { resultsEl.innerHTML = `

${err.message}

`; } } function _nearbyCard(d) { return `
${_dogAvatar(d.foto_url, d.dog_name, 56)}
${_esc(d.dog_name)}
${d.rasse ? `
${_esc(d.rasse)}
` : ''} ${d.alter ? `
${_esc(d.alter)}
` : ''}
${UI.icon('map-pin')} ${d.ort_name ? _esc(d.ort_name) + ' · ' : ''}${d.entfernung_km} km entfernt ${d.geschlecht ? `${_esc(d.geschlecht)}` : ''}
${d.beschreibung ? `

${_esc(d.beschreibung)}

` : ''}
`; } function _showRequestModal(toDogId, dogName) { const formId = 'playdate-req-form'; UI.modal.open({ title: `Anfrage an ${dogName}`, body: `
`, footer: ` `, }); document.getElementById('req-cancel-btn').addEventListener('click', () => UI.modal.close()); document.getElementById('req-send-btn').addEventListener('click', async () => { const btn = document.getElementById('req-send-btn'); const nachricht = document.getElementById('req-nachricht').value.trim(); await UI.asyncButton(btn, async () => { const result = await API.post('/playdate/request', { to_dog_id: toDogId, nachricht: nachricht || null, }); UI.modal.close(); UI.toast.success('Anfrage gesendet! Ein Chat wurde geöffnet.'); // Zum Chat navigieren if (result.conversation_id) { setTimeout(() => { App.navigate('chat', true, { conversation_id: result.conversation_id }); }, 800); } }, { errorMsg: null }); }); } // ------------------------------------------------------------------ // TAB: MEINE INSERATE // ------------------------------------------------------------------ async function _renderListings(el) { el.innerHTML = `

${UI.icon('spinner')} Lädt…

`; await _loadListings(el); } async function _loadListings(el) { const target = el || document.getElementById('playdate-content'); if (!target) return; if (_dogs.length === 0) { target.innerHTML = UI.emptyState({ icon: UI.icon('paw-print'), title: 'Noch kein Hund', text: 'Lege zuerst einen Hund in deinem Profil an.', action: ``, }); return; } // Listings für alle eigenen Hunde laden const listings = {}; await Promise.all(_dogs.map(async dog => { try { const data = await API.get(`/playdate/my-listing/${dog.id}`); listings[dog.id] = data; } catch { listings[dog.id] = null; } })); target.innerHTML = `
${_dogs.map(dog => _listingCard(dog, listings[dog.id])).join('')}
`; // Event-Delegation für alle Buttons target.addEventListener('click', async e => { const btn = e.target.closest('button[data-action]'); if (!btn) return; const action = btn.dataset.action; const dogId = parseInt(btn.dataset.dogId, 10); const dog = _dogs.find(d => d.id === dogId); if (action === 'edit') { _showListingModal(dog, listings[dogId], async () => { await _loadListings(); }); } if (action === 'deactivate') { if (!window.confirm(`Inserat für ${dog?.name} deaktivieren?`)) return; try { await API.del(`/playdate/listing/${dogId}`); UI.toast.success('Inserat deaktiviert.'); await _loadListings(); } catch (err) { UI.toast.error(err.message); } } }); } function _listingCard(dog, listing) { const isAktiv = listing && listing.aktiv; return `
${_dogAvatar(dog.foto_url, dog.name, 44)}
${_esc(dog.name)}
${dog.rasse ? `
${_esc(dog.rasse)}
` : ''}
${isAktiv ? 'Aktiv' : 'Inaktiv'}
${isAktiv ? `
${UI.icon('map-pin')} ${listing.ort_name ? _esc(listing.ort_name) + ' · ' : ''} Radius: ${listing.radius_km} km
${listing.beschreibung ? `

${_esc(listing.beschreibung)}

` : ''} ` : `

Noch kein Inserat — trage dich ein, damit andere dich finden können.

`}
${isAktiv ? ` ` : ''}
`; } function _showListingModal(dog, existing, onSaved) { const formId = 'listing-form'; UI.modal.open({ title: `Inserat für ${dog.name}`, body: `
Tipp: Klicke auf ${UI.icon('crosshair')} um deinen Ortsnamen automatisch zu ermitteln. Nur der Ortsname wird für andere sichtbar — nicht dein genauer Standort.
`, footer: ` `, }); // GPS-Button document.getElementById('listing-gps-btn').addEventListener('click', async () => { const gpsBtn = document.getElementById('listing-gps-btn'); UI.setLoading(gpsBtn, true); try { const pos = await API.getLocation(); document.getElementById('listing-lat').value = pos.lat; document.getElementById('listing-lon').value = pos.lon; // Reverse-Geocoding für Ortsname try { const rev = await fetch( `https://nominatim.openstreetmap.org/reverse?lat=${pos.lat}&lon=${pos.lon}&format=json&zoom=10&accept-language=de`, { cache: 'no-store' } ); const geoData = await rev.json(); const a = geoData.address || {}; const ort = a.city || a.town || a.village || a.municipality || ''; if (ort) document.getElementById('listing-ort').value = ort; } catch {} UI.toast.success('Standort ermittelt.'); } catch { UI.toast.error('Standort konnte nicht ermittelt werden.'); } finally { UI.setLoading(gpsBtn, false); } }); document.getElementById('listing-cancel-btn').addEventListener('click', () => UI.modal.close()); document.getElementById('listing-save-btn').addEventListener('click', async () => { const btn = document.getElementById('listing-save-btn'); const lat = parseFloat(document.getElementById('listing-lat').value); const lon = parseFloat(document.getElementById('listing-lon').value); const ort = document.getElementById('listing-ort').value.trim(); const rad = parseInt(document.getElementById('listing-radius').value, 10); const desc = document.getElementById('listing-beschreibung').value.trim(); if (!lat || !lon) { UI.toast.error('Bitte ermittle zuerst deinen Standort über den GPS-Button.'); return; } await UI.asyncButton(btn, async () => { await API.put('/playdate/listing', { dog_id: dog.id, lat, lon, ort_name: ort || null, radius_km: rad, beschreibung: desc || null, }); UI.modal.close(); UI.toast.success('Inserat gespeichert!'); onSaved?.(); }, { errorMsg: null }); }); } // ------------------------------------------------------------------ // TAB: ANFRAGEN // ------------------------------------------------------------------ async function _renderRequests(el) { el.innerHTML = `

${UI.icon('spinner')} Lädt…

`; try { const data = await API.get('/playdate/requests'); const incoming = data.incoming || []; const outgoing = data.outgoing || []; // Badge aktualisieren const pendingCount = incoming.filter(r => r.status === 'pending').length; const badge = document.getElementById('playdate-req-badge'); if (badge) { badge.textContent = pendingCount; badge.style.display = pendingCount > 0 ? '' : 'none'; } if (incoming.length === 0 && outgoing.length === 0) { el.innerHTML = UI.emptyState({ icon: UI.icon('paw-print'), title: 'Noch keine Anfragen', text: 'Wenn du oder jemand anderes eine Playdate-Anfrage schickt, erscheint sie hier.', }); return; } el.innerHTML = `
${incoming.length > 0 ? `

Eingehende Anfragen

${incoming.map(r => _incomingCard(r)).join('')}
` : ''} ${outgoing.length > 0 ? `

Ausgehende Anfragen

${outgoing.map(r => _outgoingCard(r)).join('')}
` : ''}
`; // Button-Events (Accept/Decline) el.querySelectorAll('.req-accept-btn, .req-decline-btn').forEach(btn => { btn.addEventListener('click', async () => { const reqId = parseInt(btn.dataset.reqId, 10); const status = btn.dataset.status; await UI.asyncButton(btn, async () => { const result = await API.patch(`/playdate/requests/${reqId}`, { status }); if (status === 'accepted' && result.conversation_id) { UI.toast.success('Anfrage angenommen! Chat wurde geöffnet.'); setTimeout(() => { App.navigate('chat', true, { conversation_id: result.conversation_id }); }, 800); } else { UI.toast.success(status === 'accepted' ? 'Anfrage angenommen.' : 'Anfrage abgelehnt.'); } await _renderRequests(el); }, { errorMsg: null }); }); }); // Chat-Buttons el.querySelectorAll('.req-chat-btn').forEach(btn => { btn.addEventListener('click', () => { App.navigate('chat', true); }); }); } catch (err) { el.innerHTML = `

${err.message}

`; } } function _incomingCard(r) { const isPending = r.status === 'pending'; return `
${_dogAvatar(r.from_dog_foto, r.from_dog_name, 44)}
${_esc(r.from_dog_name)}
${r.from_dog_rasse ? _esc(r.from_dog_rasse) + ' · ' : ''} ${r.alter ? _esc(r.alter) + ' · ' : ''} von ${_esc(r.from_user_name)}
${_fmtDate(r.created_at)}
${_statusBadge(r.status)}
${r.nachricht ? `
"${_esc(r.nachricht)}"
` : ''} ${isPending ? `
` : ` ${r.status === 'accepted' ? ` ` : ''} `}
`; } function _outgoingCard(r) { return `
${_dogAvatar(r.to_dog_foto, r.to_dog_name, 44)}
${_esc(r.to_dog_name)}
${r.to_dog_rasse ? _esc(r.to_dog_rasse) + ' · ' : ''} von ${_esc(r.to_user_name)}
${_fmtDate(r.created_at)}
${_statusBadge(r.status)}
${r.nachricht ? `

"${_esc(r.nachricht)}"

` : ''} ${r.status === 'accepted' ? ` ` : ''}
`; } // ------------------------------------------------------------------ return { init, refresh, onDogChange }; })();