Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
1517 lines
66 KiB
HTML
1517 lines
66 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<link rel="icon" href="/img/icon.png" type="image/png">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Einstellungen – xXx Sphere</title>
|
||
<link rel="stylesheet" href="/css/variables.css">
|
||
<link rel="stylesheet" href="/css/style.css">
|
||
<style>
|
||
.settings-page h1 {
|
||
font-size: 1.6rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
/* ── Accordion ── */
|
||
.settings-section {
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 10px;
|
||
margin-bottom: 0.75rem;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.settings-section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 0.75rem;
|
||
padding: 1rem 1.25rem;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
transition: background 0.15s;
|
||
}
|
||
|
||
.settings-section-header:hover {
|
||
background: rgba(255,255,255,0.03);
|
||
}
|
||
|
||
.settings-section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.6rem;
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.settings-section-arrow {
|
||
font-size: 0.75rem;
|
||
color: var(--color-muted);
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.settings-section.open .settings-section-arrow {
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
.settings-section-body {
|
||
display: none;
|
||
padding: 0 1.25rem 1.25rem;
|
||
border-top: 1px solid var(--color-secondary);
|
||
}
|
||
|
||
.settings-section.open .settings-section-body {
|
||
display: block;
|
||
}
|
||
|
||
/* ── Einstellungs-Zeilen ── */
|
||
.settings-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
padding: 0.9rem 0;
|
||
}
|
||
|
||
.settings-row-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.settings-row-label {
|
||
font-size: 0.95rem;
|
||
font-weight: 500;
|
||
margin-bottom: 0.15rem;
|
||
}
|
||
|
||
.settings-row-desc {
|
||
font-size: 0.8rem;
|
||
color: var(--color-muted);
|
||
}
|
||
|
||
.settings-row select {
|
||
width: auto;
|
||
min-width: 150px;
|
||
padding: 0.45rem 0.75rem;
|
||
margin: 0;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
/* ── Benachrichtigungen Grid ── */
|
||
.notif-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 5rem 5rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.notif-grid-row {
|
||
display: contents;
|
||
}
|
||
|
||
.notif-grid-row > * {
|
||
padding: 0.75rem 0;
|
||
}
|
||
|
||
.notif-grid-row.notif-header > * {
|
||
padding-bottom: 0.4rem;
|
||
}
|
||
|
||
.notif-col-check {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.no-spinner::-webkit-outer-spin-button,
|
||
.no-spinner::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
||
|
||
.notif-col-check input[type="checkbox"] {
|
||
width: 1.1rem;
|
||
height: 1.1rem;
|
||
accent-color: var(--color-primary, #c0392b);
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* ── Speichern-Feedback (Toast) ── */
|
||
.save-toast {
|
||
position: fixed;
|
||
bottom: 1.5rem;
|
||
left: 50%;
|
||
transform: translateX(-50%) translateY(2rem);
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-left: 3px solid var(--color-success, #4caf50);
|
||
border-radius: 8px;
|
||
padding: 0.6rem 1.25rem;
|
||
font-size: 0.88rem;
|
||
color: var(--color-success, #4caf50);
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.2s, transform 0.2s;
|
||
z-index: 500;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.save-toast.visible {
|
||
opacity: 1;
|
||
transform: translateX(-50%) translateY(0);
|
||
}
|
||
|
||
/* ── Spiel-Einstellungen Check-Items ── */
|
||
.spiel-check-group {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.4rem;
|
||
margin-top: 0.35rem;
|
||
}
|
||
.spiel-check-item {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
background: var(--color-secondary);
|
||
border: 1px solid transparent;
|
||
border-radius: 6px;
|
||
padding: 0.35rem 0.65rem;
|
||
cursor: pointer;
|
||
transition: border-color 0.15s;
|
||
user-select: none;
|
||
font-size: 0.88rem;
|
||
}
|
||
.spiel-check-item.is-checked { border-color: var(--color-primary); }
|
||
.spiel-check-item input {
|
||
accent-color: var(--color-primary);
|
||
width: auto;
|
||
cursor: pointer;
|
||
}
|
||
.spiel-subsection {
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
.spiel-subsection-title {
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
color: var(--color-text);
|
||
margin-bottom: 0.75rem;
|
||
padding-bottom: 0.4rem;
|
||
border-bottom: 1px solid var(--color-secondary);
|
||
}
|
||
.spiel-field { margin-bottom: 1rem; }
|
||
.spiel-field > .settings-row-label { margin-bottom: 0.2rem; }
|
||
|
||
/* ── Modal ── */
|
||
.modal-backdrop {
|
||
display: none;
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,0.6);
|
||
z-index: 200;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal-backdrop.visible { display: flex; }
|
||
|
||
.modal {
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 12px;
|
||
padding: 2rem;
|
||
width: 100%;
|
||
max-width: 360px;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
||
}
|
||
|
||
.modal h2 {
|
||
color: var(--color-primary);
|
||
font-size: 1.2rem;
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
|
||
.modal-actions {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
margin-top: 1.25rem;
|
||
}
|
||
|
||
.modal-actions button { flex: 1; margin-top: 0; }
|
||
|
||
/* ── Separator ── */
|
||
.settings-separator {
|
||
border: none;
|
||
border-top: 1px solid var(--color-secondary);
|
||
margin: 1.25rem 0 0;
|
||
}
|
||
|
||
/* ── Vorschau-Buttons ── */
|
||
.preview-row {
|
||
padding-top: 1.25rem;
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
|
||
.preview-row span {
|
||
font-size: 0.85rem;
|
||
color: var(--color-muted);
|
||
flex-basis: 100%;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.preview-row button {
|
||
width: auto;
|
||
padding: 0.5rem 1.1rem;
|
||
margin: 0;
|
||
font-size: 0.9rem;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="app">
|
||
<div class="main">
|
||
<div class="content">
|
||
<h1>⚙️ Einstellungen</h1>
|
||
|
||
<!-- Grunddaten -->
|
||
<div class="settings-section" id="sec-grunddaten">
|
||
<div class="settings-section-header" onclick="toggleSection('sec-grunddaten')">
|
||
<span class="settings-section-title">👤 Grunddaten</span>
|
||
<span class="settings-section-arrow">▸</span>
|
||
</div>
|
||
<div class="settings-section-body">
|
||
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">Nickname</div>
|
||
<div class="settings-row-desc" id="gd-name">…</div>
|
||
</div>
|
||
<button onclick="openNameDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;">Ändern</button>
|
||
</div>
|
||
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">E-Mail</div>
|
||
<div class="settings-row-desc" id="gd-email">…</div>
|
||
</div>
|
||
<button onclick="openEmailDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;">Ändern</button>
|
||
</div>
|
||
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">Geburtsdatum</div>
|
||
<div class="settings-row-desc" id="gd-geburtsdatum">…</div>
|
||
</div>
|
||
<button onclick="openGeburtsdatumDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;">Ändern</button>
|
||
</div>
|
||
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label" style="color:var(--color-primary);">Konto löschen</div>
|
||
<div class="settings-row-desc">Alle Daten werden unwiderruflich gelöscht</div>
|
||
</div>
|
||
<button onclick="openDeleteDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;background:transparent;border:1px solid var(--color-secondary);color:var(--color-muted);">Löschen</button>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Spiel Einstellungen -->
|
||
<div class="settings-section" id="sec-spiel">
|
||
<div class="settings-section-header" onclick="toggleSection('sec-spiel')">
|
||
<span class="settings-section-title">🕹️ Spiel Einstellungen</span>
|
||
<span class="settings-section-arrow">▸</span>
|
||
</div>
|
||
<div class="settings-section-body">
|
||
|
||
<!-- BDSM Game -->
|
||
<div class="spiel-subsection">
|
||
<div class="spiel-subsection-title">BDSM Game</div>
|
||
|
||
<div class="spiel-field">
|
||
<div class="settings-row-label">Spiele mit Geschlecht</div>
|
||
<div class="spiel-check-group" id="bdsm-spieltmit">
|
||
<label class="spiel-check-item"><input type="checkbox" value="WEIBLICH"> Weiblich</label>
|
||
<label class="spiel-check-item"><input type="checkbox" value="DIVERS"> Divers</label>
|
||
<label class="spiel-check-item"><input type="checkbox" value="MAENNLICH"> Männlich</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="spiel-field">
|
||
<div class="settings-row-label">Meine Rollen</div>
|
||
<div class="spiel-check-group" id="bdsm-rollen">
|
||
<label class="spiel-check-item"><input type="checkbox" value="AUFGABE_AKTIV"> Aufgaben aktiv</label>
|
||
<label class="spiel-check-item"><input type="checkbox" value="AUFGABE_PASSIV"> Aufgaben passiv</label>
|
||
<label class="spiel-check-item"><input type="checkbox" value="BESTRAFUNG_AKTIV"> Bestrafungen aktiv</label>
|
||
<label class="spiel-check-item"><input type="checkbox" value="BESTRAFUNG_PASSIV"> Bestrafungen passiv</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="spiel-field">
|
||
<div class="settings-row-label">Was ich einsetze</div>
|
||
<div class="spiel-check-group" id="bdsm-werkzeuge">
|
||
<label class="spiel-check-item"><input type="checkbox" value="MUND"> Mund</label>
|
||
<label class="spiel-check-item"><input type="checkbox" value="VAGINA"> Vagina</label>
|
||
<label class="spiel-check-item"><input type="checkbox" value="PENIS"> Penis</label>
|
||
<label class="spiel-check-item"><input type="checkbox" value="ANUS"> Anus</label>
|
||
<label class="spiel-check-item"><input type="checkbox" value="UMSCHNALLDILDO"> Umschnall-Dildo</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Benachrichtigungen -->
|
||
<div class="settings-section" id="sec-benachrichtigungen">
|
||
<div class="settings-section-header" onclick="toggleSection('sec-benachrichtigungen')">
|
||
<span class="settings-section-title">🔔 Benachrichtigungen</span>
|
||
<span class="settings-section-arrow">▸</span>
|
||
</div>
|
||
<div class="settings-section-body">
|
||
<div class="notif-grid">
|
||
|
||
<!-- Header -->
|
||
<div class="notif-grid-row notif-header">
|
||
<div></div>
|
||
<div class="notif-col-check settings-row-label">In-App</div>
|
||
<div class="notif-col-check settings-row-label">E-Mail</div>
|
||
</div>
|
||
|
||
<!-- Einladungen -->
|
||
<div class="notif-grid-row">
|
||
<div>
|
||
<div class="settings-row-label">Einladungen</div>
|
||
<div class="settings-row-desc">Einladungen zu Locks und Spielen, Annahmen und Ablehnungen</div>
|
||
</div>
|
||
<div class="notif-col-check">
|
||
<input type="checkbox" id="notif-INVITATION-inApp" onchange="saveNotifications()">
|
||
</div>
|
||
<div class="notif-col-check">
|
||
<input type="checkbox" id="notif-INVITATION-email" onchange="saveNotifications()">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Spielstatus -->
|
||
<div class="notif-grid-row">
|
||
<div>
|
||
<div class="settings-row-label">Spielstatus</div>
|
||
<div class="settings-row-desc">Karten, Aufgaben, Verifikationen, Einfrierungen und andere Spielereignisse</div>
|
||
</div>
|
||
<div class="notif-col-check">
|
||
<input type="checkbox" id="notif-GAME_STATE-inApp" onchange="saveNotifications()">
|
||
</div>
|
||
<div class="notif-col-check">
|
||
<input type="checkbox" id="notif-GAME_STATE-email" onchange="saveNotifications()">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Notfall -->
|
||
<div class="notif-grid-row">
|
||
<div>
|
||
<div class="settings-row-label">Notfall</div>
|
||
<div class="settings-row-desc">Notfall-Entsperrungen und dringende Meldungen</div>
|
||
</div>
|
||
<div class="notif-col-check">
|
||
<input type="checkbox" id="notif-EMERGENCY-inApp" onchange="saveNotifications()">
|
||
</div>
|
||
<div class="notif-col-check">
|
||
<input type="checkbox" id="notif-EMERGENCY-email" onchange="saveNotifications()">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Freundschaftsanfragen -->
|
||
<div class="notif-grid-row">
|
||
<div>
|
||
<div class="settings-row-label">Freundschaftsanfragen</div>
|
||
<div class="settings-row-desc">Neue Freundschaftsanfragen von anderen Nutzern</div>
|
||
</div>
|
||
<div class="notif-col-check">
|
||
<input type="checkbox" id="notif-FRIENDREQUEST-inApp" checked disabled
|
||
title="In-App-Benachrichtigungen für Freundschaftsanfragen sind immer aktiv">
|
||
</div>
|
||
<div class="notif-col-check">
|
||
<input type="checkbox" id="notif-FRIENDREQUEST-email" onchange="saveNotifications()">
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Abonnements -->
|
||
<div class="settings-section" id="sec-abonnements">
|
||
<div class="settings-section-header" onclick="toggleSection('sec-abonnements')">
|
||
<span class="settings-section-title">⭐ Abonnements</span>
|
||
<span class="settings-section-arrow">▸</span>
|
||
</div>
|
||
<div class="settings-section-body">
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">Aktuelles Abo</div>
|
||
<div class="settings-row-desc" id="abo-type-desc">Wird geladen…</div>
|
||
</div>
|
||
<span id="abo-type-badge" style="font-weight:600;"></span>
|
||
</div>
|
||
<div id="abo-details" style="display:none;">
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">Gültig bis</div>
|
||
</div>
|
||
<span id="abo-valid-until" style="font-size:0.9rem;"></span>
|
||
</div>
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">Kündbar ab</div>
|
||
</div>
|
||
<span id="abo-cancellable-from" style="font-size:0.9rem;"></span>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top:0.75rem;">
|
||
<a href="/community/abonnements.html" style="color:var(--color-primary);font-size:0.9rem;">
|
||
Abo-Modelle ansehen & Abo abschließen →
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Dating -->
|
||
<div class="settings-section" id="sec-dating">
|
||
<div class="settings-section-header" onclick="toggleSection('sec-dating')">
|
||
<span class="settings-section-title">♥ Dating</span>
|
||
<span class="settings-section-arrow">▸</span>
|
||
</div>
|
||
<div class="settings-section-body">
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">Dating aktivieren</div>
|
||
<div class="settings-row-desc">Zeige dein Profil im Dating-Bereich. Ein Standort ist erforderlich.</div>
|
||
</div>
|
||
<label style="display:flex;align-items:center;gap:0.5rem;cursor:pointer;">
|
||
<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="datingStadtRow" style="display:none;">
|
||
<div class="settings-row" style="flex-wrap:wrap;gap:0.5rem;">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">Standort (Stadt)</div>
|
||
<div class="settings-row-desc">Wird im Dating-Bereich angezeigt.</div>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:0.5rem;margin-bottom:0.35rem;position:relative;">
|
||
<div style="position:relative;flex:1;display:flex;">
|
||
<input type="text" id="datingStadt" placeholder="Stadt suchen und auswählen…" autocomplete="off"
|
||
style="flex:1;padding:0.55rem 2rem 0.55rem 0.8rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;"
|
||
oninput="onStadtInput()">
|
||
<button id="datingStadtClear" onclick="clearStadt()" title="Auswahl aufheben"
|
||
style="display:none;position:absolute;right:0.4rem;top:50%;transform:translateY(-50%);
|
||
background:none;border:none;color:var(--color-muted);font-size:1.1rem;cursor:pointer;padding:0.1rem 0.3rem;margin:0;line-height:1;">×</button>
|
||
</div>
|
||
<button onclick="detectLocation()" title="Standort ermitteln"
|
||
style="white-space:nowrap;padding:0.55rem 0.9rem;margin:0;font-size:0.85rem;">
|
||
⌖ Ermitteln
|
||
</button>
|
||
<ul id="stadtSuggestions" style="display:none;position:absolute;top:100%;left:0;right:3.5rem;
|
||
background:var(--color-card);border:1px solid var(--color-secondary);border-radius:6px;
|
||
z-index:100;list-style:none;margin:0.2rem 0 0;padding:0;max-height:200px;overflow-y:auto;"></ul>
|
||
</div>
|
||
<div id="datingLocMsg" style="font-size:0.82rem;color:var(--color-muted);margin-bottom:0.5rem;"></div>
|
||
</div>
|
||
<div class="settings-row" style="border-top:1px solid var(--color-secondary);padding-top:0.75rem;margin-top:0.25rem;">
|
||
<button onclick="saveDating()" id="saveDatingBtn" style="margin:0;padding:0.55rem 1.25rem;font-size:0.9rem;">Speichern</button>
|
||
<span id="datingMsg" style="font-size:0.85rem;"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Datenschutz -->
|
||
<div class="settings-section" id="sec-datenschutz">
|
||
<div class="settings-section-header" onclick="toggleSection('sec-datenschutz')">
|
||
<span class="settings-section-title">🛡️ Datenschutz</span>
|
||
<span class="settings-section-arrow">▸</span>
|
||
</div>
|
||
<div class="settings-section-body">
|
||
|
||
<!-- Grunddaten -->
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">Grunddaten</div>
|
||
<div class="settings-row-desc">Alter, Größe, Gewicht, Geschlecht, Neigung, Beziehungsstatus, Beschreibung</div>
|
||
</div>
|
||
<select id="sv-grunddaten" onchange="doSave()">
|
||
<option value="ALLE">Alle</option>
|
||
<option value="NUR_FREUNDE">Nur Freunde</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Galerie -->
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">Galerie</div>
|
||
<div class="settings-row-desc">Fotos auf dem Profil</div>
|
||
</div>
|
||
<select id="sv-galerie" onchange="doSave()">
|
||
<option value="ALLE">Alle</option>
|
||
<option value="NUR_FREUNDE">Nur Freunde</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Freunde -->
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">Freundesliste</div>
|
||
<div class="settings-row-desc">Wer kann sehen, wer deine Freunde sind</div>
|
||
</div>
|
||
<select id="sv-freunde" onchange="doSave()">
|
||
<option value="ALLE">Alle</option>
|
||
<option value="NUR_FREUNDE">Nur Freunde</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Feed -->
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">Feed / Posts</div>
|
||
<div class="settings-row-desc">Posts auf dem Profil-Tab</div>
|
||
</div>
|
||
<select id="sv-feed" onchange="doSave()">
|
||
<option value="ALLE">Alle</option>
|
||
<option value="NUR_FREUNDE">Nur Freunde</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Pinnwand -->
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">Pinnwand</div>
|
||
<div class="settings-row-desc">Einträge auf der Pinnwand</div>
|
||
</div>
|
||
<select id="sv-pinnwand" onchange="doSave()">
|
||
<option value="ALLE">Alle</option>
|
||
<option value="NUR_FREUNDE">Nur Freunde</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- XP -->
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">XP-Punkte</div>
|
||
<div class="settings-row-desc">Lockee-XP und Keyholder-XP</div>
|
||
</div>
|
||
<select id="sv-xp" onchange="doSave()">
|
||
<option value="ALLE">Alle</option>
|
||
<option value="NUR_FREUNDE">Nur Freunde</option>
|
||
<option value="NUR_ICH">Nur ich</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Lock-Historie -->
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">Lock-Historie</div>
|
||
<div class="settings-row-desc">Abgeschlossene Locks und Keyholder-Aktivitäten</div>
|
||
</div>
|
||
<select id="sv-lockhistorie" onchange="doSave()">
|
||
<option value="ALLE">Alle</option>
|
||
<option value="NUR_FREUNDE">Nur Freunde</option>
|
||
<option value="NUR_ICH">Nur ich</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Vorlieben -->
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">Vorlieben</div>
|
||
<div class="settings-row-desc">Deine Vorlieben und Bewertungen im Profil</div>
|
||
</div>
|
||
<select id="sv-vorlieben" onchange="doSave()">
|
||
<option value="ALLE">Alle</option>
|
||
<option value="NUR_FREUNDE">Nur Freunde</option>
|
||
<option value="NUR_ICH">Nur ich</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Profil bei Veröffentlichungen -->
|
||
<div class="settings-row">
|
||
<div class="settings-row-info">
|
||
<div class="settings-row-label">Profil bei Veröffentlichungen sichtbar</div>
|
||
<div class="settings-row-desc">Dein Name wird bei veröffentlichten Lock-Vorlagen angezeigt</div>
|
||
</div>
|
||
<select id="sv-veroeffentlichungen" onchange="doSave()">
|
||
<option value="true">Ja</option>
|
||
<option value="false">Nein</option>
|
||
</select>
|
||
</div>
|
||
|
||
<hr class="settings-separator">
|
||
|
||
<!-- Vorschau -->
|
||
<div class="preview-row">
|
||
<span>Profil-Vorschau – wie sieht mein Profil für andere aus?</span>
|
||
<button onclick="openPreview('FREUND')">👥 Vorschau als Freund</button>
|
||
<button onclick="openPreview('UNBEKANNT')" style="background:var(--color-secondary);color:var(--color-text);">👤 Vorschau als Fremder</button>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TTLock -->
|
||
<div class="settings-section" id="sec-ttlock">
|
||
<div class="settings-section-header" onclick="toggleSection('sec-ttlock')">
|
||
<span class="settings-section-title">🔒 TTLock</span>
|
||
<span class="settings-section-arrow">▸</span>
|
||
</div>
|
||
<div class="settings-section-body">
|
||
<p style="font-size:0.85rem;color:var(--color-muted);margin:0.75rem 0 1.25rem 0;">
|
||
Verknüpfe deinen TTLock-Account, um deine physische Schlüsselbox direkt über das Spiel zu steuern.
|
||
</p>
|
||
|
||
<div style="margin-top:1rem;font-size:0.82rem;">
|
||
<a href="/help/ttlock.html" style="color:var(--color-muted);">❓ Hilfe zur TTLock-Integration</a>
|
||
</div>
|
||
|
||
<div class="spiel-field">
|
||
<div class="settings-row-label">Benutzername (E-Mail)</div>
|
||
<input type="text" id="ttl-username" placeholder="E-Mail-Adresse bei TTLock"
|
||
style="margin-top:0.3rem;" autocomplete="off">
|
||
</div>
|
||
|
||
<div class="spiel-field">
|
||
<div class="settings-row-label">Passwort <span id="ttl-pw-hint" style="font-size:0.78rem;color:var(--color-muted);font-weight:400;"></span></div>
|
||
<input type="password" id="ttl-password" placeholder="Leer lassen = unverändert"
|
||
style="margin-top:0.3rem;" autocomplete="new-password"
|
||
onfocus="ttlPwFocus()" onblur="ttlPwBlur()">
|
||
</div>
|
||
|
||
<div class="spiel-field">
|
||
<div class="settings-row-label">Lock-ID</div>
|
||
<input type="number" id="ttl-lockid" placeholder="z.B. 30158446"
|
||
style="margin-top:0.3rem;max-width:220px;-moz-appearance:textfield;" min="0"
|
||
class="no-spinner">
|
||
</div>
|
||
|
||
<div id="ttl-error" style="font-size:0.82rem;color:var(--color-primary);min-height:1.1em;margin-bottom:0.5rem;"></div>
|
||
<!-- Verbindungsstatus -->
|
||
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.75rem;">
|
||
<span style="font-size:0.82rem;color:var(--color-muted);">Verbindungsstatus:</span>
|
||
<span id="ttl-test-status" style="font-size:0.82rem;font-weight:600;"></span>
|
||
</div>
|
||
|
||
<div style="display:flex;gap:0.6rem;flex-wrap:wrap;align-items:center;">
|
||
<button onclick="saveTtlockUserConfig()" style="width:auto;padding:0.5rem 1.25rem;margin:0;">Speichern</button>
|
||
<button onclick="testTtlockConnection()" id="ttl-test-btn" style="width:auto;padding:0.5rem 1.25rem;margin:0;background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">🔌 Verbindung testen</button>
|
||
<button onclick="ttlockOpenOnce()" id="ttl-open-btn" style="width:auto;padding:0.5rem 1.25rem;margin:0;background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">🔓 Öffnen</button>
|
||
</div>
|
||
|
||
<!-- Test-Ergebnis -->
|
||
<div id="ttl-test-result" style="display:none;margin-top:1rem;padding:0.85rem 1rem;border-radius:8px;border:1px solid var(--color-secondary);font-size:0.85rem;"></div>
|
||
|
||
<!-- Öffnen-Ergebnis -->
|
||
<div id="ttl-open-error" style="font-size:0.82rem;color:var(--color-primary);min-height:1.1em;margin-top:0.5rem;"></div>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TTLock Lade-Dialog -->
|
||
<div class="modal-backdrop" id="ttlLoadingModal">
|
||
<div class="modal" style="max-width:320px;text-align:center;padding:2rem 1.5rem;">
|
||
<div style="font-size:2rem;margin-bottom:0.75rem;">⏳</div>
|
||
<div style="font-weight:600;margin-bottom:0.4rem;">TTLock-Kommunikation läuft…</div>
|
||
<div style="font-size:0.85rem;color:var(--color-muted);">Bitte warten, der TTLock-Server wird kontaktiert.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TTLock Öffnen Modal -->
|
||
<div class="modal-backdrop" id="ttlOpenModal">
|
||
<div class="modal" style="max-width:380px;text-align:center;">
|
||
<h2>🔓 Schloss öffnen</h2>
|
||
<p style="color:var(--color-muted);font-size:0.85rem;margin-bottom:1rem;">Gib diesen PIN an deinem TTLock-Schloss ein:</p>
|
||
<div id="ttl-open-pin" style="font-size:2.2rem;font-weight:700;letter-spacing:0.25em;color:var(--color-primary);margin:0.5rem 0 1.5rem 0;"></div>
|
||
<p style="font-size:0.8rem;color:var(--color-muted);margin-bottom:1.25rem;">Nach „OK" wird dieser Code gelöscht und kann nicht mehr verwendet werden.</p>
|
||
<div class="modal-actions" style="justify-content:center;">
|
||
<button id="ttl-open-ok-btn" class="btn-save" style="min-width:120px;">OK</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Nickname Modal -->
|
||
<div class="modal-backdrop" id="nameModal">
|
||
<div class="modal">
|
||
<h2>Nickname ändern</h2>
|
||
<label for="newName">Neuer Nickname</label>
|
||
<input type="text" id="newName" placeholder="Neuer Name" autocomplete="off">
|
||
<div class="message" id="nameMessage"></div>
|
||
<div class="modal-actions">
|
||
<button class="secondary" onclick="closeNameDialog()">Abbrechen</button>
|
||
<button id="nameConfirmBtn" onclick="saveName()">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Geburtsdatum Modal -->
|
||
<div class="modal-backdrop" id="geburtsdatumModal">
|
||
<div class="modal">
|
||
<h2>Geburtsdatum ändern</h2>
|
||
<label for="newGeburtsdatum">Neues Geburtsdatum</label>
|
||
<input type="date" id="newGeburtsdatum" autocomplete="bday">
|
||
<div class="message" id="geburtsdatumMessage"></div>
|
||
<div class="modal-actions">
|
||
<button class="secondary" onclick="closeGeburtsdatumDialog()">Abbrechen</button>
|
||
<button id="geburtsdatumConfirmBtn" onclick="saveGeburtsdatum()">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- E-Mail Modal -->
|
||
<div class="modal-backdrop" id="emailModal">
|
||
<div class="modal">
|
||
<h2>E-Mail-Adresse ändern</h2>
|
||
<label for="newEmail">Neue E-Mail-Adresse</label>
|
||
<input type="email" id="newEmail" placeholder="neue@email.de" autocomplete="off">
|
||
<div class="message" id="emailMessage"></div>
|
||
<div class="modal-actions">
|
||
<button class="secondary" onclick="closeEmailDialog()">Abbrechen</button>
|
||
<button id="emailConfirmBtn" onclick="requestEmailChange()">Bestätigungsmail senden</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Konto löschen Modal -->
|
||
<div class="modal-backdrop" id="deleteModal">
|
||
<div class="modal">
|
||
<h2>Konto löschen</h2>
|
||
<p style="color:var(--color-muted);font-size:0.9rem;margin-bottom:0.75rem;">
|
||
Dein Konto sowie alle gespeicherten Aufgaben und Toys werden unwiderruflich gelöscht.
|
||
</p>
|
||
<p style="color:var(--color-primary);font-size:0.85rem;">Dieser Vorgang kann nicht rückgängig gemacht werden.</p>
|
||
<div class="message" id="deleteMessage"></div>
|
||
<div class="modal-actions">
|
||
<button class="secondary" onclick="closeDeleteDialog()">Abbrechen</button>
|
||
<button id="deleteConfirmBtn" onclick="deleteAccount()" style="background:#c0392b;">Konto löschen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="save-toast" id="saveToast">✓ Gespeichert</div>
|
||
|
||
<script src="/js/icons.js"></script>
|
||
<script src="/js/sidebar.js"></script>
|
||
<script src="/js/social-sidebar.js"></script>
|
||
<script>
|
||
let myUserId = null;
|
||
let toastTimer = null;
|
||
|
||
function toggleSection(id) {
|
||
const target = document.getElementById(id);
|
||
const isOpen = target.classList.contains('open');
|
||
document.querySelectorAll('.settings-section').forEach(s => s.classList.remove('open'));
|
||
if (!isOpen) target.classList.add('open');
|
||
}
|
||
|
||
// ── Grunddaten laden ──
|
||
async function loadGrunddaten() {
|
||
const res = await fetch('/login/me');
|
||
if (res.status === 401) { window.location.href = '/login.html'; return; }
|
||
if (!res.ok) return;
|
||
const user = await res.json();
|
||
myUserId = user.userId;
|
||
document.getElementById('gd-name').textContent = user.name || '—';
|
||
document.getElementById('gd-email').textContent = user.email || '—';
|
||
document.getElementById('gd-geburtsdatum').textContent = formatGeburtsdatum(user.geburtsdatum);
|
||
}
|
||
|
||
function formatGeburtsdatum(iso) {
|
||
if (!iso) return '—';
|
||
const [y, m, d] = iso.split('-');
|
||
const today = new Date();
|
||
const birth = new Date(iso);
|
||
const age = today.getFullYear() - birth.getFullYear()
|
||
- (today < new Date(today.getFullYear(), birth.getMonth(), birth.getDate()) ? 1 : 0);
|
||
return `${d}.${m}.${y} (${age} Jahre)`;
|
||
}
|
||
|
||
// ── Geburtsdatum Dialog ──
|
||
function openGeburtsdatumDialog() {
|
||
document.getElementById('newGeburtsdatum').value = '';
|
||
hideModalMessage('geburtsdatumMessage');
|
||
document.getElementById('geburtsdatumModal').classList.add('visible');
|
||
document.getElementById('newGeburtsdatum').focus();
|
||
}
|
||
|
||
function closeGeburtsdatumDialog() {
|
||
document.getElementById('geburtsdatumModal').classList.remove('visible');
|
||
}
|
||
|
||
async function saveGeburtsdatum() {
|
||
const val = document.getElementById('newGeburtsdatum').value;
|
||
if (!val) { showModalMessage('geburtsdatumMessage', 'Bitte ein Datum eingeben.', 'error'); return; }
|
||
const today = new Date();
|
||
const birth = new Date(val);
|
||
const age = today.getFullYear() - birth.getFullYear()
|
||
- (today < new Date(today.getFullYear(), birth.getMonth(), birth.getDate()) ? 1 : 0);
|
||
if (age < 18) {
|
||
showModalMessage('geburtsdatumMessage', 'Du musst mindestens 18 Jahre alt sein.', 'error');
|
||
return;
|
||
}
|
||
const btn = document.getElementById('geburtsdatumConfirmBtn');
|
||
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
|
||
hideModalMessage('geburtsdatumMessage');
|
||
try {
|
||
const res = await fetch('/user/me/geburtsdatum', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ geburtsdatum: val })
|
||
});
|
||
if (res.ok) {
|
||
document.getElementById('gd-geburtsdatum').textContent = formatGeburtsdatum(val);
|
||
closeGeburtsdatumDialog();
|
||
showToast();
|
||
} else if (res.status === 422) {
|
||
showModalMessage('geburtsdatumMessage', 'Du musst mindestens 18 Jahre alt sein.', 'error');
|
||
} else {
|
||
showModalMessage('geburtsdatumMessage', `Fehler: HTTP ${res.status}`, 'error');
|
||
}
|
||
} catch (err) {
|
||
showModalMessage('geburtsdatumMessage', 'Server nicht erreichbar.', 'error');
|
||
console.error(err);
|
||
} finally {
|
||
btn.disabled = false; btn.textContent = 'Speichern';
|
||
}
|
||
}
|
||
|
||
// ── Nickname Dialog ──
|
||
function openNameDialog() {
|
||
document.getElementById('newName').value = '';
|
||
hideModalMessage('nameMessage');
|
||
document.getElementById('nameModal').classList.add('visible');
|
||
document.getElementById('newName').focus();
|
||
}
|
||
|
||
function closeNameDialog() {
|
||
document.getElementById('nameModal').classList.remove('visible');
|
||
}
|
||
|
||
async function saveName() {
|
||
const newName = document.getElementById('newName').value.trim();
|
||
if (!newName) { showModalMessage('nameMessage', 'Bitte einen Namen eingeben.', 'error'); return; }
|
||
const btn = document.getElementById('nameConfirmBtn');
|
||
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
|
||
hideModalMessage('nameMessage');
|
||
try {
|
||
const res = await fetch('/user/me/name', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name: newName })
|
||
});
|
||
if (res.ok) {
|
||
document.getElementById('gd-name').textContent = newName;
|
||
closeNameDialog();
|
||
showToast();
|
||
} else if (res.status === 409) {
|
||
showModalMessage('nameMessage', 'Dieser Nickname ist bereits vergeben.', 'error');
|
||
} else {
|
||
showModalMessage('nameMessage', `Fehler: HTTP ${res.status}`, 'error');
|
||
}
|
||
} catch (err) {
|
||
showModalMessage('nameMessage', 'Server nicht erreichbar.', 'error');
|
||
console.error(err);
|
||
} finally {
|
||
btn.disabled = false; btn.textContent = 'Speichern';
|
||
}
|
||
}
|
||
|
||
// ── E-Mail Dialog ──
|
||
function openEmailDialog() {
|
||
document.getElementById('newEmail').value = '';
|
||
hideModalMessage('emailMessage');
|
||
document.getElementById('emailModal').classList.add('visible');
|
||
document.getElementById('newEmail').focus();
|
||
}
|
||
|
||
function closeEmailDialog() {
|
||
document.getElementById('emailModal').classList.remove('visible');
|
||
}
|
||
|
||
async function requestEmailChange() {
|
||
const newEmail = document.getElementById('newEmail').value.trim();
|
||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) {
|
||
showModalMessage('emailMessage', 'Bitte eine gültige E-Mail-Adresse eingeben.', 'error');
|
||
return;
|
||
}
|
||
const btn = document.getElementById('emailConfirmBtn');
|
||
btn.disabled = true; btn.textContent = 'Wird gesendet…';
|
||
hideModalMessage('emailMessage');
|
||
try {
|
||
const res = await fetch('/email-change', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ newEmail })
|
||
});
|
||
if (res.status === 202) {
|
||
showModalMessage('emailMessage',
|
||
'Bestätigungsmail wurde an die neue Adresse gesendet. Bitte bestätige die Änderung über den Link in der E-Mail.',
|
||
'success');
|
||
btn.disabled = true; btn.textContent = 'Gesendet';
|
||
} else if (res.status === 409) {
|
||
showModalMessage('emailMessage', 'Diese E-Mail-Adresse ist bereits vergeben.', 'error');
|
||
btn.disabled = false; btn.textContent = 'Bestätigungsmail senden';
|
||
} else {
|
||
showModalMessage('emailMessage', `Fehler: HTTP ${res.status}`, 'error');
|
||
btn.disabled = false; btn.textContent = 'Bestätigungsmail senden';
|
||
}
|
||
} catch (err) {
|
||
showModalMessage('emailMessage', 'Server nicht erreichbar.', 'error');
|
||
btn.disabled = false; btn.textContent = 'Bestätigungsmail senden';
|
||
console.error(err);
|
||
}
|
||
}
|
||
|
||
// ── Konto löschen Dialog ──
|
||
function openDeleteDialog() {
|
||
hideModalMessage('deleteMessage');
|
||
document.getElementById('deleteConfirmBtn').disabled = false;
|
||
document.getElementById('deleteConfirmBtn').textContent = 'Konto löschen';
|
||
document.getElementById('deleteModal').classList.add('visible');
|
||
}
|
||
|
||
function closeDeleteDialog() {
|
||
document.getElementById('deleteModal').classList.remove('visible');
|
||
}
|
||
|
||
async function deleteAccount() {
|
||
const btn = document.getElementById('deleteConfirmBtn');
|
||
btn.disabled = true; btn.textContent = 'Wird gelöscht…';
|
||
hideModalMessage('deleteMessage');
|
||
try {
|
||
const res = await fetch('/user/me', { method: 'DELETE' });
|
||
if (res.ok) {
|
||
window.location.href = '/login.html?accountDeleted=1';
|
||
} else {
|
||
showModalMessage('deleteMessage', `Fehler: HTTP ${res.status}`, 'error');
|
||
btn.disabled = false; btn.textContent = 'Konto löschen';
|
||
}
|
||
} catch (err) {
|
||
showModalMessage('deleteMessage', 'Server nicht erreichbar.', 'error');
|
||
btn.disabled = false; btn.textContent = 'Konto löschen';
|
||
console.error(err);
|
||
}
|
||
}
|
||
|
||
function showModalMessage(id, text, type) {
|
||
const el = document.getElementById(id);
|
||
el.textContent = text;
|
||
el.className = `message ${type}`;
|
||
el.style.display = 'block';
|
||
}
|
||
|
||
function hideModalMessage(id) {
|
||
document.getElementById(id).style.display = 'none';
|
||
}
|
||
|
||
// Modal-Backdrop-Klick schließt Modals
|
||
['nameModal','geburtsdatumModal','emailModal','deleteModal'].forEach(id => {
|
||
document.getElementById(id).addEventListener('click', e => {
|
||
if (e.target === document.getElementById(id)) document.getElementById(id).classList.remove('visible');
|
||
});
|
||
});
|
||
document.getElementById('newName').addEventListener('keydown', e => { if (e.key === 'Enter') saveName(); });
|
||
document.getElementById('newEmail').addEventListener('keydown', e => { if (e.key === 'Enter') requestEmailChange(); });
|
||
|
||
// ── Datenschutz laden ──
|
||
async function loadPrivacy() {
|
||
const meRes = await fetch('/login/me');
|
||
if (!meRes.ok) return;
|
||
const me = await meRes.json();
|
||
myUserId = me.userId;
|
||
|
||
const profRes = await fetch('/social/users/' + myUserId);
|
||
if (!profRes.ok) return;
|
||
const profile = await profRes.json();
|
||
|
||
setValue('sv-grunddaten', profile.sichtbarkeitGrunddaten || 'ALLE');
|
||
setValue('sv-galerie', profile.sichtbarkeitGalerie || 'ALLE');
|
||
setValue('sv-freunde', profile.sichtbarkeitFreunde || 'ALLE');
|
||
setValue('sv-feed', profile.sichtbarkeitFeed || 'ALLE');
|
||
setValue('sv-pinnwand', profile.sichtbarkeitPinnwand || 'ALLE');
|
||
setValue('sv-xp', profile.sichtbarkeitXp || 'ALLE');
|
||
setValue('sv-lockhistorie', profile.sichtbarkeitLockhistorie || 'ALLE');
|
||
setValue('sv-vorlieben', profile.sichtbarkeitVorlieben || 'ALLE');
|
||
setValue('sv-veroeffentlichungen', profile.profilBeiVeroeffentlichungenSichtbar ? 'true' : 'false');
|
||
}
|
||
|
||
function setValue(id, value) {
|
||
const el = document.getElementById(id);
|
||
if (el) el.value = value;
|
||
}
|
||
|
||
async function doSave() {
|
||
const body = {
|
||
sichtbarkeitGrunddaten: document.getElementById('sv-grunddaten').value,
|
||
sichtbarkeitGalerie: document.getElementById('sv-galerie').value,
|
||
sichtbarkeitFreunde: document.getElementById('sv-freunde').value,
|
||
sichtbarkeitFeed: document.getElementById('sv-feed').value,
|
||
sichtbarkeitPinnwand: document.getElementById('sv-pinnwand').value,
|
||
sichtbarkeitXp: document.getElementById('sv-xp').value,
|
||
sichtbarkeitLockhistorie: document.getElementById('sv-lockhistorie').value,
|
||
sichtbarkeitVorlieben: document.getElementById('sv-vorlieben').value,
|
||
profilBeiVeroeffentlichungenSichtbar: document.getElementById('sv-veroeffentlichungen').value === 'true',
|
||
};
|
||
const res = await fetch('/user/me/privacy', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body)
|
||
});
|
||
if (res.ok) showToast();
|
||
}
|
||
|
||
function showToast() {
|
||
const t = document.getElementById('saveToast');
|
||
t.classList.add('visible');
|
||
clearTimeout(toastTimer);
|
||
toastTimer = setTimeout(() => t.classList.remove('visible'), 2200);
|
||
}
|
||
|
||
function openPreview(mode) {
|
||
if (!myUserId) return;
|
||
window.open('/community/benutzer.html?userId=' + myUserId + '&preview=' + mode, '_blank');
|
||
}
|
||
|
||
// ── Benachrichtigungen laden ──
|
||
async function loadNotifications() {
|
||
const res = await fetch('/user/me/notifications');
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
for (const cause of ['INVITATION', 'GAME_STATE', 'EMERGENCY']) {
|
||
const pref = data[cause] || { inApp: true, email: false };
|
||
const inApp = document.getElementById('notif-' + cause + '-inApp');
|
||
const email = document.getElementById('notif-' + cause + '-email');
|
||
if (inApp) inApp.checked = pref.inApp;
|
||
if (email) email.checked = pref.email;
|
||
}
|
||
}
|
||
|
||
async function saveNotifications() {
|
||
const body = {};
|
||
for (const cause of ['INVITATION', 'GAME_STATE', 'EMERGENCY']) {
|
||
const inApp = document.getElementById('notif-' + cause + '-inApp');
|
||
const email = document.getElementById('notif-' + cause + '-email');
|
||
body[cause] = {
|
||
inApp: inApp ? inApp.checked : true,
|
||
email: email ? email.checked : false
|
||
};
|
||
}
|
||
const res = await fetch('/user/me/notifications', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body)
|
||
});
|
||
if (res.ok) showToast();
|
||
}
|
||
|
||
// Hash-Navigation: Sektion anhand URL-Fragment öffnen
|
||
function openSectionFromHash() {
|
||
const hash = window.location.hash.replace('#', '');
|
||
if (hash) {
|
||
const el = document.getElementById(hash);
|
||
if (el) el.classList.add('open');
|
||
}
|
||
}
|
||
|
||
// ── Spiel Einstellungen – BDSM ──
|
||
function applyCheckItems(groupId, values) {
|
||
document.querySelectorAll(`#${groupId} .spiel-check-item input`).forEach(cb => {
|
||
const checked = values.includes(cb.value);
|
||
cb.checked = checked;
|
||
cb.closest('.spiel-check-item').classList.toggle('is-checked', checked);
|
||
});
|
||
}
|
||
|
||
document.querySelectorAll('.spiel-check-item input').forEach(cb => {
|
||
cb.addEventListener('change', () => {
|
||
cb.closest('.spiel-check-item').classList.toggle('is-checked', cb.checked);
|
||
saveBdsmDefaults();
|
||
});
|
||
});
|
||
|
||
async function loadBdsmDefaults() {
|
||
const res = await fetch('/user/me/bdsm-defaults');
|
||
if (!res.ok) return;
|
||
const d = await res.json();
|
||
applyCheckItems('bdsm-spieltmit', d.spieltMit || []);
|
||
applyCheckItems('bdsm-rollen', d.rollen || []);
|
||
applyCheckItems('bdsm-werkzeuge', d.werkzeuge || []);
|
||
}
|
||
|
||
async function saveBdsmDefaults() {
|
||
const get = groupId => [...document.querySelectorAll(`#${groupId} input:checked`)].map(cb => cb.value);
|
||
await fetch('/user/me/bdsm-defaults', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
spieltMit: get('bdsm-spieltmit'),
|
||
rollen: get('bdsm-rollen'),
|
||
werkzeuge: get('bdsm-werkzeuge'),
|
||
})
|
||
});
|
||
showToast();
|
||
}
|
||
|
||
// ── Abonnement laden ──
|
||
async function loadSubscription() {
|
||
const res = await fetch('/subscription/me');
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
const type = data.subscriptionType || 'STANDARD';
|
||
const labels = { STANDARD: 'Standard', PREMIUM: 'Premium' };
|
||
document.getElementById('abo-type-badge').textContent = labels[type] || type;
|
||
if (type === 'STANDARD') {
|
||
document.getElementById('abo-type-desc').textContent =
|
||
'Kein kostenpflichtiges Abo aktiv · bis zu 6 Lock-Vorlagen, 6 Aufgabengruppen (je 50 Aufgaben), 10 Toys';
|
||
} else {
|
||
document.getElementById('abo-type-desc').textContent = 'Aktives Abo';
|
||
document.getElementById('abo-details').style.display = '';
|
||
const fmt = d => d ? new Date(d).toLocaleDateString('de-DE') : '–';
|
||
document.getElementById('abo-valid-until').textContent = fmt(data.validUntil);
|
||
document.getElementById('abo-cancellable-from').textContent = fmt(data.cancellableFrom);
|
||
}
|
||
}
|
||
|
||
loadGrunddaten();
|
||
loadPrivacy();
|
||
loadNotifications();
|
||
loadBdsmDefaults();
|
||
loadSubscription();
|
||
loadTtlockUserConfig();
|
||
loadDating();
|
||
openSectionFromHash();
|
||
|
||
// ── Dating ──────────────────────────────────────────────────────────────
|
||
|
||
async function loadDating() {
|
||
const res = await fetch('/login/me');
|
||
if (!res.ok) return;
|
||
const user = await res.json();
|
||
document.getElementById('datingAktiv').checked = !!user.datingAktiv;
|
||
if (user.datingStadt) {
|
||
const input = document.getElementById('datingStadt');
|
||
input.value = user.datingStadt;
|
||
input.readOnly = true;
|
||
document.getElementById('datingStadtClear').style.display = '';
|
||
}
|
||
if (user.datingLat != null) _datingLat = user.datingLat;
|
||
if (user.datingLon != null) _datingLon = user.datingLon;
|
||
document.getElementById('datingStadtRow').style.display = user.datingAktiv ? '' : 'none';
|
||
}
|
||
|
||
function onDatingToggle() {
|
||
const aktiv = document.getElementById('datingAktiv').checked;
|
||
document.getElementById('datingStadtRow').style.display = aktiv ? '' : 'none';
|
||
}
|
||
|
||
let _stadtSuggestTimer = null;
|
||
let _datingLat = null;
|
||
let _datingLon = null;
|
||
|
||
function onStadtInput() {
|
||
const q = document.getElementById('datingStadt').value.trim();
|
||
_datingLat = null;
|
||
_datingLon = null;
|
||
document.getElementById('datingStadtClear').style.display = 'none';
|
||
clearTimeout(_stadtSuggestTimer);
|
||
if (q.length < 2) { hideSuggestions(); return; }
|
||
_stadtSuggestTimer = setTimeout(() => fetchStadtSuggestions(q), 300);
|
||
}
|
||
|
||
async function fetchStadtSuggestions(q) {
|
||
try {
|
||
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=json&addressdetails=1&limit=5&featuretype=city`;
|
||
const res = await fetch(url, { headers: { 'Accept-Language': 'de' } });
|
||
if (!res.ok) return;
|
||
const results = await res.json();
|
||
const ul = document.getElementById('stadtSuggestions');
|
||
if (!results.length) { hideSuggestions(); return; }
|
||
ul.innerHTML = results.map(r => {
|
||
const city = r.address.city || r.address.town || r.address.village || r.address.county || r.name;
|
||
const country = r.address.country || '';
|
||
const label = city + (country ? ', ' + country : '');
|
||
return `<li style="padding:0.5rem 0.75rem;cursor:pointer;font-size:0.9rem;border-bottom:1px solid var(--color-secondary);"
|
||
onmousedown="selectStadt(event, '${label.replace(/'/g, "\\'")}', ${parseFloat(r.lat)}, ${parseFloat(r.lon)})">${label}</li>`;
|
||
}).join('');
|
||
ul.style.display = '';
|
||
} catch (_) { hideSuggestions(); }
|
||
}
|
||
|
||
function selectStadt(e, label, lat, lon) {
|
||
e.preventDefault();
|
||
const input = document.getElementById('datingStadt');
|
||
input.value = label;
|
||
input.readOnly = true;
|
||
_datingLat = lat;
|
||
_datingLon = lon;
|
||
document.getElementById('datingStadtClear').style.display = '';
|
||
hideSuggestions();
|
||
}
|
||
|
||
function clearStadt() {
|
||
const input = document.getElementById('datingStadt');
|
||
input.value = '';
|
||
input.readOnly = false;
|
||
_datingLat = null;
|
||
_datingLon = null;
|
||
document.getElementById('datingStadtClear').style.display = 'none';
|
||
input.focus();
|
||
}
|
||
|
||
function hideSuggestions() {
|
||
document.getElementById('stadtSuggestions').style.display = 'none';
|
||
}
|
||
|
||
document.addEventListener('click', e => {
|
||
if (!e.target.closest('#datingStadtRow')) hideSuggestions();
|
||
});
|
||
|
||
async function detectLocation() {
|
||
const msgEl = document.getElementById('datingLocMsg');
|
||
if (!navigator.geolocation) { msgEl.textContent = 'Geolocation nicht unterstützt.'; return; }
|
||
msgEl.textContent = 'Standort wird ermittelt…';
|
||
navigator.geolocation.getCurrentPosition(async pos => {
|
||
try {
|
||
const { latitude, longitude } = pos.coords;
|
||
const res = await fetch(
|
||
`https://nominatim.openstreetmap.org/reverse?lat=${latitude}&lon=${longitude}&format=json&addressdetails=1`,
|
||
{ headers: { 'Accept-Language': 'de' } }
|
||
);
|
||
if (!res.ok) throw new Error();
|
||
const data = await res.json();
|
||
const city = data.address.city || data.address.town || data.address.village || data.address.county || '';
|
||
const country = data.address.country || '';
|
||
const input = document.getElementById('datingStadt');
|
||
input.value = city + (country ? ', ' + country : '');
|
||
input.readOnly = true;
|
||
_datingLat = latitude;
|
||
_datingLon = longitude;
|
||
document.getElementById('datingStadtClear').style.display = '';
|
||
msgEl.textContent = '';
|
||
} catch (_) { msgEl.textContent = 'Standort konnte nicht ermittelt werden.'; }
|
||
}, () => { msgEl.textContent = 'Zugriff auf Standort verweigert.'; });
|
||
}
|
||
|
||
async function saveDating() {
|
||
const aktiv = document.getElementById('datingAktiv').checked;
|
||
const stadt = document.getElementById('datingStadt').value.trim();
|
||
const msgEl = document.getElementById('datingMsg');
|
||
if (aktiv && (!stadt || _datingLat == null || _datingLon == null)) {
|
||
msgEl.textContent = 'Bitte eine Stadt aus der Liste auswählen oder per GPS ermitteln.';
|
||
msgEl.style.color = 'var(--color-primary)';
|
||
return;
|
||
}
|
||
const btn = document.getElementById('saveDatingBtn');
|
||
btn.disabled = true;
|
||
try {
|
||
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 })
|
||
});
|
||
if (res.ok) {
|
||
showToast();
|
||
msgEl.textContent = '';
|
||
} else {
|
||
msgEl.textContent = 'Fehler beim Speichern.';
|
||
msgEl.style.color = 'var(--color-primary)';
|
||
}
|
||
} catch (_) {
|
||
msgEl.textContent = 'Server nicht erreichbar.';
|
||
msgEl.style.color = 'var(--color-primary)';
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// ── TTLock ──────────────────────────────────────────────────────────────
|
||
|
||
let _ttlPasswordSet = false;
|
||
const TTL_PW_DOTS = '••••••';
|
||
|
||
async function loadTtlockUserConfig() {
|
||
const r = await fetch('/user/me/ttlock');
|
||
if (!r.ok) return;
|
||
const cfg = await r.json();
|
||
document.getElementById('ttl-username').value = cfg.username || '';
|
||
document.getElementById('ttl-lockid').value = cfg.lockId != null ? cfg.lockId : '';
|
||
_ttlPasswordSet = !!cfg.passwordSet;
|
||
const pwField = document.getElementById('ttl-password');
|
||
if (_ttlPasswordSet) {
|
||
pwField.value = TTL_PW_DOTS;
|
||
document.getElementById('ttl-pw-hint').textContent = '';
|
||
} else {
|
||
pwField.value = '';
|
||
document.getElementById('ttl-pw-hint').textContent = '(noch nicht gesetzt)';
|
||
}
|
||
renderTtlockTestStatus(cfg.testSuccessful);
|
||
}
|
||
|
||
function renderTtlockTestStatus(ok) {
|
||
const el = document.getElementById('ttl-test-status');
|
||
if (ok) {
|
||
el.textContent = '✅ Verbindung erfolgreich getestet';
|
||
el.style.color = '#27ae60';
|
||
} else {
|
||
el.textContent = '⚠️ Noch nicht getestet';
|
||
el.style.color = 'var(--color-muted)';
|
||
}
|
||
}
|
||
|
||
function ttlPwFocus() {
|
||
const pwField = document.getElementById('ttl-password');
|
||
if (pwField.value === TTL_PW_DOTS) {
|
||
pwField.value = '';
|
||
document.getElementById('ttl-pw-hint').textContent = '(leer lassen = unverändert)';
|
||
}
|
||
}
|
||
|
||
function ttlPwBlur() {
|
||
const pwField = document.getElementById('ttl-password');
|
||
if (pwField.value === '' && _ttlPasswordSet) {
|
||
pwField.value = TTL_PW_DOTS;
|
||
document.getElementById('ttl-pw-hint').textContent = '';
|
||
}
|
||
}
|
||
|
||
function showTtlockLoading() {
|
||
document.getElementById('ttlLoadingModal').classList.add('visible');
|
||
}
|
||
function hideTtlockLoading() {
|
||
document.getElementById('ttlLoadingModal').classList.remove('visible');
|
||
}
|
||
|
||
async function testTtlockConnection() {
|
||
const btn = document.getElementById('ttl-test-btn');
|
||
const result = document.getElementById('ttl-test-result');
|
||
btn.disabled = true;
|
||
result.style.display = 'none';
|
||
showTtlockLoading();
|
||
|
||
try {
|
||
const r = await fetch('/user/me/ttlock/test');
|
||
const data = await r.json();
|
||
|
||
if (r.ok) {
|
||
renderTtlockTestStatus(true);
|
||
const battery = data.electricQuantity != null ? `${data.electricQuantity}%` : '–';
|
||
const state = data.state || '–';
|
||
result.style.background = 'rgba(39,174,96,0.08)';
|
||
result.style.borderColor = '#27ae60';
|
||
result.innerHTML = `
|
||
<div style="font-weight:700;color:#27ae60;margin-bottom:0.5rem;">✅ Verbindung erfolgreich</div>
|
||
<table style="border-collapse:collapse;width:100%;">
|
||
<tr><td style="color:var(--color-muted);padding:0.2rem 0.6rem 0.2rem 0;width:40%;">Name</td><td>${escHtml(data.lockName || '–')}</td></tr>
|
||
<tr><td style="color:var(--color-muted);padding:0.2rem 0.6rem 0.2rem 0;">Alias</td><td>${escHtml(data.lockAlias || '–')}</td></tr>
|
||
<tr><td style="color:var(--color-muted);padding:0.2rem 0.6rem 0.2rem 0;">Modell</td><td>${escHtml(data.modelNum || '–')}</td></tr>
|
||
<tr><td style="color:var(--color-muted);padding:0.2rem 0.6rem 0.2rem 0;">Akku</td><td>${escHtml(battery)}</td></tr>
|
||
<tr><td style="color:var(--color-muted);padding:0.2rem 0.6rem 0.2rem 0;">Status</td><td>${escHtml(state)}</td></tr>
|
||
</table>`;
|
||
} else {
|
||
const msgs = {
|
||
ttlock_not_configured: 'TTLock-Zugangsdaten sind noch nicht gespeichert.',
|
||
admin_config_missing: 'Systemkonfiguration für TTLock fehlt (Admin).',
|
||
auth_failed: 'Anmeldung bei TTLock fehlgeschlagen – Zugangsdaten prüfen.',
|
||
lock_detail_failed: `Lock-Abfrage fehlgeschlagen: ${data.message || ''}`,
|
||
};
|
||
result.style.background = 'rgba(231,76,60,0.08)';
|
||
result.style.borderColor = '#e74c3c';
|
||
result.innerHTML = `<div style="color:#e74c3c;">❌ ${escHtml(msgs[data.error] || 'Unbekannter Fehler.')}</div>`;
|
||
}
|
||
} catch {
|
||
result.style.background = 'rgba(231,76,60,0.08)';
|
||
result.style.borderColor = '#e74c3c';
|
||
result.innerHTML = `<div style="color:#e74c3c;">❌ Netzwerkfehler.</div>`;
|
||
} finally {
|
||
hideTtlockLoading();
|
||
}
|
||
|
||
result.style.display = '';
|
||
btn.disabled = false;
|
||
btn.textContent = '🔌 Verbindung testen';
|
||
}
|
||
|
||
function escHtml(s) {
|
||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|
||
async function ttlockOpenOnce() {
|
||
const btn = document.getElementById('ttl-open-btn');
|
||
const errEl = document.getElementById('ttl-open-error');
|
||
btn.disabled = true;
|
||
errEl.textContent = '';
|
||
showTtlockLoading();
|
||
|
||
try {
|
||
const r = await fetch('/user/me/ttlock/open', { method: 'POST' });
|
||
const data = await r.json();
|
||
if (!r.ok) {
|
||
const msgs = {
|
||
ttlock_not_configured: 'TTLock-Zugangsdaten sind noch nicht gespeichert.',
|
||
admin_config_missing: 'Systemkonfiguration fehlt (Admin).',
|
||
auth_failed: 'Anmeldung bei TTLock fehlgeschlagen.',
|
||
passcode_failed: 'PIN konnte nicht am Schloss angelegt werden.',
|
||
active_lock_exists: 'Du befindest dich in einem aktiven Lock – manuelles Öffnen ist nicht möglich.',
|
||
};
|
||
errEl.textContent = msgs[data.error] || 'Unbekannter Fehler.';
|
||
return;
|
||
}
|
||
|
||
const pwdId = data.keyboardPwdId;
|
||
document.getElementById('ttl-open-pin').textContent = data.pin;
|
||
hideTtlockLoading();
|
||
|
||
const modal = document.getElementById('ttlOpenModal');
|
||
modal.classList.add('visible');
|
||
|
||
const okBtn = document.getElementById('ttl-open-ok-btn');
|
||
const onOk = async () => {
|
||
okBtn.removeEventListener('click', onOk);
|
||
okBtn.disabled = true;
|
||
modal.classList.remove('visible');
|
||
showTtlockLoading();
|
||
await fetch(`/user/me/ttlock/open/${pwdId}`, { method: 'DELETE' });
|
||
hideTtlockLoading();
|
||
okBtn.disabled = false;
|
||
};
|
||
okBtn.addEventListener('click', onOk);
|
||
} catch {
|
||
hideTtlockLoading();
|
||
errEl.textContent = 'Netzwerkfehler.';
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = '🔓 Öffnen';
|
||
}
|
||
}
|
||
|
||
async function saveTtlockUserConfig() {
|
||
const errEl = document.getElementById('ttl-error');
|
||
errEl.textContent = '';
|
||
const lockIdVal = document.getElementById('ttl-lockid').value.trim();
|
||
const pwVal = document.getElementById('ttl-password').value;
|
||
const body = {
|
||
username: document.getElementById('ttl-username').value.trim(),
|
||
password: pwVal === TTL_PW_DOTS ? '' : pwVal,
|
||
lockId: lockIdVal !== '' ? parseInt(lockIdVal) : null
|
||
};
|
||
const r = await fetch('/user/me/ttlock', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body)
|
||
});
|
||
if (r.ok) {
|
||
showToast();
|
||
await loadTtlockUserConfig();
|
||
} else {
|
||
errEl.textContent = 'Fehler beim Speichern.';
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|