/* ============================================================
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: `Anmelden `,
});
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=${_esc(encodeURIComponent(myName))}
Teilen
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);
});
// 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 = `
`;
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: 'diary', label: 'Tagebuch' },
{ key: 'walk', label: 'Gassi-Treffen' },
{ key: 'health', label: 'Gesundheit' },
{ key: 'new_dog', label: 'Neuer Hund' },
];
const chips = FILTERS.map(f => `
${f.label}
`).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;
if (page) App.navigate(page);
});
});
}
const _ACTIVITY_PAGE = {
diary: 'diary',
health: 'health',
walk: 'walks',
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
? `${_esc(item.dog_name)} `
: '';
const avatar = item.dog_foto
? ` `
: item.avatar_url
? ` `
: `
${_esc((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}"` : ''}>
${_esc(item.user_name)}
${dogLabel}
${text ? `
${_esc(text)}
` : ''}
${_esc(ago)}
${page ? 'button' : 'div'}>
`;
}
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)}
${_esc(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 => `
${_esc((r.addressee_name || '?')[0].toUpperCase())}
${_esc(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.',
`Freunde suchen `
);
el.querySelector('#fr-empty-search')?.addEventListener('click', () => {
_container.querySelector('#fr-search')?.focus();
});
return;
}
el.innerHTML = `
Freunde · ${list.length}
${list.map(f => _friendCard(f)).join('')}
`;
// 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)}
${_esc(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 => `
${_esc(d.name)}
`).join('')}
`;
}
// ----------------------------------------------------------
// MINI-PROFIL MODAL
// ----------------------------------------------------------
function _showProfile(friendId, friendName, dogs, profile = {}) {
const dogsHTML = dogs.length
? `
${dogs.map(d => `
${d.foto_url
? `
`
: `
🐕
`
}
${_esc(d.name)}
${d.rasse
? `
${_esc(d.rasse)}
`
: ''}
`).join('')}
`
: `
Noch kein Hund eingetragen.
`;
const profileInfoHTML = (() => {
const parts = [];
if (profile.wohnort) {
parts.push(`
📍 ${_esc(profile.wohnort)}
`);
}
if (profile.erfahrung && _erfahrungBadge[profile.erfahrung]) {
parts.push(`
${_erfahrungBadge[profile.erfahrung]}
`);
}
if (profile.bio && profile.profil_sichtbarkeit !== 'private') {
parts.push(`
${_esc(profile.bio)}
`);
}
if (profile.social_link) {
parts.push(``);
}
if (!parts.length) return '';
return `${parts.join('')}
`;
})();
UI.modal.open({
title: _esc(friendName),
body: `
${profileInfoHTML}
${dogs.length === 1 ? 'Hund' : 'Hunde'}
${dogsHTML}
`,
footer: `
Nachricht schreiben
Entfernen
`,
});
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)}
${_esc(u.name)}
${_erfahrungSpan(u.erfahrung)}
${_wohnortLine(u.wohnort)}
${_bioLine(u.bio, u.profil_sichtbarkeit)}
${u.dogs?.length
? `
${u.dogs.map(d => _esc(d.name) + (d.rasse ? ` · ${_esc(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 ` `;
}
if (firstDog?.foto_url) {
return ` `;
}
return `
${_esc((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 `📍 ${_esc(wohnort)} `;
}
function _bioLine(bio, sichtbarkeit) {
if (!bio || sichtbarkeit === 'private') return '';
const text = bio.length > 120 ? bio.substring(0, 120) + '…' : bio;
return `${_esc(text)}
`;
}
function _dogPills(dogs, max) {
if (!dogs?.length) return '';
const visible = dogs.slice(0, max);
const rest = dogs.length - max;
return `
${visible.map(d => `
🐕 ${_esc(d.name)}${d.rasse ? ` · ${_esc(d.rasse)}` : ''}
`).join('')}
${rest > 0 ? `+${rest} ` : ''}
`;
}
function _esc(s) {
if (!s) return '';
return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
}
function _emptyState(icon, title, text, cta = '') {
return `
${title}
${text ? `
${text}
` : ''}
${cta ? `
${cta}
` : ''}
`;
}
// ----------------------------------------------------------
return { init, refresh, onDogChange, _accept, _decline, _cancel, _removeFriend, _openChat };
})();