Hashtags eingeführt
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled

This commit is contained in:
2026-04-11 01:14:33 +02:00
parent ec1409820b
commit e2a71ab096
57 changed files with 2365 additions and 740 deletions

View File

@@ -68,6 +68,11 @@
.empty-hint { color:var(--color-muted); font-size:0.9rem; margin-top:0.5rem; }
.sentinel { height:1px; }
.hashtag-banner { display:flex; align-items:center; gap:0.75rem; margin-bottom:1.25rem; padding:0.65rem 1rem; background:var(--color-card); border:1px solid var(--color-secondary); border-radius:8px; }
.hashtag-banner-tag { font-size:1.05rem; font-weight:700; color:var(--color-primary); }
.hashtag-banner-back { margin-left:auto; font-size:0.82rem; color:var(--color-muted); text-decoration:none; }
.hashtag-banner-back:hover { color:var(--color-primary); }
/* Lightbox */
.lightbox { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.88); z-index:300; align-items:center; justify-content:center; }
.lightbox.open { display:flex; }
@@ -92,7 +97,13 @@
<div class="main">
<div class="content">
<div class="tabs">
<!-- Hashtag-Banner (nur sichtbar wenn ?tag=… gesetzt) -->
<div class="hashtag-banner" id="hashtagBanner" style="display:none;">
<span>Posts mit </span><span class="hashtag-banner-tag" id="hashtagBannerLabel"></span>
<a class="hashtag-banner-back" href="/community/feed.html">× Zurück zum Feed</a>
</div>
<div class="tabs" id="feedTabs">
<button class="tab-btn active" id="tabMine" data-tab="mine" onclick="switchTab('mine', this)">Mein Feed</button>
<button class="tab-btn" id="tabPublic" data-tab="public" onclick="switchTab('public', this)">Öffentlicher Feed</button>
</div>
@@ -140,6 +151,13 @@
<div class="sentinel" id="publicSentinel"></div>
</div>
<!-- Hashtag-Feed (wird angezeigt wenn ?tag=… gesetzt) -->
<div id="tab-hashtag" style="display:none;">
<div id="hashtagFeed"></div>
<p class="empty-hint" id="hashtagEmpty" style="display:none;">Keine Posts mit diesem Hashtag.</p>
<div class="sentinel" id="hashtagSentinel"></div>
</div>
</div>
</div>
@@ -165,37 +183,68 @@
<script src="/js/shared.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/social-sidebar.js"></script>
<script src="/js/meldung.js"></script>
<script src="/js/hashtag.js"></script>
<script>
// ── State ──
let myUserId = null;
let activeLbPostId = null;
let activeLbPostId = null;
let activeLbPostType = null;
let activeHashtag = null; // set when ?tag=... is in URL
const feedState = {
mine: { page:0, hasMore:true, loading:false, loaded:false },
public: { page:0, hasMore:true, loading:false, loaded:false }
mine: { page:0, hasMore:true, loading:false, loaded:false },
public: { page:0, hasMore:true, loading:false, loaded:false },
hashtag: { page:0, hasMore:true, loading:false, loaded:false }
};
let composeBilderArr = [];
// ── Hashtag-Modus prüfen ──
const _urlTag = new URLSearchParams(window.location.search).get('tag');
if (_urlTag) {
activeHashtag = _urlTag.replace(/^#/, '').toLowerCase();
document.getElementById('hashtagBanner').style.display = '';
document.getElementById('hashtagBannerLabel').textContent = '#' + activeHashtag;
document.getElementById('feedTabs').style.display = 'none';
document.getElementById('tab-mine').style.display = 'none';
document.getElementById('tab-public').style.display = 'none';
document.getElementById('tab-hashtag').style.display = '';
document.getElementById('compose').style.display = 'none';
}
// ── Boot ──
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
if (user) {
myUserId = user.userId;
const raw = sessionStorage.getItem('feedOpenPost');
if (raw) {
sessionStorage.removeItem('feedOpenPost');
loadFeed('mine');
openLbWithData(JSON.parse(raw));
if (activeHashtag) {
await loadFeed('hashtag');
} else {
await loadFeed('mine');
const raw = sessionStorage.getItem('feedOpenPost');
if (raw) {
sessionStorage.removeItem('feedOpenPost');
loadFeed('mine');
openLbWithData(JSON.parse(raw));
} else {
await loadFeed('mine');
}
}
}
}).catch(() => {});
// ── Autocomplete für Compose ──
document.addEventListener('DOMContentLoaded', () => {
const ta = document.getElementById('composeText');
if (ta) attachHashtagAutocomplete(ta);
});
// Fallback falls DOMContentLoaded bereits gefeuert
if (document.readyState !== 'loading') {
const ta = document.getElementById('composeText');
if (ta) attachHashtagAutocomplete(ta);
}
// ── Tab switching ──
function switchTab(name, btn) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
@@ -218,8 +267,14 @@
state.loading = true;
state.loaded = true;
try {
const endpoint = tab === 'mine' ? '/feed/mine' : '/feed/public';
const res = await fetch(`${endpoint}?page=${state.page}&size=10`);
let url;
if (tab === 'hashtag') {
url = `/feed/hashtag?tag=${encodeURIComponent(activeHashtag)}&page=${state.page}&size=10`;
} else {
const base = tab === 'mine' ? '/feed/mine' : '/feed/public';
url = `${base}?page=${state.page}&size=10`;
}
const res = await fetch(url);
if (!res.ok) return;
const data = await res.json();
const feedEl = document.getElementById(tab + 'Feed');
@@ -238,12 +293,14 @@
const observer = new IntersectionObserver(entries => {
entries.forEach(e => {
if (!e.isIntersecting) return;
if (e.target.id === 'mineSentinel') loadFeed('mine');
if (e.target.id === 'publicSentinel') loadFeed('public');
if (e.target.id === 'mineSentinel') loadFeed('mine');
if (e.target.id === 'publicSentinel') loadFeed('public');
if (e.target.id === 'hashtagSentinel') loadFeed('hashtag');
});
}, { threshold: 0.5 });
observer.observe(document.getElementById('mineSentinel'));
observer.observe(document.getElementById('publicSentinel'));
observer.observe(document.getElementById('hashtagSentinel'));
// bilderCarousel und carNav kommen aus shared.js
@@ -297,7 +354,7 @@
</div>
${deleteBtn}
</div>
<div class="post-text">${esc(p.text)}</div>
<div class="post-text">${renderTextWithHashtags(p.text)}</div>
${bildHtml}
${umfrageHtml}
<div class="post-actions">

View File

@@ -143,7 +143,7 @@
<!-- Friends tab -->
<div class="tab-panel active" id="tab-friends">
<ul class="user-list" id="friendsList"></ul>
<p class="empty-hint" id="friendsEmpty" style="display:none;">Du hast noch keine Freunde. <a href="/community/personen-suchen.html" style="color:var(--color-primary);">Personen suchen</a></p>
<p class="empty-hint" id="friendsEmpty" style="display:none;">Du hast noch keine Freunde. <a href="/search.html" style="color:var(--color-primary);">Personen suchen</a></p>
</div>
<!-- Pending tab -->

View File

@@ -297,8 +297,9 @@
<script src="/js/shared.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/social-sidebar.js"></script>
<script src="/js/hashtag.js"></script>
<script>
// ── Generic modal helpers ──
@@ -377,6 +378,9 @@
await loadGruppe();
await loadPosts();
const _composeText = document.getElementById('composeText');
if (_composeText) attachHashtagAutocomplete(_composeText);
const _savedTab = localStorage.getItem('tab_gruppe_' + gruppeId);
if (_savedTab) {
const _btn = document.querySelector(`.tab-btn[data-tab="${_savedTab}"]`);
@@ -488,7 +492,7 @@
}).join('');
body = bars + `<div class="umfrage-total">${total} Stimme${total !== 1 ? 'n' : ''} gesamt${p.multiChoice?' · Multi-Choice':''}</div>`;
} else {
body = `<div class="post-text">${esc(p.text)}</div>${bildHtml}`;
body = `<div class="post-text">${renderTextWithHashtags(p.text)}</div>${bildHtml}`;
}
return `
@@ -500,7 +504,7 @@
</div>
<div class="post-date">${fmtDate(p.createdAt)}</div>
</div>
${p.beitragTyp === 'UMFRAGE' ? `<div style="font-weight:600;margin-bottom:0.5rem;">${esc(p.text)}</div>${bildHtml}` : ''}
${p.beitragTyp === 'UMFRAGE' ? `<div style="font-weight:600;margin-bottom:0.5rem;">${renderTextWithHashtags(p.text)}</div>${bildHtml}` : ''}
${body}
<div class="post-actions">
<button class="post-action-btn ${p.likedByMe?'active':''}" onclick="event.stopPropagation(); toggleLike('${p.beitragId}',this)" id="like-btn-${p.beitragId}">

View File

@@ -22,16 +22,9 @@
.gruppe-card-body { padding:0.85rem; flex:1; display:flex; flex-direction:column; gap:0.4rem; }
.gruppe-card-name { font-weight:700; font-size:1rem; }
.gruppe-card-meta { font-size:0.78rem; color:var(--color-muted); }
.gruppe-card-actions { display:flex; gap:0.5rem; flex-wrap:wrap; margin-top:auto; padding-top:0.5rem; }
.gruppe-card-actions button, .gruppe-card-actions a.btn { margin-top:0; padding:0.3rem 0.7rem; font-size:0.8rem; width:auto; }
.role-badge { font-size:0.7rem; font-weight:700; padding:0.1rem 0.4rem; border-radius:4px; background:var(--color-primary); color:#fff; display:inline-block; }
.role-badge.mitglied { background:var(--color-secondary); color:var(--color-text); }
.search-row { display:flex; gap:0.75rem; margin-bottom:1rem; }
.search-row input { flex:1; }
.search-row button { white-space:nowrap; width:auto; margin-top:0; }
.anfrage-list { list-style:none; margin:0; padding:0; }
.anfrage-item { display:flex; align-items:center; justify-content:space-between; gap:1rem; padding:0.75rem 0; border-bottom:1px solid var(--color-secondary); }
.anfrage-item:last-child { border-bottom:none; }
@@ -64,12 +57,14 @@
<div class="content">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1.25rem;">
<h1 style="margin:0;">Gruppen</h1>
<button onclick="openCreateDialog()" style="width:auto; padding:0.4rem 1rem; font-size:0.9rem; margin:0;">+ Erstellen</button>
<div style="display:flex; gap:0.5rem;">
<button onclick="location.href='/search.html?tab=gruppen'" style="width:auto; padding:0.4rem 1rem; font-size:0.9rem; margin:0;">🔍 Suchen</button>
<button onclick="openCreateDialog()" style="width:auto; padding:0.4rem 1rem; font-size:0.9rem; margin:0;">+ Erstellen</button>
</div>
</div>
<div class="tabs">
<button class="tab-btn active" data-tab="mine" onclick="switchTab('mine', this)">Meine Gruppen</button>
<button class="tab-btn" data-tab="discover" onclick="switchTab('discover', this)">Entdecken</button>
<button class="tab-btn" data-tab="requests" onclick="switchTab('requests', this)">Meine Anfragen</button>
</div>
@@ -79,16 +74,6 @@
<p class="empty-hint" id="mineEmpty" style="display:none;">Du bist noch in keiner Gruppe.</p>
</div>
<!-- Entdecken -->
<div class="tab-panel" id="tab-discover">
<div class="search-row">
<input type="text" id="searchInput" placeholder="Gruppenname suchen…" onkeydown="if(event.key==='Enter')doSearch()">
<button onclick="doSearch()">Suchen</button>
</div>
<div class="gruppe-grid" id="discoverGrid"></div>
<p class="empty-hint" id="discoverHint">Gib einen Suchbegriff ein.</p>
</div>
<!-- Meine Anfragen -->
<div class="tab-panel" id="tab-requests">
<ul class="anfrage-list" id="requestsList"></ul>
@@ -121,20 +106,6 @@
</div>
</div>
<!-- Beitrittsanfrage Dialog -->
<div class="dialog-backdrop" id="joinDialog">
<div class="dialog">
<h3>Beitrittsanfrage senden</h3>
<p id="joinDialogGroupName" style="font-weight:600; margin-bottom:0.5rem;"></p>
<label>Nachricht (optional)</label>
<textarea id="joinNachricht" placeholder="Warum möchtest du beitreten?"></textarea>
<p class="message error" id="joinError" style="display:none; margin-top:0.75rem;"></p>
<div class="dialog-actions">
<button class="secondary" onclick="closeJoinDialog()">Abbrechen</button>
<button onclick="sendJoinRequest()">Anfrage senden</button>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
@@ -157,7 +128,7 @@
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
function gruppeCard(g, showJoin = false) {
function gruppeCard(g) {
const img = g.bild
? `<div class="gruppe-card-img"><img src="data:image/jpeg;base64,${g.bild}" alt=""></div>`
: `<div class="gruppe-card-img">👥</div>`;
@@ -165,16 +136,6 @@
? `<span class="role-badge ${g.myRole === 'ADMIN' ? '' : 'mitglied'}">${g.myRole === 'ADMIN' ? 'Admin' : 'Mitglied'}</span>`
: '';
const privBadge = g.isPrivate ? ' 🔒' : '';
let actions = '';
if (showJoin && !g.myRole) {
if (g.myRequestStatus === 'AUSSTEHEND') {
actions = `<button disabled style="opacity:0.6;" onclick="event.stopPropagation()">Anfrage ausstehend</button>`;
} else if (g.isPrivate) {
actions = `<button onclick="event.stopPropagation(); openJoinDialog('${g.gruppeId}','${esc(g.name)}')">Anfrage senden</button>`;
} else {
actions = `<button onclick="event.stopPropagation(); joinGruppe('${g.gruppeId}', this)">Beitreten</button>`;
}
}
return `
<div class="gruppe-card" id="gc-${g.gruppeId}" onclick="location.href='/community/gruppe.html?gruppeId=${g.gruppeId}'" style="cursor:pointer;">
${img}
@@ -183,7 +144,6 @@
<div class="gruppe-card-meta">${g.memberCount} Mitglied${g.memberCount !== 1 ? 'er' : ''} · ${g.postCount} Beiträge</div>
${g.beschreibung ? `<div style="font-size:0.82rem;color:var(--color-muted);">${esc(g.beschreibung.substring(0,80))}${g.beschreibung.length>80?'…':''}</div>` : ''}
<div class="card-notif" id="notif-${g.gruppeId}"></div>
${actions ? `<div class="gruppe-card-actions">${actions}</div>` : ''}
</div>
</div>`;
}
@@ -223,37 +183,6 @@
}));
}
async function doSearch() {
const q = document.getElementById('searchInput').value.trim();
if (!q) return;
try {
const res = await fetch('/gruppen/search?q=' + encodeURIComponent(q));
if (!res.ok) return;
const data = await res.json();
const grid = document.getElementById('discoverGrid');
const hint = document.getElementById('discoverHint');
grid.innerHTML = '';
if (data.length === 0) { hint.textContent = 'Keine Gruppen gefunden.'; hint.style.display = ''; return; }
hint.style.display = 'none';
data.forEach(g => grid.insertAdjacentHTML('beforeend', gruppeCard(g, true)));
} catch(e) { console.error(e); }
}
async function joinGruppe(gruppeId, btn) {
btn.disabled = true;
btn.textContent = '…';
try {
const res = await fetch('/gruppen/' + gruppeId + '/join', { method: 'POST', headers:{'Content-Type':'application/json'}, body:'{}' });
if (res.ok || res.status === 201) {
btn.textContent = 'Beigetreten ✓';
setTimeout(loadMine, 500);
} else {
btn.disabled = false;
btn.textContent = 'Beitreten';
}
} catch(e) { btn.disabled = false; btn.textContent = 'Beitreten'; }
}
async function loadRequests() {
try {
const reqRes = await fetch('/gruppen/requests/mine');
@@ -333,42 +262,6 @@
} catch(e) { showCreateError('Fehler: ' + e.message); }
}
// ── Join dialog ──
let pendingJoinGruppeId = null;
function openJoinDialog(gruppeId, name) {
pendingJoinGruppeId = gruppeId;
document.getElementById('joinDialogGroupName').textContent = name;
document.getElementById('joinNachricht').value = '';
document.getElementById('joinDialog').classList.add('visible');
}
function closeJoinDialog() { document.getElementById('joinDialog').classList.remove('visible'); pendingJoinGruppeId = null; }
async function sendJoinRequest() {
if (!pendingJoinGruppeId) return;
document.getElementById('joinError').style.display = 'none';
const nachricht = document.getElementById('joinNachricht').value.trim() || null;
try {
const res = await fetch('/gruppen/' + pendingJoinGruppeId + '/join', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ nachricht })
});
if (res.ok || res.status === 201) {
closeJoinDialog();
doSearch();
} else {
const el = document.getElementById('joinError');
el.textContent = 'Fehler beim Senden der Anfrage.';
el.style.display = 'block';
}
} catch(e) {
const el = document.getElementById('joinError');
el.textContent = 'Fehler: ' + e.message;
el.style.display = 'block';
}
}
// ── Image preview ──
function previewBild(input, previewId, dataId) {
@@ -401,9 +294,6 @@
document.getElementById('createDialog').addEventListener('click', e => {
if (e.target === document.getElementById('createDialog')) closeCreateDialog();
});
document.getElementById('joinDialog').addEventListener('click', e => {
if (e.target === document.getElementById('joinDialog')) closeJoinDialog();
});
loadMine();
</script>

View File

@@ -360,7 +360,7 @@
const list = document.getElementById('convList');
list.innerHTML = '';
if (convs.length === 0) {
list.innerHTML = '<li style="padding:1rem; color:var(--color-muted); font-size:0.9rem;">Noch keine Nachrichten. <a href="/community/personen-suchen.html" style="color:var(--color-primary);">Personen suchen</a></li>';
list.innerHTML = '<li style="padding:1rem; color:var(--color-muted); font-size:0.9rem;">Noch keine Nachrichten. <a href="/search.html" style="color:var(--color-primary);">Personen suchen</a></li>';
return;
}
convs.forEach(c => {

View File

@@ -1,206 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Personen suchen xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.search-bar {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.search-bar input { flex: 1; }
.search-bar button { width: auto; margin-top: 0; padding: 0.65rem 1.25rem; }
.user-list { list-style: none; margin: 0; padding: 0; }
.user-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 0;
border-bottom: 1px solid var(--color-secondary);
}
.user-item:last-child { border-bottom: none; }
.user-avatar {
width: 42px; height: 42px;
border-radius: 50%;
background: var(--color-secondary);
display: flex; align-items: center; justify-content: center;
font-size: 1.2rem;
flex-shrink: 0;
overflow: hidden;
border: 1px solid var(--color-secondary);
}
.user-avatar img { width: 100%; height: 100%; object-fit: cover; }
.user-name { font-weight: 600; flex: 1; }
.user-actions { display: flex; gap: 0.5rem; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
.user-actions button, .user-actions a.btn {
margin-top: 0;
padding: 0.35rem 0.75rem;
font-size: 0.8rem;
width: auto;
}
.user-profile-link {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
min-width: 0;
text-decoration: none;
color: inherit;
}
.user-profile-link:hover .user-name { color: var(--color-primary); }
.hint { color: var(--color-muted); font-size: 0.9rem; margin-top: 0.5rem; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<h1 style="margin-bottom: 1.25rem;">Personen suchen</h1>
<div class="search-bar">
<input type="text" id="searchInput" placeholder="Name eingeben (mind. 2 Zeichen)…" autocomplete="off">
<button onclick="doSearch()">Suchen</button>
</div>
<ul class="user-list" id="resultList"></ul>
<p class="hint" id="hint">Gib mindestens 2 Zeichen ein, um zu suchen.</p>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/social-sidebar.js"></script>
<script>
let debounceTimer;
document.getElementById('searchInput').addEventListener('input', function () {
clearTimeout(debounceTimer);
const q = this.value.trim();
if (q.length < 2) {
document.getElementById('resultList').innerHTML = '';
document.getElementById('hint').textContent = 'Gib mindestens 2 Zeichen ein, um zu suchen.';
document.getElementById('hint').style.display = '';
return;
}
document.getElementById('hint').style.display = 'none';
debounceTimer = setTimeout(doSearch, 400);
});
document.getElementById('searchInput').addEventListener('keydown', e => {
if (e.key === 'Enter') { clearTimeout(debounceTimer); doSearch(); }
});
async function doSearch() {
const q = document.getElementById('searchInput').value.trim();
if (q.length < 2) return;
try {
const res = await fetch('/social/users/search?q=' + encodeURIComponent(q));
if (!res.ok) return;
renderResults(await res.json());
} catch (e) { console.error(e); }
}
function renderResults(users) {
const list = document.getElementById('resultList');
const hint = document.getElementById('hint');
list.innerHTML = '';
if (users.length === 0) {
hint.textContent = 'Keine Ergebnisse gefunden.';
hint.style.display = '';
return;
}
hint.style.display = 'none';
users.forEach(u => {
const avatar = u.profilePicture
? `<img src="data:image/png;base64,${u.profilePicture}" alt="">`
: '◉';
list.insertAdjacentHTML('beforeend', `
<li class="user-item" data-user-id="${u.userId}">
<a href="/community/benutzer.html?userId=${u.userId}" class="user-profile-link">
<div class="user-avatar">${avatar}</div>
<div class="user-name">${esc(u.name)}</div>
</a>
<div class="user-actions">${buildActions(u)}</div>
</li>`);
});
}
function buildActions(u) {
if (u.friendStatus === 'FRIEND') {
return `<a href="/community/nachrichten.html?userId=${u.userId}" class="btn" style="background:var(--color-secondary); color:var(--color-text);">✉ Nachricht</a>`;
}
if (u.friendStatus === 'PENDING_SENT') {
return `<button disabled>Anfrage gesendet</button>`;
}
if (u.friendStatus === 'PENDING_RECEIVED') {
return `<button onclick="acceptByUserId('${u.userId}', this)">✓ Annehmen</button>`;
}
return `<button onclick="sendRequest('${u.userId}', this)">+ Freund hinzufügen</button>`;
}
async function sendRequest(receiverId, btn) {
btn.disabled = true;
btn.textContent = 'Wird gesendet…';
try {
const res = await fetch('/social/friends/request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ receiverId })
});
btn.textContent = (res.ok || res.status === 201 || res.status === 409) ? 'Anfrage gesendet' : 'Fehler';
} catch (e) {
btn.disabled = false;
btn.textContent = '+ Freund hinzufügen';
}
}
async function acceptByUserId(senderId, btn) {
btn.disabled = true;
btn.textContent = '…';
try {
const pendingRes = await fetch('/social/friends/pending');
const pending = await pendingRes.json();
const f = pending.find(p => p.user.userId === senderId);
if (!f) { btn.textContent = 'Fehler'; return; }
const res = await fetch('/social/friends/accept', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ friendshipId: f.friendshipId })
});
if (res.ok) {
btn.textContent = '✓ Freund';
const item = btn.closest('.user-item');
if (item) {
item.querySelector('.user-actions').innerHTML =
`<a href="/community/nachrichten.html?userId=${senderId}" class="btn" style="background:var(--color-secondary); color:var(--color-text);">✉ Nachricht</a>`;
}
} else {
btn.disabled = false;
btn.textContent = '✓ Annehmen';
}
} catch (e) {
btn.disabled = false;
btn.textContent = '✓ Annehmen';
}
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
</script>
</body>
</html>