Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
821 lines
34 KiB
HTML
821 lines
34 KiB
HTML
<!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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|