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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

@@ -700,6 +700,11 @@
}
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
if (profile.datingAktiv) {
loadDatingLikeButton(profile.userId);
}
}
}
@@ -1410,6 +1415,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

@@ -7,26 +7,814 @@
<title>Dating xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
/* ── Toolbar ── */
.dating-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
gap: 0.75rem;
}
.results-count { font-size: 0.85rem; color: var(--color-muted); }
.filter-open-btn {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.5rem 1rem;
font-size: 0.88rem;
}
.filter-badge {
display: inline-flex;
align-items: center;
justify-content: center;
background: #fff;
color: var(--color-primary);
border-radius: 50%;
width: 1.1rem;
height: 1.1rem;
font-size: 0.7rem;
font-weight: 700;
line-height: 1;
}
/* ── Filter-Overlay ── */
.filter-overlay-bg {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.55);
z-index: 200;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.filter-overlay-bg.open { opacity: 1; pointer-events: all; }
.filter-drawer {
position: fixed;
top: 0; right: 0; bottom: 0;
width: min(320px, 92vw);
background: var(--color-card);
border-left: 1px solid var(--color-secondary);
z-index: 201;
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 0.25s ease;
overflow: hidden;
}
.filter-drawer.open { transform: translateX(0); }
.filter-drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem 0.75rem;
border-bottom: 1px solid var(--color-secondary);
flex-shrink: 0;
}
.filter-drawer-header h2 {
font-size: 1rem;
font-weight: 700;
color: var(--color-primary);
margin: 0;
}
.filter-close-btn {
background: none;
border: none;
color: var(--color-muted);
font-size: 1.3rem;
cursor: pointer;
padding: 0.2rem 0.4rem;
line-height: 1;
border-radius: 4px;
}
.filter-close-btn:hover { background: var(--color-secondary); color: var(--color-text); }
.filter-drawer-body {
flex: 1;
overflow-y: auto;
padding: 1rem 1.25rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.filter-drawer-footer {
padding: 0.75rem 1.25rem;
border-top: 1px solid var(--color-secondary);
display: flex;
gap: 0.5rem;
flex-shrink: 0;
}
.apply-btn { flex: 1; padding: 0.65rem; font-size: 0.9rem; }
.reset-btn {
padding: 0.65rem 1rem;
font-size: 0.9rem;
background: var(--color-secondary);
color: var(--color-muted);
border: 1px solid var(--color-secondary);
border-radius: 6px;
cursor: pointer;
}
.reset-btn:hover { color: var(--color-text); }
/* ── Filter-Elemente ── */
.filter-group { display: flex; flex-direction: column; gap: 0.35rem; }
.filter-group label {
font-size: 0.78rem;
color: var(--color-muted);
margin: 0;
display: flex;
justify-content: space-between;
}
.range-val { color: var(--color-text); }
.filter-group input[type="range"] {
padding: 0; background: none; border: none;
accent-color: var(--color-primary); width: 100%;
}
/* ── Dual-Handle Alters-Slider ── */
.dual-slider {
position: relative;
height: 20px;
margin: 0.5rem 0 0.25rem;
}
.dual-slider-track {
position: absolute;
top: 50%; left: 0; right: 0;
height: 4px;
background: var(--color-secondary);
border-radius: 2px;
transform: translateY(-50%);
}
.dual-slider-range {
position: absolute;
top: 0; height: 100%;
background: var(--color-primary);
border-radius: 2px;
}
.dual-slider-thumb {
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);
transition: box-shadow 0.1s;
touch-action: none;
}
.dual-slider-thumb:active { cursor: grabbing; box-shadow: 0 2px 8px rgba(0,0,0,0.5); }
.dual-slider-thumb:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.filter-divider { border: none; border-top: 1px solid var(--color-secondary); margin: 0; }
.chip-group { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.chip {
padding: 0.25rem 0.65rem;
border-radius: 20px;
font-size: 0.78rem;
cursor: pointer;
border: 1px solid var(--color-secondary);
background: var(--color-secondary);
color: var(--color-muted);
transition: background 0.15s, color 0.15s, border-color 0.15s;
user-select: none;
}
.chip.active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
.logic-toggle { display: flex; gap: 0.4rem; }
.logic-btn {
flex: 1; padding: 0.3rem; font-size: 0.78rem; font-weight: 600;
text-align: center; border-radius: 6px; cursor: pointer;
border: 1px solid var(--color-secondary);
background: var(--color-secondary); color: var(--color-muted);
transition: background 0.15s, color 0.15s;
}
.logic-btn.active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
.vorlieben-search { position: relative; }
.vorlieben-search input { padding: 0.4rem 0.7rem; font-size: 0.82rem; }
.vorlieben-dropdown {
position: absolute; top: calc(100% + 4px); left: 0; right: 0;
background: var(--color-card); border: 1px solid var(--color-secondary);
border-radius: 8px; max-height: 180px; overflow-y: auto; z-index: 210; display: none;
}
.vorlieben-dropdown.open { display: block; }
.vl-item { padding: 0.45rem 0.75rem; font-size: 0.82rem; cursor: pointer; color: var(--color-text); }
.vl-item:hover { background: var(--color-secondary); }
.selected-vorlieben { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.vl-tag {
display: inline-flex; align-items: center; gap: 0.3rem;
padding: 0.2rem 0.55rem; background: var(--color-primary);
color: #fff; border-radius: 20px; font-size: 0.75rem;
}
.vl-tag button {
background: none; border: none; color: #fff; padding: 0;
font-size: 0.75rem; cursor: pointer; line-height: 1; min-width: unset;
}
.vl-tag button:hover { background: none; opacity: 0.7; }
/* ── Profilkarten-Grid ── */
.profiles-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.profile-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 12px;
overflow: hidden;
transition: border-color 0.15s, box-shadow 0.15s;
text-decoration: none;
color: var(--color-text);
display: flex;
flex-direction: column;
}
.profile-card:hover {
border-color: var(--color-primary);
box-shadow: 0 4px 18px rgba(0,0,0,0.35);
}
.profile-card-img { display: none; } /* ersetzt durch .profile-card-img-wrap */
.profile-card-body {
padding: 0.75rem; display: flex; flex-direction: column; gap: 0.3rem; flex: 1;
}
.profile-card-name {
font-weight: 700; font-size: 1rem;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.profile-card-meta { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.meta-chip {
padding: 0.1rem 0.45rem; border-radius: 20px;
background: var(--color-secondary); font-size: 0.73rem; color: var(--color-muted);
}
.meta-chip.dist { color: var(--color-primary); }
.profile-card-desc {
font-size: 0.78rem; color: var(--color-muted); line-height: 1.4;
overflow: hidden; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-box-orient: vertical; margin-top: 0.15rem;
}
/* ── Skeleton-Karten beim Nachladen ── */
.profile-card-skeleton {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.skeleton-img {
width: 100%; aspect-ratio: 1;
background: linear-gradient(90deg, var(--color-secondary) 25%, var(--color-card) 50%, var(--color-secondary) 75%);
background-size: 200% 100%;
animation: shimmer 1.2s infinite;
}
.skeleton-body { padding: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; }
.skeleton-line {
height: 0.75rem; border-radius: 4px;
background: linear-gradient(90deg, var(--color-secondary) 25%, var(--color-card) 50%, var(--color-secondary) 75%);
background-size: 200% 100%;
animation: shimmer 1.2s infinite;
}
@keyframes shimmer { to { background-position: -200% 0; } }
/* ── Like-Button auf Karten ── */
.profile-card-like {
position: absolute;
bottom: 0.5rem;
right: 0.5rem;
background: rgba(0,0,0,0.55);
border: none;
border-radius: 50%;
width: 2.2rem;
height: 2.2rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 1.1rem;
transition: background 0.15s, transform 0.1s;
padding: 0;
color: #fff;
flex-shrink: 0;
}
.profile-card-like:hover { background: rgba(0,0,0,0.75); transform: scale(1.1); }
.profile-card-like.liked { background: var(--color-primary); }
.profile-card-img-wrap {
position: relative;
width: 100%;
aspect-ratio: 1;
flex-shrink: 0;
overflow: hidden;
background: var(--color-secondary);
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
}
.profile-card-img-wrap img { width: 100%; height: 100%; object-fit: cover; }
/* ── Sentinel & Leer/Fehler ── */
#sentinel { height: 1px; margin-top: 1rem; }
.empty-state {
text-align: center; padding: 3rem 1rem;
color: var(--color-muted); grid-column: 1 / -1;
}
.empty-state .icon { font-size: 2.5rem; margin-bottom: 0.75rem; }
@media (max-width: 500px) {
.profiles-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
}
</style>
</head>
<body class="app">
<!-- ── Filter-Overlay ── -->
<div class="filter-overlay-bg" id="filterBg"></div>
<div class="filter-drawer" id="filterDrawer">
<div class="filter-drawer-header">
<h2>Filter</h2>
<button class="filter-close-btn" id="filterCloseBtn" aria-label="Filter schließen"></button>
</div>
<div class="filter-drawer-body">
<div class="filter-group">
<label>Umkreis <span class="range-val" id="distVal">50 km</span></label>
<input type="range" id="maxDist" min="5" max="500" value="50" step="5"
oninput="document.getElementById('distVal').textContent=this.value+' km'">
</div>
<div class="filter-group">
<label>Alter <span class="range-val" id="ageVal">18 60</span></label>
<div class="dual-slider" id="ageSlider" role="group" aria-label="Altersbereich">
<div class="dual-slider-track">
<div class="dual-slider-range" id="ageRange"></div>
</div>
<div class="dual-slider-thumb" id="thumbMin" tabindex="0" role="slider"
aria-label="Mindestalter" aria-valuemin="18" aria-valuemax="99" aria-valuenow="18"></div>
<div class="dual-slider-thumb" id="thumbMax" tabindex="0" role="slider"
aria-label="Höchstalter" aria-valuemin="18" aria-valuemax="99" aria-valuenow="60"></div>
</div>
</div>
<hr class="filter-divider">
<div class="filter-group">
<label>Geschlecht</label>
<div class="chip-group" id="geschlechtChips">
<span class="chip" data-val="WEIBLICH">weiblich</span>
<span class="chip" data-val="MAENNLICH">männlich</span>
<span class="chip" data-val="DIVERS">divers</span>
</div>
</div>
<div class="filter-group">
<label>Neigung</label>
<div class="chip-group" id="neigungChips">
<span class="chip" data-val="DEVOT">devot</span>
<span class="chip" data-val="EHER_DEVOT">eher devot</span>
<span class="chip" data-val="SWITCHER">Switcher</span>
<span class="chip" data-val="EHER_DOMINANT">eher dominant</span>
<span class="chip" data-val="DOMINANT">dominant</span>
</div>
</div>
<hr class="filter-divider">
<div class="filter-group">
<label>Vorlieben</label>
<div class="logic-toggle" id="logicToggle">
<span class="logic-btn active" data-val="false">ODER</span>
<span class="logic-btn" data-val="true">UND</span>
</div>
<div class="vorlieben-search" style="margin-top:0.4rem;">
<input type="text" id="vlSearch" placeholder="Vorliebe suchen …" autocomplete="off">
<div class="vorlieben-dropdown" id="vlDropdown"></div>
</div>
<div class="selected-vorlieben" id="selectedVorlieben"></div>
</div>
</div>
<div class="filter-drawer-footer">
<button class="btn apply-btn" id="applyBtn">Suchen</button>
<button class="reset-btn" id="resetBtn">Zurücksetzen</button>
</div>
</div>
<div class="main">
<div class="content">
<h1>Dating</h1>
<p style="color:var(--color-muted);">Kommt bald.</p>
<div class="dating-toolbar">
<span class="results-count" id="resultsCount"></span>
<button class="btn filter-open-btn" id="filterOpenBtn">
Filter <span class="filter-badge" id="filterBadge" style="display:none;"></span>
</button>
</div>
<div class="profiles-grid" id="profilesGrid"></div>
<div id="sentinel"></div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script>
(function () {
const BATCH_SIZE = 20;
let allIds = []; // vollständig gefilterte, nach Entfernung sortierte ID-Liste
let loadedCount = 0;
let loading = false;
let vorliebenUnd = false;
const selectedVorlieben = new Map();
let allVorlieben = [];
// ── Auth-Check ────────────────────────────────────────────────────────────
fetch('/login/me').then(r => {
if (r.status === 401) window.location.href = '/login.html';
if (r.status === 401) { window.location.href = '/login.html'; return null; }
return r.ok ? r.json() : null;
}).then(user => {
if (!user) return;
if (!user.datingAktiv) window.location.href = '/konto/einstellungen.html#sec-dating';
if (!user.datingAktiv) { window.location.href = '/konto/einstellungen.html#sec-dating'; return; }
loadVorlieben();
if (user.datingGeschlechter && user.datingGeschlechter.length > 0) {
document.querySelectorAll('#geschlechtChips .chip').forEach(chip => {
chip.classList.toggle('active', user.datingGeschlechter.includes(chip.dataset.val));
});
}
runSearch();
}).catch(() => { window.location.href = '/login.html'; });
// ── IntersectionObserver für Infinite Scroll ──────────────────────────────
const sentinel = document.getElementById('sentinel');
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) loadNextBatch();
}, { rootMargin: '300px' });
observer.observe(sentinel);
// ── Overlay öffnen/schließen ──────────────────────────────────────────────
const filterDrawer = document.getElementById('filterDrawer');
const filterBg = document.getElementById('filterBg');
function openFilter() {
filterDrawer.classList.add('open');
filterBg.classList.add('open');
document.body.style.overflow = 'hidden';
}
function closeFilter() {
filterDrawer.classList.remove('open');
filterBg.classList.remove('open');
document.body.style.overflow = '';
}
document.getElementById('filterOpenBtn').addEventListener('click', openFilter);
document.getElementById('filterCloseBtn').addEventListener('click', closeFilter);
filterBg.addEventListener('click', closeFilter);
// ── Vorlieben laden ───────────────────────────────────────────────────────
function loadVorlieben() {
fetch('/vorlieben/items').then(r => r.json()).then(kategorien => {
allVorlieben = kategorien.flatMap(k => k.items.map(i => ({ itemId: i.itemId, name: i.name })));
}).catch(() => {});
}
// ── Chip-Toggle ───────────────────────────────────────────────────────────
document.querySelectorAll('.chip-group').forEach(group => {
group.addEventListener('click', e => {
const chip = e.target.closest('.chip');
if (chip) chip.classList.toggle('active');
});
});
// ── Logik-Toggle ─────────────────────────────────────────────────────────
document.querySelectorAll('#logicToggle .logic-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#logicToggle .logic-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
vorliebenUnd = btn.dataset.val === 'true';
});
});
// ── Dual-Handle Alters-Slider ─────────────────────────────────────────────
const AGE_MIN = 18, AGE_MAX = 99;
let ageFrom = 18, ageTo = 60;
const thumbMin = document.getElementById('thumbMin');
const thumbMax = document.getElementById('thumbMax');
const ageRange = document.getElementById('ageRange');
const ageSlider = document.getElementById('ageSlider');
function pct(val) { return (val - AGE_MIN) / (AGE_MAX - AGE_MIN) * 100; }
function updateAgeSlider() {
const lo = pct(ageFrom), hi = pct(ageTo);
thumbMin.style.left = lo + '%';
thumbMax.style.left = hi + '%';
ageRange.style.left = lo + '%';
ageRange.style.width = (hi - lo) + '%';
thumbMin.setAttribute('aria-valuenow', ageFrom);
thumbMax.setAttribute('aria-valuenow', ageTo);
document.getElementById('ageVal').textContent = ageFrom + ' ' + ageTo;
}
function makeDraggable(thumb, isMin) {
function onMove(clientX) {
const rect = ageSlider.getBoundingClientRect();
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
const raw = AGE_MIN + Math.round(ratio * (AGE_MAX - AGE_MIN));
if (isMin) {
ageFrom = Math.min(raw, ageTo - 1);
} else {
ageTo = Math.max(raw, ageFrom + 1);
}
updateAgeSlider();
}
// Mouse
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);
});
// Touch
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 });
// Keyboard
thumb.addEventListener('keydown', e => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
if (isMin) ageFrom = Math.max(AGE_MIN, ageFrom - 1);
else ageTo = Math.max(ageFrom + 1, ageTo - 1);
updateAgeSlider(); e.preventDefault();
} else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
if (isMin) ageFrom = Math.min(ageTo - 1, ageFrom + 1);
else ageTo = Math.min(AGE_MAX, ageTo + 1);
updateAgeSlider(); e.preventDefault();
}
});
}
makeDraggable(thumbMin, true);
makeDraggable(thumbMax, false);
updateAgeSlider();
// ── Vorlieben-Suche ───────────────────────────────────────────────────────
const vlSearchEl = document.getElementById('vlSearch');
const vlDropdown = document.getElementById('vlDropdown');
vlSearchEl.addEventListener('input', () => {
const q = vlSearchEl.value.trim().toLowerCase();
if (!q) { vlDropdown.classList.remove('open'); return; }
const matches = allVorlieben
.filter(v => v.name.toLowerCase().includes(q) && !selectedVorlieben.has(v.itemId))
.slice(0, 15);
vlDropdown.innerHTML = matches.map(v =>
`<div class="vl-item" data-id="${v.itemId}" data-name="${escHtml(v.name)}">${escHtml(v.name)}</div>`
).join('');
vlDropdown.classList.toggle('open', matches.length > 0);
});
vlDropdown.addEventListener('click', e => {
const item = e.target.closest('.vl-item');
if (!item) return;
selectedVorlieben.set(item.dataset.id, item.dataset.name);
renderSelectedVorlieben();
vlSearchEl.value = '';
vlDropdown.classList.remove('open');
});
document.addEventListener('click', e => {
if (!vlSearchEl.contains(e.target) && !vlDropdown.contains(e.target))
vlDropdown.classList.remove('open');
});
function renderSelectedVorlieben() {
document.getElementById('selectedVorlieben').innerHTML =
[...selectedVorlieben.entries()].map(([id, name]) =>
`<span class="vl-tag">${escHtml(name)}<button onclick="removeVorliebe('${id}')" title="Entfernen">✕</button></span>`
).join('');
}
window.removeVorliebe = function (id) {
selectedVorlieben.delete(id);
renderSelectedVorlieben();
updateFilterBadge();
};
// ── Reset ─────────────────────────────────────────────────────────────────
document.getElementById('resetBtn').addEventListener('click', () => {
document.getElementById('maxDist').value = 50;
document.getElementById('distVal').textContent = '50 km';
ageFrom = 18; ageTo = 60;
updateAgeSlider();
document.querySelectorAll('.chip.active').forEach(c => c.classList.remove('active'));
document.querySelectorAll('#logicToggle .logic-btn').forEach((b, i) => b.classList.toggle('active', i === 0));
vorliebenUnd = false;
selectedVorlieben.clear();
renderSelectedVorlieben();
updateFilterBadge();
});
// ── Suchen-Button ─────────────────────────────────────────────────────────
document.getElementById('applyBtn').addEventListener('click', () => {
closeFilter();
updateFilterBadge();
runSearch();
});
// ── Filter-Badge ──────────────────────────────────────────────────────────
function updateFilterBadge() {
let count = 0;
if (parseInt(document.getElementById('maxDist').value) !== 50) count++;
if (ageFrom !== 18 || ageTo !== 60) count++;
if (document.querySelectorAll('#geschlechtChips .chip.active').length > 0) count++;
if (document.querySelectorAll('#neigungChips .chip.active').length > 0) count++;
if (selectedVorlieben.size > 0) count++;
const badge = document.getElementById('filterBadge');
badge.textContent = count;
badge.style.display = count > 0 ? 'inline-flex' : 'none';
}
// ── Schritt 1: IDs laden ──────────────────────────────────────────────────
async function runSearch() {
allIds = [];
loadedCount = 0;
loading = false;
document.getElementById('profilesGrid').innerHTML = skeletons(BATCH_SIZE);
document.getElementById('resultsCount').textContent = '';
try {
const res = await fetch('/dating/profile-ids?' + buildParams());
if (res.status === 403) { window.location.href = '/konto/einstellungen.html#sec-dating'; return; }
if (!res.ok) throw new Error();
const data = await res.json();
allIds = data.ids;
document.getElementById('resultsCount').textContent =
data.total === 0
? 'Keine Ergebnisse'
: data.total + ' Person' + (data.total === 1 ? '' : 'en') + ' gefunden';
document.getElementById('profilesGrid').innerHTML = '';
if (allIds.length === 0) {
document.getElementById('profilesGrid').innerHTML =
'<div class="empty-state"><div class="icon">🔍</div><p>Niemand in deinem Umkreis mit diesen Filtern gefunden.</p></div>';
return;
}
loadNextBatch();
} catch {
document.getElementById('profilesGrid').innerHTML =
'<div class="empty-state"><div class="icon">⚠️</div><p>Fehler beim Laden.</p></div>';
}
}
// ── Schritt 2: Profildetails batchweise nachladen ─────────────────────────
async function loadNextBatch() {
if (loading || loadedCount >= allIds.length) return;
loading = true;
const batchIds = allIds.slice(loadedCount, loadedCount + BATCH_SIZE);
showSkeletons(batchIds.length);
try {
const res = await fetch('/dating/profiles/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(batchIds)
});
if (!res.ok) throw new Error();
const profiles = await res.json();
removeSkeletons();
appendProfiles(profiles);
loadedCount += batchIds.length;
} catch {
removeSkeletons();
}
loading = false;
}
// ── Render ────────────────────────────────────────────────────────────────
function appendProfiles(profiles) {
const grid = document.getElementById('profilesGrid');
profiles.forEach(p => {
const card = document.createElement('div');
card.className = 'profile-card';
const img = p.profilePicture
? `<img src="${p.profilePicture}" alt="${escHtml(p.name)}" loading="lazy">`
: `<span>👤</span>`;
const chips = [
p.alter ? `<span class="meta-chip">${p.alter} J.</span>` : '',
p.distanzKm != null ? `<span class="meta-chip dist">${p.distanzKm < 1 ? '< 1' : p.distanzKm} km</span>` : '',
p.geschlecht ? `<span class="meta-chip">${escHtml(p.geschlecht)}</span>` : '',
p.neigung ? `<span class="meta-chip">${escHtml(p.neigung)}</span>` : '',
p.datingStadt ? `<span class="meta-chip">${escHtml(p.datingStadt)}</span>` : '',
].filter(Boolean).join('');
const desc = p.beschreibung
? `<div class="profile-card-desc">${escHtml(p.beschreibung)}</div>` : '';
card.innerHTML = `
<a href="/community/benutzer.html?userId=${p.userId}" style="text-decoration:none;color:inherit;display:contents;">
<div class="profile-card-img-wrap">${img}
<button class="profile-card-like${p.likedByMe ? ' liked' : ''}"
data-user-id="${p.userId}"
title="${p.likedByMe ? 'Unlike' : 'Like'}">♥</button>
</div>
<div class="profile-card-body">
<div class="profile-card-name">${escHtml(p.name)}</div>
<div class="profile-card-meta">${chips}</div>
${desc}
</div>
</a>`;
card.querySelector('.profile-card-like').addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
toggleLike(e.currentTarget, p.userId);
});
grid.appendChild(card);
});
}
async function toggleLike(btn, userId) {
btn.disabled = true;
try {
const res = await fetch('/dating/like/' + userId, { method: 'POST' });
if (!res.ok) return;
const data = await res.json();
btn.classList.toggle('liked', data.liked);
btn.title = data.liked ? 'Unlike' : 'Like';
if (data.newMatch) showMatchToast();
} finally {
btn.disabled = false;
}
}
function showMatchToast() {
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', boxShadow:'0 4px 16px rgba(0,0,0,0.4)'
});
document.body.appendChild(t);
setTimeout(() => t.remove(), 3500);
}
// ── Skeleton-Helpers ──────────────────────────────────────────────────────
function skeletons(n) {
return Array.from({ length: n }, () => `
<div class="profile-card-skeleton" data-skeleton>
<div class="skeleton-img"></div>
<div class="skeleton-body">
<div class="skeleton-line" style="width:60%"></div>
<div class="skeleton-line" style="width:80%"></div>
</div>
</div>`).join('');
}
function showSkeletons(n) {
const grid = document.getElementById('profilesGrid');
grid.insertAdjacentHTML('beforeend', skeletons(n));
}
function removeSkeletons() {
document.querySelectorAll('[data-skeleton]').forEach(el => el.remove());
}
// ── Params-Builder ────────────────────────────────────────────────────────
function buildParams() {
const p = new URLSearchParams();
p.set('maxDistanceKm', document.getElementById('maxDist').value);
p.set('minAge', ageFrom);
p.set('maxAge', ageTo);
p.set('vorliebenUnd', vorliebenUnd);
document.querySelectorAll('#geschlechtChips .chip.active').forEach(c => p.append('geschlechter', c.dataset.val));
document.querySelectorAll('#neigungChips .chip.active').forEach(c => p.append('neigungen', c.dataset.val));
selectedVorlieben.forEach((_, id) => p.append('vorliebenIds', id));
return p;
}
// ── Utility ───────────────────────────────────────────────────────────────
function escHtml(s) {
if (!s) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
})();
</script>
</body>
</html>

View File

@@ -488,6 +488,31 @@
<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>
<div id="datingStadtRow" style="display:none;">
<div class="settings-row" style="flex-wrap:wrap;gap:0.5rem;">
<div class="settings-row-info">
@@ -1201,12 +1226,19 @@
}
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');
}
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 +1344,15 @@
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)
})
});
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');