Light- und Darkmode hinzugefügt
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled

This commit is contained in:
2026-04-28 14:07:32 +02:00
parent 34e5fcd777
commit 843acea652
75 changed files with 830 additions and 511 deletions

View File

@@ -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

Binary file not shown.

View 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">📷

View File

@@ -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">
<input type="checkbox" id="editPrivate">
<label for="editPrivate">Private Gruppe</label>
<label class="toggle-switch">
<input type="checkbox" id="editPrivate">
<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>`
: '';

View File

@@ -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">

View File

@@ -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}

View File

@@ -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; }

View 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;
}

View File

@@ -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) -->
@@ -186,8 +178,11 @@
</div>
<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 ─────────────────────────────────────────────────────────────────

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -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 ''; };

View File

@@ -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>`

View File

@@ -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, '&amp;').replace(/</g, '&lt;')
@@ -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');

View File

@@ -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;">

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -633,17 +633,16 @@ public class CardLockController {
if (l.getGameCardParkedAt() != null) {
LocalDateTime deadline = l.getGameCardParkedAt().plusHours(1);
if (deadline.isBefore(LocalDateTime.now())) {
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.",
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);
} else {
LocalDateTime freezeUntil = LocalDateTime.now().plusHours(4);
l.setFrozenUntil(freezeUntil);
l.setNextCardIn(freezeUntil);
result.put("frozenUntill", freezeUntil.toString());
result.put("nextCardIn", freezeUntil.toString());
}
l.setGameCardParkedAt(null);
cardlockRepository.save(l);

View File

@@ -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

View File

@@ -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());
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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<>(); }
}
}

View File

@@ -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;
}

View File

@@ -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<>(); }
}
}

View File

@@ -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;
}

View File

@@ -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<>(); }
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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

View 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">📷

View File

@@ -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">
<input type="checkbox" id="editPrivate">
<label for="editPrivate">Private Gruppe</label>
<label class="toggle-switch">
<input type="checkbox" id="editPrivate">
<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>`
: '';

View File

@@ -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">

View File

@@ -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}

View File

@@ -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; }

View 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;
}

View File

@@ -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) -->
@@ -186,8 +178,11 @@
</div>
<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 ─────────────────────────────────────────────────────────────────

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -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 ''; };

View File

@@ -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>`

View File

@@ -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, '&amp;').replace(/</g, '&lt;')
@@ -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');

View File

@@ -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;">

View File

@@ -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>