/* ============================================================ BAN YARO — Freunde ============================================================ */ window.Page_friends = (() => { let _container = null; let _appState = null; let _searchTimer = null; // ---------------------------------------------------------- async function init(container, appState, params = {}) { _container = container; _appState = appState; _render(params.suche || null); } function refresh() { _loadFriends(); } function onDogChange() {} // ---------------------------------------------------------- // HAUPT-RENDER // ---------------------------------------------------------- function _render(prefill = null) { if (!_appState.user) { _container.innerHTML = UI.emptyState({ icon: UI.icon('users'), title: 'Anmelden erforderlich', text: 'Melde dich an, um Freunde zu finden und Anfragen zu verwalten.', action: ``, }); return; } const myName = _appState?.user?.name || ''; const myLink = `${location.origin}/#friends?suche=${encodeURIComponent(myName)}`; _container.innerHTML = `
Dein Freundes-Link
Teile ihn — der andere tippt drauf und findet dich sofort.
banyaro.app/#friends?suche=${UI.escape(encodeURIComponent(myName))}

Tipp: Lass dir den Freundes-Link einer anderen Person schicken — dann klappt die Suche automatisch.

`; // Copy-Button _container.querySelector('#fr-copy-btn')?.addEventListener('click', () => { navigator.clipboard.writeText(myLink).then(() => { UI.toast.success('Link kopiert!'); }).catch(() => { UI.toast.info('Link: ' + myLink); }); }); // Share-Button (Web Share API, Fallback: Copy) _container.querySelector('#fr-share-btn')?.addEventListener('click', async () => { if (navigator.share) { try { await navigator.share({ title: `${myName} auf Ban Yaro`, text: `Füge mich auf Ban Yaro als Freund hinzu!`, url: myLink, }); } catch { /* abgebrochen */ } } else { navigator.clipboard.writeText(myLink).then(() => { UI.toast.success('Link kopiert!'); }); } }); // Suche const searchInput = _container.querySelector('#fr-search'); searchInput.addEventListener('input', e => { clearTimeout(_searchTimer); const q = e.target.value.trim(); if (q.length < 2) { _container.querySelector('#fr-search-results').innerHTML = ''; return; } _searchTimer = setTimeout(() => _doSearch(q), 380); }); // Delegierter Click-Handler — robust auch unter strikter CSP / für // dynamisch nachgerenderte Buttons (Anfragen, Freundesliste). // Ersetzt Inline-onclick, das auf manchen iOS-PWA-Sessions nicht feuerte. _container.addEventListener('click', e => { const btn = e.target.closest('[data-fr-action]'); if (!btn) return; const id = parseInt(btn.dataset.frId, 10); switch (btn.dataset.frAction) { case 'accept': _accept(id); break; case 'decline': _decline(id); break; case 'cancel': _cancel(id); break; case 'chat': _openChat(id); break; } }); // Prefill aus URL-Parameter → sofort suchen if (prefill && prefill.length >= 2) { _doSearch(prefill); } _loadFriends(); } // ---------------------------------------------------------- // DATEN LADEN // ---------------------------------------------------------- async function _loadFriends() { if (!_appState.user) return; try { const data = await API.friends.list(); _renderIncoming(data.incoming || []); _renderOutgoing(data.outgoing || []); _renderFriends(data.friends || []); _updateBadge((data.incoming || []).length); } catch { /* silent — 401 bei abgemeldeter Session */ } _loadActivity(); } async function _loadActivity() { if (!_appState.user) return; const el = _container.querySelector('#fr-activity'); if (!el) return; // Ladeindikator el.innerHTML = `
Aktivitäten
`; try { const items = await API.friends.activity(); _renderActivity(items || []); } catch { el.innerHTML = ''; } } let _activityFilter = 'alle'; let _activityAll = []; function _renderActivity(items) { _activityAll = items; const el = _container.querySelector('#fr-activity'); if (!el) return; const FILTERS = [ { key: 'alle', label: 'Alle' }, { key: 'walk', label: 'Gassi-Treffen' }, { key: 'forum', label: 'Forum' }, { key: 'health', label: 'Gesundheit' }, { key: 'new_dog', label: 'Neuer Hund' }, ]; const chips = FILTERS.map(f => ` `).join(''); const filtered = _activityFilter === 'alle' ? items : items.filter(i => i.type === _activityFilter); el.innerHTML = `
Aktivitäten
${chips}
${!filtered.length ? `

Keine Einträge in dieser Kategorie.

` : `
${filtered.map(item => _activityItem(item)).join('')}
` }
`; el.querySelectorAll('[data-af]').forEach(btn => { btn.addEventListener('click', () => { _activityFilter = btn.dataset.af; _renderActivity(_activityAll); }); }); el.querySelectorAll('.fr-activity-item[data-nav]').forEach(btn => { btn.addEventListener('click', () => { const page = btn.dataset.nav; const entryId = btn.dataset.entryId ? parseInt(btn.dataset.entryId) : null; const type = btn.dataset.type; if (!page) return; if (entryId && type === 'diary') { App.callModule('diary', 'openDetail', entryId); } else if (entryId && type === 'walk') { App.callModule('walks', 'openDetail', entryId); } else if (entryId && type === 'forum') { App.callModule('forum', 'openThread', entryId); } else { App.navigate(page); } }); }); } const _ACTIVITY_PAGE = { health: 'health', walk: 'walks', forum: 'forum', new_dog: null, }; function _activityItem(item) { const ago = _timeAgo(item.created_at); const text = item.text || ''; const page = _ACTIVITY_PAGE[item.type] || ''; const dogLabel = item.dog_name ? `${UI.escape(item.dog_name)}` : ''; const avatar = item.dog_foto ? `${UI.escape(item.dog_name || '')}` : item.avatar_url ? `${UI.escape(item.user_name)}` : `
${UI.escape((item.user_name || '?')[0].toUpperCase())}
`; const tag = page ? `button type="button"` : `div`; return ` <${tag} class="fr-activity-item${page ? ' fr-activity-item--link' : ''}" ${page ? `data-nav="${page}"` : ''} ${item.entry_id ? `data-entry-id="${item.entry_id}"` : ''} data-type="${item.type}">
${avatar}
${UI.escape(item.user_name)} ${dogLabel}
${text ? `
${UI.escape(text)}
` : ''}
${UI.escape(ago)}
`; } function _timeAgo(iso) { if (!iso) return ''; const diff = Math.floor((Date.now() - new Date(iso + (iso.endsWith('Z') ? '' : 'Z')).getTime()) / 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`; if (diff < 86400 * 7) return `vor ${Math.floor(diff / 86400)} Tagen`; return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); } function _updateBadge(count) { const el = document.getElementById('friends-badge'); if (!el) return; el.textContent = count; el.style.display = count > 0 ? '' : 'none'; } // ---------------------------------------------------------- // EINGEHENDE ANFRAGEN // ---------------------------------------------------------- function _renderIncoming(list) { const el = _container.querySelector('#fr-incoming'); if (!list.length) { el.innerHTML = ''; return; } el.innerHTML = `
Anfragen · ${list.length}
${list.map(r => `
${_userAvatar(r.requester_name, r.dogs?.[0], r.avatar_url)}
${UI.escape(r.requester_name)}
${_dogPills(r.dogs, 2)}
`).join('')}
`; } // ---------------------------------------------------------- // GESENDETE ANFRAGEN // ---------------------------------------------------------- function _renderOutgoing(list) { const el = _container.querySelector('#fr-outgoing'); if (!list.length) { el.innerHTML = ''; return; } el.innerHTML = `
Gesendet
${list.map(r => `
${UI.escape((r.addressee_name || '?')[0].toUpperCase())}
${UI.escape(r.addressee_name)}
Anfrage ausstehend
`).join('')}
`; } // ---------------------------------------------------------- // FREUNDESLISTE // ---------------------------------------------------------- function _renderFriends(list) { const el = _container.querySelector('#fr-list'); if (!list.length) { el.innerHTML = _emptyState( 'users-three', 'Noch keine Freunde', 'Verbinde dich mit anderen Hundebesitzern. Teile Routen, sieh Aktivitäten und schreib Nachrichten.', `` ); el.querySelector('#fr-empty-search')?.addEventListener('click', () => { _container.querySelector('#fr-search')?.focus(); }); return; } el.innerHTML = `
Freunde · ${list.length}
${list.map(f => _friendCard(f)).join('')}
`; // Notiz-Buttons el.querySelectorAll('.fr-note-btn').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); const id = parseInt(btn.dataset.frNoteId); const name = btn.dataset.frNoteName || ''; UI.noteModal('friends', id, name, null); }); }); // Klick auf Karte → Mini-Profil el.querySelectorAll('.fr-card').forEach(card => { card.addEventListener('click', e => { if (e.target.closest('button')) return; // Buttons nicht überschreiben const fid = parseInt(card.dataset.friendId); const fname = card.dataset.friendName; const fdogs = JSON.parse(card.dataset.dogs || '[]'); const fprofile = JSON.parse(card.dataset.profile || '{}'); _showProfile(fid, fname, fdogs, fprofile); }); }); } function _friendCard(f) { const dogs = f.dogs || []; const profile = { bio: f.bio || null, wohnort: f.wohnort || null, erfahrung: f.erfahrung || null, social_link: f.social_link || null, profil_sichtbarkeit: f.profil_sichtbarkeit || null, avatar_url: f.avatar_url || null, }; return `
${_userAvatar(f.friend_name, dogs[0], f.avatar_url)}
${UI.escape(f.friend_name)} ${_erfahrungSpan(f.erfahrung)}
${_wohnortLine(f.wohnort)} ${_bioLine(f.bio, f.profil_sichtbarkeit)}
${dogs.length ? `
${_dogPills(dogs, 3)}
` : `
Noch kein Hund eingetragen
` }
${_dogPhotoRow(dogs)}
`; } function _dogPhotoRow(dogs) { const withPhotos = dogs.filter(d => d.foto_url); if (withPhotos.length < 2) return ''; // 1 Foto schon im Avatar, < 2 lohnt sich nicht return `
${withPhotos.slice(0, 4).map(d => `
${UI.escape(d.name)}
${UI.escape(d.name)}
`).join('')}
`; } // ---------------------------------------------------------- // MINI-PROFIL MODAL // ---------------------------------------------------------- function _showProfile(friendId, friendName, dogs, profile = {}) { const dogsHTML = dogs.length ? `
${dogs.map(d => `
${d.foto_url ? `${UI.escape(d.name)}` : `
🐕
` }
${UI.escape(d.name)}
${d.rasse ? `
${UI.escape(d.rasse)}
` : ''}
`).join('')}
` : `

