Weiter am Dating gearbeitet
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled

This commit is contained in:
2026-04-04 00:09:08 +02:00
parent 87c85b1b17
commit d386f5a7a9
61 changed files with 29863 additions and 350 deletions

View File

@@ -0,0 +1,9 @@
-- Ersetzt ungültigen Wert 'NIEMAND' durch 'NUR_ICH' in allen Sichtbarkeit-Spalten der user-Tabelle.
UPDATE `user` SET sichtbarkeit_grunddaten = 'NUR_ICH' WHERE sichtbarkeit_grunddaten = 'NIEMAND';
UPDATE `user` SET sichtbarkeit_galerie = 'NUR_ICH' WHERE sichtbarkeit_galerie = 'NIEMAND';
UPDATE `user` SET sichtbarkeit_freunde = 'NUR_ICH' WHERE sichtbarkeit_freunde = 'NIEMAND';
UPDATE `user` SET sichtbarkeit_feed = 'NUR_ICH' WHERE sichtbarkeit_feed = 'NIEMAND';
UPDATE `user` SET sichtbarkeit_pinnwand = 'NUR_ICH' WHERE sichtbarkeit_pinnwand = 'NIEMAND';
UPDATE `user` SET sichtbarkeit_xp = 'NUR_ICH' WHERE sichtbarkeit_xp = 'NIEMAND';
UPDATE `user` SET sichtbarkeit_lockhistorie = 'NUR_ICH' WHERE sichtbarkeit_lockhistorie = 'NIEMAND';
UPDATE `user` SET sichtbarkeit_vorlieben = 'NUR_ICH' WHERE sichtbarkeit_vorlieben = 'NIEMAND';

File diff suppressed because it is too large Load Diff

View File

