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:
BIN
bin/main/de/oaa/xxx/dating/DatingController.class
Normal file
BIN
bin/main/de/oaa/xxx/dating/DatingController.class
Normal file
Binary file not shown.
BIN
bin/main/de/oaa/xxx/dating/DatingLikeEntity.class
Normal file
BIN
bin/main/de/oaa/xxx/dating/DatingLikeEntity.class
Normal file
Binary file not shown.
BIN
bin/main/de/oaa/xxx/dating/DatingLikeRepository.class
Normal file
BIN
bin/main/de/oaa/xxx/dating/DatingLikeRepository.class
Normal file
Binary file not shown.
BIN
bin/main/de/oaa/xxx/dating/DatingMatchEntity.class
Normal file
BIN
bin/main/de/oaa/xxx/dating/DatingMatchEntity.class
Normal file
Binary file not shown.
BIN
bin/main/de/oaa/xxx/dating/DatingMatchRepository.class
Normal file
BIN
bin/main/de/oaa/xxx/dating/DatingMatchRepository.class
Normal file
Binary file not shown.
BIN
bin/main/de/oaa/xxx/dating/DatingService$DatingFilter.class
Normal file
BIN
bin/main/de/oaa/xxx/dating/DatingService$DatingFilter.class
Normal file
Binary file not shown.
BIN
bin/main/de/oaa/xxx/dating/DatingService$DatingProfileDto.class
Normal file
BIN
bin/main/de/oaa/xxx/dating/DatingService$DatingProfileDto.class
Normal file
Binary file not shown.
BIN
bin/main/de/oaa/xxx/dating/DatingService$IdsResult.class
Normal file
BIN
bin/main/de/oaa/xxx/dating/DatingService$IdsResult.class
Normal file
Binary file not shown.
BIN
bin/main/de/oaa/xxx/dating/DatingService$LikeResult.class
Normal file
BIN
bin/main/de/oaa/xxx/dating/DatingService$LikeResult.class
Normal file
Binary file not shown.
BIN
bin/main/de/oaa/xxx/dating/DatingService$LikerDto.class
Normal file
BIN
bin/main/de/oaa/xxx/dating/DatingService$LikerDto.class
Normal file
Binary file not shown.
BIN
bin/main/de/oaa/xxx/dating/DatingService$MatchDto.class
Normal file
BIN
bin/main/de/oaa/xxx/dating/DatingService$MatchDto.class
Normal file
Binary file not shown.
BIN
bin/main/de/oaa/xxx/dating/DatingService$WhoLikesMeResult.class
Normal file
BIN
bin/main/de/oaa/xxx/dating/DatingService$WhoLikesMeResult.class
Normal file
Binary file not shown.
BIN
bin/main/de/oaa/xxx/dating/DatingService.class
Normal file
BIN
bin/main/de/oaa/xxx/dating/DatingService.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
9
bin/main/sql/fix_sichtbarkeit_niemand.sql
Normal file
9
bin/main/sql/fix_sichtbarkeit_niemand.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- Ersetzt ungültigen Wert 'NIEMAND' durch 'NUR_ICH' in allen Sichtbarkeit-Spalten der user-Tabelle.
|
||||
UPDATE `user` SET sichtbarkeit_grunddaten = 'NUR_ICH' WHERE sichtbarkeit_grunddaten = 'NIEMAND';
|
||||
UPDATE `user` SET sichtbarkeit_galerie = 'NUR_ICH' WHERE sichtbarkeit_galerie = 'NIEMAND';
|
||||
UPDATE `user` SET sichtbarkeit_freunde = 'NUR_ICH' WHERE sichtbarkeit_freunde = 'NIEMAND';
|
||||
UPDATE `user` SET sichtbarkeit_feed = 'NUR_ICH' WHERE sichtbarkeit_feed = 'NIEMAND';
|
||||
UPDATE `user` SET sichtbarkeit_pinnwand = 'NUR_ICH' WHERE sichtbarkeit_pinnwand = 'NIEMAND';
|
||||
UPDATE `user` SET sichtbarkeit_xp = 'NUR_ICH' WHERE sichtbarkeit_xp = 'NIEMAND';
|
||||
UPDATE `user` SET sichtbarkeit_lockhistorie = 'NUR_ICH' WHERE sichtbarkeit_lockhistorie = 'NIEMAND';
|
||||
UPDATE `user` SET sichtbarkeit_vorlieben = 'NUR_ICH' WHERE sichtbarkeit_vorlieben = 'NIEMAND';
|
||||
25933
bin/main/sql/testuser_dating.sql
Normal file
25933
bin/main/sql/testuser_dating.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -700,6 +700,11 @@
|
||||
}
|
||||
html += ` <button onclick="openMeldungDialog('PROFIL','${profile.userId}')" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);border-radius:6px;padding:0.4rem 0.8rem;cursor:pointer;font-size:0.85rem;">⚑ Melden</button>`;
|
||||
actions.innerHTML = html;
|
||||
|
||||
// Dating-Like-Button – nur anzeigen wenn Ziel-User Dating aktiviert hat
|
||||
if (profile.datingAktiv) {
|
||||
loadDatingLikeButton(profile.userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1410,6 +1415,60 @@
|
||||
});
|
||||
|
||||
// esc, fmtDate, toggleEmojiPicker, insertEmoji kommen aus shared.js
|
||||
|
||||
// ── Dating-Like auf Profilseite ──────────────────────────────────────────
|
||||
async function loadDatingLikeButton(targetUserId) {
|
||||
// Nur einblenden wenn eigener User Dating aktiviert hat
|
||||
const meRes = await fetch('/login/me');
|
||||
if (!meRes.ok) return;
|
||||
const me = await meRes.json();
|
||||
if (!me.datingAktiv) return;
|
||||
|
||||
let liked = false;
|
||||
try {
|
||||
const idsRes = await fetch('/dating/liked-by-me');
|
||||
if (idsRes.ok) {
|
||||
const ids = await idsRes.json();
|
||||
liked = ids.includes(targetUserId);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.id = 'datingLikeBtn';
|
||||
btn.title = liked ? 'Unlike' : 'Like';
|
||||
btn.style.cssText = 'padding:0.4rem 0.9rem;font-size:0.9rem;' +
|
||||
(liked ? 'background:var(--color-primary);color:#fff;' : 'background:none;border:1px solid var(--color-primary);color:var(--color-primary);') +
|
||||
'border-radius:6px;cursor:pointer;';
|
||||
btn.textContent = liked ? '♥ Liked' : '♥ Liken';
|
||||
btn.addEventListener('click', async () => {
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const res = await fetch('/dating/like/' + targetUserId, { method: 'POST' });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
liked = data.liked;
|
||||
btn.textContent = liked ? '♥ Liked' : '♥ Liken';
|
||||
btn.title = liked ? 'Unlike' : 'Like';
|
||||
btn.style.background = liked ? 'var(--color-primary)' : 'none';
|
||||
btn.style.color = liked ? '#fff' : 'var(--color-primary)';
|
||||
btn.style.border = liked ? 'none' : '1px solid var(--color-primary)';
|
||||
if (data.newMatch) {
|
||||
const t = document.createElement('div');
|
||||
t.textContent = '🎉 Es ist ein Match!';
|
||||
Object.assign(t.style, {
|
||||
position:'fixed', bottom:'2rem', left:'50%', transform:'translateX(-50%)',
|
||||
background:'var(--color-primary)', color:'#fff', padding:'0.75rem 1.5rem',
|
||||
borderRadius:'8px', fontWeight:'700', zIndex:'999'
|
||||
});
|
||||
document.body.appendChild(t);
|
||||
setTimeout(() => t.remove(), 3500);
|
||||
}
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
document.getElementById('profileActions').appendChild(btn);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -488,6 +488,31 @@
|
||||
<input type="checkbox" id="datingAktiv" style="width:1.1rem;height:1.1rem;accent-color:var(--color-primary);cursor:pointer;" onchange="onDatingToggle()">
|
||||
</label>
|
||||
</div>
|
||||
<div id="datingSucheRow" style="display:none;">
|
||||
<div class="settings-row" style="flex-direction:column;align-items:flex-start;gap:0.5rem;">
|
||||
<div class="settings-row-info">
|
||||
<div class="settings-row-label">Interesse an</div>
|
||||
<div class="settings-row-desc">Welche Geschlechter sollen standardmäßig in der Dating-Suche angezeigt werden?</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:1rem;flex-wrap:wrap;">
|
||||
<label style="display:flex;align-items:center;gap:0.4rem;cursor:pointer;margin:0;font-size:0.9rem;color:var(--color-text);">
|
||||
<input type="checkbox" id="sucheWeiblich" value="WEIBLICH"
|
||||
style="width:1rem;height:1rem;accent-color:var(--color-primary);cursor:pointer;">
|
||||
weiblich
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.4rem;cursor:pointer;margin:0;font-size:0.9rem;color:var(--color-text);">
|
||||
<input type="checkbox" id="sucheMaennlich" value="MAENNLICH"
|
||||
style="width:1rem;height:1rem;accent-color:var(--color-primary);cursor:pointer;">
|
||||
männlich
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.4rem;cursor:pointer;margin:0;font-size:0.9rem;color:var(--color-text);">
|
||||
<input type="checkbox" id="sucheDivers" value="DIVERS"
|
||||
style="width:1rem;height:1rem;accent-color:var(--color-primary);cursor:pointer;">
|
||||
divers
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="datingStadtRow" style="display:none;">
|
||||
<div class="settings-row" style="flex-wrap:wrap;gap:0.5rem;">
|
||||
<div class="settings-row-info">
|
||||
@@ -1201,12 +1226,19 @@
|
||||
}
|
||||
if (user.datingLat != null) _datingLat = user.datingLat;
|
||||
if (user.datingLon != null) _datingLon = user.datingLon;
|
||||
document.getElementById('datingStadtRow').style.display = user.datingAktiv ? '' : 'none';
|
||||
const show = user.datingAktiv ? '' : 'none';
|
||||
document.getElementById('datingStadtRow').style.display = show;
|
||||
document.getElementById('datingSucheRow').style.display = show;
|
||||
const aktiveGeschlechter = user.datingGeschlechter || [];
|
||||
document.getElementById('sucheWeiblich').checked = aktiveGeschlechter.includes('WEIBLICH');
|
||||
document.getElementById('sucheMaennlich').checked = aktiveGeschlechter.includes('MAENNLICH');
|
||||
document.getElementById('sucheDivers').checked = aktiveGeschlechter.includes('DIVERS');
|
||||
}
|
||||
|
||||
function onDatingToggle() {
|
||||
const aktiv = document.getElementById('datingAktiv').checked;
|
||||
document.getElementById('datingStadtRow').style.display = aktiv ? '' : 'none';
|
||||
const show = document.getElementById('datingAktiv').checked ? '' : 'none';
|
||||
document.getElementById('datingStadtRow').style.display = show;
|
||||
document.getElementById('datingSucheRow').style.display = show;
|
||||
}
|
||||
|
||||
let _stadtSuggestTimer = null;
|
||||
@@ -1312,7 +1344,15 @@
|
||||
const res = await fetch('/user/me/dating', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ datingAktiv: aktiv, datingStadt: stadt || null, datingLat: _datingLat, datingLon: _datingLon })
|
||||
body: JSON.stringify({
|
||||
datingAktiv: aktiv,
|
||||
datingStadt: stadt || null,
|
||||
datingLat: _datingLat,
|
||||
datingLon: _datingLon,
|
||||
datingGeschlechter: ['sucheWeiblich','sucheMaennlich','sucheDivers']
|
||||
.filter(id => document.getElementById(id).checked)
|
||||
.map(id => document.getElementById(id).value)
|
||||
})
|
||||
});
|
||||
if (res.ok) {
|
||||
showToast();
|
||||
|
||||
@@ -60,6 +60,53 @@
|
||||
width: 100%;
|
||||
}
|
||||
.visitor-time { font-size: 0.68rem; color: var(--color-muted); text-align: center; }
|
||||
|
||||
/* ── Dating: Likes & Matches ── */
|
||||
.dating-strip { display: flex; flex-wrap: wrap; gap: 0.75rem; }
|
||||
.dating-card {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
|
||||
text-decoration: none; color: var(--color-text); width: 72px;
|
||||
}
|
||||
.dating-card:hover .dating-avatar { border-color: var(--color-primary); }
|
||||
.dating-avatar {
|
||||
width: 56px; height: 56px; border-radius: 50%;
|
||||
background: var(--color-secondary);
|
||||
border: 2px solid var(--color-secondary);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.4rem; overflow: hidden; flex-shrink: 0;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.dating-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
|
||||
.dating-name {
|
||||
font-size: 0.75rem; text-align: center;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%;
|
||||
}
|
||||
/* Verschwommene Karte für nicht-Premium */
|
||||
.dating-card-locked {
|
||||
width: 72px; display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
|
||||
cursor: default;
|
||||
}
|
||||
.dating-avatar-blurred {
|
||||
width: 56px; height: 56px; border-radius: 50%;
|
||||
background: var(--color-secondary);
|
||||
border: 2px solid var(--color-secondary);
|
||||
overflow: hidden; flex-shrink: 0; position: relative;
|
||||
}
|
||||
.dating-avatar-blurred img {
|
||||
width: 100%; height: 100%; object-fit: cover; border-radius: 50%;
|
||||
filter: blur(6px); transform: scale(1.1);
|
||||
}
|
||||
.dating-avatar-blurred .lock-icon {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.premium-hint {
|
||||
font-size: 0.65rem; color: var(--color-primary); text-align: center; font-weight: 600;
|
||||
}
|
||||
.match-badge {
|
||||
font-size: 0.65rem; color: var(--color-primary); text-align: center; font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
@@ -105,6 +152,18 @@
|
||||
<div class="section-label">Profilbesucher</div>
|
||||
<div class="visitors-strip" id="visitorsStrip"></div>
|
||||
</div>
|
||||
|
||||
<!-- Wer hat mich geliked (Dating) -->
|
||||
<div id="likesSection" style="display:none;">
|
||||
<div class="section-label">Dating – Wer mag mich ♥</div>
|
||||
<div class="dating-strip" id="likesStrip"></div>
|
||||
</div>
|
||||
|
||||
<!-- Matches -->
|
||||
<div id="matchesSection" style="display:none;">
|
||||
<div class="section-label">Dating – Matches 🎉</div>
|
||||
<div class="dating-strip" id="matchesStrip"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -120,6 +179,10 @@
|
||||
if (user) {
|
||||
document.getElementById('greeting').textContent = 'Willkommen zurück, ' + user.name + '!';
|
||||
loadVisitors();
|
||||
if (user.datingAktiv) {
|
||||
loadWhoLikesMe();
|
||||
loadMatches();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => { window.location.href = '/login.html'; });
|
||||
@@ -132,6 +195,69 @@
|
||||
return 'vor ' + Math.floor(diff / 86400) + ' Tag' + (Math.floor(diff / 86400) === 1 ? '' : 'en');
|
||||
}
|
||||
|
||||
async function loadWhoLikesMe() {
|
||||
try {
|
||||
const res = await fetch('/dating/who-likes-me');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (data.total === 0) return;
|
||||
|
||||
const strip = document.getElementById('likesStrip');
|
||||
strip.innerHTML = data.likers.map(l => {
|
||||
const pic = l.profilePicture
|
||||
? `<img src="data:image/png;base64,${l.profilePicture}" alt="">`
|
||||
: '◉';
|
||||
|
||||
if (data.premium && l.userId) {
|
||||
return `
|
||||
<a class="dating-card" href="/community/benutzer.html?userId=${l.userId}">
|
||||
<div class="dating-avatar">${pic}</div>
|
||||
<span class="dating-name">${esc(l.name)}</span>
|
||||
</a>`;
|
||||
} else {
|
||||
return `
|
||||
<div class="dating-card-locked">
|
||||
<div class="dating-avatar-blurred">
|
||||
${pic}
|
||||
<span class="lock-icon">🔒</span>
|
||||
</div>
|
||||
<span class="premium-hint">Premium</span>
|
||||
</div>`;
|
||||
}
|
||||
}).join('');
|
||||
|
||||
document.getElementById('likesSection').style.display = '';
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function loadMatches() {
|
||||
try {
|
||||
const res = await fetch('/dating/matches');
|
||||
if (!res.ok) return;
|
||||
const matches = await res.json();
|
||||
if (!matches.length) return;
|
||||
|
||||
const strip = document.getElementById('matchesStrip');
|
||||
strip.innerHTML = matches.map(m => `
|
||||
<a class="dating-card" href="/community/benutzer.html?userId=${m.userId}">
|
||||
<div class="dating-avatar">
|
||||
${m.profilePicture
|
||||
? `<img src="data:image/png;base64,${m.profilePicture}" alt="${esc(m.name)}">`
|
||||
: '◉'}
|
||||
</div>
|
||||
<span class="dating-name">${esc(m.name)}</span>
|
||||
<span class="match-badge">♥ Match</span>
|
||||
</a>
|
||||
`).join('');
|
||||
document.getElementById('matchesSection').style.display = '';
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
if (!s) return '';
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
async function loadVisitors() {
|
||||
try {
|
||||
const res = await fetch('/social/profile-visits/my-visitors');
|
||||
|
||||
Reference in New Issue
Block a user