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:
parent
96bd57f0ad
commit
097295c628
44 changed files with 9980 additions and 300 deletions
282
backend/static/js/pages/friends.js
Normal file
282
backend/static/js/pages/friends.js
Normal 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
return {
|
||||
init,
|
||||
_sendRequest, _accept, _decline, _cancel, _removeFriend, _openChat,
|
||||
};
|
||||
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue