Files
xxx-sphere-web/bin/main/static/dating.html
Mario b81ad25c9f
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Bugfixes im Dating und im Profil
2026-04-04 15:45:55 +02:00

2420 lines
108 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dating xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
/* ── Toolbar ── */
.results-count { font-size: 0.85rem; color: var(--color-muted); }
.filter-badge {
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--color-primary);
color: #fff;
border-radius: 50%;
width: 1rem;
height: 1rem;
font-size: 0.62rem;
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)); }
}
/* ── Discovery (Match-Tab) ── */
.discovery-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem 0 2rem;
gap: 1.25rem;
}
.discovery-card {
width: 100%;
max-width: 380px;
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 6px 28px rgba(0,0,0,0.35);
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
position: relative;
}
.discovery-card:hover { transform: translateY(-2px); box-shadow: 0 10px 36px rgba(0,0,0,0.45); }
.discovery-card-img {
width: 100%;
aspect-ratio: 3 / 4;
background: var(--color-secondary);
display: flex;
align-items: center;
justify-content: center;
font-size: 5rem;
color: var(--color-muted);
overflow: hidden;
position: relative;
}
.discovery-card-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.disc-nav-btn {
position: absolute;
top: 50%; transform: translateY(-50%);
background: rgba(0,0,0,0.45);
border: none; color: #fff;
width: 2rem; height: 2rem; border-radius: 50%;
font-size: 1.5rem; line-height: 1;
cursor: pointer; z-index: 3;
display: flex; align-items: center; justify-content: center;
padding: 0; backdrop-filter: blur(2px);
transition: background 0.15s;
}
.disc-nav-btn:hover { background: rgba(0,0,0,0.7); }
.disc-nav-prev { left: 0.45rem; }
.disc-nav-next { right: 0.45rem; }
.disc-img-dots {
position: absolute; bottom: 0.45rem; left: 50%; transform: translateX(-50%);
display: flex; gap: 0.3rem; z-index: 3; pointer-events: none;
}
.disc-img-dot {
width: 6px; height: 6px; border-radius: 50%;
background: rgba(255,255,255,0.45);
transition: background 0.15s;
}
.disc-img-dot.active { background: #fff; }
.discovery-card-gradient {
position: absolute;
bottom: 0; left: 0; right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.75));
padding: 2rem 1.1rem 1rem;
pointer-events: none;
}
.discovery-card-name {
font-size: 1.35rem;
font-weight: 700;
color: #fff;
text-shadow: 0 1px 4px rgba(0,0,0,0.5);
}
.discovery-card-meta {
font-size: 0.85rem;
color: rgba(255,255,255,0.82);
margin-top: 0.2rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.disc-match-chips {
position: absolute;
top: 0.6rem;
right: 0.6rem;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.3rem;
pointer-events: none;
z-index: 2;
}
.disc-match-chip {
font-size: 0.7rem;
font-weight: 600;
padding: 0.2rem 0.55rem;
border-radius: 20px;
background: rgba(0,0,0,0.55);
color: #fff;
border: 1px solid rgba(255,255,255,0.25);
backdrop-filter: blur(4px);
white-space: nowrap;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
}
.discovery-card-desc {
padding: 0.75rem 1.1rem;
font-size: 0.88rem;
color: var(--color-muted);
line-height: 1.45;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.discovery-actions {
display: flex;
gap: 2rem;
align-items: center;
justify-content: center;
}
.discovery-btn {
width: 3.6rem;
height: 3.6rem;
border-radius: 50%;
border: none;
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 3px 12px rgba(0,0,0,0.35);
transition: transform 0.12s, box-shadow 0.12s;
flex-shrink: 0;
padding: 0;
margin: 0;
line-height: 1;
}
.discovery-btn:hover { transform: scale(1.1); box-shadow: 0 5px 18px rgba(0,0,0,0.45); }
.discovery-btn:active { transform: scale(0.95); }
.discovery-btn-no { background: #c0392b; color: #fff; }
.discovery-btn-yes { background: #27ae60; color: #fff; }
.discovery-counter { font-size: 0.8rem; color: var(--color-muted); }
/* ── Match-Overlay ── */
.match-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.82);
z-index: 500;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 1.25rem;
text-align: center;
padding: 2rem;
}
.match-overlay.open { display: flex; }
.match-overlay-title {
font-size: 2rem;
font-weight: 900;
color: #fff;
text-shadow: 0 2px 12px rgba(0,0,0,0.5);
letter-spacing: 0.03em;
}
.match-overlay-sub { font-size: 1rem; color: rgba(255,255,255,0.75); }
.match-overlay-avatars {
display: flex;
gap: 1rem;
align-items: center;
justify-content: center;
}
.match-avatar {
width: 90px; height: 90px;
border-radius: 50%;
border: 3px solid var(--color-primary);
background: var(--color-secondary);
overflow: hidden;
display: flex; align-items: center; justify-content: center;
font-size: 2.5rem; color: var(--color-muted);
}
.match-avatar img { width: 100%; height: 100%; object-fit: cover; }
.match-heart { font-size: 2rem; color: var(--color-primary); }
.match-overlay-btn { margin-top: 0.5rem; padding: 0.75rem 2rem; font-size: 1rem; }
/* ── Discovery-Popup (Vollprofil) ── */
.discovery-popup-bg {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.7);
z-index: 300;
align-items: center;
justify-content: center;
padding: 1rem;
}
.discovery-popup-bg.open { display: flex; }
.discovery-popup {
background: var(--color-card);
border-radius: 16px;
width: 100%;
max-width: 420px;
max-height: 90vh;
overflow-y: auto;
padding: 1.5rem 1.25rem 2rem;
position: relative;
}
.discovery-popup-close {
position: absolute;
top: 0.85rem; right: 1rem;
background: var(--color-secondary);
border: none;
color: var(--color-text);
width: 2rem; height: 2rem;
border-radius: 50%;
font-size: 1rem;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
padding: 0; margin: 0;
}
.pd-layout { display: flex; flex-direction: column; }
.pd-info { /* Info-Bereich Layout-neutral auf kleinen Screens */ }
.pd-photo-box {
position: relative; width: 100%; aspect-ratio: 1 / 1; max-height: 280px;
background: var(--color-secondary); border-radius: 12px; overflow: hidden;
display: flex; align-items: center; justify-content: center;
margin-bottom: 0.75rem; font-size: 3rem; color: var(--color-muted);
}
.pd-photo-box img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* ── Breites Layout: Bild links (bis 1024px), Info rechts ── */
@media (min-width: 900px) {
.profile-dialog-bg { align-items: center; }
.profile-dialog {
border-radius: 16px;
max-width: min(1340px, calc(100vw - 2rem));
max-height: 90vh;
overflow: hidden;
padding: 0;
}
.discovery-popup {
max-width: min(1340px, calc(100vw - 2rem));
max-height: 90vh;
overflow: hidden;
padding: 0;
}
.pd-layout {
flex-direction: row;
height: min(90vh, 1024px);
}
.pd-photo-box {
flex: 1 1 0; min-width: 0; max-width: 1024px;
height: 100%; max-height: none; aspect-ratio: auto;
border-radius: 0; margin: 0;
}
.pd-info {
width: 300px; flex-shrink: 0; overflow-y: auto;
padding: 1.5rem 1.25rem 2rem;
border-left: 1px solid var(--color-secondary);
}
}
.pd-nav-btn {
position: absolute; top: 50%; transform: translateY(-50%);
background: rgba(0,0,0,0.45); border: 1px solid rgba(255,255,255,0.2); color: #fff;
width: 2rem; height: 2rem; border-radius: 50%; font-size: 1.3rem;
cursor: pointer; display: flex; align-items: center; justify-content: center;
padding: 0; margin: 0; line-height: 1; z-index: 2; transition: background 0.15s;
}
.pd-nav-btn:hover { background: rgba(0,0,0,0.75); }
.pd-nav-btn:disabled { opacity: 0.25; cursor: default; }
.pd-nav-prev { left: 0.4rem; }
.pd-nav-next { right: 0.4rem; }
.pd-photo-dots {
position: absolute; bottom: 0.4rem; left: 0; right: 0;
display: flex; justify-content: center; gap: 0.3rem; pointer-events: none;
}
.pd-photo-dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,0.45); }
.pd-photo-dot.active { background: #fff; }
.dp-name { text-align: center; font-size: 1.25rem; font-weight: 700; margin-bottom: 0.5rem; }
.dp-tags { display: flex; flex-wrap: wrap; gap: 0.4rem; justify-content: center; margin-bottom: 0.75rem; }
.dp-tag {
background: var(--color-secondary);
border-radius: 20px;
padding: 0.25rem 0.7rem;
font-size: 0.78rem;
color: var(--color-muted);
}
.dp-desc {
background: var(--color-secondary);
border-radius: 8px;
padding: 0.85rem 1rem;
font-size: 0.9rem;
line-height: 1.55;
color: var(--color-text);
white-space: pre-wrap;
margin-top: 0.75rem;
margin-bottom: 1rem;
}
.dp-actions { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; }
.dp-link { padding: 0.55rem 1.25rem; font-size: 0.9rem; }
/* ── Tabs ── */
.dating-tabs {
display: flex;
gap: 0;
align-items: center;
border-bottom: 1px solid var(--color-secondary);
margin-bottom: 1.25rem;
}
.dating-tab-btn {
background: none;
border: none;
border-bottom: 3px solid transparent;
border-radius: 0;
padding: 0.6rem 1.4rem;
font-size: 0.95rem;
font-weight: 600;
color: var(--color-muted);
cursor: pointer;
margin-bottom: -1px;
transition: color 0.15s, border-color 0.15s;
width: auto;
margin-top: 0;
}
.dating-tab-btn:hover { color: var(--color-text); background: none; }
.dating-tab-btn.active { color: var(--color-primary); border-bottom-color: var(--color-primary); }
.dating-tab-panel { display: none; }
.dating-tab-panel.active { display: block; }
/* ── Dates-Tab ── */
.dates-section-title {
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--color-muted);
margin: 0 0 0.6rem;
}
.dates-section { margin-bottom: 1.75rem; }
.dates-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
gap: 0.75rem;
}
.dates-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
.date-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
display: flex;
flex-direction: column;
}
.date-card:hover {
border-color: var(--color-primary);
box-shadow: 0 4px 18px rgba(0,0,0,0.35);
}
.date-card-img {
width: 100%;
aspect-ratio: 16/9;
background: var(--color-secondary);
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
color: var(--color-muted);
overflow: hidden;
flex-shrink: 0;
}
.date-card-img img { width: 100%; height: 100%; object-fit: cover; }
.date-card-body { padding: 0.75rem; display: flex; flex-direction: column; gap: 0.4rem; flex: 1; }
.date-card-title { font-weight: 700; font-size: 0.95rem; line-height: 1.3; }
.date-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;
}
.date-card-footer {
display: flex; align-items: center; gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-top: 1px solid var(--color-secondary);
font-size: 0.76rem;
flex-shrink: 0;
}
.date-card-avatar {
width: 24px; height: 24px; border-radius: 50%;
background: var(--color-secondary);
overflow: hidden; display: flex; align-items: center; justify-content: center;
font-size: 0.9rem; flex-shrink: 0;
}
.date-card-avatar img { width: 100%; height: 100%; object-fit: cover; }
.date-card-creator { color: var(--color-muted); flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.date-card-meta { color: var(--color-muted); white-space: nowrap; }
.date-card-interest { color: var(--color-primary); font-weight: 600; }
.date-card-date { color: var(--color-muted); font-size: 0.73rem; }
/* ── Date-Dialog (Details / Eigenes Date) ── */
.date-dialog-bg {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.65);
z-index: 400;
align-items: center;
justify-content: center;
padding: 1rem;
}
.date-dialog-bg.open { display: flex; }
.date-dialog {
background: var(--color-card);
border-radius: 16px;
width: 100%;
max-width: 520px;
max-height: 92vh;
overflow-y: auto;
position: relative;
display: flex;
flex-direction: column;
}
.date-dialog-img {
width: 100%; aspect-ratio: 16/9;
background: var(--color-secondary);
display: flex; align-items: center; justify-content: center;
font-size: 3rem; color: var(--color-muted);
overflow: hidden; flex-shrink: 0;
border-radius: 16px 16px 0 0;
}
.date-dialog-img img { width: 100%; height: 100%; object-fit: cover; }
.date-dialog-body { padding: 1.25rem; display: flex; flex-direction: column; gap: 0.75rem; }
.date-dialog-close {
position: absolute; top: 0.75rem; right: 0.85rem;
background: rgba(0,0,0,0.45); border: none; color: #fff;
width: 2rem; height: 2rem; border-radius: 50%;
font-size: 1rem; cursor: pointer;
display: flex; align-items: center; justify-content: center;
padding: 0; z-index: 1;
}
.date-dialog-close:hover { background: rgba(0,0,0,0.65); }
.date-dialog-title { font-size: 1.2rem; font-weight: 700; line-height: 1.3; }
.date-dialog-creator {
display: flex; align-items: center; gap: 0.6rem;
cursor: pointer; width: fit-content;
}
.date-dialog-creator:hover .date-dialog-creator-name { text-decoration: underline; }
.date-dialog-creator-avatar {
width: 36px; height: 36px; border-radius: 50%;
background: var(--color-secondary); overflow: hidden;
display: flex; align-items: center; justify-content: center;
font-size: 1.1rem; flex-shrink: 0;
}
.date-dialog-creator-avatar img { width: 100%; height: 100%; object-fit: cover; }
.date-dialog-creator-name { font-weight: 600; font-size: 0.9rem; }
.date-dialog-when {
display: flex; align-items: center; gap: 0.4rem;
font-size: 0.85rem; color: var(--color-muted);
}
.date-dialog-desc {
background: var(--color-secondary); border-radius: 8px;
padding: 0.85rem 1rem; font-size: 0.9rem; line-height: 1.55;
white-space: pre-wrap;
}
.date-dialog-stats {
font-size: 0.82rem; color: var(--color-muted);
}
.date-dialog-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.date-interest-btn { flex: 1; min-width: 120px; }
.date-interest-btn.active { background: var(--color-primary); color: #fff; }
/* ── Interessenten-Liste (eigenes Date) ── */
.interest-list { display: flex; flex-direction: column; gap: 0.5rem; }
.interest-item {
display: flex; align-items: center; gap: 0.75rem;
padding: 0.6rem 0.75rem;
background: var(--color-secondary); border-radius: 8px;
}
.interest-item-avatar {
width: 38px; height: 38px; border-radius: 50%;
background: var(--color-card); overflow: hidden;
display: flex; align-items: center; justify-content: center;
font-size: 1.2rem; flex-shrink: 0; cursor: pointer;
}
.interest-item-avatar img { width: 100%; height: 100%; object-fit: cover; }
.interest-item-name { font-weight: 600; font-size: 0.88rem; flex: 1; cursor: pointer; }
.interest-item-name:hover { text-decoration: underline; }
.interest-item-dm {
padding: 0.3rem 0.75rem; font-size: 0.78rem;
}
.interest-item-blocked {
font-size: 0.75rem; color: var(--color-muted); font-style: italic;
}
/* ── Erstellen/Bearbeiten-Dialog ── */
.date-form-bg {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.65);
z-index: 450;
align-items: center;
justify-content: center;
padding: 1rem;
}
.date-form-bg.open { display: flex; }
.date-form-dialog {
background: var(--color-card);
border-radius: 16px;
width: 100%;
max-width: 480px;
max-height: 92vh;
overflow-y: auto;
padding: 1.5rem 1.25rem;
position: relative;
}
.date-form-close {
position: absolute; top: 0.85rem; right: 1rem;
background: none; border: none; color: var(--color-muted);
font-size: 1.3rem; cursor: pointer; line-height: 1; padding: 0.2rem 0.4rem; border-radius: 4px;
}
.date-form-close:hover { background: var(--color-secondary); color: var(--color-text); }
.date-form-title { font-size: 1.1rem; font-weight: 700; margin-bottom: 1rem; }
.date-form-group { display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 0.85rem; }
.date-form-group label { font-size: 0.78rem; color: var(--color-muted); }
.date-form-group input, .date-form-group textarea {
padding: 0.55rem 0.75rem; font-size: 0.9rem; border-radius: 8px;
}
.date-form-group textarea { min-height: 100px; resize: vertical; }
.date-form-img-preview {
width: 100%; aspect-ratio: 16/9;
background: var(--color-secondary); border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 2rem; color: var(--color-muted); overflow: hidden;
margin-bottom: 0.5rem; cursor: pointer; position: relative;
}
.date-form-img-preview img { width: 100%; height: 100%; object-fit: cover; }
.date-form-img-remove {
position: absolute; top: 0.4rem; right: 0.4rem;
background: rgba(0,0,0,0.5); border: none; color: #fff;
border-radius: 50%; width: 1.6rem; height: 1.6rem;
font-size: 0.85rem; cursor: pointer; display: none;
align-items: center; justify-content: center; padding: 0;
}
.date-form-img-remove.visible { display: flex; }
.date-form-actions { display: flex; gap: 0.6rem; margin-top: 1rem; }
.date-form-save { flex: 1; }
.date-form-limit-hint { font-size: 0.75rem; color: var(--color-muted); text-align: center; margin-top: 0.25rem; }
/* ── Profil-Dialog (Mini) ── */
.profile-dialog-bg {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.65);
z-index: 600;
align-items: flex-end;
justify-content: center;
}
.profile-dialog-bg.open { display: flex; }
.profile-dialog {
background: var(--color-card);
border-radius: 18px 18px 0 0;
width: 100%; max-width: 520px;
max-height: 80vh; overflow-y: auto;
padding: 1.5rem 1.25rem 2rem;
position: relative;
}
.profile-dialog-close {
position: absolute; top: 0.85rem; right: 1rem;
background: var(--color-secondary); border: none; color: var(--color-text);
width: 2rem; height: 2rem; border-radius: 50%; font-size: 1rem;
cursor: pointer; display: flex; align-items: center; justify-content: center;
padding: 0; margin: 0;
}
.dp-vorlieben {
display: flex; flex-wrap: wrap; gap: 0.4rem;
margin-top: 0.6rem;
}
.dp-vorliebe-chip {
font-size: 0.75rem; padding: 0.2rem 0.55rem;
border-radius: 20px; border: 1px solid currentColor;
white-space: nowrap;
}
.dp-vorliebe-chip.bw-UNBEDINGT { color: #2e7d32; }
.dp-vorliebe-chip.bw-MAG_ICH { color: #81c784; }
.dp-vorliebe-chip.bw-WILL_AUSPROBIEREN { color: #1e88e5; }
@media (max-width: 500px) {
.dates-grid { grid-template-columns: 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">Anwenden</button>
<button class="reset-btn" id="resetBtn">Zurücksetzen</button>
</div>
</div>
<div class="main">
<div class="content">
<div class="dating-tabs">
<button class="dating-tab-btn active" data-tab="match">Match</button>
<button class="dating-tab-btn" data-tab="entdecken">Entdecken</button>
<button class="dating-tab-btn" data-tab="dates">Dates</button>
<div style="margin-left:auto;display:flex;align-items:center;gap:0.4rem;padding-bottom:1px;">
<a href="/konto/einstellungen.html#sec-dating" title="Dating-Einstellungen"
style="display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:50%;background:var(--color-secondary);color:var(--color-muted);text-decoration:none;font-size:1.1rem;transition:background 0.15s,color 0.15s;"
onmouseover="this.style.background='var(--color-primary)';this.style.color='#fff';"
onmouseout="this.style.background='var(--color-secondary)';this.style.color='var(--color-muted)';"></a>
<button id="filterOpenBtn" title="Filter"
style="display:flex;align-items:center;justify-content:center;position:relative;width:2rem;height:2rem;border-radius:50%;background:var(--color-secondary);color:var(--color-muted);border:none;cursor:pointer;transition:background 0.15s,color 0.15s;padding:0;"
onmouseover="this.style.background='var(--color-primary)';this.style.color='#fff';"
onmouseout="this.style.background='var(--color-secondary)';this.style.color='var(--color-muted)';">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<line x1="4" y1="6" x2="20" y2="6"/><line x1="8" y1="12" x2="16" y2="12"/><line x1="11" y1="18" x2="13" y2="18"/>
</svg>
<span class="filter-badge" id="filterBadge" style="display:none;position:absolute;top:-3px;right:-3px;"></span>
</button>
</div>
</div>
<div class="dating-tab-panel" id="tab-entdecken">
<span class="results-count" id="resultsCount" style="display:block;margin-bottom:0.75rem;"></span>
<div class="profiles-grid" id="profilesGrid"></div>
<div id="sentinel"></div>
</div>
<div class="dating-tab-panel active" id="tab-match">
<div class="discovery-wrap">
<div id="discoveryCard" class="discovery-card" style="display:none;" onclick="openDiscoveryPopup()">
<div class="discovery-card-img" id="discCardImg">
<span id="discNoPic" style="display:none;">👤</span>
<img id="discCardPhoto" alt="" style="display:none;width:100%;height:100%;object-fit:cover;">
<div class="discovery-card-gradient">
<div class="discovery-card-name" id="discCardName"></div>
<div class="discovery-card-meta" id="discCardMeta"></div>
</div>
<div class="disc-img-dots" id="discImgDots"></div>
<button class="disc-nav-btn disc-nav-prev" id="discNavPrev"
onclick="discNavStep(-1,event)" style="display:none;">&#8249;</button>
<button class="disc-nav-btn disc-nav-next" id="discNavNext"
onclick="discNavStep(1,event)" style="display:none;">&#8250;</button>
</div>
<div class="disc-match-chips" id="discMatchChips"></div>
<div class="discovery-card-desc" id="discCardDesc"></div>
</div>
<div id="discoveryEmpty" style="display:none; text-align:center; padding:3rem 1rem; color:var(--color-muted);">
<div style="font-size:2.5rem; margin-bottom:0.75rem;">🎉</div>
<p>Keine weiteren Profile gefunden.<br>Schau später wieder vorbei!</p>
</div>
<div id="discoveryLoading" style="text-align:center; padding:3rem 1rem; color:var(--color-muted);">
Wird geladen…
</div>
<div class="discovery-actions" id="discoveryActions" style="display:none;">
<button class="discovery-btn discovery-btn-no" title="Nein" onclick="discardProfile()"></button>
<button class="discovery-btn discovery-btn-yes" title="Gefällt mir" onclick="likeProfile()"></button>
</div>
</div>
</div>
<div class="dating-tab-panel" id="tab-dates">
<div class="dates-toolbar">
<span class="results-count" id="datesCount"></span>
<button class="btn" id="createDateBtn" style="padding:0.5rem 1rem;font-size:0.88rem;">
+ Date erstellen
</button>
</div>
<div class="dates-section" id="myDatesSection" style="display:none;">
<p class="dates-section-title">Meine Dates</p>
<div class="dates-grid" id="myDatesGrid"></div>
</div>
<div class="dates-section">
<p class="dates-section-title">Verfügbare Dates</p>
<div class="dates-grid" id="availableDatesGrid"></div>
</div>
</div>
</div>
</div>
<!-- ── Date-Detail-Dialog ── -->
<div class="date-dialog-bg" id="dateBg" onclick="closeDateDialog(event)">
<div class="date-dialog" id="dateDialog">
<button class="date-dialog-close" onclick="closeDateDialogBtn()"></button>
<div class="date-dialog-img" id="ddImg"></div>
<div class="date-dialog-body">
<div class="date-dialog-title" id="ddTitle"></div>
<div class="date-dialog-creator" id="ddCreator" style="cursor:pointer;">
<div class="date-dialog-creator-avatar" id="ddCreatorAvatar"></div>
<div class="date-dialog-creator-name" id="ddCreatorName"></div>
</div>
<div class="date-dialog-when" id="ddWhen" style="display:none;">
🗓️ <span id="ddWhenText"></span>
</div>
<div class="date-dialog-desc" id="ddDesc"></div>
<div class="date-dialog-stats" id="ddStats"></div>
<div class="date-dialog-actions" id="ddActions"></div>
</div>
</div>
</div>
<!-- ── Eigenes-Date-Dialog ── -->
<div class="date-dialog-bg" id="myDateBg" onclick="closeMyDateDialog(event)">
<div class="date-dialog" id="myDateDialog">
<button class="date-dialog-close" onclick="closeMyDateDialogBtn()"></button>
<div class="date-dialog-img" id="mdImg"></div>
<div class="date-dialog-body">
<div class="date-dialog-title" id="mdTitle"></div>
<div class="date-dialog-when" id="mdWhen" style="display:none;">
🗓️ <span id="mdWhenText"></span>
</div>
<div class="date-dialog-desc" id="mdDesc"></div>
<div class="date-dialog-actions">
<button class="btn" style="flex:1;" id="mdEditBtn" onclick="openEditDate()">Bearbeiten</button>
<button class="btn" style="flex:1;background:var(--color-secondary);color:var(--color-text);"
id="mdDeleteBtn" onclick="deleteMyDate()">Löschen</button>
</div>
<hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.25rem 0;">
<p class="dates-section-title" style="margin:0.25rem 0 0.5rem;">Interessenten</p>
<div class="interest-list" id="mdInterestList">
<div style="color:var(--color-muted);font-size:0.85rem;">Wird geladen…</div>
</div>
</div>
</div>
</div>
<!-- ── Date Erstellen/Bearbeiten-Dialog ── -->
<div class="date-form-bg" id="dateFormBg" onclick="closeDateForm(event)">
<div class="date-form-dialog" id="dateFormDialog">
<button class="date-form-close" onclick="closeDateFormBtn()"></button>
<div class="date-form-title" id="dateFormTitle">Date erstellen</div>
<div class="date-form-group">
<label>Bild (optional)</label>
<div class="date-form-img-preview" id="dfImgPreview" onclick="triggerImgPick()">
<span id="dfImgPlaceholder">📷 Bild hinzufügen</span>
<button class="date-form-img-remove" id="dfImgRemove" onclick="removeFormImg(event)"></button>
</div>
<input type="file" id="dfImgInput" accept="image/*" style="display:none;" onchange="onImgSelected(event)">
</div>
<div class="date-form-group">
<label>Titel *</label>
<input type="text" id="dfTitle" maxlength="200" placeholder="z.B. Spaziergang am See">
</div>
<div class="date-form-group">
<label>Beschreibung *</label>
<textarea id="dfDesc" maxlength="2000" placeholder="Was planst du? Wen suchst du?"></textarea>
</div>
<div class="date-form-group">
<label>Datum & Uhrzeit (optional)</label>
<input type="datetime-local" id="dfDateTime">
</div>
<div class="date-form-actions">
<button class="btn date-form-save" id="dfSaveBtn" onclick="saveDate()">Speichern</button>
</div>
<div class="date-form-limit-hint" id="dfLimitHint"></div>
</div>
</div>
<!-- ── Profil-Mini-Dialog ── -->
<div class="profile-dialog-bg" id="profileDialogBg" onclick="closeProfileDialog(event)">
<div class="profile-dialog">
<button class="profile-dialog-close" onclick="closeProfileDialogBtn()"></button>
<div class="pd-layout">
<div class="pd-photo-box" id="pdPhotoBox">
<span id="pdNoPic">👤</span>
<img id="pdPhoto" alt="" style="display:none;">
<div class="pd-photo-dots" id="pdPhotoDots"></div>
<button class="pd-nav-btn pd-nav-prev" id="pdNavPrev" style="display:none;" onclick="pdNavStep(-1)">&#8249;</button>
<button class="pd-nav-btn pd-nav-next" id="pdNavNext" style="display:none;" onclick="pdNavStep(1)">&#8250;</button>
</div>
<div class="pd-info">
<div class="dp-name" id="pdName"></div>
<div class="dp-tags" id="pdTags"></div>
<div class="dp-vorlieben" id="pdVorlieben"></div>
<div class="dp-desc" id="pdDesc" style="display:none;"></div>
<div class="dp-actions">
<a id="pdProfileLink" href="#" class="btn dp-link">Profil ansehen</a>
</div>
</div>
</div>
</div>
</div>
<!-- ── Match-Overlay ── -->
<div class="match-overlay" id="matchOverlay">
<div class="match-overlay-title">💕 Match! 💕</div>
<div class="match-overlay-avatars">
<div class="match-avatar" id="matchAvatarMe"></div>
<div class="match-heart"></div>
<div class="match-avatar" id="matchAvatarPartner"></div>
</div>
<div class="match-overlay-sub" id="matchOverlaySub"></div>
<button class="btn match-overlay-btn" onclick="closeMatchOverlay()">Weiter swipen</button>
<a id="matchChatLink" href="#" class="btn match-overlay-btn" style="background:var(--color-secondary);color:var(--color-text);">Nachricht schreiben</a>
</div>
<!-- ── Discovery-Popup ── -->
<div class="discovery-popup-bg" id="discoveryPopupBg" onclick="closeDiscoveryPopup(event)">
<div class="discovery-popup" id="discoveryPopup">
<button class="discovery-popup-close" onclick="closeDiscoveryPopup(null)"></button>
<div class="pd-layout">
<div class="pd-photo-box" id="dpPhotoBox">
<span id="dpNoPic">👤</span>
<img id="dpPhoto" alt="" style="display:none;">
<div class="pd-photo-dots" id="dpPhotoDots"></div>
<button class="pd-nav-btn pd-nav-prev" id="dpNavPrev" style="display:none;" onclick="dpNavStep(-1)">&#8249;</button>
<button class="pd-nav-btn pd-nav-next" id="dpNavNext" style="display:none;" onclick="dpNavStep(1)">&#8250;</button>
</div>
<div class="pd-info">
<div class="dp-name" id="dpName"></div>
<div class="dp-tags" id="dpTags"></div>
<div class="dp-vorlieben" id="dpVorlieben"></div>
<div class="dp-desc" id="dpDesc" style="display:none;"></div>
<div class="dp-actions">
<a id="dpProfileLink" href="#" target="_blank" rel="noopener" class="btn dp-link">Profil ansehen</a>
</div>
</div>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/social-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 = [];
// Default-Werte aus Einstellungen (werden beim Login-Fetch gesetzt)
let defaultDistKm = 50, defaultAgeFrom = 18, defaultAgeTo = 60;
let defaultGeschlechter = [];
// ── Profilbild-Galerie in Dialog/Popup ────────────────────────────────────
let pdImages = [], pdImageIdx = 0;
let dpImages = [], dpImageIdx = 0;
function renderPopupImage(prefix, images, idx) {
const photo = document.getElementById(prefix + 'Photo');
const noPic = document.getElementById(prefix + 'NoPic');
const prev = document.getElementById(prefix + 'NavPrev');
const next = document.getElementById(prefix + 'NavNext');
const dots = document.getElementById(prefix + 'PhotoDots');
if (!images.length) {
photo.style.display = 'none'; noPic.style.display = '';
prev.style.display = next.style.display = 'none';
dots.innerHTML = '';
} else {
photo.src = images[idx]; photo.style.display = ''; noPic.style.display = 'none';
const multi = images.length > 1;
prev.style.display = next.style.display = multi ? '' : 'none';
if (multi) { prev.disabled = idx === 0; next.disabled = idx === images.length - 1; }
dots.innerHTML = multi
? images.map((_, i) => `<span class="pd-photo-dot${i === idx ? ' active' : ''}"></span>`).join('')
: '';
}
}
window.pdNavStep = function(dir) {
pdImageIdx = Math.max(0, Math.min(pdImages.length - 1, pdImageIdx + dir));
renderPopupImage('pd', pdImages, pdImageIdx);
};
window.dpNavStep = function(dir) {
dpImageIdx = Math.max(0, Math.min(dpImages.length - 1, dpImageIdx + dir));
renderPopupImage('dp', dpImages, dpImageIdx);
};
// ── Tab-Navigation ────────────────────────────────────────────────────────
function activateTab(tabName) {
document.querySelectorAll('.dating-tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.dating-tab-panel').forEach(p => p.classList.remove('active'));
const btn = document.querySelector(`.dating-tab-btn[data-tab="${tabName}"]`);
if (btn) btn.classList.add('active');
const panel = document.getElementById('tab-' + tabName);
if (panel) panel.classList.add('active');
document.getElementById('filterOpenBtn').style.display = 'flex';
const url = new URL(window.location);
url.searchParams.set('tab', tabName);
history.replaceState(null, '', url);
if (tabName === 'match' && !discoveryLoaded) {
discoveryLoaded = true;
loadDiscovery();
}
if (tabName === 'entdecken' && !entdeckenLoaded) {
entdeckenLoaded = true;
runSearch();
}
if (tabName === 'dates' && !datesLoaded) {
datesLoaded = true;
loadDates();
}
}
document.querySelectorAll('.dating-tab-btn').forEach(btn => {
btn.addEventListener('click', () => activateTab(btn.dataset.tab));
});
// ── 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; }
myPicture = user.profilePicture || null;
myUserId = user.userId || null;
loadVorlieben();
if (user.datingGeschlechter && user.datingGeschlechter.length > 0) {
defaultGeschlechter = user.datingGeschlechter;
document.querySelectorAll('#geschlechtChips .chip').forEach(chip => {
chip.classList.toggle('active', defaultGeschlechter.includes(chip.dataset.val));
});
}
// Standard-Filterwerte aus den Einstellungen übernehmen
if (user.datingMaxDistanzKm != null) defaultDistKm = user.datingMaxDistanzKm;
if (user.datingMinAlter != null) defaultAgeFrom = user.datingMinAlter;
if (user.datingMaxAlter != null) defaultAgeTo = user.datingMaxAlter;
document.getElementById('maxDist').value = defaultDistKm;
document.getElementById('distVal').textContent = defaultDistKm + ' km';
ageFrom = defaultAgeFrom;
ageTo = defaultAgeTo;
updateAgeSlider();
// Tab aus URL-Parameter aktivieren, sonst Default (Match)
const urlTab = new URLSearchParams(window.location.search).get('tab');
activateTab(urlTab || 'match');
}).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 = defaultDistKm;
document.getElementById('distVal').textContent = defaultDistKm + ' km';
ageFrom = defaultAgeFrom; ageTo = defaultAgeTo;
updateAgeSlider();
// Geschlecht-Chips auf gespeicherte Einstellungswerte zurücksetzen
document.querySelectorAll('#geschlechtChips .chip').forEach(c =>
c.classList.toggle('active', defaultGeschlechter.includes(c.dataset.val)));
document.querySelectorAll('#neigungChips .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();
saveFilterToDb();
const activeTab = document.querySelector('.dating-tab-btn.active')?.dataset?.tab;
if (activeTab === 'dates') {
loadDates();
} else if (activeTab === 'match') {
discoveryLoaded = false;
loadDiscovery();
} else {
runSearch();
}
});
function saveFilterToDb() {
const geschlechter = [...document.querySelectorAll('#geschlechtChips .chip.active')]
.map(c => c.dataset.val);
fetch('/user/me/dating-filter', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
datingGeschlechter: geschlechter,
datingMaxDistanzKm: parseInt(document.getElementById('maxDist').value),
datingMinAlter: ageFrom,
datingMaxAlter: ageTo
})
}).then(r => {
if (r.ok) {
defaultDistKm = parseInt(document.getElementById('maxDist').value);
defaultAgeFrom = ageFrom;
defaultAgeTo = ageTo;
defaultGeschlechter = geschlechter;
}
}).catch(() => {});
}
// ── 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 picSrc = p.profilePictureHq || p.profilePicture;
const img = picSrc
? `<img src="data:image/png;base64,${picSrc}" alt="${escHtml(p.name)}" loading="lazy">`
: `<span>👤</span>`;
const chips = [
p.alter ? `<span class="meta-chip">${p.alter} J.</span>` : '',
p.distanzKm != null ? `<span class="meta-chip dist">${p.distanzKm < 1 ? '< 1' : p.distanzKm} km</span>` : '',
p.geschlecht ? `<span class="meta-chip">${escHtml(p.geschlecht)}</span>` : '',
p.neigung ? `<span class="meta-chip">${escHtml(p.neigung)}</span>` : '',
p.datingStadt ? `<span class="meta-chip">${escHtml(p.datingStadt)}</span>` : '',
].filter(Boolean).join('');
const desc = p.beschreibung
? `<div class="profile-card-desc">${escHtml(p.beschreibung)}</div>` : '';
card.innerHTML = `
<a href="/community/benutzer.html?userId=${p.userId}" style="text-decoration:none;color:inherit;display:contents;">
<div class="profile-card-img-wrap">${img}
<button class="profile-card-like${p.likedByMe ? ' liked' : ''}"
data-user-id="${p.userId}"
title="${p.likedByMe ? 'Unlike' : 'Like'}">♥</button>
</div>
<div class="profile-card-body">
<div class="profile-card-name">${escHtml(p.name)}</div>
<div class="profile-card-meta">${chips}</div>
${desc}
</div>
</a>`;
card.querySelector('.profile-card-like').addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
toggleLike(e.currentTarget, p.userId);
});
grid.appendChild(card);
});
}
async function toggleLike(btn, userId) {
btn.disabled = true;
try {
const res = await fetch('/dating/like/' + userId, { method: 'POST' });
if (!res.ok) return;
const data = await res.json();
btn.classList.toggle('liked', data.liked);
btn.title = data.liked ? 'Unlike' : 'Like';
if (data.newMatch) showMatchToast();
} finally {
btn.disabled = false;
}
}
function showMatchToast() {
const t = document.createElement('div');
t.textContent = '🎉 Es ist ein Match!';
Object.assign(t.style, {
position:'fixed', bottom:'2rem', left:'50%', transform:'translateX(-50%)',
background:'var(--color-primary)', color:'#fff', padding:'0.75rem 1.5rem',
borderRadius:'8px', fontWeight:'700', zIndex:'999', boxShadow:'0 4px 16px rgba(0,0,0,0.4)'
});
document.body.appendChild(t);
setTimeout(() => t.remove(), 3500);
}
// ── Skeleton-Helpers ──────────────────────────────────────────────────────
function skeletons(n) {
return Array.from({ length: n }, () => `
<div class="profile-card-skeleton" data-skeleton>
<div class="skeleton-img"></div>
<div class="skeleton-body">
<div class="skeleton-line" style="width:60%"></div>
<div class="skeleton-line" style="width:80%"></div>
</div>
</div>`).join('');
}
function showSkeletons(n) {
const grid = document.getElementById('profilesGrid');
grid.insertAdjacentHTML('beforeend', skeletons(n));
}
function removeSkeletons() {
document.querySelectorAll('[data-skeleton]').forEach(el => el.remove());
}
// ── Params-Builder ────────────────────────────────────────────────────────
function buildParams() {
const p = new URLSearchParams();
p.set('maxDistanceKm', document.getElementById('maxDist').value);
p.set('minAge', ageFrom);
p.set('maxAge', ageTo);
p.set('vorliebenUnd', vorliebenUnd);
document.querySelectorAll('#geschlechtChips .chip.active').forEach(c => p.append('geschlechter', c.dataset.val));
document.querySelectorAll('#neigungChips .chip.active').forEach(c => p.append('neigungen', c.dataset.val));
selectedVorlieben.forEach((_, id) => p.append('vorliebenIds', id));
return p;
}
// ── Utility ───────────────────────────────────────────────────────────────
function escHtml(s) {
if (!s) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Discovery (Match-Tab) ─────────────────────────────────────────────────
let discoveryQueue = [];
let discoveryIdx = 0;
let discoveryProfile = null;
let myPicture = null;
let discoveryLoaded = false;
let entdeckenLoaded = false;
let discImages = []; // Bildquellen (volle data-URL) für aktuelle Karte
let discImageIdx = 0;
// (Tab-Wechsel-Logik ist oben in der Tab-Navigation)
async function loadDiscovery() {
document.getElementById('discoveryLoading').style.display = '';
document.getElementById('discoveryCard').style.display = 'none';
document.getElementById('discoveryActions').style.display = 'none';
document.getElementById('discoveryEmpty').style.display = 'none';
try {
const res = await fetch('/dating/discovery');
if (res.status === 403) return;
if (!res.ok) throw new Error();
discoveryQueue = await res.json();
discoveryIdx = 0;
} catch { discoveryQueue = []; }
document.getElementById('discoveryLoading').style.display = 'none';
showDiscoveryCard();
}
async function showDiscoveryCard() {
if (discoveryIdx >= discoveryQueue.length) {
document.getElementById('discoveryCard').style.display = 'none';
document.getElementById('discoveryActions').style.display = 'none';
document.getElementById('discoveryEmpty').style.display = '';
return;
}
const userId = discoveryQueue[discoveryIdx];
try {
const [profRes, galleryRes] = await Promise.all([
fetch('/dating/profiles/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([userId])
}),
fetch('/social/profile-images?userId=' + userId)
]);
if (!profRes.ok) { discoveryIdx++; showDiscoveryCard(); return; }
const profiles = await profRes.json();
if (!profiles || !profiles.length) { discoveryIdx++; showDiscoveryCard(); return; }
discoveryProfile = profiles[0];
// Bildersammlung aufbauen: Profilfoto + Galerie
discImages = [];
const pic = discoveryProfile.profilePictureHq || discoveryProfile.profilePicture;
if (pic) discImages.push('data:image/png;base64,' + pic);
if (galleryRes.ok) {
const gallery = await galleryRes.json();
gallery.forEach(img => {
if (img.imageData) discImages.push('data:image/jpeg;base64,' + img.imageData);
});
}
discImageIdx = 0;
} catch { discoveryIdx++; showDiscoveryCard(); return; }
// Texte setzen
document.getElementById('discCardName').textContent = discoveryProfile.name || '';
const meta = [];
if (discoveryProfile.alter) meta.push(discoveryProfile.alter + ' J.');
if (discoveryProfile.geschlecht) meta.push({ WEIBLICH:'weiblich', MAENNLICH:'männlich', DIVERS:'divers' }[discoveryProfile.geschlecht] || discoveryProfile.geschlecht);
if (discoveryProfile.neigung) meta.push({ DEVOT:'devot', EHER_DEVOT:'eher devot', SWITCHER:'Switcher', EHER_DOMINANT:'eher dominant', DOMINANT:'dominant' }[discoveryProfile.neigung] || discoveryProfile.neigung);
document.getElementById('discCardMeta').textContent = meta.join(' · ');
const descEl = document.getElementById('discCardDesc');
if (discoveryProfile.beschreibung) { descEl.textContent = discoveryProfile.beschreibung; descEl.style.display = ''; }
else { descEl.style.display = 'none'; }
// Matching Vorlieben
const chipsEl = document.getElementById('discMatchChips');
const matchIds = discoveryProfile.matchingVorliebenIds || [];
if (matchIds.length > 0 && allVorlieben.length > 0) {
const nameMap = new Map(allVorlieben.map(v => [v.itemId, v.name]));
chipsEl.innerHTML = matchIds.filter(id => nameMap.has(id))
.map(id => `<span class="disc-match-chip">${escHtml(nameMap.get(id))}</span>`).join('');
} else {
chipsEl.innerHTML = '';
}
renderDiscImage();
document.getElementById('discoveryCard').style.display = '';
document.getElementById('discoveryActions').style.display = '';
}
function renderDiscImage() {
const src = discImages[discImageIdx] || null;
const photoEl = document.getElementById('discCardPhoto');
const noPicEl = document.getElementById('discNoPic');
if (src) {
photoEl.src = src;
photoEl.style.display = '';
noPicEl.style.display = 'none';
} else {
photoEl.style.display = 'none';
noPicEl.style.display = '';
}
const hasMany = discImages.length > 1;
document.getElementById('discNavPrev').style.display = hasMany && discImageIdx > 0 ? '' : 'none';
document.getElementById('discNavNext').style.display = hasMany && discImageIdx < discImages.length - 1 ? '' : 'none';
// Dots
const dotsEl = document.getElementById('discImgDots');
if (hasMany) {
dotsEl.innerHTML = discImages.map((_, i) =>
`<span class="disc-img-dot${i === discImageIdx ? ' active' : ''}"></span>`).join('');
} else {
dotsEl.innerHTML = '';
}
}
window.discNavStep = function(dir, e) {
e.stopPropagation();
discImageIdx = Math.max(0, Math.min(discImages.length - 1, discImageIdx + dir));
renderDiscImage();
};
window.discardProfile = async function () {
if (!discoveryProfile) return;
fetch('/dating/pass/' + discoveryProfile.userId, { method: 'POST' }).catch(() => {});
discoveryIdx++;
showDiscoveryCard();
};
window.likeProfile = async function () {
if (!discoveryProfile) return;
const userId = discoveryProfile.userId;
const name = discoveryProfile.name;
const pic = discoveryProfile.profilePicture || null;
discoveryIdx++;
showDiscoveryCard();
try {
const res = await fetch('/dating/like/' + userId, { method: 'POST' });
if (!res.ok) return;
const data = await res.json();
if (data.newMatch) showMatchOverlay(userId, name, pic);
} catch { /* ignore */ }
};
window.openDiscoveryPopup = async function () {
if (!discoveryProfile) return;
const p = discoveryProfile;
// Bilder aus der Match-Karte übernehmen
dpImages = [...discImages]; dpImageIdx = discImageIdx;
renderPopupImage('dp', dpImages, dpImageIdx);
document.getElementById('dpName').textContent = p.name || '';
document.getElementById('dpProfileLink').href = '/community/benutzer.html?userId=' + p.userId;
document.getElementById('dpVorlieben').innerHTML = '';
const tags = document.getElementById('dpTags');
tags.innerHTML = '';
const addTag = v => { if (!v) return; const s = document.createElement('span'); s.className = 'dp-tag'; s.textContent = v; tags.appendChild(s); };
if (p.alter) addTag(p.alter + ' J.');
if (p.geschlecht) addTag({ WEIBLICH:'weiblich', MAENNLICH:'männlich', DIVERS:'divers' }[p.geschlecht] || p.geschlecht);
if (p.neigung) addTag({ DEVOT:'devot', EHER_DEVOT:'eher devot', SWITCHER:'Switcher', EHER_DOMINANT:'eher dominant', DOMINANT:'dominant' }[p.neigung] || p.neigung);
if (p.beziehungsstatus) addTag({ SINGLE:'single', IN_EINER_BEZIEHUNG:'in einer Beziehung', VERHEIRATET:'verheiratet', IN_EINER_OFFENEN_BEZIEHUNG:'offene Beziehung', IN_EINER_OFFENEN_EHE:'offene Ehe' }[p.beziehungsstatus] || p.beziehungsstatus);
const descEl = document.getElementById('dpDesc');
if (p.beschreibung) { descEl.textContent = p.beschreibung; descEl.style.display = ''; }
else { descEl.style.display = 'none'; }
document.getElementById('discoveryPopupBg').classList.add('open');
try {
const vlRes = await fetch('/dating/vorlieben/' + p.userId);
if (vlRes.ok) {
const chips = await vlRes.json();
renderVorliebenChips(document.getElementById('dpVorlieben'), chips);
}
} catch {}
};
window.closeDiscoveryPopup = function (e) {
if (e && e.target !== document.getElementById('discoveryPopupBg')) return;
document.getElementById('discoveryPopupBg').classList.remove('open');
};
function showMatchOverlay(partnerId, partnerName, partnerPic) {
const meAv = document.getElementById('matchAvatarMe');
meAv.innerHTML = myPicture
? `<img src="data:image/png;base64,${myPicture}" alt="">`
: '👤';
const theirAv = document.getElementById('matchAvatarPartner');
theirAv.innerHTML = partnerPic
? `<img src="data:image/png;base64,${partnerPic}" alt="">`
: '👤';
document.getElementById('matchOverlaySub').textContent = 'Du und ' + (partnerName || 'jemand') + ' mögt euch gegenseitig!';
document.getElementById('matchChatLink').href = '/community/nachrichten.html?userId=' + partnerId;
document.getElementById('matchOverlay').classList.add('open');
}
window.closeMatchOverlay = function () {
document.getElementById('matchOverlay').classList.remove('open');
};
// SSE-Match-Events (auch aus social-sidebar SSE-Handler aufrufbar)
window._onSseMatch = function (data) {
showMatchOverlay(data.partnerId, data.partnerName, data.partnerPicture || null);
};
// ── Dates-Tab ─────────────────────────────────────────────────────────────
let datesLoaded = false;
let currentDate = null; // aktuell geöffnetes Date (Detail- oder Eigenes-Dialog)
let editingDateId = null; // UUID wenn Bearbeitung, null wenn Neu-Erstellen
let formImgData = null; // base64 des gewählten Formular-Bildes
let myUserId = null; // eigene User-ID (wird beim Login-Fetch gesetzt)
document.getElementById('createDateBtn').addEventListener('click', () => openCreateDate());
async function loadDates() {
document.getElementById('datesCount').textContent = '';
document.getElementById('myDatesSection').style.display = 'none';
document.getElementById('myDatesGrid').innerHTML = '';
document.getElementById('availableDatesGrid').innerHTML =
'<div style="color:var(--color-muted);font-size:0.85rem;padding:1rem 0;">Wird geladen…</div>';
try {
const res = await fetch('/dating/dates?' + buildParams());
if (res.status === 403) return;
if (!res.ok) throw new Error();
const data = await res.json();
renderMyDates(data.mine || []);
renderAvailableDates(data.available || []);
const total = (data.available || []).length;
document.getElementById('datesCount').textContent =
total === 0 ? 'Keine Dates gefunden' : total + ' Date' + (total === 1 ? '' : 's') + ' gefunden';
} catch {
document.getElementById('availableDatesGrid').innerHTML =
'<div style="color:var(--color-muted);font-size:0.85rem;">Fehler beim Laden.</div>';
}
}
function renderMyDates(dates) {
const section = document.getElementById('myDatesSection');
const grid = document.getElementById('myDatesGrid');
if (dates.length === 0) { section.style.display = 'none'; return; }
section.style.display = '';
grid.innerHTML = '';
dates.forEach(d => grid.appendChild(buildDateCard(d, true)));
}
function renderAvailableDates(dates) {
const grid = document.getElementById('availableDatesGrid');
grid.innerHTML = '';
if (dates.length === 0) {
grid.innerHTML = '<div style="color:var(--color-muted);font-size:0.85rem;padding:1rem 0;">Keine Dates in deinem Umkreis gefunden.</div>';
return;
}
dates.forEach(d => grid.appendChild(buildDateCard(d, false)));
}
function buildDateCard(d, isMine) {
const card = document.createElement('div');
card.className = 'date-card';
const imgHtml = d.imageData
? `<img src="${escHtml(d.imageData)}" alt="" loading="lazy">`
: '📅';
const footerRight = isMine
? `<span class="date-card-interest">♥ ${d.interestCount}</span>`
: (d.myInterest
? `<span class="date-card-interest">♥ Interesse</span>`
: `<span class="date-card-meta">♥ ${d.interestCount}</span>`);
const avatarHtml = d.creatorProfilePicture
? `<img src="data:image/png;base64,${d.creatorProfilePicture}" alt="">`
: '👤';
const dateHint = d.scheduledAt
? `<span class="date-card-date">${formatDateTime(d.scheduledAt)}</span>` : '';
card.innerHTML = `
<div class="date-card-img">${imgHtml}</div>
<div class="date-card-body">
<div class="date-card-title">${escHtml(d.title)}</div>
<div class="date-card-desc">${escHtml(d.description)}</div>
${dateHint ? '<div class="date-card-date">' + escHtml(formatDateTime(d.scheduledAt)) + '</div>' : ''}
</div>
<div class="date-card-footer">
<div class="date-card-avatar" data-creator-id="${d.creatorId}">${avatarHtml}</div>
<span class="date-card-creator">${escHtml(d.creatorName || '')}</span>
${footerRight}
</div>`;
card.addEventListener('click', () => {
if (isMine) openMyDateDialog(d);
else openDateDialog(d);
});
return card;
}
// ── Detail-Dialog (fremdes Date) ──────────────────────────────────────────
function openDateDialog(d) {
currentDate = d;
const imgEl = document.getElementById('ddImg');
imgEl.innerHTML = d.imageData ? `<img src="${escHtml(d.imageData)}" alt="">` : '📅';
document.getElementById('ddTitle').textContent = d.title;
const avatarEl = document.getElementById('ddCreatorAvatar');
avatarEl.innerHTML = d.creatorProfilePicture
? `<img src="data:image/png;base64,${d.creatorProfilePicture}" alt="">` : '👤';
document.getElementById('ddCreatorName').textContent = d.creatorName || '';
document.getElementById('ddCreator').onclick =
() => window.open('/community/benutzer.html?userId=' + d.creatorId, '_blank');
const whenEl = document.getElementById('ddWhen');
if (d.scheduledAt) {
document.getElementById('ddWhenText').textContent = formatDateTime(d.scheduledAt);
whenEl.style.display = 'flex';
} else {
whenEl.style.display = 'none';
}
document.getElementById('ddDesc').textContent = d.description;
document.getElementById('ddStats').textContent =
d.interestCount + ' Person' + (d.interestCount === 1 ? '' : 'en') + ' interessiert';
const actionsEl = document.getElementById('ddActions');
const interested = d.myInterest;
actionsEl.innerHTML = `
<button class="btn date-interest-btn${interested ? ' active' : ''}" id="ddInterestBtn"
onclick="toggleInterest()">
${interested ? '♥ Interesse bekundet' : '♥ Interesse bekunden'}
</button>`;
document.getElementById('dateBg').classList.add('open');
}
window.closeDateDialog = function(e) {
if (e && e.target !== document.getElementById('dateBg')) return;
document.getElementById('dateBg').classList.remove('open');
currentDate = null;
};
window.closeDateDialogBtn = function() {
document.getElementById('dateBg').classList.remove('open');
currentDate = null;
};
window.toggleInterest = async function() {
if (!currentDate) return;
const btn = document.getElementById('ddInterestBtn');
btn.disabled = true;
try {
const res = await fetch('/dating/dates/' + currentDate.dateId + '/interest', { method: 'POST' });
if (!res.ok) return;
const data = await res.json();
currentDate = { ...currentDate, myInterest: data.myInterest, interestCount: data.interestCount };
btn.className = 'btn date-interest-btn' + (data.myInterest ? ' active' : '');
btn.textContent = data.myInterest ? '♥ Interesse bekundet' : '♥ Interesse bekunden';
document.getElementById('ddStats').textContent =
data.interestCount + ' Person' + (data.interestCount === 1 ? '' : 'en') + ' interessiert';
// Karte aktualisieren
datesLoaded = false;
} finally {
btn.disabled = false;
}
};
// ── Eigenes-Date-Dialog ───────────────────────────────────────────────────
function openMyDateDialog(d) {
currentDate = d;
const imgEl = document.getElementById('mdImg');
imgEl.innerHTML = d.imageData ? `<img src="${escHtml(d.imageData)}" alt="">` : '📅';
document.getElementById('mdTitle').textContent = d.title;
const whenEl = document.getElementById('mdWhen');
if (d.scheduledAt) {
document.getElementById('mdWhenText').textContent = formatDateTime(d.scheduledAt);
whenEl.style.display = 'flex';
} else {
whenEl.style.display = 'none';
}
document.getElementById('mdDesc').textContent = d.description;
// Interessenten laden
loadInterests(d.dateId);
document.getElementById('myDateBg').classList.add('open');
}
async function loadInterests(dateId) {
const list = document.getElementById('mdInterestList');
list.innerHTML = '<div style="color:var(--color-muted);font-size:0.85rem;">Wird geladen…</div>';
try {
const res = await fetch('/dating/dates/' + dateId + '/interests');
if (!res.ok) throw new Error();
const interests = await res.json();
if (interests.length === 0) {
list.innerHTML = '<div style="color:var(--color-muted);font-size:0.85rem;">Noch keine Interessenten.</div>';
return;
}
list.innerHTML = '';
interests.forEach(i => {
const item = document.createElement('div');
item.className = 'interest-item';
const avatarHtml = i.profilePicture
? `<img src="${escHtml(i.profilePicture)}" alt="">` : '👤';
const actionHtml = i.blocked
? `<span class="interest-item-blocked">blockiert</span>`
: `<button class="btn interest-item-dm" onclick="openDm('${i.userId}')">Nachricht</button>`;
item.innerHTML = `
<div class="interest-item-avatar" onclick="loadAndOpenProfile('${i.userId}','${escHtml(i.name || '')}', '${i.profilePicture || ''}')">${avatarHtml}</div>
<span class="interest-item-name" onclick="loadAndOpenProfile('${i.userId}','${escHtml(i.name || '')}','${i.profilePicture || ''}')">${escHtml(i.name || 'Unbekannt')}</span>
${actionHtml}`;
list.appendChild(item);
});
} catch {
list.innerHTML = '<div style="color:var(--color-muted);font-size:0.85rem;">Fehler beim Laden.</div>';
}
}
window.closeMyDateDialog = function(e) {
if (e && e.target !== document.getElementById('myDateBg')) return;
document.getElementById('myDateBg').classList.remove('open');
currentDate = null;
};
window.closeMyDateDialogBtn = function() {
document.getElementById('myDateBg').classList.remove('open');
currentDate = null;
};
window.deleteMyDate = async function() {
if (!currentDate) return;
if (!confirm('Date wirklich löschen?')) return;
try {
const res = await fetch('/dating/dates/' + currentDate.dateId, { method: 'DELETE' });
if (!res.ok) return;
closeMyDateDialogBtn();
datesLoaded = false;
loadDates();
} catch {}
};
window.openDm = function(userId) {
window.location.href = '/community/nachrichten.html?userId=' + userId;
};
// ── Erstellen/Bearbeiten-Dialog ───────────────────────────────────────────
function openCreateDate() {
editingDateId = null;
formImgData = null;
document.getElementById('dateFormTitle').textContent = 'Date erstellen';
document.getElementById('dfTitle').value = '';
document.getElementById('dfDesc').value = '';
document.getElementById('dfDateTime').value = '';
resetFormImg();
document.getElementById('dfLimitHint').textContent = '';
document.getElementById('dateFormBg').classList.add('open');
}
window.openEditDate = function() {
if (!currentDate) return;
editingDateId = currentDate.dateId;
formImgData = currentDate.imageData || null;
document.getElementById('dateFormTitle').textContent = 'Date bearbeiten';
document.getElementById('dfTitle').value = currentDate.title || '';
document.getElementById('dfDesc').value = currentDate.description || '';
document.getElementById('dfDateTime').value = currentDate.scheduledAt
? currentDate.scheduledAt.substring(0, 16) : '';
const preview = document.getElementById('dfImgPreview');
const removeBtn = document.getElementById('dfImgRemove');
const placeholder = document.getElementById('dfImgPlaceholder');
if (formImgData) {
preview.style.backgroundImage = '';
let imgTag = preview.querySelector('img');
if (!imgTag) { imgTag = document.createElement('img'); preview.appendChild(imgTag); }
imgTag.src = formImgData;
placeholder.style.display = 'none';
removeBtn.classList.add('visible');
} else {
resetFormImg();
}
document.getElementById('myDateBg').classList.remove('open');
document.getElementById('dateFormBg').classList.add('open');
};
window.closeDateForm = function(e) {
if (e && e.target !== document.getElementById('dateFormBg')) return;
document.getElementById('dateFormBg').classList.remove('open');
};
window.closeDateFormBtn = function() {
document.getElementById('dateFormBg').classList.remove('open');
};
window.triggerImgPick = function() {
document.getElementById('dfImgInput').click();
};
window.onImgSelected = function(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
formImgData = e.target.result;
const preview = document.getElementById('dfImgPreview');
let imgTag = preview.querySelector('img');
if (!imgTag) { imgTag = document.createElement('img'); preview.appendChild(imgTag); }
imgTag.src = formImgData;
document.getElementById('dfImgPlaceholder').style.display = 'none';
document.getElementById('dfImgRemove').classList.add('visible');
};
reader.readAsDataURL(file);
event.target.value = '';
};
window.removeFormImg = function(e) {
e.stopPropagation();
formImgData = null;
resetFormImg();
};
function resetFormImg() {
const preview = document.getElementById('dfImgPreview');
const imgTag = preview.querySelector('img');
if (imgTag) imgTag.remove();
document.getElementById('dfImgPlaceholder').style.display = '';
document.getElementById('dfImgRemove').classList.remove('visible');
}
window.saveDate = async function() {
const title = document.getElementById('dfTitle').value.trim();
const desc = document.getElementById('dfDesc').value.trim();
const dtVal = document.getElementById('dfDateTime').value;
const saveBtn = document.getElementById('dfSaveBtn');
const hintEl = document.getElementById('dfLimitHint');
if (!title) { hintEl.textContent = 'Bitte Titel eingeben.'; return; }
if (!desc) { hintEl.textContent = 'Bitte Beschreibung eingeben.'; return; }
hintEl.textContent = '';
saveBtn.disabled = true;
const body = {
title,
description: desc,
imageData: formImgData || null,
scheduledAt: dtVal ? dtVal + ':00' : null
};
try {
const url = editingDateId ? '/dating/dates/' + editingDateId : '/dating/dates';
const method = editingDateId ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.status === 422) {
const data = await res.json().catch(() => ({}));
hintEl.textContent = data.message || 'Limit erreicht.';
return;
}
if (!res.ok) { hintEl.textContent = 'Fehler beim Speichern.'; return; }
closeDateFormBtn();
datesLoaded = false;
loadDates();
} catch {
hintEl.textContent = 'Fehler beim Speichern.';
} finally {
saveBtn.disabled = false;
}
};
// ── Profil-Mini-Dialog ────────────────────────────────────────────────────
let profileDialogCreatorId = null;
window.openProfileDialog = function() {
if (!currentDate) return;
loadAndOpenProfile(currentDate.creatorId, currentDate.creatorName, currentDate.creatorProfilePicture);
};
function renderVorliebenChips(container, chips) {
container.innerHTML = chips.map(c =>
`<span class="dp-vorliebe-chip bw-${c.bewertung}">${escHtml(c.name)}</span>`
).join('');
}
window.loadAndOpenProfile = async function(userId, name, picture) {
profileDialogCreatorId = userId;
pdImages = []; pdImageIdx = 0;
renderPopupImage('pd', pdImages, pdImageIdx);
document.getElementById('pdName').textContent = name || '';
document.getElementById('pdProfileLink').href = '/community/benutzer.html?userId=' + userId;
document.getElementById('pdTags').innerHTML = '';
document.getElementById('pdVorlieben').innerHTML = '';
document.getElementById('pdDesc').style.display = 'none';
document.getElementById('profileDialogBg').classList.add('open');
// Weitere Details nachladen
try {
const [profRes, vlRes, galleryRes] = await Promise.all([
fetch('/social/users/' + userId),
fetch('/dating/vorlieben/' + userId),
fetch('/social/profile-images?userId=' + userId)
]);
if (profRes.ok) {
const p = await profRes.json();
const tags = document.getElementById('pdTags');
const addTag = v => {
if (!v) return;
const s = document.createElement('span');
s.className = 'dp-tag';
s.textContent = v;
tags.appendChild(s);
};
if (p.alter) addTag(p.alter + ' J.');
if (p.geschlecht) addTag({ WEIBLICH:'weiblich', MAENNLICH:'männlich', DIVERS:'divers' }[p.geschlecht] || p.geschlecht);
if (p.neigung) addTag({ DEVOT:'devot', EHER_DEVOT:'eher devot', SWITCHER:'Switcher', EHER_DOMINANT:'eher dominant', DOMINANT:'dominant' }[p.neigung] || p.neigung);
if (p.beziehungsstatus) addTag({ SINGLE:'single', IN_EINER_BEZIEHUNG:'in einer Beziehung', VERHEIRATET:'verheiratet', IN_EINER_OFFENEN_BEZIEHUNG:'offene Beziehung', IN_EINER_OFFENEN_EHE:'offene Ehe' }[p.beziehungsstatus] || p.beziehungsstatus);
const descEl = document.getElementById('pdDesc');
if (p.beschreibung) { descEl.textContent = p.beschreibung; descEl.style.display = ''; }
const hqPic = p.profilePictureHq || p.profilePicture;
if (hqPic) pdImages.push('data:image/png;base64,' + hqPic);
}
if (galleryRes.ok) {
const gallery = await galleryRes.json();
gallery.forEach(img => { if (img.imageData) pdImages.push('data:image/jpeg;base64,' + img.imageData); });
}
renderPopupImage('pd', pdImages, pdImageIdx);
if (vlRes.ok) {
const chips = await vlRes.json();
renderVorliebenChips(document.getElementById('pdVorlieben'), chips);
}
} catch {}
};
window.closeProfileDialog = function(e) {
if (e && e.target !== document.getElementById('profileDialogBg')) return;
document.getElementById('profileDialogBg').classList.remove('open');
profileDialogCreatorId = null;
};
window.closeProfileDialogBtn = function() {
document.getElementById('profileDialogBg').classList.remove('open');
profileDialogCreatorId = null;
};
// ── Tastatur-Steuerung ────────────────────────────────────────────────────
document.addEventListener('keydown', e => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
const dir = e.key === 'ArrowLeft' ? -1 : 1;
if (document.getElementById('profileDialogBg').classList.contains('open')) {
pdNavStep(dir); return;
}
if (document.getElementById('discoveryPopupBg').classList.contains('open')) {
dpNavStep(dir); return;
}
// Pfeiltasten: Bilder-Navigation in der Match-Karte
if (document.getElementById('discoveryCard').style.display !== 'none' &&
!document.getElementById('filterDrawer').classList.contains('open')) {
discNavStep(dir, { stopPropagation: () => {} });
return;
}
}
// Escape schließt Dialoge (höchster z-index zuerst)
if (e.key !== 'Escape') return;
if (document.getElementById('profileDialogBg').classList.contains('open')) { closeProfileDialogBtn(); return; }
if (document.getElementById('matchOverlay').classList.contains('open')) { closeMatchOverlay(); return; }
if (document.getElementById('dateFormBg').classList.contains('open')) { closeDateFormBtn(); return; }
if (document.getElementById('dateBg').classList.contains('open')) { closeDateDialogBtn(); return; }
if (document.getElementById('myDateBg').classList.contains('open')) { closeMyDateDialogBtn(); return; }
if (document.getElementById('discoveryPopupBg').classList.contains('open')) { closeDiscoveryPopup(null); return; }
if (document.getElementById('filterDrawer').classList.contains('open')) { closeFilter(); return; }
});
// ── Datum formatieren ─────────────────────────────────────────────────────
function formatDateTime(isoStr) {
if (!isoStr) return '';
try {
const d = new Date(isoStr);
return d.toLocaleString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric', hour:'2-digit', minute:'2-digit' });
} catch { return isoStr; }
}
})();
</script>
</body>
</html>