@@ -536,6 +536,7 @@
let isOwnProfile = false;
let profileData = null;
let avatarSrc = null;
let blockedByMe = false;
let allImages = [];
let galleryOffset = 0;
@@ -575,10 +576,11 @@
async function loadProfile() {
try {
const [me, profile, images] = await Promise.all([
const [me, profile, images, blockStatus] = await Promise.all([
fetch('/login/me').then(r => r.ok ? r.json() : null),
fetch('/social/users/' + targetUserId).then(r => r.ok ? r.json() : null),
fetch('/social/profile-images?userId=' + targetUserId).then(r => r.ok ? r.json() : [])
fetch('/social/profile-images?userId=' + targetUserId).then(r => r.ok ? r.json() : []),
fetch('/social/block/' + targetUserId).then(r => r.ok ? r.json() : { blockedByMe: false })
]);
document.getElementById('loadingHint').style.display = 'none';
@@ -592,6 +594,7 @@
isOwnProfile = !previewMode && me && me.userId === profile.userId;
profileData = profile;
allImages = images;
blockedByMe = blockStatus?.blockedByMe ?? false;
// Profilbesuch tracken (nur fremde Profile, kein Preview-Modus)
if (!isOwnProfile && !previewMode && myUserId) {
@@ -689,17 +692,26 @@
actions.innerHTML = '';
} else {
let html = '';
if (profile.friendStatus === 'FRIEND') {
if (!blockedByMe) {
html += `<a href="/community/nachrichten.html?userId=${profile.userId}" class="btn">✉ Nachricht</a>`;
} else if (profile.friendStatus === 'PENDING_SENT') {
html += `<button disabled>Anfrage gesendet</button>`;
} else if (profile.friendStatus === 'PENDING_RECEIVED') {
html += `<button id="friendActionBtn" onclick="acceptFriend()">✓ Anfrage annehmen</button>`;
if (profile.friendStatus === 'PENDING_SENT') {
html += ` <button disabled>Anfrage gesendet</button>`;
} else if (profile.friendStatus === 'PENDING_RECEIVED') {
html += ` <button id="friendActionBtn" onclick="acceptFriend()">✓ Anfrage annehmen</button>`;
} else if (profile.friendStatus !== 'FRIEND') {
html += ` <button id="friendActionBtn" onclick="addFriend()">+ Freund hinzufügen</button>`;
}
html += ` <button onclick="openMeldungDialog('PROFIL','${profile.userId}')" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);border-radius:6px;padding:0.4rem 0.8rem;cursor:pointer;font-size:0.85rem;">⚑ Melden</button>`;
html += ` <button onclick="confirmBlock('${profile.userId}','${esc(profile.name)}')" style="background:none;border:1px solid #7a1a1a;color:#c0392b;border-radius:6px;padding:0.4rem 0.8rem;cursor:pointer;font-size:0.85rem;">⊘ Blockieren</button>`;
} else {
html += `<button id="friendActionBtn" onclick="addFriend()">+ Freund hinzufügen</button>`;
html += `<button onclick="unblockUser('${profile.userId}')" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);border-radius:6px;padding:0.4rem 0.8rem;cursor:pointer;font-size:0.85rem;">Blockierung aufheben</button>`;
}
html += ` <button onclick="openMeldungDialog('PROFIL','${profile.userId}')" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);border-radius:6px;padding:0.4rem 0.8rem;cursor:pointer;font-size:0.85rem;">⚑ Melden</button>`;
actions.innerHTML = html;
// Dating-Like-Button nur anzeigen wenn Ziel-User Dating aktiviert hat und nicht blockiert
if (profile.datingAktiv && !blockedByMe) {
loadDatingLikeButton(profile.userId);
}
}
}
@@ -983,6 +995,11 @@
// ── Pinnwand ──
async function loadPinnwand() {
// Schreibbereich ausblenden wenn wir die Person blockiert haben oder es das eigene Profil ist
if (blockedByMe || isOwnProfile) {
document.querySelector('.pinnwand-write').style.display = 'none';
}
const res = await fetch('/social/pinnwand?userId=' + targetUserId);
if (!res.ok) return;
const eintraege = await res.json();
@@ -1131,6 +1148,31 @@
body: JSON.stringify({ profilUserId: targetUserId, text })
});
if (res.ok) { ta.value = ''; await loadPinnwand(); }
else if (res.status === 403) {
alert('Du kannst auf dieser Pinnwand keinen Eintrag hinterlassen.');
}
}
// ── Blockieren ──
function confirmBlock(userId, userName) {
if (!confirm(`Möchtest du ${userName} wirklich blockieren?\n\nDer gesamte bisherige Chat wird gelöscht. Die Person kann dich nicht mehr kontaktieren und keine Pinnwand-Einträge hinterlassen.`)) return;
blockUser(userId);
}
async function blockUser(userId) {
const res = await fetch('/social/block/' + userId, { method: 'POST' });
if (res.ok || res.status === 409) {
blockedByMe = true;
location.reload();
}
}
async function unblockUser(userId) {
const res = await fetch('/social/block/' + userId, { method: 'DELETE' });
if (res.ok || res.status === 404) {
blockedByMe = false;
location.reload();
}
}
async function deleteEintrag(eintragId) {
@@ -1410,6 +1452,60 @@
});
// esc, fmtDate, toggleEmojiPicker, insertEmoji kommen aus shared.js
// ── Dating-Like auf Profilseite ──────────────────────────────────────────
async function loadDatingLikeButton(targetUserId) {
// Nur einblenden wenn eigener User Dating aktiviert hat
const meRes = await fetch('/login/me');
if (!meRes.ok) return;
const me = await meRes.json();
if (!me.datingAktiv) return;
let liked = false;
try {
const idsRes = await fetch('/dating/liked-by-me');
if (idsRes.ok) {
const ids = await idsRes.json();
liked = ids.includes(targetUserId);
}
} catch (_) {}
const btn = document.createElement('button');
btn.id = 'datingLikeBtn';
btn.title = liked ? 'Unlike' : 'Like';
btn.style.cssText = 'padding:0.4rem 0.9rem;font-size:0.9rem;' +
(liked ? 'background:var(--color-primary);color:#fff;' : 'background:none;border:1px solid var(--color-primary);color:var(--color-primary);') +
'border-radius:6px;cursor:pointer;';
btn.textContent = liked ? '♥ Liked' : '♥ Liken';
btn.addEventListener('click', async () => {
btn.disabled = true;
try {
const res = await fetch('/dating/like/' + targetUserId, { method: 'POST' });
if (!res.ok) return;
const data = await res.json();
liked = data.liked;
btn.textContent = liked ? '♥ Liked' : '♥ Liken';
btn.title = liked ? 'Unlike' : 'Like';
btn.style.background = liked ? 'var(--color-primary)' : 'none';
btn.style.color = liked ? '#fff' : 'var(--color-primary)';
btn.style.border = liked ? 'none' : '1px solid var(--color-primary)';
if (data.newMatch) {
const t = document.createElement('div');
t.textContent = '🎉 Es ist ein Match!';
Object.assign(t.style, {
position:'fixed', bottom:'2rem', left:'50%', transform:'translateX(-50%)',
background:'var(--color-primary)', color:'#fff', padding:'0.75rem 1.5rem',
borderRadius:'8px', fontWeight:'700', zIndex:'999'
});
document.body.appendChild(t);
setTimeout(() => t.remove(), 3500);
}
} finally {
btn.disabled = false;
}
});
document.getElementById('profileActions').appendChild(btn);
}
</script>
</body>
</html>

View File

@@ -529,15 +529,41 @@
if (!text) return;
input.value = '';
try {
await fetch('/social/messages', {
const res = await fetch('/social/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ receiverId: activePartnerId, text })
});
if (res.status === 403) {
let reason = '';
try { const body = await res.json(); reason = body.reason; } catch (_) {}
if (reason === 'FIRST_MESSAGE_RESTRICTED') {
showThreadNotice('Du kannst diese Person nur anschreiben, wenn ihr befreundet seid, ein Match habt oder du ein Pro-Abo hast.');
} else if (reason === 'BLOCKED') {
showThreadNotice('Diese Konversation ist nicht mehr möglich.');
} else {
showThreadNotice('Nachricht konnte nicht gesendet werden.');
}
// Text wieder zurücksetzen, damit der User ihn nicht verliert
input.value = text;
return;
}
await pollNewMessages();
} catch (e) { console.error(e); }
}
function showThreadNotice(msg) {
const existing = document.getElementById('threadNotice');
if (existing) existing.remove();
const notice = document.createElement('div');
notice.id = 'threadNotice';
notice.style.cssText = 'background:rgba(180,0,60,0.12);border:1px solid rgba(180,0,60,0.35);border-radius:8px;padding:0.75rem 1rem;font-size:0.88rem;color:var(--color-text);margin:0.5rem 0;line-height:1.45;';
notice.textContent = msg;
const container = document.getElementById('threadMessages');
container.appendChild(notice);
container.scrollTop = container.scrollHeight;
}
document.getElementById('msgInput').addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMsg(); }
});

