Sprint 11: Freunde & Chat + Phosphor-Icon-Vollmigration

- Freundschaften (pending/accepted), Nutzersuche, Anfragen per Push
- Direktnachrichten mit Polling, iMessage-Stil, Deep-Links aus Push
- Alle Seiten (map, places, diary, health, dog-profile, sitting, knigge,
  forum, wiki, walks) vollständig auf Phosphor-Icons migriert
- Wikidata-Rassen-Scraper (~833 neue Rassen, lokal gespiegelte Fotos)
- TheDogAPI lokal gespiegelt (169 Rassen + Fotos)
- Quiz-Result-Cards horizontal (korrekte Bildproportionen)
- SW by-v89
This commit is contained in:
rene 2026-04-15 21:33:53 +02:00
parent 96bd57f0ad
commit 097295c628
44 changed files with 9980 additions and 300 deletions

View file

@ -0,0 +1,282 @@
/* ============================================================
BAN YARO Freunde-Seite
============================================================ */
window.Page_friends = (() => {
let _container = null;
let _searchTimer = null;
// ----------------------------------------------------------
function init(container) {
_container = container;
render();
}
// ----------------------------------------------------------
async function render() {
_container.innerHTML = `
<div style="padding:var(--space-4)">
<h2 style="font-size:var(--text-xl);font-weight:var(--weight-bold);margin-bottom:var(--space-4)">
Freunde
</h2>
<!-- Suche -->
<div class="friends-search-row">
<input id="fr-search" class="form-input" type="search"
placeholder="Namen suchen…" autocomplete="off" style="flex:1">
</div>
<div id="fr-search-results"></div>
<!-- Incoming requests -->
<div id="fr-incoming"></div>
<!-- Outgoing -->
<div id="fr-outgoing"></div>
<!-- Friends list -->
<div id="fr-list"></div>
</div>
`;
document.getElementById('fr-search').addEventListener('input', e => {
clearTimeout(_searchTimer);
_searchTimer = setTimeout(() => _doSearch(e.target.value.trim()), 400);
});
await _loadFriends();
}
// ----------------------------------------------------------
async function _loadFriends() {
try {
const data = await API.friends.list();
_renderIncoming(data.incoming);
_renderOutgoing(data.outgoing);
_renderFriends(data.friends);
_updateBadge(data.incoming.length);
} catch (e) {
if (e.status === 401) {
document.getElementById('fr-list').innerHTML =
`<div class="empty-state"><p>Bitte melde dich an, um Freunde zu verwalten.</p></div>`;
}
}
}
// ----------------------------------------------------------
function _updateBadge(count) {
const el = document.getElementById('friends-badge');
if (!el) return;
el.textContent = count;
el.style.display = count > 0 ? '' : 'none';
}
// ----------------------------------------------------------
function _renderIncoming(list) {
const el = document.getElementById('fr-incoming');
if (!list.length) { el.innerHTML = ''; return; }
el.innerHTML = `
<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text-secondary);margin-bottom:var(--space-2)">
Anfragen (${list.length})
</h3>
${list.map(r => `
<div class="friend-request-card">
<div class="friend-avatar">${_initial(r.requester_name)}</div>
<div style="flex:1">
<div style="font-weight:var(--weight-semibold)">${_esc(r.requester_name)}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">möchte mit dir befreundet sein</div>
</div>
<div class="friend-item-actions">
<button class="btn btn-primary btn-sm"
onclick="Page_friends._accept(${r.id})">
<svg class="ph-icon"><use href="/icons/phosphor.svg#check"></use></svg>
</button>
<button class="btn btn-ghost btn-sm"
onclick="Page_friends._decline(${r.id})">
<svg class="ph-icon"><use href="/icons/phosphor.svg#x"></use></svg>
</button>
</div>
</div>
`).join('')}
`;
}
// ----------------------------------------------------------
function _renderOutgoing(list) {
const el = document.getElementById('fr-outgoing');
if (!list.length) { el.innerHTML = ''; return; }
el.innerHTML = `
<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text-secondary);margin:var(--space-4) 0 var(--space-2)">
Gesendete Anfragen
</h3>
${list.map(r => `
<div class="friend-item">
<div class="friend-avatar">${_initial(r.addressee_name)}</div>
<div class="friend-item-name">${_esc(r.addressee_name)}</div>
<span style="font-size:var(--text-xs);color:var(--c-text-muted)">ausstehend</span>
<button class="btn btn-ghost btn-sm"
onclick="Page_friends._cancel(${r.id})"
title="Anfrage zurückziehen">
<svg class="ph-icon"><use href="/icons/phosphor.svg#x"></use></svg>
</button>
</div>
`).join('')}
`;
}
// ----------------------------------------------------------
function _renderFriends(list) {
const el = document.getElementById('fr-list');
if (!list.length) {
el.innerHTML = `
<div class="empty-state" style="margin-top:var(--space-6)">
<svg class="ph-icon" style="font-size:3rem;opacity:0.3"><use href="/icons/phosphor.svg#users"></use></svg>
<p style="margin-top:var(--space-3);color:var(--c-text-muted)">
Noch keine Freunde. Suche oben nach Nutzern!
</p>
</div>
`;
return;
}
el.innerHTML = `
<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text-secondary);margin:var(--space-4) 0 var(--space-2)">
Freunde (${list.length})
</h3>
${list.map(f => `
<div class="friend-item">
<div class="friend-avatar">${_initial(f.friend_name)}</div>
<div class="friend-item-name">${_esc(f.friend_name)}</div>
<div class="friend-item-actions">
<button class="btn btn-ghost btn-sm"
onclick="Page_friends._openChat(${f.friend_id})"
title="Nachricht senden">
<svg class="ph-icon"><use href="/icons/phosphor.svg#chat-circle-dots"></use></svg>
</button>
<button class="btn btn-ghost btn-sm"
onclick="Page_friends._removeFriend(${f.friend_id}, '${_esc(f.friend_name)}')"
title="Freund entfernen">
<svg class="ph-icon"><use href="/icons/phosphor.svg#user-minus"></use></svg>
</button>
</div>
</div>
`).join('')}
`;
}
// ----------------------------------------------------------
async function _doSearch(q) {
const el = document.getElementById('fr-search-results');
if (q.length < 2) { el.innerHTML = ''; return; }
try {
const results = await API.friends.search(q);
if (!results.length) {
el.innerHTML = `<div class="friends-search-results">
<div style="padding:var(--space-3) var(--space-4);color:var(--c-text-muted);font-size:var(--text-sm)">
Keine Nutzer gefunden.
</div>
</div>`;
return;
}
el.innerHTML = `
<div class="friends-search-results">
${results.map(u => `
<div class="friend-result-item">
<div class="friend-avatar">${_initial(u.name)}</div>
<div style="flex:1;font-size:var(--text-sm)">${_esc(u.name)}</div>
<button class="btn btn-primary btn-sm"
onclick="Page_friends._sendRequest(${u.id}, this)">
<svg class="ph-icon"><use href="/icons/phosphor.svg#user-plus"></use></svg>
Anfrage
</button>
</div>
`).join('')}
</div>
`;
} catch (e) {
el.innerHTML = '';
}
}
// ----------------------------------------------------------
async function _sendRequest(userId, btn) {
btn.disabled = true;
try {
await API.friends.sendRequest(userId);
UI.toast('Freundschaftsanfrage gesendet!', 'success');
document.getElementById('fr-search').value = '';
document.getElementById('fr-search-results').innerHTML = '';
await _loadFriends();
} catch (e) {
UI.toast(e.message, 'danger');
btn.disabled = false;
}
}
async function _accept(id) {
try {
await API.friends.accept(id);
UI.toast('Freundschaft angenommen!', 'success');
await _loadFriends();
} catch (e) {
UI.toast(e.message, 'danger');
}
}
async function _decline(id) {
try {
await API.friends.decline(id);
await _loadFriends();
} catch (e) {
UI.toast(e.message, 'danger');
}
}
async function _cancel(id) {
try {
await API.friends.decline(id);
await _loadFriends();
} catch (e) {
UI.toast(e.message, 'danger');
}
}
async function _removeFriend(userId, name) {
if (!confirm(`${name} als Freund entfernen?`)) return;
try {
await API.friends.remove(userId);
UI.toast('Freund entfernt.', 'info');
await _loadFriends();
} catch (e) {
UI.toast(e.message, 'danger');
}
}
async function _openChat(userId) {
try {
const { conversation_id } = await API.chat.start(userId);
App.navigate('chat', true, { conversation_id });
} catch (e) {
UI.toast(e.message, 'danger');
}
}
// ----------------------------------------------------------
function _initial(name) {
return (name || '?')[0].toUpperCase();
}
function _esc(s) {
if (!s) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ----------------------------------------------------------
return {
init,
_sendRequest, _accept, _decline, _cancel, _removeFriend, _openChat,
};
})();