From 692e6f937856ace638d9773c17f7447ca439d881 Mon Sep 17 00:00:00 2001 From: rene Date: Fri, 17 Apr 2026 09:23:28 +0200 Subject: [PATCH] Sprint 14: Freunde-Seite mit Profildaten (Avatar, Wohnort, Bio, Erfahrungs-Badge) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend: friends-API liefert jetzt bio, wohnort, erfahrung, social_link, profil_sichtbarkeit, avatar_url für friends/search/incoming - Frontend: User-Cards (Suche + Freundesliste) zeigen Avatar-Foto (statt Buchstaben-Kreis wenn avatar_url vorhanden), Wohnort mit Pin-Icon, Bio-Vorschau (2 Zeilen, max 120 Zeichen, bei private ausgeblendet) und Erfahrungs-Badge neben dem Namen - Profil-Modal erweitert um Wohnort, Erfahrung, vollständige Bio und Social-Link --- backend/routes/friends.py | 5 ++ backend/static/js/pages/friends.js | 134 ++++++++++++++++++++++++----- 2 files changed, 116 insertions(+), 23 deletions(-) diff --git a/backend/routes/friends.py b/backend/routes/friends.py index 273ef1c..56528dd 100644 --- a/backend/routes/friends.py +++ b/backend/routes/friends.py @@ -30,6 +30,8 @@ async def list_friends(user=Depends(get_current_user)): SELECT f.id, f.status, f.created_at, CASE WHEN f.requester_id=? THEN f.addressee_id ELSE f.requester_id END AS friend_id, u.name AS friend_name, + u.bio, u.wohnort, u.erfahrung, u.social_link, + u.profil_sichtbarkeit, u.avatar_url, {dogs_sq} AS dogs_json FROM friendships f JOIN users u ON u.id = CASE WHEN f.requester_id=? THEN f.addressee_id ELSE f.requester_id END @@ -39,6 +41,7 @@ async def list_friends(user=Depends(get_current_user)): incoming = conn.execute(f""" SELECT f.id, f.created_at, u.name AS requester_name, u.id AS requester_id, + u.avatar_url, {dogs_sq} AS dogs_json FROM friendships f JOIN users u ON u.id=f.requester_id @@ -87,6 +90,8 @@ async def search_users(q: str = "", user=Depends(get_current_user)): with db() as conn: rows = conn.execute(""" SELECT u.id, u.name, + u.bio, u.wohnort, u.erfahrung, u.social_link, + u.profil_sichtbarkeit, u.avatar_url, (SELECT json_group_array(json_object('name', d.name, 'rasse', d.rasse)) FROM dogs d WHERE d.user_id=u.id AND d.is_public=1) AS dogs_json FROM users u diff --git a/backend/static/js/pages/friends.js b/backend/static/js/pages/friends.js index 61adb19..17a4ff2 100644 --- a/backend/static/js/pages/friends.js +++ b/backend/static/js/pages/friends.js @@ -187,7 +187,7 @@ window.Page_friends = (() => {
- ${_userAvatar(r.requester_name, r.dogs?.[0])} + ${_userAvatar(r.requester_name, r.dogs?.[0], r.avatar_url)}
${_esc(r.requester_name)} @@ -281,39 +281,56 @@ window.Page_friends = (() => { 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 || '[]'); - _showProfile(fid, fname, fdogs); + 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 `
+ data-dogs="${_esc(JSON.stringify(dogs))}" + data-profile="${_esc(JSON.stringify(profile))}">
- - ${_userAvatar(f.friend_name, dogs[0])} + + ${_userAvatar(f.friend_name, dogs[0], f.avatar_url)} - +
-
- ${_esc(f.friend_name)} + + ${_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
` + }
- ${dogs.length - ? `
- ${_dogPills(dogs, 3)} -
` - : `
Noch kein Hund eingetragen
` - }
@@ -362,7 +379,7 @@ window.Page_friends = (() => { // ---------------------------------------------------------- // MINI-PROFIL MODAL // ---------------------------------------------------------- - function _showProfile(friendId, friendName, dogs) { + function _showProfile(friendId, friendName, dogs, profile = {}) { const dogsHTML = dogs.length ? `
@@ -389,10 +406,41 @@ window.Page_friends = (() => { 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} ${dogsHTML}
@@ -443,12 +491,19 @@ window.Page_friends = (() => {
- ${_userAvatar(u.name, null)} + ${_userAvatar(u.name, null, u.avatar_url)}
-
${_esc(u.name)}
+
+ ${_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('  |  ')}
` : ''} @@ -537,7 +592,12 @@ window.Page_friends = (() => { // ---------------------------------------------------------- // RENDER-HELPERS // ---------------------------------------------------------- - function _userAvatar(name, firstDog) { + function _userAvatar(name, firstDog, avatarUrl) { + if (avatarUrl) { + return `${_esc(name)}`; + } if (firstDog?.foto_url) { return `${_esc(firstDog.name)}${_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);