Light- und Darkmode hinzugefügt
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
@@ -38,7 +38,8 @@ jwt.keystore.alias=xxx
|
||||
app.base-url=http://localhost:8080
|
||||
app.cookie.secure=true
|
||||
|
||||
# Theme – alle Farben hier ändern, Email-Style passt sich automatisch an
|
||||
# Theme – Dark-Mode-Farben hier ändern (Light-Mode ist fest im ThemeController).
|
||||
# MailTemplateService übernimmt diese Werte automatisch für E-Mail-Templates.
|
||||
app.theme.color-bg=#1a1a2e
|
||||
app.theme.color-card=#16213e
|
||||
app.theme.color-primary=#e94560
|
||||
|
||||
BIN
bin/main/de/oaa/xxx/user/UserController$ThemeRequest.class
Normal file
@@ -38,11 +38,11 @@
|
||||
<div id="optionList"></div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.5rem;">
|
||||
<button onclick="addOption()" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;">+ Option</button>
|
||||
<label class="multi-toggle"><input type="checkbox" id="multiChoice"> Mehrfachauswahl möglich</label>
|
||||
<label class="toggle-switch"><input type="checkbox" id="multiChoice"><span class="toggle-track"></span> Mehrfachauswahl möglich</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compose-footer">
|
||||
<label class="privacy-toggle"><input type="checkbox" id="isPublic"> Öffentlich</label>
|
||||
<label class="toggle-switch"><input type="checkbox" id="isPublic"><span class="toggle-track"></span> Öffentlich</label>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;">
|
||||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'composeText')" title="Emoji">😊</button>
|
||||
<label class="compose-action-btn" title="Fotos">📷
|
||||
|
||||
@@ -105,9 +105,7 @@
|
||||
<div id="optionList"></div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.5rem;">
|
||||
<button onclick="addOption()" style="width:auto; margin:0; padding:0.3rem 0.75rem; font-size:0.8rem;">+ Option</button>
|
||||
<label class="multi-toggle">
|
||||
<input type="checkbox" id="multiChoice"> Mehrfachauswahl möglich
|
||||
</label>
|
||||
<label class="toggle-switch"><input type="checkbox" id="multiChoice"><span class="toggle-track"></span> Mehrfachauswahl möglich</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compose-footer">
|
||||
@@ -147,8 +145,11 @@
|
||||
<img id="editBildPreview" class="img-preview" alt="">
|
||||
<input type="hidden" id="editBildData">
|
||||
<div class="toggle-row">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="editPrivate">
|
||||
<label for="editPrivate">Private Gruppe</label>
|
||||
<span class="toggle-track"></span>
|
||||
Private Gruppe
|
||||
</label>
|
||||
</div>
|
||||
<button onclick="saveGruppe()" style="margin-top:0.75rem; width:auto; padding:0.4rem 1rem;">Speichern</button>
|
||||
<p class="message" id="saveMsg" style="display:none; margin-top:0.5rem;"></p>
|
||||
@@ -496,9 +497,7 @@
|
||||
</div>`).join('')}
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.3rem;" onclick="event.stopPropagation()">
|
||||
<button onmousedown="event.stopPropagation()" onclick="event.stopPropagation();gruppeEditAddOption('${beitragId}')" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;">+ Option</button>
|
||||
<label class="multi-toggle" onmousedown="event.stopPropagation()" onclick="event.stopPropagation()">
|
||||
<input type="checkbox" id="gpmc-${beitragId}" ${post.multiChoice ? 'checked' : ''}> Mehrfachauswahl möglich
|
||||
</label>
|
||||
<label class="toggle-switch" onmousedown="event.stopPropagation()" onclick="event.stopPropagation()"><input type="checkbox" id="gpmc-${beitragId}" ${post.multiChoice ? 'checked' : ''}><span class="toggle-track"></span> Mehrfachauswahl möglich</label>
|
||||
</div>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
@@ -417,7 +417,7 @@ function renderPage() {
|
||||
<div id="locOptionList"></div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.5rem;">
|
||||
<button onclick="addLocOption()" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;">+ Option</button>
|
||||
<label class="multi-toggle"><input type="checkbox" id="locMultiChoice"> Mehrfachauswahl möglich</label>
|
||||
<label class="toggle-switch"><input type="checkbox" id="locMultiChoice"><span class="toggle-track"></span> Mehrfachauswahl möglich</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compose-footer">
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
.compose-action-btn:hover{border-color:var(--color-primary);color:var(--color-primary);background:none}
|
||||
.compose-action-btn.active{border-color:var(--color-primary);color:var(--color-primary)}
|
||||
label.compose-action-btn{display:inline-flex;align-items:center}
|
||||
.multi-toggle{font-size:0.85rem;display:flex;align-items:center;gap:0.4rem}
|
||||
.privacy-toggle{font-size:0.85rem;display:flex;align-items:center;gap:0.4rem}
|
||||
|
||||
/* ── Umfrage-Compose ── */
|
||||
.umfrage-options{margin-top:0.5rem}
|
||||
|
||||
@@ -35,7 +35,7 @@ p {
|
||||
padding: 2.5rem;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--shadow);
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ button, .btn {
|
||||
}
|
||||
|
||||
button:hover:not(:disabled), .btn:hover {
|
||||
background: #c73652;
|
||||
background: var(--btn-primary-hover);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
@@ -117,7 +117,7 @@ button.secondary {
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: #1a4a8a;
|
||||
background: var(--btn-secondary-hover);
|
||||
}
|
||||
|
||||
/* ── Messages ── */
|
||||
@@ -131,19 +131,19 @@ button.secondary:hover {
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #3d0f1a;
|
||||
background: var(--msg-error-bg);
|
||||
border: 2px solid var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.message.warning {
|
||||
background: #3a2c0a;
|
||||
border: 2px solid #f5c518;
|
||||
color: #f5c518;
|
||||
background: var(--msg-warning-bg);
|
||||
border: 2px solid var(--msg-warning-text);
|
||||
color: var(--msg-warning-text);
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #0f3d1a;
|
||||
background: var(--msg-success-bg);
|
||||
border: 1px solid var(--color-success);
|
||||
color: var(--color-success);
|
||||
}
|
||||
@@ -180,7 +180,7 @@ body.app {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -205,7 +205,7 @@ body.app {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -338,7 +338,7 @@ body.app {
|
||||
.sidebar-overlay {
|
||||
display: none;
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
background: var(--overlay-bg);
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ body.app {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-wrapper.open { transform: translateX(0); box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5); }
|
||||
.sidebar-wrapper.open { transform: translateX(0); box-shadow: -4px 0 20px var(--overlay-bg); }
|
||||
|
||||
.sidebar {
|
||||
flex: none;
|
||||
@@ -414,7 +414,7 @@ body.app {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: flex-start;
|
||||
@@ -508,7 +508,7 @@ body.app {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
.sidebar-footer ul {
|
||||
@@ -572,7 +572,7 @@ body.app {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
@@ -648,7 +648,7 @@ body.app {
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-top: none;
|
||||
border-radius: 0 0 10px 10px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 600;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
@@ -810,7 +810,7 @@ body.app {
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-top: none;
|
||||
border-radius: 0 0 12px 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.65);
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 550;
|
||||
width: 360px;
|
||||
max-height: 500px;
|
||||
@@ -1022,6 +1022,43 @@ body.app {
|
||||
.topbar-profile-link:hover { background: var(--color-secondary); }
|
||||
.topbar-profile-link--danger { color: var(--color-primary); }
|
||||
|
||||
.topbar-theme-row { cursor: pointer; justify-content: space-between; }
|
||||
|
||||
/* ── Toggle Switch ─────────────────────────────────────────────────────────── */
|
||||
.toggle-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.toggle-switch input[type="checkbox"] { display: none; }
|
||||
.toggle-switch .toggle-track {
|
||||
width: 2.4rem;
|
||||
height: 1.35rem;
|
||||
background: var(--color-secondary);
|
||||
border-radius: 999px;
|
||||
position: relative;
|
||||
transition: background 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.toggle-switch input:checked ~ .toggle-track { background: var(--color-primary); }
|
||||
.toggle-switch .toggle-track::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0.15rem;
|
||||
left: 0.15rem;
|
||||
width: 1.05rem;
|
||||
height: 1.05rem;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||||
}
|
||||
.toggle-switch input:checked ~ .toggle-track::after { transform: translateX(1.05rem); }
|
||||
.toggle-switch input:disabled ~ .toggle-track { opacity: 0.45; }
|
||||
.toggle-switch:has(input:disabled) { cursor: default; }
|
||||
|
||||
/* ── Mobile: Topbar ausblenden, Content-Rahmen entfernen ── */
|
||||
@media (max-width: 768px) {
|
||||
.topbar { display: none; }
|
||||
|
||||
157
bin/main/static/css/variables.css
Normal file
@@ -0,0 +1,157 @@
|
||||
/* ──────────────────────────────────────────────────────────────────────────
|
||||
xXx Sphere – Design Tokens
|
||||
Dark ist der Default.
|
||||
Reihenfolge der Auflösung:
|
||||
1. :root → dark (Fallback, Spez. 0,1,0)
|
||||
2. @media prefers-light → light via OS (Spez. 0,1,0, überschreibt via Position)
|
||||
3. :root[data-theme=dark] → dark manuell (Spez. 0,2,0 – schlägt immer :root)
|
||||
4. :root[data-theme=light]→ light manuell (Spez. 0,2,0 – schlägt immer :root)
|
||||
JS setzt data-theme auf <html> und gewinnt damit immer.
|
||||
────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* ── 1. Dark mode – Default ───────────────────────────────────────────────── */
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
/* Surfaces */
|
||||
--color-bg: #0e0c15;
|
||||
--color-card: #19162a;
|
||||
--color-secondary: #272338;
|
||||
--color-surface: #201d2f;
|
||||
|
||||
/* Text */
|
||||
--color-text: #eae8f0;
|
||||
--color-muted: #8a879a;
|
||||
|
||||
/* Brand */
|
||||
--color-primary: #e94560;
|
||||
--color-primary-rgb: 233, 69, 96;
|
||||
|
||||
/* Semantic */
|
||||
--color-success: #27ae60;
|
||||
--color-danger: #e74c3c;
|
||||
|
||||
/* Aliases */
|
||||
--color-border: #272338;
|
||||
--color-hover: #272338;
|
||||
|
||||
/* Shadows */
|
||||
--shadow: 0 8px 32px rgba(0, 0, 0, 0.55);
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.35);
|
||||
|
||||
/* Overlay */
|
||||
--overlay-bg: rgba(0, 0, 0, 0.55);
|
||||
|
||||
/* Message boxes */
|
||||
--msg-error-bg: #3d0f1a;
|
||||
--msg-warning-bg: #3a2c0a;
|
||||
--msg-warning-text: #f5c518;
|
||||
--msg-success-bg: #0f3d1a;
|
||||
|
||||
/* Buttons */
|
||||
--btn-primary-hover: #c73652;
|
||||
--btn-secondary-hover: #1a3a6a;
|
||||
|
||||
/* Misc */
|
||||
--placeholder: rgba(234, 232, 240, 0.35);
|
||||
--unread: #e94560;
|
||||
--accept: #27ae60;
|
||||
--decline: #c0392b;
|
||||
--breakpoint-mobile: 768px;
|
||||
}
|
||||
|
||||
/* ── 2. Light mode via OS (CSS-Fallback, wenn JS nicht verfügbar) ─────────── */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
--color-bg: #f2f0f9;
|
||||
--color-card: #ffffff;
|
||||
--color-secondary: #e9e6f5;
|
||||
--color-surface: #f7f5fc;
|
||||
--color-text: #1a1628;
|
||||
--color-muted: #6a677e;
|
||||
--color-primary: #c9324d;
|
||||
--color-primary-rgb: 201, 50, 77;
|
||||
--color-success: #1a8c4e;
|
||||
--color-danger: #c0392b;
|
||||
--color-border: #d8d4ed;
|
||||
--color-hover: #e9e6f5;
|
||||
--shadow: 0 4px 16px rgba(0, 0, 0, 0.10);
|
||||
--shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
--overlay-bg: rgba(0, 0, 0, 0.35);
|
||||
--msg-error-bg: #fde8ec;
|
||||
--msg-warning-bg: #fdf5e0;
|
||||
--msg-warning-text: #b38800;
|
||||
--msg-success-bg: #e0f5e8;
|
||||
--btn-primary-hover: #a82942;
|
||||
--btn-secondary-hover: #d8d4ed;
|
||||
--placeholder: rgba(26, 22, 40, 0.30);
|
||||
--unread: #c9324d;
|
||||
--accept: #1a8c4e;
|
||||
--decline: #c0392b;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 3+4. JS-Override via data-theme auf <html> ───────────────────────────── */
|
||||
/* :root[data-theme] hat Spezifität (0,2,0) und schlägt :root (0,1,0) immer. */
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
|
||||
--color-bg: #0e0c15;
|
||||
--color-card: #19162a;
|
||||
--color-secondary: #272338;
|
||||
--color-surface: #201d2f;
|
||||
--color-text: #eae8f0;
|
||||
--color-muted: #8a879a;
|
||||
--color-primary: #e94560;
|
||||
--color-primary-rgb: 233, 69, 96;
|
||||
--color-success: #27ae60;
|
||||
--color-danger: #e74c3c;
|
||||
--color-border: #272338;
|
||||
--color-hover: #272338;
|
||||
--shadow: 0 8px 32px rgba(0, 0, 0, 0.55);
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.35);
|
||||
--overlay-bg: rgba(0, 0, 0, 0.55);
|
||||
--msg-error-bg: #3d0f1a;
|
||||
--msg-warning-bg: #3a2c0a;
|
||||
--msg-warning-text: #f5c518;
|
||||
--msg-success-bg: #0f3d1a;
|
||||
--btn-primary-hover: #c73652;
|
||||
--btn-secondary-hover: #1a3a6a;
|
||||
--placeholder: rgba(234, 232, 240, 0.35);
|
||||
--unread: #e94560;
|
||||
--accept: #27ae60;
|
||||
--decline: #c0392b;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
|
||||
--color-bg: #f2f0f9;
|
||||
--color-card: #ffffff;
|
||||
--color-secondary: #e9e6f5;
|
||||
--color-surface: #f7f5fc;
|
||||
--color-text: #1a1628;
|
||||
--color-muted: #6a677e;
|
||||
--color-primary: #c9324d;
|
||||
--color-primary-rgb: 201, 50, 77;
|
||||
--color-success: #1a8c4e;
|
||||
--color-danger: #c0392b;
|
||||
--color-border: #d8d4ed;
|
||||
--color-hover: #e9e6f5;
|
||||
--shadow: 0 4px 16px rgba(0, 0, 0, 0.10);
|
||||
--shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
--overlay-bg: rgba(0, 0, 0, 0.35);
|
||||
--msg-error-bg: #fde8ec;
|
||||
--msg-warning-bg: #fdf5e0;
|
||||
--msg-warning-text: #b38800;
|
||||
--msg-success-bg: #e0f5e8;
|
||||
--btn-primary-hover: #a82942;
|
||||
--btn-secondary-hover: #d8d4ed;
|
||||
--placeholder: rgba(26, 22, 40, 0.30);
|
||||
--unread: #c9324d;
|
||||
--accept: #1a8c4e;
|
||||
--decline: #c0392b;
|
||||
}
|
||||
@@ -39,19 +39,16 @@
|
||||
}
|
||||
.game-timer.urgent { color: #e74c3c; }
|
||||
|
||||
.game-level-bar {
|
||||
.level-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.game-level-dot {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-secondary);
|
||||
transition: background 0.3s;
|
||||
.level-display img {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.game-level-dot.active { background: var(--color-primary); }
|
||||
|
||||
.lock-messages {
|
||||
background: rgba(233,69,96,0.1);
|
||||
@@ -120,19 +117,14 @@
|
||||
#finisherBox h2 { font-size: 1.1rem; margin: 0 0 0.75rem; color: var(--color-primary); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="container" style="max-width:480px;margin:0 auto;padding:1rem;">
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1.25rem;">
|
||||
<button id="btnBack" onclick="goBack()"
|
||||
style="background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.4rem;">‹</button>
|
||||
<h1 style="margin:0;font-size:1.15rem;font-weight:700;">🎯 Task Game</h1>
|
||||
</div>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<h2 style="margin-bottom:1.25rem;">🎯 Task Game</h2>
|
||||
|
||||
<!-- Level-Anzeige -->
|
||||
<div class="game-level-bar" id="levelBar" style="display:none;">
|
||||
<span style="font-size:0.78rem;color:var(--color-muted);font-weight:600;">Level</span>
|
||||
<div id="levelDots" style="display:flex;gap:0.35rem;"></div>
|
||||
<span id="levelText" style="font-size:0.78rem;color:var(--color-muted);margin-left:auto;"></span>
|
||||
<div class="level-display" id="levelDisplay" style="display:none;">
|
||||
<img id="levelImg" src="" alt="Level">
|
||||
</div>
|
||||
|
||||
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
|
||||
@@ -187,7 +179,10 @@
|
||||
<div id="errorBox" style="display:none;background:rgba(231,76,60,0.1);border:1px solid rgba(231,76,60,0.3);
|
||||
border-radius:10px;padding:1rem;font-size:0.88rem;color:#e74c3c;margin-top:1rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const lockId = params.get('lockId');
|
||||
@@ -411,13 +406,9 @@
|
||||
// ── Level-Bar ─────────────────────────────────────────────────────────────
|
||||
|
||||
function renderLevelBar(level) {
|
||||
const bar = document.getElementById('levelBar');
|
||||
const dots = document.getElementById('levelDots');
|
||||
dots.innerHTML = [1,2,3,4,5].map(i =>
|
||||
`<div class="game-level-dot${i <= level ? ' active' : ''}"></div>`
|
||||
).join('');
|
||||
document.getElementById('levelText').textContent = 'Level ' + Math.min(level, 5);
|
||||
show('levelBar');
|
||||
const lvl = Math.min(Math.max(level, 1), 5);
|
||||
document.getElementById('levelImg').src = `/img/lvl${lvl}.png`;
|
||||
show('levelDisplay');
|
||||
}
|
||||
|
||||
// ── Timer ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 18 KiB |
@@ -1,4 +1,23 @@
|
||||
(function () {
|
||||
// ── Theme-Init (synchron, vor allem anderen) ──────────────────────────────
|
||||
window._applyTheme = function (theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
};
|
||||
(function () {
|
||||
const saved = localStorage.getItem('xxx-theme');
|
||||
if (saved === 'light' || saved === 'dark') {
|
||||
window._applyTheme(saved);
|
||||
} else {
|
||||
const prefersLight = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
window._applyTheme(prefersLight ? 'light' : 'dark');
|
||||
}
|
||||
})();
|
||||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function (e) {
|
||||
if (!localStorage.getItem('xxx-theme')) {
|
||||
window._applyTheme(e.matches ? 'light' : 'dark');
|
||||
}
|
||||
});
|
||||
|
||||
const path = window.location.pathname;
|
||||
const I = window.IC || function () { return ''; };
|
||||
|
||||
|
||||
@@ -496,8 +496,8 @@ function startPostEdit(cfg) {
|
||||
</div>`).join('')}
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.3rem;" onclick="event.stopPropagation()">
|
||||
<button onmousedown="event.stopPropagation()" onclick="event.stopPropagation();${addOptionFn}('${postId}')" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;">+ Option</button>
|
||||
<label class="multi-toggle" onmousedown="event.stopPropagation()" onclick="event.stopPropagation()">
|
||||
<input type="checkbox" id="${prefix}mc-${postId}" ${data.multiChoice ? 'checked' : ''}> Mehrfachauswahl möglich
|
||||
<label class="toggle-switch" onmousedown="event.stopPropagation()" onclick="event.stopPropagation()">
|
||||
<input type="checkbox" id="${prefix}mc-${postId}" ${data.multiChoice ? 'checked' : ''}><span class="toggle-track"></span> Mehrfachauswahl möglich
|
||||
</label>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
(function () {
|
||||
if (document.querySelector('.topbar')) return;
|
||||
|
||||
function _applyTheme(theme) {
|
||||
window._applyTheme ? window._applyTheme(theme) : document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', e => {
|
||||
if (!localStorage.getItem('xxx-theme')) {
|
||||
_applyTheme(e.matches ? 'light' : 'dark');
|
||||
}
|
||||
});
|
||||
|
||||
function esc(s) {
|
||||
return String(s ?? '')
|
||||
.replace(/&/g, '&').replace(/</g, '<')
|
||||
@@ -99,6 +109,13 @@
|
||||
<a id="topbarProfileLink" href="/community/benutzer.html" class="topbar-profile-link">
|
||||
<span>${IC('PROFILE')}</span> Mein Profil
|
||||
</a>
|
||||
<label class="topbar-profile-link topbar-theme-row" title="Dark / Light Mode">
|
||||
<span>☾ Dark Mode</span>
|
||||
<span class="toggle-switch" style="gap:0;">
|
||||
<input type="checkbox" id="topbarDarkModeCheck">
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
<a href="/konto/einstellungen.html" class="topbar-profile-link">
|
||||
<span>${IC('SETTINGS')}</span> Einstellungen
|
||||
</a>
|
||||
@@ -139,10 +156,37 @@
|
||||
}
|
||||
const profileLink = document.getElementById('topbarProfileLink');
|
||||
if (profileLink && user.userId) profileLink.href = '/community/benutzer.html?userId=' + user.userId;
|
||||
|
||||
// Theme aus DB anwenden (überschreibt OS-Fallback aus nav.js)
|
||||
let theme;
|
||||
if (user.darkMode === true) { theme = 'dark'; localStorage.setItem('xxx-theme', 'dark'); }
|
||||
else if (user.darkMode === false) { theme = 'light'; localStorage.setItem('xxx-theme', 'light'); }
|
||||
else { // null → OS-Präferenz, localStorage-Cache löschen
|
||||
localStorage.removeItem('xxx-theme');
|
||||
theme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
}
|
||||
_applyTheme(theme);
|
||||
setupDarkModeCheckbox(theme === 'dark');
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function setupDarkModeCheckbox(isDark) {
|
||||
const cb = document.getElementById('topbarDarkModeCheck');
|
||||
if (!cb) return;
|
||||
cb.checked = isDark;
|
||||
cb.addEventListener('change', () => {
|
||||
const dark = cb.checked;
|
||||
_applyTheme(dark ? 'dark' : 'light');
|
||||
localStorage.setItem('xxx-theme', dark ? 'dark' : 'light');
|
||||
fetch('/user/me/theme', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ darkMode: dark })
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Suche ──
|
||||
function setupSearch() {
|
||||
const input = document.getElementById('topbarSearchInput');
|
||||
|
||||
@@ -382,10 +382,10 @@
|
||||
<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()">
|
||||
<label class="toggle-switch"><input type="checkbox" id="notif-INVITATION-inApp" onchange="saveNotifications()"><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
<div class="notif-col-check">
|
||||
<input type="checkbox" id="notif-INVITATION-email" onchange="saveNotifications()">
|
||||
<label class="toggle-switch"><input type="checkbox" id="notif-INVITATION-email" onchange="saveNotifications()"><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -396,10 +396,10 @@
|
||||
<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()">
|
||||
<label class="toggle-switch"><input type="checkbox" id="notif-GAME_STATE-inApp" onchange="saveNotifications()"><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
<div class="notif-col-check">
|
||||
<input type="checkbox" id="notif-GAME_STATE-email" onchange="saveNotifications()">
|
||||
<label class="toggle-switch"><input type="checkbox" id="notif-GAME_STATE-email" onchange="saveNotifications()"><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -410,10 +410,10 @@
|
||||
<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()">
|
||||
<label class="toggle-switch"><input type="checkbox" id="notif-EMERGENCY-inApp" onchange="saveNotifications()"><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
<div class="notif-col-check">
|
||||
<input type="checkbox" id="notif-EMERGENCY-email" onchange="saveNotifications()">
|
||||
<label class="toggle-switch"><input type="checkbox" id="notif-EMERGENCY-email" onchange="saveNotifications()"><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -424,11 +424,10 @@
|
||||
<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">
|
||||
<label class="toggle-switch" title="In-App-Benachrichtigungen für Freundschaftsanfragen sind immer aktiv"><input type="checkbox" id="notif-FRIENDREQUEST-inApp" checked disabled><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
<div class="notif-col-check">
|
||||
<input type="checkbox" id="notif-FRIENDREQUEST-email" onchange="saveNotifications()">
|
||||
<label class="toggle-switch"><input type="checkbox" id="notif-FRIENDREQUEST-email" onchange="saveNotifications()"><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -484,8 +483,9 @@
|
||||
<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 class="toggle-switch">
|
||||
<input type="checkbox" id="datingAktiv" onchange="onDatingToggle()">
|
||||
<span class="toggle-track"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="datingSucheRow" style="display:none;">
|
||||
|
||||
@@ -365,16 +365,12 @@
|
||||
<div id="homeOptionList"></div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.5rem;">
|
||||
<button onclick="homeAddOption()" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;">+ Option</button>
|
||||
<label class="multi-toggle">
|
||||
<input type="checkbox" id="homeMultiChoice"> Mehrfachauswahl möglich
|
||||
</label>
|
||||
<label class="toggle-switch"><input type="checkbox" id="homeMultiChoice"><span class="toggle-track"></span> Mehrfachauswahl möglich</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compose-footer">
|
||||
<div style="display:flex;gap:1rem;align-items:center;flex-wrap:wrap;">
|
||||
<label class="privacy-toggle">
|
||||
<input type="checkbox" id="homeIsPublic"> Öffentlich
|
||||
</label>
|
||||
<label class="toggle-switch"><input type="checkbox" id="homeIsPublic"><span class="toggle-track"></span> Öffentlich</label>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;">
|
||||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'homeComposeText')" title="Emoji einfügen">😊</button>
|
||||
|
||||
@@ -5,50 +5,162 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Serves /css/variables.css dynamically from application.properties theme settings.
|
||||
* All HTML pages load this first, so changing app.theme.* immediately updates the whole UI.
|
||||
* Serves /css/variables.css dynamically. Dark-mode core colors come from app.theme.* properties
|
||||
* (shared with MailTemplateService for email styling). Light-mode values are hardcoded defaults.
|
||||
*/
|
||||
@RestController
|
||||
public class ThemeController {
|
||||
|
||||
// ── Dark mode (also used by MailTemplateService) ─────────────────────────
|
||||
@Value("${app.theme.color-bg:#1a1a2e}")
|
||||
private String colorBg;
|
||||
private String dBg;
|
||||
|
||||
@Value("${app.theme.color-card:#16213e}")
|
||||
private String colorCard;
|
||||
private String dCard;
|
||||
|
||||
@Value("${app.theme.color-primary:#e94560}")
|
||||
private String colorPrimary;
|
||||
private String dPrimary;
|
||||
|
||||
@Value("${app.theme.color-secondary:#0f3460}")
|
||||
private String colorSecondary;
|
||||
private String dSecondary;
|
||||
|
||||
@Value("${app.theme.color-text:#eeeeee}")
|
||||
private String colorText;
|
||||
private String dText;
|
||||
|
||||
@Value("${app.theme.color-muted:#888888}")
|
||||
private String colorMuted;
|
||||
private String dMuted;
|
||||
|
||||
@Value("${app.theme.color-success:#2ecc71}")
|
||||
private String colorSuccess;
|
||||
private String dSuccess;
|
||||
|
||||
/** Mobile breakpoint in px (unitless integer). Used by sidebar.js and lightbox layout. */
|
||||
@Value("${app.theme.breakpoint-mobile:768}")
|
||||
private int breakpointMobile;
|
||||
|
||||
@GetMapping(value = "/css/variables.css", produces = "text/css")
|
||||
public String variables() {
|
||||
return """
|
||||
/* ── 1. Dark – Default (:root) ──────────────────────────────────── */
|
||||
:root {
|
||||
--color-bg: %s;
|
||||
--color-card: %s;
|
||||
--color-primary: %s;
|
||||
--color-secondary: %s;
|
||||
--color-text: %s;
|
||||
--color-muted: %s;
|
||||
--color-success: %s;
|
||||
--breakpoint-mobile: %d;
|
||||
color-scheme: dark;
|
||||
--color-bg: %1$s;
|
||||
--color-card: %2$s;
|
||||
--color-primary: %3$s;
|
||||
--color-primary-rgb: 233, 69, 96;
|
||||
--color-secondary: %4$s;
|
||||
--color-surface: #201d2f;
|
||||
--color-text: %5$s;
|
||||
--color-muted: %6$s;
|
||||
--color-success: %7$s;
|
||||
--color-danger: #e74c3c;
|
||||
--color-border: %4$s;
|
||||
--color-hover: %4$s;
|
||||
--shadow: 0 8px 32px rgba(0,0,0,0.55);
|
||||
--shadow-sm: 0 2px 8px rgba(0,0,0,0.35);
|
||||
--overlay-bg: rgba(0,0,0,0.55);
|
||||
--msg-error-bg: #3d0f1a;
|
||||
--msg-warning-bg: #3a2c0a;
|
||||
--msg-warning-text: #f5c518;
|
||||
--msg-success-bg: #0f3d1a;
|
||||
--btn-primary-hover: #c73652;
|
||||
--btn-secondary-hover: #1a3a6a;
|
||||
--placeholder: rgba(234,232,240,0.35);
|
||||
--unread: %3$s;
|
||||
--accept: %7$s;
|
||||
--decline: #c0392b;
|
||||
--breakpoint-mobile: %8$dpx;
|
||||
}
|
||||
""".formatted(colorBg, colorCard, colorPrimary, colorSecondary, colorText, colorMuted, colorSuccess, breakpointMobile);
|
||||
|
||||
/* ── 2. Light via OS preference (CSS fallback, no JS) ────────────── */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--color-bg: #f2f0f9;
|
||||
--color-card: #ffffff;
|
||||
--color-primary: #c9324d;
|
||||
--color-primary-rgb: 201, 50, 77;
|
||||
--color-secondary: #e9e6f5;
|
||||
--color-surface: #f7f5fc;
|
||||
--color-text: #1a1628;
|
||||
--color-muted: #6a677e;
|
||||
--color-success: #1a8c4e;
|
||||
--color-danger: #c0392b;
|
||||
--color-border: #d8d4ed;
|
||||
--color-hover: #e9e6f5;
|
||||
--shadow: 0 4px 16px rgba(0,0,0,0.10);
|
||||
--shadow-sm: 0 1px 4px rgba(0,0,0,0.06);
|
||||
--overlay-bg: rgba(0,0,0,0.35);
|
||||
--msg-error-bg: #fde8ec;
|
||||
--msg-warning-bg: #fdf5e0;
|
||||
--msg-warning-text: #b38800;
|
||||
--msg-success-bg: #e0f5e8;
|
||||
--btn-primary-hover: #a82942;
|
||||
--btn-secondary-hover: #d8d4ed;
|
||||
--placeholder: rgba(26,22,40,0.30);
|
||||
--unread: #c9324d;
|
||||
--accept: #1a8c4e;
|
||||
--decline: #c0392b;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 3. JS-Override: dark (:root[data-theme="dark"]) ─────────────── */
|
||||
:root[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--color-bg: %1$s;
|
||||
--color-card: %2$s;
|
||||
--color-primary: %3$s;
|
||||
--color-primary-rgb: 233, 69, 96;
|
||||
--color-secondary: %4$s;
|
||||
--color-surface: #201d2f;
|
||||
--color-text: %5$s;
|
||||
--color-muted: %6$s;
|
||||
--color-success: %7$s;
|
||||
--color-danger: #e74c3c;
|
||||
--color-border: %4$s;
|
||||
--color-hover: %4$s;
|
||||
--shadow: 0 8px 32px rgba(0,0,0,0.55);
|
||||
--shadow-sm: 0 2px 8px rgba(0,0,0,0.35);
|
||||
--overlay-bg: rgba(0,0,0,0.55);
|
||||
--msg-error-bg: #3d0f1a;
|
||||
--msg-warning-bg: #3a2c0a;
|
||||
--msg-warning-text: #f5c518;
|
||||
--msg-success-bg: #0f3d1a;
|
||||
--btn-primary-hover: #c73652;
|
||||
--btn-secondary-hover: #1a3a6a;
|
||||
--placeholder: rgba(234,232,240,0.35);
|
||||
--unread: %3$s;
|
||||
--accept: %7$s;
|
||||
--decline: #c0392b;
|
||||
}
|
||||
|
||||
/* ── 4. JS-Override: light (:root[data-theme="light"]) ───────────── */
|
||||
:root[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
--color-bg: #f2f0f9;
|
||||
--color-card: #ffffff;
|
||||
--color-primary: #c9324d;
|
||||
--color-primary-rgb: 201, 50, 77;
|
||||
--color-secondary: #e9e6f5;
|
||||
--color-surface: #f7f5fc;
|
||||
--color-text: #1a1628;
|
||||
--color-muted: #6a677e;
|
||||
--color-success: #1a8c4e;
|
||||
--color-danger: #c0392b;
|
||||
--color-border: #d8d4ed;
|
||||
--color-hover: #e9e6f5;
|
||||
--shadow: 0 4px 16px rgba(0,0,0,0.10);
|
||||
--shadow-sm: 0 1px 4px rgba(0,0,0,0.06);
|
||||
--overlay-bg: rgba(0,0,0,0.35);
|
||||
--msg-error-bg: #fde8ec;
|
||||
--msg-warning-bg: #fdf5e0;
|
||||
--msg-warning-text: #b38800;
|
||||
--msg-success-bg: #e0f5e8;
|
||||
--btn-primary-hover: #a82942;
|
||||
--btn-secondary-hover: #d8d4ed;
|
||||
--placeholder: rgba(26,22,40,0.30);
|
||||
--unread: #c9324d;
|
||||
--accept: #1a8c4e;
|
||||
--decline: #c0392b;
|
||||
}
|
||||
""".formatted(dBg, dCard, dPrimary, dSecondary, dText, dMuted, dSuccess, breakpointMobile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -633,17 +633,16 @@ public class CardLockController {
|
||||
if (l.getGameCardParkedAt() != null) {
|
||||
LocalDateTime deadline = l.getGameCardParkedAt().plusHours(1);
|
||||
if (deadline.isBefore(LocalDateTime.now())) {
|
||||
if (l.getKeyholder() != null) {
|
||||
String meName = userRepository.findById(myId).map(u -> u.getName()).orElse("");
|
||||
sendMessage(myId, l.getKeyholder(),
|
||||
meName + " hat die Spiel-Karte nicht innerhalb einer Stunde gestartet.",
|
||||
"/games/chastity/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
|
||||
} else {
|
||||
LocalDateTime freezeUntil = LocalDateTime.now().plusHours(4);
|
||||
l.setFrozenUntil(freezeUntil);
|
||||
l.setNextCardIn(freezeUntil);
|
||||
result.put("frozenUntill", freezeUntil.toString());
|
||||
result.put("nextCardIn", freezeUntil.toString());
|
||||
if (l.getKeyholder() != null) {
|
||||
String meName = userRepository.findById(myId).map(u -> u.getName()).orElse("");
|
||||
sendMessage(myId, l.getKeyholder(),
|
||||
meName + " hat die Spiel-Karte nicht innerhalb einer Stunde gestartet. Das Lock wurde für 4 Stunden eingefroren.",
|
||||
"/games/chastity/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
|
||||
}
|
||||
l.setGameCardParkedAt(null);
|
||||
cardlockRepository.save(l);
|
||||
|
||||
@@ -17,18 +17,17 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardLockEntity;
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
|
||||
import de.oaa.xxx.games.common.aufgaben.AufgabenList;
|
||||
import de.oaa.xxx.games.common.aufgaben.AvailableIn;
|
||||
import de.oaa.xxx.games.common.repository.AufgabeRepository;
|
||||
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.games.common.repository.FinisherRepository;
|
||||
import de.oaa.xxx.games.common.repository.SperreRepository;
|
||||
import de.oaa.xxx.games.common.entity.AufgabeEntity;
|
||||
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.games.common.entity.FinisherEntity;
|
||||
import de.oaa.xxx.games.common.entity.SperreEntity;
|
||||
import de.oaa.xxx.games.common.repository.AufgabeRepository;
|
||||
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.games.common.repository.FinisherRepository;
|
||||
import de.oaa.xxx.games.common.repository.SperreRepository;
|
||||
import de.oaa.xxx.user.UserService;
|
||||
|
||||
@RestController
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.gameset;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import de.oaa.xxx.user.UserService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/chastity/game-sets")
|
||||
public class ChastityGameSetController {
|
||||
|
||||
private static final int MAX_SETS = 5;
|
||||
private static final int MIN_AUFGABEN_PER_LEVEL = 3;
|
||||
|
||||
private final ChastityGameSetRepository repository;
|
||||
private final UserService userService;
|
||||
|
||||
public ChastityGameSetController(ChastityGameSetRepository repository, UserService userService) {
|
||||
this.repository = repository;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
record GameSetRequest(
|
||||
String name,
|
||||
List<GameSetAufgabe> aufgaben,
|
||||
List<GameSetZeitstrafe> zeitstrafen,
|
||||
List<GameSetFinisher> finisher) {}
|
||||
|
||||
private Map<String, Object> toDto(ChastityGameSetEntity e) {
|
||||
Map<String, Object> dto = new LinkedHashMap<>();
|
||||
dto.put("id", e.getId());
|
||||
dto.put("name", e.getName());
|
||||
dto.put("aufgaben", e.getAufgaben() != null ? e.getAufgaben() : List.of());
|
||||
dto.put("zeitstrafen", e.getZeitstrafen() != null ? e.getZeitstrafen() : List.of());
|
||||
dto.put("finisher", e.getFinisher() != null ? e.getFinisher() : List.of());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private Optional<String> validate(GameSetRequest req) {
|
||||
if (req.name() == null || req.name().isBlank()) return Optional.of("Name ist ein Pflichtfeld.");
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<Map<String, Object>>> list(Principal principal) {
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
return ResponseEntity.ok(
|
||||
repository.findByOwnerIdOrderByName(myId).stream().map(this::toDto).toList());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<?> create(@RequestBody GameSetRequest req, Principal principal) {
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
if (repository.countByOwnerId(myId) >= MAX_SETS)
|
||||
return ResponseEntity.status(409).body(Map.of("error", "Maximal " + MAX_SETS + " Spiel-Sets erlaubt."));
|
||||
var err = validate(req);
|
||||
if (err.isPresent()) return ResponseEntity.badRequest().body(Map.of("error", err.get()));
|
||||
ChastityGameSetEntity e = new ChastityGameSetEntity();
|
||||
e.setOwnerId(myId);
|
||||
applyRequest(e, req);
|
||||
repository.save(e);
|
||||
return ResponseEntity.ok(toDto(e));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<?> update(@PathVariable UUID id, @RequestBody GameSetRequest req, Principal principal) {
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
var opt = repository.findById(id);
|
||||
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var e = opt.get();
|
||||
if (!e.getOwnerId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
var err = validate(req);
|
||||
if (err.isPresent()) return ResponseEntity.badRequest().body(Map.of("error", err.get()));
|
||||
applyRequest(e, req);
|
||||
repository.save(e);
|
||||
return ResponseEntity.ok(toDto(e));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> delete(@PathVariable UUID id, Principal principal) {
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
var opt = repository.findById(id);
|
||||
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!opt.get().getOwnerId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
repository.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
private void applyRequest(ChastityGameSetEntity e, GameSetRequest req) {
|
||||
e.setName(req.name().trim());
|
||||
e.setAufgaben(req.aufgaben() != null ? req.aufgaben() : List.of());
|
||||
e.setZeitstrafen(req.zeitstrafen() != null ? req.zeitstrafen() : List.of());
|
||||
e.setFinisher(req.finisher() != null ? req.finisher() : List.of());
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.gameset;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "chastity_game_set")
|
||||
public class ChastityGameSetEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID ownerId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Convert(converter = GameSetAufgabeListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<GameSetAufgabe> aufgaben;
|
||||
|
||||
@Convert(converter = GameSetZeitstrafeListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<GameSetZeitstrafe> zeitstrafen;
|
||||
|
||||
@Convert(converter = GameSetFinisherListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<GameSetFinisher> finisher;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.gameset;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface ChastityGameSetRepository extends JpaRepository<ChastityGameSetEntity, UUID> {
|
||||
List<ChastityGameSetEntity> findByOwnerIdOrderByName(UUID ownerId);
|
||||
long countByOwnerId(UUID ownerId);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.gameset;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class GameSetAufgabe {
|
||||
private String title;
|
||||
private String description;
|
||||
private Integer minutes;
|
||||
private Integer level; // 1-5
|
||||
private List<String> benoetigt;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.gameset;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Converter
|
||||
public class GameSetAufgabeListConverter implements AttributeConverter<List<GameSetAufgabe>, String> {
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public String convertToDatabaseColumn(List<GameSetAufgabe> list) {
|
||||
if (list == null || list.isEmpty()) return null;
|
||||
try { return mapper.writeValueAsString(list); } catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GameSetAufgabe> convertToEntityAttribute(String json) {
|
||||
if (json == null || json.isBlank()) return new ArrayList<>();
|
||||
try { return new ArrayList<>(mapper.readValue(json, new TypeReference<List<GameSetAufgabe>>() {})); }
|
||||
catch (Exception e) { return new ArrayList<>(); }
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.gameset;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class GameSetFinisher {
|
||||
private String title;
|
||||
private String description;
|
||||
private Boolean tempUnlockBeforeRequired;
|
||||
private Boolean tempUnlockAfterRequired;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.gameset;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Converter
|
||||
public class GameSetFinisherListConverter implements AttributeConverter<List<GameSetFinisher>, String> {
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public String convertToDatabaseColumn(List<GameSetFinisher> list) {
|
||||
if (list == null || list.isEmpty()) return null;
|
||||
try { return mapper.writeValueAsString(list); } catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GameSetFinisher> convertToEntityAttribute(String json) {
|
||||
if (json == null || json.isBlank()) return new ArrayList<>();
|
||||
try { return new ArrayList<>(mapper.readValue(json, new TypeReference<List<GameSetFinisher>>() {})); }
|
||||
catch (Exception e) { return new ArrayList<>(); }
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.gameset;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class GameSetZeitstrafe {
|
||||
private String title;
|
||||
private String description;
|
||||
private Integer level; // 1-5
|
||||
private Integer minMinutes;
|
||||
private Integer maxMinutes;
|
||||
private Boolean tempUnlockBeforeRequired;
|
||||
private Boolean tempUnlockAfterRequired;
|
||||
private List<String> sperrt;
|
||||
private String releaseText;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.gameset;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Converter
|
||||
public class GameSetZeitstrafeListConverter implements AttributeConverter<List<GameSetZeitstrafe>, String> {
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public String convertToDatabaseColumn(List<GameSetZeitstrafe> list) {
|
||||
if (list == null || list.isEmpty()) return null;
|
||||
try { return mapper.writeValueAsString(list); } catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GameSetZeitstrafe> convertToEntityAttribute(String json) {
|
||||
if (json == null || json.isBlank()) return new ArrayList<>();
|
||||
try { return new ArrayList<>(mapper.readValue(json, new TypeReference<List<GameSetZeitstrafe>>() {})); }
|
||||
catch (Exception e) { return new ArrayList<>(); }
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ public class User {
|
||||
private Double filterLat;
|
||||
private Double filterLon;
|
||||
private Integer filterMaxDistKm;
|
||||
private Boolean darkMode;
|
||||
|
||||
public Integer getAlter() {
|
||||
return geburtsdatum != null ? Period.between(geburtsdatum, LocalDate.now()).getYears() : null;
|
||||
|
||||
@@ -316,6 +316,16 @@ public class UserController {
|
||||
return List.of(s.split(","));
|
||||
}
|
||||
|
||||
record ThemeRequest(Boolean darkMode) {}
|
||||
|
||||
@PutMapping("/me/theme")
|
||||
public ResponseEntity<Void> updateTheme(@RequestBody ThemeRequest body, Principal principal) {
|
||||
var user = userService.requireUser(principal);
|
||||
user.setDarkMode(body.darkMode());
|
||||
userRepository.save(user);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PutMapping("/me/geburtsdatum")
|
||||
public ResponseEntity<Void> updateGeburtsdatum(@RequestBody GeburtsdatumChangeRequest request, Principal principal) {
|
||||
if (request.geburtsdatum() == null
|
||||
|
||||
@@ -137,6 +137,10 @@ public class UserEntity {
|
||||
@Column
|
||||
private Integer datingMaxAlter;
|
||||
|
||||
/** null = OS-Präferenz, true = Dark, false = Light */
|
||||
@Column(nullable = true)
|
||||
private Boolean darkMode;
|
||||
|
||||
// ── Locations/Events-Filter ──
|
||||
@Column(length = 200)
|
||||
private String filterCity;
|
||||
@@ -188,6 +192,7 @@ public class UserEntity {
|
||||
user.setFilterLat(filterLat);
|
||||
user.setFilterLon(filterLon);
|
||||
user.setFilterMaxDistKm(filterMaxDistKm);
|
||||
user.setDarkMode(darkMode);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ jwt.keystore.alias=xxx
|
||||
app.base-url=http://localhost:8080
|
||||
app.cookie.secure=true
|
||||
|
||||
# Theme – alle Farben hier ändern, Email-Style passt sich automatisch an
|
||||
# Theme – Dark-Mode-Farben hier ändern (Light-Mode ist fest im ThemeController).
|
||||
# MailTemplateService übernimmt diese Werte automatisch für E-Mail-Templates.
|
||||
app.theme.color-bg=#1a1a2e
|
||||
app.theme.color-card=#16213e
|
||||
app.theme.color-primary=#e94560
|
||||
|
||||
@@ -38,11 +38,11 @@
|
||||
<div id="optionList"></div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.5rem;">
|
||||
<button onclick="addOption()" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;">+ Option</button>
|
||||
<label class="multi-toggle"><input type="checkbox" id="multiChoice"> Mehrfachauswahl möglich</label>
|
||||
<label class="toggle-switch"><input type="checkbox" id="multiChoice"><span class="toggle-track"></span> Mehrfachauswahl möglich</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compose-footer">
|
||||
<label class="privacy-toggle"><input type="checkbox" id="isPublic"> Öffentlich</label>
|
||||
<label class="toggle-switch"><input type="checkbox" id="isPublic"><span class="toggle-track"></span> Öffentlich</label>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;">
|
||||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'composeText')" title="Emoji">😊</button>
|
||||
<label class="compose-action-btn" title="Fotos">📷
|
||||
|
||||
@@ -105,9 +105,7 @@
|
||||
<div id="optionList"></div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.5rem;">
|
||||
<button onclick="addOption()" style="width:auto; margin:0; padding:0.3rem 0.75rem; font-size:0.8rem;">+ Option</button>
|
||||
<label class="multi-toggle">
|
||||
<input type="checkbox" id="multiChoice"> Mehrfachauswahl möglich
|
||||
</label>
|
||||
<label class="toggle-switch"><input type="checkbox" id="multiChoice"><span class="toggle-track"></span> Mehrfachauswahl möglich</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compose-footer">
|
||||
@@ -147,8 +145,11 @@
|
||||
<img id="editBildPreview" class="img-preview" alt="">
|
||||
<input type="hidden" id="editBildData">
|
||||
<div class="toggle-row">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="editPrivate">
|
||||
<label for="editPrivate">Private Gruppe</label>
|
||||
<span class="toggle-track"></span>
|
||||
Private Gruppe
|
||||
</label>
|
||||
</div>
|
||||
<button onclick="saveGruppe()" style="margin-top:0.75rem; width:auto; padding:0.4rem 1rem;">Speichern</button>
|
||||
<p class="message" id="saveMsg" style="display:none; margin-top:0.5rem;"></p>
|
||||
@@ -496,9 +497,7 @@
|
||||
</div>`).join('')}
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.3rem;" onclick="event.stopPropagation()">
|
||||
<button onmousedown="event.stopPropagation()" onclick="event.stopPropagation();gruppeEditAddOption('${beitragId}')" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;">+ Option</button>
|
||||
<label class="multi-toggle" onmousedown="event.stopPropagation()" onclick="event.stopPropagation()">
|
||||
<input type="checkbox" id="gpmc-${beitragId}" ${post.multiChoice ? 'checked' : ''}> Mehrfachauswahl möglich
|
||||
</label>
|
||||
<label class="toggle-switch" onmousedown="event.stopPropagation()" onclick="event.stopPropagation()"><input type="checkbox" id="gpmc-${beitragId}" ${post.multiChoice ? 'checked' : ''}><span class="toggle-track"></span> Mehrfachauswahl möglich</label>
|
||||
</div>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
@@ -417,7 +417,7 @@ function renderPage() {
|
||||
<div id="locOptionList"></div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.5rem;">
|
||||
<button onclick="addLocOption()" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;">+ Option</button>
|
||||
<label class="multi-toggle"><input type="checkbox" id="locMultiChoice"> Mehrfachauswahl möglich</label>
|
||||
<label class="toggle-switch"><input type="checkbox" id="locMultiChoice"><span class="toggle-track"></span> Mehrfachauswahl möglich</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compose-footer">
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
.compose-action-btn:hover{border-color:var(--color-primary);color:var(--color-primary);background:none}
|
||||
.compose-action-btn.active{border-color:var(--color-primary);color:var(--color-primary)}
|
||||
label.compose-action-btn{display:inline-flex;align-items:center}
|
||||
.multi-toggle{font-size:0.85rem;display:flex;align-items:center;gap:0.4rem}
|
||||
.privacy-toggle{font-size:0.85rem;display:flex;align-items:center;gap:0.4rem}
|
||||
|
||||
/* ── Umfrage-Compose ── */
|
||||
.umfrage-options{margin-top:0.5rem}
|
||||
|
||||
@@ -35,7 +35,7 @@ p {
|
||||
padding: 2.5rem;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--shadow);
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ button, .btn {
|
||||
}
|
||||
|
||||
button:hover:not(:disabled), .btn:hover {
|
||||
background: #c73652;
|
||||
background: var(--btn-primary-hover);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
@@ -117,7 +117,7 @@ button.secondary {
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: #1a4a8a;
|
||||
background: var(--btn-secondary-hover);
|
||||
}
|
||||
|
||||
/* ── Messages ── */
|
||||
@@ -131,19 +131,19 @@ button.secondary:hover {
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #3d0f1a;
|
||||
background: var(--msg-error-bg);
|
||||
border: 2px solid var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.message.warning {
|
||||
background: #3a2c0a;
|
||||
border: 2px solid #f5c518;
|
||||
color: #f5c518;
|
||||
background: var(--msg-warning-bg);
|
||||
border: 2px solid var(--msg-warning-text);
|
||||
color: var(--msg-warning-text);
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #0f3d1a;
|
||||
background: var(--msg-success-bg);
|
||||
border: 1px solid var(--color-success);
|
||||
color: var(--color-success);
|
||||
}
|
||||
@@ -180,7 +180,7 @@ body.app {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -205,7 +205,7 @@ body.app {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -338,7 +338,7 @@ body.app {
|
||||
.sidebar-overlay {
|
||||
display: none;
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
background: var(--overlay-bg);
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ body.app {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-wrapper.open { transform: translateX(0); box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5); }
|
||||
.sidebar-wrapper.open { transform: translateX(0); box-shadow: -4px 0 20px var(--overlay-bg); }
|
||||
|
||||
.sidebar {
|
||||
flex: none;
|
||||
@@ -414,7 +414,7 @@ body.app {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: flex-start;
|
||||
@@ -508,7 +508,7 @@ body.app {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
.sidebar-footer ul {
|
||||
@@ -572,7 +572,7 @@ body.app {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
@@ -648,7 +648,7 @@ body.app {
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-top: none;
|
||||
border-radius: 0 0 10px 10px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 600;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
@@ -810,7 +810,7 @@ body.app {
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-top: none;
|
||||
border-radius: 0 0 12px 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.65);
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 550;
|
||||
width: 360px;
|
||||
max-height: 500px;
|
||||
@@ -1022,6 +1022,43 @@ body.app {
|
||||
.topbar-profile-link:hover { background: var(--color-secondary); }
|
||||
.topbar-profile-link--danger { color: var(--color-primary); }
|
||||
|
||||
.topbar-theme-row { cursor: pointer; justify-content: space-between; }
|
||||
|
||||
/* ── Toggle Switch ─────────────────────────────────────────────────────────── */
|
||||
.toggle-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.toggle-switch input[type="checkbox"] { display: none; }
|
||||
.toggle-switch .toggle-track {
|
||||
width: 2.4rem;
|
||||
height: 1.35rem;
|
||||
background: var(--color-secondary);
|
||||
border-radius: 999px;
|
||||
position: relative;
|
||||
transition: background 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.toggle-switch input:checked ~ .toggle-track { background: var(--color-primary); }
|
||||
.toggle-switch .toggle-track::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0.15rem;
|
||||
left: 0.15rem;
|
||||
width: 1.05rem;
|
||||
height: 1.05rem;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||||
}
|
||||
.toggle-switch input:checked ~ .toggle-track::after { transform: translateX(1.05rem); }
|
||||
.toggle-switch input:disabled ~ .toggle-track { opacity: 0.45; }
|
||||
.toggle-switch:has(input:disabled) { cursor: default; }
|
||||
|
||||
/* ── Mobile: Topbar ausblenden, Content-Rahmen entfernen ── */
|
||||
@media (max-width: 768px) {
|
||||
.topbar { display: none; }
|
||||
|
||||
157
src/main/resources/static/css/variables.css
Normal file
@@ -0,0 +1,157 @@
|
||||
/* ──────────────────────────────────────────────────────────────────────────
|
||||
xXx Sphere – Design Tokens
|
||||
Dark ist der Default.
|
||||
Reihenfolge der Auflösung:
|
||||
1. :root → dark (Fallback, Spez. 0,1,0)
|
||||
2. @media prefers-light → light via OS (Spez. 0,1,0, überschreibt via Position)
|
||||
3. :root[data-theme=dark] → dark manuell (Spez. 0,2,0 – schlägt immer :root)
|
||||
4. :root[data-theme=light]→ light manuell (Spez. 0,2,0 – schlägt immer :root)
|
||||
JS setzt data-theme auf <html> und gewinnt damit immer.
|
||||
────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* ── 1. Dark mode – Default ───────────────────────────────────────────────── */
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
/* Surfaces */
|
||||
--color-bg: #0e0c15;
|
||||
--color-card: #19162a;
|
||||
--color-secondary: #272338;
|
||||
--color-surface: #201d2f;
|
||||
|
||||
/* Text */
|
||||
--color-text: #eae8f0;
|
||||
--color-muted: #8a879a;
|
||||
|
||||
/* Brand */
|
||||
--color-primary: #e94560;
|
||||
--color-primary-rgb: 233, 69, 96;
|
||||
|
||||
/* Semantic */
|
||||
--color-success: #27ae60;
|
||||
--color-danger: #e74c3c;
|
||||
|
||||
/* Aliases */
|
||||
--color-border: #272338;
|
||||
--color-hover: #272338;
|
||||
|
||||
/* Shadows */
|
||||
--shadow: 0 8px 32px rgba(0, 0, 0, 0.55);
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.35);
|
||||
|
||||
/* Overlay */
|
||||
--overlay-bg: rgba(0, 0, 0, 0.55);
|
||||
|
||||
/* Message boxes */
|
||||
--msg-error-bg: #3d0f1a;
|
||||
--msg-warning-bg: #3a2c0a;
|
||||
--msg-warning-text: #f5c518;
|
||||
--msg-success-bg: #0f3d1a;
|
||||
|
||||
/* Buttons */
|
||||
--btn-primary-hover: #c73652;
|
||||
--btn-secondary-hover: #1a3a6a;
|
||||
|
||||
/* Misc */
|
||||
--placeholder: rgba(234, 232, 240, 0.35);
|
||||
--unread: #e94560;
|
||||
--accept: #27ae60;
|
||||
--decline: #c0392b;
|
||||
--breakpoint-mobile: 768px;
|
||||
}
|
||||
|
||||
/* ── 2. Light mode via OS (CSS-Fallback, wenn JS nicht verfügbar) ─────────── */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
--color-bg: #f2f0f9;
|
||||
--color-card: #ffffff;
|
||||
--color-secondary: #e9e6f5;
|
||||
--color-surface: #f7f5fc;
|
||||
--color-text: #1a1628;
|
||||
--color-muted: #6a677e;
|
||||
--color-primary: #c9324d;
|
||||
--color-primary-rgb: 201, 50, 77;
|
||||
--color-success: #1a8c4e;
|
||||
--color-danger: #c0392b;
|
||||
--color-border: #d8d4ed;
|
||||
--color-hover: #e9e6f5;
|
||||
--shadow: 0 4px 16px rgba(0, 0, 0, 0.10);
|
||||
--shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
--overlay-bg: rgba(0, 0, 0, 0.35);
|
||||
--msg-error-bg: #fde8ec;
|
||||
--msg-warning-bg: #fdf5e0;
|
||||
--msg-warning-text: #b38800;
|
||||
--msg-success-bg: #e0f5e8;
|
||||
--btn-primary-hover: #a82942;
|
||||
--btn-secondary-hover: #d8d4ed;
|
||||
--placeholder: rgba(26, 22, 40, 0.30);
|
||||
--unread: #c9324d;
|
||||
--accept: #1a8c4e;
|
||||
--decline: #c0392b;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 3+4. JS-Override via data-theme auf <html> ───────────────────────────── */
|
||||
/* :root[data-theme] hat Spezifität (0,2,0) und schlägt :root (0,1,0) immer. */
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
|
||||
--color-bg: #0e0c15;
|
||||
--color-card: #19162a;
|
||||
--color-secondary: #272338;
|
||||
--color-surface: #201d2f;
|
||||
--color-text: #eae8f0;
|
||||
--color-muted: #8a879a;
|
||||
--color-primary: #e94560;
|
||||
--color-primary-rgb: 233, 69, 96;
|
||||
--color-success: #27ae60;
|
||||
--color-danger: #e74c3c;
|
||||
--color-border: #272338;
|
||||
--color-hover: #272338;
|
||||
--shadow: 0 8px 32px rgba(0, 0, 0, 0.55);
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.35);
|
||||
--overlay-bg: rgba(0, 0, 0, 0.55);
|
||||
--msg-error-bg: #3d0f1a;
|
||||
--msg-warning-bg: #3a2c0a;
|
||||
--msg-warning-text: #f5c518;
|
||||
--msg-success-bg: #0f3d1a;
|
||||
--btn-primary-hover: #c73652;
|
||||
--btn-secondary-hover: #1a3a6a;
|
||||
--placeholder: rgba(234, 232, 240, 0.35);
|
||||
--unread: #e94560;
|
||||
--accept: #27ae60;
|
||||
--decline: #c0392b;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
|
||||
--color-bg: #f2f0f9;
|
||||
--color-card: #ffffff;
|
||||
--color-secondary: #e9e6f5;
|
||||
--color-surface: #f7f5fc;
|
||||
--color-text: #1a1628;
|
||||
--color-muted: #6a677e;
|
||||
--color-primary: #c9324d;
|
||||
--color-primary-rgb: 201, 50, 77;
|
||||
--color-success: #1a8c4e;
|
||||
--color-danger: #c0392b;
|
||||
--color-border: #d8d4ed;
|
||||
--color-hover: #e9e6f5;
|
||||
--shadow: 0 4px 16px rgba(0, 0, 0, 0.10);
|
||||
--shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
--overlay-bg: rgba(0, 0, 0, 0.35);
|
||||
--msg-error-bg: #fde8ec;
|
||||
--msg-warning-bg: #fdf5e0;
|
||||
--msg-warning-text: #b38800;
|
||||
--msg-success-bg: #e0f5e8;
|
||||
--btn-primary-hover: #a82942;
|
||||
--btn-secondary-hover: #d8d4ed;
|
||||
--placeholder: rgba(26, 22, 40, 0.30);
|
||||
--unread: #c9324d;
|
||||
--accept: #1a8c4e;
|
||||
--decline: #c0392b;
|
||||
}
|
||||
@@ -39,19 +39,16 @@
|
||||
}
|
||||
.game-timer.urgent { color: #e74c3c; }
|
||||
|
||||
.game-level-bar {
|
||||
.level-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.game-level-dot {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-secondary);
|
||||
transition: background 0.3s;
|
||||
.level-display img {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.game-level-dot.active { background: var(--color-primary); }
|
||||
|
||||
.lock-messages {
|
||||
background: rgba(233,69,96,0.1);
|
||||
@@ -120,19 +117,14 @@
|
||||
#finisherBox h2 { font-size: 1.1rem; margin: 0 0 0.75rem; color: var(--color-primary); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="container" style="max-width:480px;margin:0 auto;padding:1rem;">
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1.25rem;">
|
||||
<button id="btnBack" onclick="goBack()"
|
||||
style="background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.4rem;">‹</button>
|
||||
<h1 style="margin:0;font-size:1.15rem;font-weight:700;">🎯 Task Game</h1>
|
||||
</div>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<h2 style="margin-bottom:1.25rem;">🎯 Task Game</h2>
|
||||
|
||||
<!-- Level-Anzeige -->
|
||||
<div class="game-level-bar" id="levelBar" style="display:none;">
|
||||
<span style="font-size:0.78rem;color:var(--color-muted);font-weight:600;">Level</span>
|
||||
<div id="levelDots" style="display:flex;gap:0.35rem;"></div>
|
||||
<span id="levelText" style="font-size:0.78rem;color:var(--color-muted);margin-left:auto;"></span>
|
||||
<div class="level-display" id="levelDisplay" style="display:none;">
|
||||
<img id="levelImg" src="" alt="Level">
|
||||
</div>
|
||||
|
||||
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
|
||||
@@ -187,7 +179,10 @@
|
||||
<div id="errorBox" style="display:none;background:rgba(231,76,60,0.1);border:1px solid rgba(231,76,60,0.3);
|
||||
border-radius:10px;padding:1rem;font-size:0.88rem;color:#e74c3c;margin-top:1rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const lockId = params.get('lockId');
|
||||
@@ -411,13 +406,9 @@
|
||||
// ── Level-Bar ─────────────────────────────────────────────────────────────
|
||||
|
||||
function renderLevelBar(level) {
|
||||
const bar = document.getElementById('levelBar');
|
||||
const dots = document.getElementById('levelDots');
|
||||
dots.innerHTML = [1,2,3,4,5].map(i =>
|
||||
`<div class="game-level-dot${i <= level ? ' active' : ''}"></div>`
|
||||
).join('');
|
||||
document.getElementById('levelText').textContent = 'Level ' + Math.min(level, 5);
|
||||
show('levelBar');
|
||||
const lvl = Math.min(Math.max(level, 1), 5);
|
||||
document.getElementById('levelImg').src = `/img/lvl${lvl}.png`;
|
||||
show('levelDisplay');
|
||||
}
|
||||
|
||||
// ── Timer ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 18 KiB |
@@ -1,4 +1,23 @@
|
||||
(function () {
|
||||
// ── Theme-Init (synchron, vor allem anderen) ──────────────────────────────
|
||||
window._applyTheme = function (theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
};
|
||||
(function () {
|
||||
const saved = localStorage.getItem('xxx-theme');
|
||||
if (saved === 'light' || saved === 'dark') {
|
||||
window._applyTheme(saved);
|
||||
} else {
|
||||
const prefersLight = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
window._applyTheme(prefersLight ? 'light' : 'dark');
|
||||
}
|
||||
})();
|
||||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function (e) {
|
||||
if (!localStorage.getItem('xxx-theme')) {
|
||||
window._applyTheme(e.matches ? 'light' : 'dark');
|
||||
}
|
||||
});
|
||||
|
||||
const path = window.location.pathname;
|
||||
const I = window.IC || function () { return ''; };
|
||||
|
||||
|
||||
@@ -496,8 +496,8 @@ function startPostEdit(cfg) {
|
||||
</div>`).join('')}
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.3rem;" onclick="event.stopPropagation()">
|
||||
<button onmousedown="event.stopPropagation()" onclick="event.stopPropagation();${addOptionFn}('${postId}')" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;">+ Option</button>
|
||||
<label class="multi-toggle" onmousedown="event.stopPropagation()" onclick="event.stopPropagation()">
|
||||
<input type="checkbox" id="${prefix}mc-${postId}" ${data.multiChoice ? 'checked' : ''}> Mehrfachauswahl möglich
|
||||
<label class="toggle-switch" onmousedown="event.stopPropagation()" onclick="event.stopPropagation()">
|
||||
<input type="checkbox" id="${prefix}mc-${postId}" ${data.multiChoice ? 'checked' : ''}><span class="toggle-track"></span> Mehrfachauswahl möglich
|
||||
</label>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
(function () {
|
||||
if (document.querySelector('.topbar')) return;
|
||||
|
||||
function _applyTheme(theme) {
|
||||
window._applyTheme ? window._applyTheme(theme) : document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', e => {
|
||||
if (!localStorage.getItem('xxx-theme')) {
|
||||
_applyTheme(e.matches ? 'light' : 'dark');
|
||||
}
|
||||
});
|
||||
|
||||
function esc(s) {
|
||||
return String(s ?? '')
|
||||
.replace(/&/g, '&').replace(/</g, '<')
|
||||
@@ -99,6 +109,13 @@
|
||||
<a id="topbarProfileLink" href="/community/benutzer.html" class="topbar-profile-link">
|
||||
<span>${IC('PROFILE')}</span> Mein Profil
|
||||
</a>
|
||||
<label class="topbar-profile-link topbar-theme-row" title="Dark / Light Mode">
|
||||
<span>☾ Dark Mode</span>
|
||||
<span class="toggle-switch" style="gap:0;">
|
||||
<input type="checkbox" id="topbarDarkModeCheck">
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
<a href="/konto/einstellungen.html" class="topbar-profile-link">
|
||||
<span>${IC('SETTINGS')}</span> Einstellungen
|
||||
</a>
|
||||
@@ -139,10 +156,37 @@
|
||||
}
|
||||
const profileLink = document.getElementById('topbarProfileLink');
|
||||
if (profileLink && user.userId) profileLink.href = '/community/benutzer.html?userId=' + user.userId;
|
||||
|
||||
// Theme aus DB anwenden (überschreibt OS-Fallback aus nav.js)
|
||||
let theme;
|
||||
if (user.darkMode === true) { theme = 'dark'; localStorage.setItem('xxx-theme', 'dark'); }
|
||||
else if (user.darkMode === false) { theme = 'light'; localStorage.setItem('xxx-theme', 'light'); }
|
||||
else { // null → OS-Präferenz, localStorage-Cache löschen
|
||||
localStorage.removeItem('xxx-theme');
|
||||
theme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
}
|
||||
_applyTheme(theme);
|
||||
setupDarkModeCheckbox(theme === 'dark');
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function setupDarkModeCheckbox(isDark) {
|
||||
const cb = document.getElementById('topbarDarkModeCheck');
|
||||
if (!cb) return;
|
||||
cb.checked = isDark;
|
||||
cb.addEventListener('change', () => {
|
||||
const dark = cb.checked;
|
||||
_applyTheme(dark ? 'dark' : 'light');
|
||||
localStorage.setItem('xxx-theme', dark ? 'dark' : 'light');
|
||||
fetch('/user/me/theme', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ darkMode: dark })
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Suche ──
|
||||
function setupSearch() {
|
||||
const input = document.getElementById('topbarSearchInput');
|
||||
|
||||
@@ -382,10 +382,10 @@
|
||||
<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()">
|
||||
<label class="toggle-switch"><input type="checkbox" id="notif-INVITATION-inApp" onchange="saveNotifications()"><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
<div class="notif-col-check">
|
||||
<input type="checkbox" id="notif-INVITATION-email" onchange="saveNotifications()">
|
||||
<label class="toggle-switch"><input type="checkbox" id="notif-INVITATION-email" onchange="saveNotifications()"><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -396,10 +396,10 @@
|
||||
<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()">
|
||||
<label class="toggle-switch"><input type="checkbox" id="notif-GAME_STATE-inApp" onchange="saveNotifications()"><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
<div class="notif-col-check">
|
||||
<input type="checkbox" id="notif-GAME_STATE-email" onchange="saveNotifications()">
|
||||
<label class="toggle-switch"><input type="checkbox" id="notif-GAME_STATE-email" onchange="saveNotifications()"><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -410,10 +410,10 @@
|
||||
<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()">
|
||||
<label class="toggle-switch"><input type="checkbox" id="notif-EMERGENCY-inApp" onchange="saveNotifications()"><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
<div class="notif-col-check">
|
||||
<input type="checkbox" id="notif-EMERGENCY-email" onchange="saveNotifications()">
|
||||
<label class="toggle-switch"><input type="checkbox" id="notif-EMERGENCY-email" onchange="saveNotifications()"><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -424,11 +424,10 @@
|
||||
<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">
|
||||
<label class="toggle-switch" title="In-App-Benachrichtigungen für Freundschaftsanfragen sind immer aktiv"><input type="checkbox" id="notif-FRIENDREQUEST-inApp" checked disabled><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
<div class="notif-col-check">
|
||||
<input type="checkbox" id="notif-FRIENDREQUEST-email" onchange="saveNotifications()">
|
||||
<label class="toggle-switch"><input type="checkbox" id="notif-FRIENDREQUEST-email" onchange="saveNotifications()"><span class="toggle-track"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -484,8 +483,9 @@
|
||||
<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 class="toggle-switch">
|
||||
<input type="checkbox" id="datingAktiv" onchange="onDatingToggle()">
|
||||
<span class="toggle-track"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="datingSucheRow" style="display:none;">
|
||||
|
||||
@@ -365,16 +365,12 @@
|
||||
<div id="homeOptionList"></div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.5rem;">
|
||||
<button onclick="homeAddOption()" style="width:auto;margin:0;padding:0.3rem 0.75rem;font-size:0.8rem;">+ Option</button>
|
||||
<label class="multi-toggle">
|
||||
<input type="checkbox" id="homeMultiChoice"> Mehrfachauswahl möglich
|
||||
</label>
|
||||
<label class="toggle-switch"><input type="checkbox" id="homeMultiChoice"><span class="toggle-track"></span> Mehrfachauswahl möglich</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compose-footer">
|
||||
<div style="display:flex;gap:1rem;align-items:center;flex-wrap:wrap;">
|
||||
<label class="privacy-toggle">
|
||||
<input type="checkbox" id="homeIsPublic"> Öffentlich
|
||||
</label>
|
||||
<label class="toggle-switch"><input type="checkbox" id="homeIsPublic"><span class="toggle-track"></span> Öffentlich</label>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;">
|
||||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'homeComposeText')" title="Emoji einfügen">😊</button>
|
||||
|
||||