Noch kein Hund eingetragen.

`; const profileInfoHTML = (() => { const parts = []; if (profile.wohnort) { parts.push(`
📍 ${UI.escape(profile.wohnort)}
`); } if (profile.erfahrung && _erfahrungBadge[profile.erfahrung]) { parts.push(`
${_erfahrungBadge[profile.erfahrung]}
`); } if (profile.bio && profile.profil_sichtbarkeit !== 'private') { parts.push(`
${UI.escape(profile.bio)}
`); } if (profile.social_link) { parts.push(`
${UI.escape(profile.social_link)}
`); } if (!parts.length) return ''; return `
${parts.join('')}
`; })(); const badgesHTML = (profile.is_founder || profile.is_partner) ? `
${profile.is_founder ? ` ${profile.founder_number ? `Gründer #${profile.founder_number}` : 'Gründer'} ` : ''} ${profile.is_partner ? ` Partner ` : ''}
` : ''; UI.modal.open({ title: UI.escape(friendName), body: `
${badgesHTML} ${profileInfoHTML}
${dogs.length === 1 ? 'Hund' : 'Hunde'}
${dogsHTML}
`, footer: ` `, }); document.getElementById('modal-chat-btn')?.addEventListener('click', () => { UI.modal.close(); _openChat(friendId); }); document.getElementById('modal-remove-btn')?.addEventListener('click', async () => { UI.modal.close(); await _removeFriend(friendId, friendName); }); } // ---------------------------------------------------------- // SUCHE // ---------------------------------------------------------- async function _doSearch(q) { if (!_appState.user) return; const el = _container.querySelector('#fr-search-results'); try { const results = await API.friends.search(q); if (!results.length) { el.innerHTML = `
Kein Nutzer gefunden.
`; return; } el.innerHTML = `
${results.map((u, i) => `
${_userAvatar(u.name, null, u.avatar_url)}
${UI.escape(u.name)} ${u.is_founder ? `${u.founder_number ? `Gründer #${u.founder_number}` : 'Gründer'}` : ''} ${u.is_partner ? `Partner` : ''} ${_erfahrungSpan(u.erfahrung)}
${_wohnortLine(u.wohnort)} ${_bioLine(u.bio, u.profil_sichtbarkeit)} ${u.dogs?.length ? `
${u.dogs.map(d => UI.escape(d.name) + (d.rasse ? ` · ${UI.escape(d.rasse)}` : '')).join('  |  ')}
` : ''}
`).join('')}
`; el.querySelectorAll('.fr-add-btn').forEach(btn => { btn.addEventListener('click', () => _sendRequest(parseInt(btn.dataset.userId), btn)); }); } catch { el.innerHTML = ''; } } // ---------------------------------------------------------- // AKTIONEN // ---------------------------------------------------------- async function _sendRequest(userId, btn) { if (!_appState.user) { App.navigate('settings'); return; } btn.disabled = true; btn.innerHTML = ``; try { await API.friends.sendRequest(userId); UI.toast.success('Freundschaftsanfrage gesendet!'); _container.querySelector('#fr-search').value = ''; _container.querySelector('#fr-search-results').innerHTML = ''; await _loadFriends(); } catch (e) { UI.toast.error(e.message || 'Fehler beim Senden.'); btn.disabled = false; btn.innerHTML = ` Anfrage`; } } async function _accept(id) { try { await API.friends.accept(id); UI.toast.success('Freundschaft angenommen!'); await _loadFriends(); } catch (e) { UI.toast.error(e.message); } } async function _decline(id) { try { await API.friends.decline(id); await _loadFriends(); } catch (e) { UI.toast.error(e.message); } } async function _cancel(id) { try { await API.friends.decline(id); await _loadFriends(); } catch (e) { UI.toast.error(e.message); } } async function _removeFriend(userId, name) { const ok = await UI.modal.confirm({ title: 'Freund entfernen?', message: `${name} wird aus deiner Freundesliste entfernt.`, confirmText: 'Entfernen', }); if (!ok) return; try { await API.friends.remove(userId); UI.toast.info('Freund entfernt.'); await _loadFriends(); } catch (e) { UI.toast.error(e.message); } } async function _openChat(userId) { if (!_appState.user) { App.navigate('settings'); return; } try { const { conversation_id } = await API.chat.start(userId); App.navigate('chat', true, { conversation_id }); } catch (e) { UI.toast.error(e.message); } } // ---------------------------------------------------------- // RENDER-HELPERS // ---------------------------------------------------------- function _userAvatar(name, firstDog, avatarUrl) { if (avatarUrl) { return `${UI.escape(name)}`; } if (firstDog?.foto_url) { return `${UI.escape(firstDog.name)}`; } return `
${UI.escape((name || '?')[0].toUpperCase())}
`; } const _erfahrungBadge = { einsteiger: '🐾 Einsteiger', erfahren: '⭐ Erfahren', trainer: '🎓 Trainer', zuechter: '🏅 Züchter', }; function _erfahrungSpan(erfahrung) { if (!erfahrung || !_erfahrungBadge[erfahrung]) return ''; return `${_erfahrungBadge[erfahrung]}`; } function _wohnortLine(wohnort) { if (!wohnort) return ''; return `📍 ${UI.escape(wohnort)}`; } function _bioLine(bio, sichtbarkeit) { if (!bio || sichtbarkeit === 'private') return ''; const text = bio.length > 120 ? bio.substring(0, 120) + '…' : bio; return `
${UI.escape(text)}
`; } function _dogPills(dogs, max) { if (!dogs?.length) return ''; const visible = dogs.slice(0, max); const rest = dogs.length - max; return `
${visible.map(d => ` 🐕 ${UI.escape(d.name)}${d.rasse ? ` · ${UI.escape(d.rasse)}` : ''} `).join('')} ${rest > 0 ? `+${rest}` : ''}
`; } function _emptyState(icon, title, text, cta = '') { return `
${title}
${text ? `

${text}

` : ''} ${cta ? `
${cta}
` : ''}
`; } // ---------------------------------------------------------- // NOTIZ-MODAL // ---------------------------------------------------------- // ---------------------------------------------------------- return { init, refresh, onDogChange, _accept, _decline, _cancel, _removeFriend, _openChat }; })();