Sprint 14: Freunde-Seite mit Profildaten (Avatar, Wohnort, Bio, Erfahrungs-Badge)
- 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
This commit is contained in:
parent
41c4ba3dd6
commit
692e6f9378
2 changed files with 116 additions and 23 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ window.Page_friends = (() => {
|
|||
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-3);
|
||||
border-left:3px solid var(--c-primary)">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
||||
${_userAvatar(r.requester_name, r.dogs?.[0])}
|
||||
${_userAvatar(r.requester_name, r.dogs?.[0], r.avatar_url)}
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:var(--weight-semibold);color:var(--c-text)">
|
||||
${_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 `
|
||||
<div class="card fr-card" style="padding:var(--space-4);margin-bottom:var(--space-3);cursor:pointer;
|
||||
transition:box-shadow 0.15s"
|
||||
data-friend-id="${f.friend_id}"
|
||||
data-friend-name="${_esc(f.friend_name)}"
|
||||
data-dogs="${_esc(JSON.stringify(dogs))}">
|
||||
data-dogs="${_esc(JSON.stringify(dogs))}"
|
||||
data-profile="${_esc(JSON.stringify(profile))}">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
||||
|
||||
<!-- Avatar (erstes Hunde-Foto oder Initiale) -->
|
||||
${_userAvatar(f.friend_name, dogs[0])}
|
||||
<!-- Avatar (User-Avatar, erstes Hunde-Foto oder Initiale) -->
|
||||
${_userAvatar(f.friend_name, dogs[0], f.avatar_url)}
|
||||
|
||||
<!-- Name + Hunde -->
|
||||
<!-- Name + Infos + Hunde -->
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:var(--weight-semibold);color:var(--c-text);
|
||||
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:2px;
|
||||
margin-bottom:var(--space-1)">
|
||||
${_esc(f.friend_name)}
|
||||
<span style="font-weight:var(--weight-semibold);color:var(--c-text)">
|
||||
${_esc(f.friend_name)}
|
||||
</span>
|
||||
${_erfahrungSpan(f.erfahrung)}
|
||||
</div>
|
||||
${_wohnortLine(f.wohnort)}
|
||||
${_bioLine(f.bio, f.profil_sichtbarkeit)}
|
||||
<div style="margin-top:var(--space-1)">
|
||||
${dogs.length
|
||||
? `<div style="display:flex;flex-wrap:wrap;gap:var(--space-1);align-items:center">
|
||||
${_dogPills(dogs, 3)}
|
||||
</div>`
|
||||
: `<div style="font-size:var(--text-xs);color:var(--c-text-muted)">Noch kein Hund eingetragen</div>`
|
||||
}
|
||||
</div>
|
||||
${dogs.length
|
||||
? `<div style="display:flex;flex-wrap:wrap;gap:var(--space-1);align-items:center">
|
||||
${_dogPills(dogs, 3)}
|
||||
</div>`
|
||||
: `<div style="font-size:var(--text-xs);color:var(--c-text-muted)">Noch kein Hund eingetragen</div>`
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Aktionen -->
|
||||
|
|
@ -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
|
||||
? `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));
|
||||
gap:var(--space-3);margin-top:var(--space-4)">
|
||||
|
|
@ -389,10 +406,41 @@ window.Page_friends = (() => {
|
|||
Noch kein Hund eingetragen.
|
||||
</p>`;
|
||||
|
||||
const profileInfoHTML = (() => {
|
||||
const parts = [];
|
||||
if (profile.wohnort) {
|
||||
parts.push(`<div style="display:flex;align-items:center;gap:var(--space-2);
|
||||
font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
📍 ${_esc(profile.wohnort)}
|
||||
</div>`);
|
||||
}
|
||||
if (profile.erfahrung && _erfahrungBadge[profile.erfahrung]) {
|
||||
parts.push(`<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
${_erfahrungBadge[profile.erfahrung]}
|
||||
</div>`);
|
||||
}
|
||||
if (profile.bio && profile.profil_sichtbarkeit !== 'private') {
|
||||
parts.push(`<div style="font-size:var(--text-sm);color:var(--c-text);
|
||||
line-height:1.5;padding-top:var(--space-2)">
|
||||
${_esc(profile.bio)}
|
||||
</div>`);
|
||||
}
|
||||
if (profile.social_link) {
|
||||
parts.push(`<div style="font-size:var(--text-xs)">
|
||||
<a href="${_esc(profile.social_link)}" target="_blank" rel="noopener noreferrer"
|
||||
style="color:var(--c-primary)">${_esc(profile.social_link)}</a>
|
||||
</div>`);
|
||||
}
|
||||
if (!parts.length) return '';
|
||||
return `<div style="display:flex;flex-direction:column;gap:var(--space-2);
|
||||
margin-bottom:var(--space-4)">${parts.join('')}</div>`;
|
||||
})();
|
||||
|
||||
UI.modal.open({
|
||||
title: _esc(friendName),
|
||||
body: `
|
||||
<div>
|
||||
${profileInfoHTML}
|
||||
<div class="by-section-label">${dogs.length === 1 ? 'Hund' : 'Hunde'}</div>
|
||||
${dogsHTML}
|
||||
</div>
|
||||
|
|
@ -443,12 +491,19 @@ window.Page_friends = (() => {
|
|||
<div style="display:flex;align-items:center;gap:var(--space-3);
|
||||
padding:var(--space-3) var(--space-4);
|
||||
${i < results.length - 1 ? 'border-bottom:1px solid var(--c-border)' : ''}">
|
||||
${_userAvatar(u.name, null)}
|
||||
${_userAvatar(u.name, null, u.avatar_url)}
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text)">${_esc(u.name)}</div>
|
||||
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:2px;
|
||||
margin-bottom:2px">
|
||||
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text)">${_esc(u.name)}</span>
|
||||
${_erfahrungSpan(u.erfahrung)}
|
||||
</div>
|
||||
${_wohnortLine(u.wohnort)}
|
||||
${_bioLine(u.bio, u.profil_sichtbarkeit)}
|
||||
${u.dogs?.length
|
||||
? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||||
? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||||
margin-top:2px">
|
||||
${u.dogs.map(d => _esc(d.name) + (d.rasse ? ` · ${_esc(d.rasse)}` : '')).join(' | ')}
|
||||
</div>`
|
||||
: ''}
|
||||
|
|
@ -537,7 +592,12 @@ window.Page_friends = (() => {
|
|||
// ----------------------------------------------------------
|
||||
// RENDER-HELPERS
|
||||
// ----------------------------------------------------------
|
||||
function _userAvatar(name, firstDog) {
|
||||
function _userAvatar(name, firstDog, avatarUrl) {
|
||||
if (avatarUrl) {
|
||||
return `<img src="${_esc(avatarUrl)}" alt="${_esc(name)}"
|
||||
style="width:44px;height:44px;border-radius:50%;object-fit:cover;
|
||||
border:2px solid var(--c-primary);flex-shrink:0">`;
|
||||
}
|
||||
if (firstDog?.foto_url) {
|
||||
return `<img src="${_esc(firstDog.foto_url)}" alt="${_esc(firstDog.name)}"
|
||||
style="width:44px;height:44px;border-radius:50%;object-fit:cover;
|
||||
|
|
@ -553,6 +613,34 @@ window.Page_friends = (() => {
|
|||
</div>`;
|
||||
}
|
||||
|
||||
const _erfahrungBadge = {
|
||||
einsteiger: '🐾 Einsteiger',
|
||||
erfahren: '⭐ Erfahren',
|
||||
trainer: '🎓 Trainer',
|
||||
zuechter: '🏅 Züchter',
|
||||
};
|
||||
|
||||
function _erfahrungSpan(erfahrung) {
|
||||
if (!erfahrung || !_erfahrungBadge[erfahrung]) return '';
|
||||
return `<span style="font-size:10px;padding:1px 5px;border-radius:3px;
|
||||
background:var(--c-surface-2);color:var(--c-text-secondary);
|
||||
margin-left:4px;white-space:nowrap">${_erfahrungBadge[erfahrung]}</span>`;
|
||||
}
|
||||
|
||||
function _wohnortLine(wohnort) {
|
||||
if (!wohnort) return '';
|
||||
return `<span style="font-size:var(--text-xs);color:var(--c-text-muted)">📍 ${_esc(wohnort)}</span>`;
|
||||
}
|
||||
|
||||
function _bioLine(bio, sichtbarkeit) {
|
||||
if (!bio || sichtbarkeit === 'private') return '';
|
||||
const text = bio.length > 120 ? bio.substring(0, 120) + '…' : bio;
|
||||
return `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||||
margin-top:var(--space-1);line-height:1.4;
|
||||
overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;
|
||||
-webkit-box-orient:vertical">${_esc(text)}</div>`;
|
||||
}
|
||||
|
||||
function _dogPills(dogs, max) {
|
||||
if (!dogs?.length) return '';
|
||||
const visible = dogs.slice(0, max);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue