Weiter am Dating gearbeitet
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
This commit is contained in:
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user