Files
xxx-sphere-web/bin/main/static/dating.html
Mario d386f5a7a9
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Weiter am Dating gearbeitet
2026-04-04 00:09:08 +02:00

821 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>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>
<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'; return null; }
return r.ok ? r.json() : null;
}).then(user => {
if (!user) return;
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>