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

@@ -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>