File diff suppressed because it is too large Load Diff

View File

@@ -91,6 +91,13 @@
} catch(ex) {}
});
es.addEventListener('MATCH', e => {
try {
const data = JSON.parse(e.data);
if (typeof window._onSseMatch === 'function') window._onSseMatch(data);
} catch(ex) {}
});
es.onerror = () => {
es.close();
// Vor dem Reconnect prüfen ob noch eingeloggt (verhindert Endlos-Schleife bei abgelaufener Session)

View File

@@ -488,6 +488,63 @@
<input type="checkbox" id="datingAktiv" style="width:1.1rem;height:1.1rem;accent-color:var(--color-primary);cursor:pointer;" onchange="onDatingToggle()">
</label>
</div>
<div id="datingSucheRow" style="display:none;">
<div class="settings-row" style="flex-direction:column;align-items:flex-start;gap:0.5rem;">
<div class="settings-row-info">
<div class="settings-row-label">Interesse an</div>
<div class="settings-row-desc">Welche Geschlechter sollen standardmäßig in der Dating-Suche angezeigt werden?</div>
</div>
<div style="display:flex;gap:1rem;flex-wrap:wrap;">
<label style="display:flex;align-items:center;gap:0.4rem;cursor:pointer;margin:0;font-size:0.9rem;color:var(--color-text);">
<input type="checkbox" id="sucheWeiblich" value="WEIBLICH"
style="width:1rem;height:1rem;accent-color:var(--color-primary);cursor:pointer;">
weiblich
</label>
<label style="display:flex;align-items:center;gap:0.4rem;cursor:pointer;margin:0;font-size:0.9rem;color:var(--color-text);">
<input type="checkbox" id="sucheMaennlich" value="MAENNLICH"
style="width:1rem;height:1rem;accent-color:var(--color-primary);cursor:pointer;">
männlich
</label>
<label style="display:flex;align-items:center;gap:0.4rem;cursor:pointer;margin:0;font-size:0.9rem;color:var(--color-text);">
<input type="checkbox" id="sucheDivers" value="DIVERS"
style="width:1rem;height:1rem;accent-color:var(--color-primary);cursor:pointer;">
divers
</label>
</div>
</div>
<div class="settings-row" style="flex-direction:column;align-items:flex-start;gap:0.5rem;">
<div class="settings-row-info">
<div class="settings-row-label">Standard-Umkreis</div>
<div class="settings-row-desc">Maximale Entfernung als Standardwert im Dating-Filter.</div>
</div>
<div style="width:100%;max-width:320px;">
<div style="display:flex;justify-content:space-between;font-size:0.82rem;color:var(--color-muted);margin-bottom:0.25rem;">
<span>Umkreis</span><span id="datingDistValDisplay" style="color:var(--color-text);">50 km</span>
</div>
<input type="range" id="datingMaxDistanz" min="5" max="500" value="50" step="5"
style="width:100%;accent-color:var(--color-primary);padding:0;background:none;border:none;"
oninput="document.getElementById('datingDistValDisplay').textContent=this.value+' km'">
</div>
</div>
<div class="settings-row" style="flex-direction:column;align-items:flex-start;gap:0.5rem;">
<div class="settings-row-info">
<div class="settings-row-label">Standard-Altersbereich</div>
<div class="settings-row-desc">Gesuchter Altersbereich als Standardwert im Dating-Filter.</div>
</div>
<div style="width:100%;max-width:320px;">
<div style="display:flex;justify-content:space-between;font-size:0.82rem;color:var(--color-muted);margin-bottom:0.25rem;">
<span>Alter</span><span id="datingAgeValDisplay" style="color:var(--color-text);">18 60</span>
</div>
<div style="position:relative;height:20px;margin:0.25rem 0;" id="datingAgeSlider">
<div style="position:absolute;top:50%;left:0;right:0;height:4px;background:var(--color-secondary);border-radius:2px;transform:translateY(-50%);">
<div id="datingAgeRange" style="position:absolute;top:0;height:100%;background:var(--color-primary);border-radius:2px;"></div>
</div>
<div id="datingThumbMin" tabindex="0" style="position:absolute;top:50%;width:18px;height:18px;background:var(--color-primary);border:2px solid #fff;border-radius:50%;transform:translate(-50%,-50%);cursor:grab;box-shadow:0 1px 4px rgba(0,0,0,0.4);touch-action:none;"></div>
<div id="datingThumbMax" tabindex="0" style="position:absolute;top:50%;width:18px;height:18px;background:var(--color-primary);border:2px solid #fff;border-radius:50%;transform:translate(-50%,-50%);cursor:grab;box-shadow:0 1px 4px rgba(0,0,0,0.4);touch-action:none;"></div>
</div>
</div>
</div>
</div>
<div id="datingStadtRow" style="display:none;">
<div class="settings-row" style="flex-wrap:wrap;gap:0.5rem;">
<div class="settings-row-info">
@@ -1201,12 +1258,85 @@
}
if (user.datingLat != null) _datingLat = user.datingLat;
if (user.datingLon != null) _datingLon = user.datingLon;
document.getElementById('datingStadtRow').style.display = user.datingAktiv ? '' : 'none';
const show = user.datingAktiv ? '' : 'none';
document.getElementById('datingStadtRow').style.display = show;
document.getElementById('datingSucheRow').style.display = show;
const aktiveGeschlechter = user.datingGeschlechter || [];
document.getElementById('sucheWeiblich').checked = aktiveGeschlechter.includes('WEIBLICH');
document.getElementById('sucheMaennlich').checked = aktiveGeschlechter.includes('MAENNLICH');
document.getElementById('sucheDivers').checked = aktiveGeschlechter.includes('DIVERS');
if (user.datingMaxDistanzKm != null) {
document.getElementById('datingMaxDistanz').value = user.datingMaxDistanzKm;
document.getElementById('datingDistValDisplay').textContent = user.datingMaxDistanzKm + ' km';
}
if (user.datingMinAlter != null) _datingAgeFrom = user.datingMinAlter;
if (user.datingMaxAlter != null) _datingAgeTo = user.datingMaxAlter;
updateDatingAgeSlider();
}
// ── Dating-Alters-Slider ──
const _DATING_AGE_MIN = 18, _DATING_AGE_MAX = 99;
let _datingAgeFrom = 18, _datingAgeTo = 60;
function _datingAgePct(v) { return (v - _DATING_AGE_MIN) / (_DATING_AGE_MAX - _DATING_AGE_MIN) * 100; }
function updateDatingAgeSlider() {
const lo = _datingAgePct(_datingAgeFrom), hi = _datingAgePct(_datingAgeTo);
document.getElementById('datingThumbMin').style.left = lo + '%';
document.getElementById('datingThumbMax').style.left = hi + '%';
document.getElementById('datingAgeRange').style.left = lo + '%';
document.getElementById('datingAgeRange').style.width = (hi - lo) + '%';
document.getElementById('datingAgeValDisplay').textContent = _datingAgeFrom + ' ' + _datingAgeTo;
}
(function initDatingAgeSlider() {
const slider = document.getElementById('datingAgeSlider');
function makeThumb(thumbId, isMin) {
const thumb = document.getElementById(thumbId);
function onMove(clientX) {
const rect = slider.getBoundingClientRect();
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
const raw = _DATING_AGE_MIN + Math.round(ratio * (_DATING_AGE_MAX - _DATING_AGE_MIN));
if (isMin) _datingAgeFrom = Math.min(raw, _datingAgeTo - 1);
else _datingAgeTo = Math.max(raw, _datingAgeFrom + 1);
updateDatingAgeSlider();
}
thumb.addEventListener('mousedown', e => {
e.preventDefault();
const move = ev => onMove(ev.clientX);
const up = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); };
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', up);
});
thumb.addEventListener('touchstart', e => {
e.preventDefault();
const move = ev => onMove(ev.touches[0].clientX);
const end = () => { document.removeEventListener('touchmove', move); document.removeEventListener('touchend', end); };
document.addEventListener('touchmove', move, { passive: false });
document.addEventListener('touchend', end);
}, { passive: false });
thumb.addEventListener('keydown', e => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
if (isMin) _datingAgeFrom = Math.max(_DATING_AGE_MIN, _datingAgeFrom - 1);
else _datingAgeTo = Math.max(_datingAgeFrom + 1, _datingAgeTo - 1);
updateDatingAgeSlider(); e.preventDefault();
} else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
if (isMin) _datingAgeFrom = Math.min(_datingAgeTo - 1, _datingAgeFrom + 1);
else _datingAgeTo = Math.min(_DATING_AGE_MAX, _datingAgeTo + 1);
updateDatingAgeSlider(); e.preventDefault();
}
});
}
makeThumb('datingThumbMin', true);
makeThumb('datingThumbMax', false);
updateDatingAgeSlider();
})();
function onDatingToggle() {
const aktiv = document.getElementById('datingAktiv').checked;
document.getElementById('datingStadtRow').style.display = aktiv ? '' : 'none';
const show = document.getElementById('datingAktiv').checked ? '' : 'none';
document.getElementById('datingStadtRow').style.display = show;
document.getElementById('datingSucheRow').style.display = show;
}
let _stadtSuggestTimer = null;
@@ -1312,7 +1442,18 @@
const res = await fetch('/user/me/dating', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ datingAktiv: aktiv, datingStadt: stadt || null, datingLat: _datingLat, datingLon: _datingLon })
body: JSON.stringify({
datingAktiv: aktiv,
datingStadt: stadt || null,
datingLat: _datingLat,
datingLon: _datingLon,
datingGeschlechter: ['sucheWeiblich','sucheMaennlich','sucheDivers']
.filter(id => document.getElementById(id).checked)
.map(id => document.getElementById(id).value),
datingMaxDistanzKm: parseInt(document.getElementById('datingMaxDistanz').value),
datingMinAlter: _datingAgeFrom,
datingMaxAlter: _datingAgeTo
})
});
if (res.ok) {
showToast();

View File

@@ -60,6 +60,53 @@
width: 100%;
}
.visitor-time { font-size: 0.68rem; color: var(--color-muted); text-align: center; }
/* ── Dating: Likes & Matches ── */
.dating-strip { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.dating-card {
display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
text-decoration: none; color: var(--color-text); width: 72px;
}
.dating-card:hover .dating-avatar { border-color: var(--color-primary); }
.dating-avatar {
width: 56px; height: 56px; border-radius: 50%;
background: var(--color-secondary);
border: 2px solid var(--color-secondary);
display: flex; align-items: center; justify-content: center;
font-size: 1.4rem; overflow: hidden; flex-shrink: 0;
transition: border-color 0.15s;
}
.dating-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.dating-name {
font-size: 0.75rem; text-align: center;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%;
}
/* Verschwommene Karte für nicht-Premium */
.dating-card-locked {
width: 72px; display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
cursor: default;
}
.dating-avatar-blurred {
width: 56px; height: 56px; border-radius: 50%;
background: var(--color-secondary);
border: 2px solid var(--color-secondary);
overflow: hidden; flex-shrink: 0; position: relative;
}
.dating-avatar-blurred img {
width: 100%; height: 100%; object-fit: cover; border-radius: 50%;
filter: blur(6px); transform: scale(1.1);
}
.dating-avatar-blurred .lock-icon {
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
font-size: 1.1rem;
}
.premium-hint {
font-size: 0.65rem; color: var(--color-primary); text-align: center; font-weight: 600;
}
.match-badge {
font-size: 0.65rem; color: var(--color-primary); text-align: center; font-weight: 600;
}
</style>
</head>
<body class="app">
@@ -105,6 +152,18 @@
<div class="section-label">Profilbesucher</div>
<div class="visitors-strip" id="visitorsStrip"></div>
</div>
<!-- Wer hat mich geliked (Dating) -->
<div id="likesSection" style="display:none;">
<div class="section-label">Dating Wer mag mich ♥</div>
<div class="dating-strip" id="likesStrip"></div>
</div>
<!-- Matches -->
<div id="matchesSection" style="display:none;">
<div class="section-label">Dating Matches 🎉</div>
<div class="dating-strip" id="matchesStrip"></div>
</div>
</div>
</div>
@@ -120,6 +179,10 @@
if (user) {
document.getElementById('greeting').textContent = 'Willkommen zurück, ' + user.name + '!';
loadVisitors();
if (user.datingAktiv) {
loadWhoLikesMe();
loadMatches();
}
}
})
.catch(() => { window.location.href = '/login.html'; });
@@ -132,6 +195,69 @@
return 'vor ' + Math.floor(diff / 86400) + ' Tag' + (Math.floor(diff / 86400) === 1 ? '' : 'en');
}
async function loadWhoLikesMe() {
try {
const res = await fetch('/dating/who-likes-me');
if (!res.ok) return;
const data = await res.json();
if (data.total === 0) return;
const strip = document.getElementById('likesStrip');
strip.innerHTML = data.likers.map(l => {
const pic = l.profilePicture
? `<img src="data:image/png;base64,${l.profilePicture}" alt="">`
: '◉';
if (data.premium && l.userId) {
return `
<a class="dating-card" href="/community/benutzer.html?userId=${l.userId}">
<div class="dating-avatar">${pic}</div>
<span class="dating-name">${esc(l.name)}</span>
</a>`;
} else {
return `
<div class="dating-card-locked">
<div class="dating-avatar-blurred">
${pic}
<span class="lock-icon">🔒</span>
</div>
<span class="premium-hint">Premium</span>
</div>`;
}
}).join('');
document.getElementById('likesSection').style.display = '';
} catch (_) {}
}
async function loadMatches() {
try {
const res = await fetch('/dating/matches');
if (!res.ok) return;
const matches = await res.json();
if (!matches.length) return;
const strip = document.getElementById('matchesStrip');
strip.innerHTML = matches.map(m => `
<a class="dating-card" href="/community/benutzer.html?userId=${m.userId}">
<div class="dating-avatar">
${m.profilePicture
? `<img src="data:image/png;base64,${m.profilePicture}" alt="${esc(m.name)}">`
: '◉'}
</div>
<span class="dating-name">${esc(m.name)}</span>
<span class="match-badge">♥ Match</span>
</a>
`).join('');
document.getElementById('matchesSection').style.display = '';
} catch (_) {}
}
function esc(s) {
if (!s) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
async function loadVisitors() {
try {
const res = await fetch('/social/profile-visits/my-visitors');