Weiter am Ingame Chastity Game gearbeitet
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
This commit is contained in:
@@ -260,6 +260,33 @@
|
||||
.radio-group label { display:flex; align-items:center; gap:0.5rem; cursor:pointer; font-size:0.9rem; margin:0; color:var(--color-text); }
|
||||
.radio-group input[type="radio"] { width:auto; padding:0; margin:0; }
|
||||
|
||||
/* ── Spiel-Set Suche ── */
|
||||
.gs-dropdown {
|
||||
display:none; position:absolute; top:100%; left:0; right:0; z-index:200;
|
||||
background:var(--color-card); border:1px solid var(--color-secondary);
|
||||
border-radius:6px; max-height:200px; overflow-y:auto;
|
||||
box-shadow:0 4px 14px rgba(0,0,0,0.35); margin-top:2px;
|
||||
}
|
||||
.gs-dropdown-item {
|
||||
padding:0.55rem 0.9rem; cursor:pointer; font-size:0.9rem;
|
||||
border-bottom:1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.gs-dropdown-item:last-child { border-bottom:none; }
|
||||
.gs-dropdown-item:hover { background:rgba(255,255,255,0.07); }
|
||||
.gs-item-name { font-weight:600; color:var(--color-text); }
|
||||
.gs-item-desc { font-size:0.78rem; color:var(--color-muted); margin-top:0.1rem; }
|
||||
.gs-selected {
|
||||
display:flex; align-items:center; gap:0.6rem;
|
||||
background:rgba(255,255,255,0.05); border:1px solid var(--color-secondary);
|
||||
border-radius:6px; padding:0.45rem 0.75rem; margin-top:0.35rem;
|
||||
font-size:0.88rem; color:var(--color-text);
|
||||
}
|
||||
.gs-selected button {
|
||||
background:none; border:none; color:var(--color-muted);
|
||||
cursor:pointer; padding:0; margin:0; font-size:1rem; width:auto; line-height:1;
|
||||
}
|
||||
.gs-selected button:hover { color:#e74c3c; background:none; }
|
||||
|
||||
/* ── Simulation ── */
|
||||
.sim-bar-track { background:var(--color-secondary); border-radius:6px; height:8px; overflow:hidden; margin:0.5rem 0 0.25rem; }
|
||||
.sim-bar-fill { height:100%; background:var(--color-primary); border-radius:6px; transition:width 0.1s linear; width:0%; }
|
||||
@@ -268,47 +295,6 @@
|
||||
.sim-stat-val { font-size:1rem; font-weight:700; color:var(--color-text); }
|
||||
.sim-stat-lbl { font-size:0.7rem; color:var(--color-muted); margin-top:0.15rem; text-transform:uppercase; letter-spacing:0.05em; }
|
||||
|
||||
/* ── Spiel-Sets (gruppe-card style) ── */
|
||||
.gs-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; overflow:hidden; cursor:pointer; transition:border-color 0.15s; }
|
||||
.gs-card:hover { border-color:var(--color-primary); }
|
||||
.gs-card-header { display:flex; align-items:center; gap:0.75rem; padding:0.85rem 1rem; user-select:none; }
|
||||
.gs-card-meta { flex:1; min-width:0; }
|
||||
.gs-card-name { font-size:0.95rem; font-weight:600; color:var(--color-text); }
|
||||
.gs-card-header-actions { display:flex; gap:0.4rem; flex-shrink:0; }
|
||||
|
||||
.gs-sub { margin-bottom:0.85rem; }
|
||||
.gs-sub:last-child { margin-bottom:0; }
|
||||
.gs-sub-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:0.35rem; }
|
||||
.gs-sub-title { font-size:0.7rem; font-weight:700; letter-spacing:0.06em; text-transform:uppercase; color:var(--color-primary); }
|
||||
.gs-sub-warn { color:#e74c3c !important; }
|
||||
|
||||
.gs-item-list { display:flex; flex-direction:column; gap:0.25rem; }
|
||||
.gs-list-item { border-radius:6px; background:var(--color-secondary); overflow:hidden; }
|
||||
.gs-list-item-row { display:flex; align-items:center; gap:0.5rem; padding:0.3rem 0.5rem 0.3rem 0.75rem; cursor:pointer; user-select:none; transition:background 0.12s; }
|
||||
.gs-list-item-row:hover { background:rgba(255,255,255,0.04); }
|
||||
.gs-list-item.open .gs-list-item-row { background:rgba(233,69,96,0.08); }
|
||||
.gs-list-item-text { color:var(--color-text); flex:1; min-width:0; font-size:0.83rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.gs-list-item-badges { display:flex; gap:0.3rem; flex-shrink:0; align-items:center; }
|
||||
.gs-badge { font-size:0.68rem; padding:0.1rem 0.4rem; border-radius:20px; background:rgba(233,69,96,0.15); color:var(--color-primary); white-space:nowrap; }
|
||||
.gs-badge-neutral { background:rgba(255,255,255,0.07); color:var(--color-muted); }
|
||||
.gs-list-item-detail { display:none; padding:0.4rem 0.75rem 0.5rem; border-top:1px solid rgba(255,255,255,0.06); font-size:0.79rem; color:var(--color-muted); line-height:1.5; }
|
||||
.gs-list-item.open .gs-list-item-detail { display:block; }
|
||||
.gs-detail-text { color:var(--color-text); white-space:pre-wrap; margin-bottom:0.3rem; }
|
||||
.gs-detail-actions { display:flex; gap:0.35rem; margin-top:0.4rem; justify-content:flex-end; }
|
||||
.gs-btn-sub-add { background:none; border:1px solid var(--color-secondary); border-radius:5px; color:var(--color-muted); font-size:0.73rem; padding:0.15rem 0.5rem; cursor:pointer; transition:border-color 0.15s,color 0.15s; width:auto; }
|
||||
.gs-btn-sub-add:hover { border-color:var(--color-primary); color:var(--color-primary); }
|
||||
.gs-btn-item-edit { background:none; border:1px solid rgba(136,136,136,0.45); border-radius:5px; color:var(--color-muted); font-size:0.73rem; padding:0.18rem 0.55rem; cursor:pointer; transition:border-color 0.15s,color 0.15s; width:auto; }
|
||||
.gs-btn-item-edit:hover { border-color:var(--color-text); color:var(--color-text); }
|
||||
.gs-btn-item-delete { background:none; border:1px solid rgba(233,69,96,0.4); border-radius:5px; color:var(--color-primary); font-size:0.73rem; padding:0.18rem 0.55rem; cursor:pointer; transition:background 0.15s; width:auto; }
|
||||
.gs-btn-item-delete:hover { background:rgba(233,69,96,0.15); }
|
||||
.gs-sub-empty { font-size:0.78rem; color:var(--color-muted); padding:0.15rem 0; }
|
||||
|
||||
#gsSetModal, #gsItemModal { z-index:600; }
|
||||
|
||||
.gs-check-group { display:flex; flex-wrap:wrap; gap:0.4rem; }
|
||||
.gs-check-chip { display:inline-flex; align-items:center; gap:0.4rem; background:var(--color-secondary); border:1px solid rgba(255,255,255,0.1); border-radius:20px; padding:0.25rem 0.7rem; cursor:pointer; font-size:0.82rem; color:var(--color-text); transition:border-color 0.15s; user-select:none; }
|
||||
.gs-check-chip:has(input:checked) { border-color:var(--color-primary); }
|
||||
.gs-check-chip input { accent-color:var(--color-primary); width:auto; cursor:pointer; margin:0; flex-shrink:0; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
@@ -329,17 +315,6 @@
|
||||
<div class="template-list" id="taskSetList"></div>
|
||||
<p id="taskSetEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Noch keine Aufgaben-Sets vorhanden.</p>
|
||||
|
||||
<!-- Spiel-Sets (für Spiel-Karte) -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin:2rem 0 1rem;gap:1rem;flex-wrap:wrap;">
|
||||
<div>
|
||||
<h2 style="margin:0;">Spiel-Sets</h2>
|
||||
<p style="margin:0.25rem 0 0;font-size:0.8rem;color:var(--color-muted);">Aufgaben-Sets für die Spiel-Karte im Karten-Lock · max. 5 Sets</p>
|
||||
</div>
|
||||
<button id="btnNewGameSet" onclick="openGsSetModal(null)" style="width:auto;padding:0.55rem 1.2rem;">+ Set anlegen</button>
|
||||
</div>
|
||||
<div class="template-list" id="gameSetList"></div>
|
||||
<p id="gameSetEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Noch keine Spiel-Sets vorhanden.</p>
|
||||
|
||||
<h2 style="margin:2rem 0 1rem;">Abonnierte Vorlagen</h2>
|
||||
<div class="template-list" id="subscribedList"></div>
|
||||
<p id="subscribedEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Keine abonnierten Vorlagen vorhanden.</p>
|
||||
@@ -443,43 +418,57 @@
|
||||
<label for="fShowRemaining">Art der verbleibenden Karten anzeigen</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Spiel-Karte (CardLock) -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Spiel-Karte (optional)</div>
|
||||
<div class="form-row" style="margin-bottom:0.5rem;">
|
||||
<label>Spiel-Set</label>
|
||||
<select id="fGameSetId" onchange="onGameSetChange()">
|
||||
<option value="">Kein Spiel-Set</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-add" type="button" onclick="openGsSetModal(null,'template')">+ Neues Set anlegen</button>
|
||||
<div id="gameSetSpieldauerRow" style="display:none;margin-top:0.75rem;">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.3rem;">
|
||||
<label for="sldGameSpieldauer" style="font-size:0.88rem;">Spieldauer</label>
|
||||
<span class="setting-value" id="valGameSpieldauer">Mittel</span>
|
||||
<!-- Aufgaben (CardLock) – nur sichtbar wenn TASK-Karte > 0 -->
|
||||
<div id="sectionCardTasks" style="display:none;">
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Aufgaben</div>
|
||||
<div style="margin-bottom:0.65rem;">
|
||||
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.45rem;">Wer entscheidet über die Aufgabe?</div>
|
||||
<div class="radio-group">
|
||||
<label><input type="radio" name="modalCardTaskMode" value="RANDOM" checked> Zufall</label>
|
||||
<label><input type="radio" name="modalCardTaskMode" value="KEYHOLDER" > Keyholder*In</label>
|
||||
<label><input type="radio" name="modalCardTaskMode" value="COMMUNITY" > Community</label>
|
||||
</div>
|
||||
</div>
|
||||
<input type="range" id="sldGameSpieldauer" min="0" max="4" value="2" oninput="updateGameSpieldauer(this.value)" style="width:100%;">
|
||||
<div class="form-row" style="margin-bottom:0.5rem;">
|
||||
<label>Aufgaben-Set <span class="required-star">*</span></label>
|
||||
<select id="fCardTaskSetId" onchange="onTaskSetChange('card')">
|
||||
<option value="">Kein Aufgaben-Set</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-add" type="button" onclick="openTaskSetModal(null,'card')">+ Neues Set anlegen</button>
|
||||
<div id="cardTaskSetPreview" class="task-set-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Aufgaben (CardLock) -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Aufgaben (optional)</div>
|
||||
<div style="margin-bottom:0.65rem;">
|
||||
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.45rem;">Wer entscheidet über die Aufgabe?</div>
|
||||
<div class="radio-group">
|
||||
<label><input type="radio" name="modalCardTaskMode" value="RANDOM" checked> Zufall</label>
|
||||
<label><input type="radio" name="modalCardTaskMode" value="KEYHOLDER" > Keyholder*In</label>
|
||||
<label><input type="radio" name="modalCardTaskMode" value="COMMUNITY" > Community</label>
|
||||
|
||||
<!-- Spiel-Karte – nur sichtbar wenn GAME_CARD > 0 -->
|
||||
<div id="sectionGameSetConfig" style="display:none;">
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Spiel-Karte – Minispiel</div>
|
||||
<p style="font-size:0.82rem;color:var(--color-muted);margin:0 0 0.75rem;">Wenn eine Spiel-Karte gezogen wird, startet ein Minispiel mit dem gewählten Chastity-Aufgaben-Set.</p>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label>
|
||||
<div style="position:relative;">
|
||||
<input type="text" id="gameSetSearch" placeholder="Name eingeben zum Suchen…"
|
||||
autocomplete="off" oninput="onGameSetSearch(this.value)" onfocus="onGameSetSearchFocus()">
|
||||
<div id="gameSetDropdown" class="gs-dropdown"></div>
|
||||
</div>
|
||||
<div id="gameSetSelected" class="gs-selected" style="display:none;"></div>
|
||||
<input type="hidden" id="fGameSetId">
|
||||
<div class="field-error-msg" id="errGameSet" style="display:none;">Bitte ein Aufgaben-Set für Spiel-Karten auswählen.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.4rem;">
|
||||
<label for="sldGameSpieldauer" style="margin:0;">Spieldauer</label>
|
||||
<span id="valGameSpieldauer" style="font-size:0.85rem;color:var(--color-muted);">Mittel</span>
|
||||
</div>
|
||||
<input type="range" id="sldGameSpieldauer" min="0" max="4" value="2"
|
||||
oninput="updateGameSpieldauer(this.value)"
|
||||
style="width:100%;accent-color:var(--color-primary);">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:0.5rem;">
|
||||
<label>Aufgaben-Set</label>
|
||||
<select id="fCardTaskSetId" onchange="onTaskSetChange('card')">
|
||||
<option value="">Kein Aufgaben-Set</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-add" type="button" onclick="openTaskSetModal(null,'card')">+ Neues Set anlegen</button>
|
||||
<div id="cardTaskSetPreview" class="task-set-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -719,114 +708,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spiel-Set: Set erstellen / umbenennen -->
|
||||
<div class="modal-backdrop" id="gsSetModal">
|
||||
<div class="modal-box" style="max-width:420px;" onclick="event.stopPropagation()">
|
||||
<h2 id="gsSetModalTitle" style="margin:0 0 1.25rem;color:var(--color-primary);">Neues Spiel-Set</h2>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Name *</label>
|
||||
<input type="text" id="gsSetName" maxlength="100" placeholder="Set-Name"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;transition:border-color 0.2s;"
|
||||
onkeydown="if(event.key==='Enter')saveGsSet()">
|
||||
<div id="gsSetError" style="color:#e74c3c;font-size:0.82rem;margin-top:0.75rem;display:none;"></div>
|
||||
<div style="display:flex;justify-content:flex-end;gap:0.75rem;margin-top:1.5rem;">
|
||||
<button onclick="closeGsSetModal()" style="background:var(--color-secondary);color:var(--color-text);border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;cursor:pointer;width:auto;">Abbrechen</button>
|
||||
<button onclick="saveGsSet()" style="background:var(--color-primary);color:#fff;border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;font-weight:600;cursor:pointer;width:auto;">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spiel-Set: Item hinzufügen / bearbeiten -->
|
||||
<div class="modal-backdrop" id="gsItemModal">
|
||||
<div class="modal-box" style="max-width:460px;" onclick="event.stopPropagation()">
|
||||
<h2 id="gsItemModalTitle" style="margin:0 0 1.25rem;color:var(--color-primary);">Aufgabe</h2>
|
||||
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Titel *</label>
|
||||
<input type="text" id="gsItemTitle" maxlength="150" placeholder="Titel …"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;margin-bottom:0.85rem;transition:border-color 0.2s;">
|
||||
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Beschreibung (optional)</label>
|
||||
<textarea id="gsItemDesc" rows="3" maxlength="600" placeholder="Beschreibung …"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;font-family:inherit;resize:vertical;outline:none;margin-bottom:0.85rem;"></textarea>
|
||||
|
||||
<!-- Aufgabe: Level + Dauer -->
|
||||
<div id="gsItemAufgabeRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Level *</label>
|
||||
<select id="gsItemAufgabeLevel" style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;appearance:none;margin-bottom:0.65rem;">
|
||||
<option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option>
|
||||
</select>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Dauer (Minuten, optional)</label>
|
||||
<input type="number" id="gsItemMinutes" min="1" max="9999" placeholder="–"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
|
||||
</div>
|
||||
|
||||
<!-- Aufgabe: benötigt -->
|
||||
<div id="gsItemBenoetigtRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Benötigt</div>
|
||||
<div class="gs-check-group">
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemBen_MUND">Mund</label>
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemBen_ANUS">Anus</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zeitstrafe: Level + Strafminuten -->
|
||||
<div id="gsItemZeitstrafeRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Level *</label>
|
||||
<select id="gsItemZeitstrafeLevel" style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;appearance:none;margin-bottom:0.65rem;">
|
||||
<option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option>
|
||||
</select>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Strafminuten (Von – Bis)</label>
|
||||
<div style="display:flex;gap:0.6rem;align-items:center;">
|
||||
<input type="number" id="gsItemMinMin" min="1" max="9999" placeholder="Min."
|
||||
style="flex:1;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
|
||||
<span style="color:var(--color-muted);">–</span>
|
||||
<input type="number" id="gsItemMaxMin" min="1" max="9999" placeholder="Max."
|
||||
style="flex:1;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
|
||||
</div>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin:0.65rem 0 0.3rem;">Text bei Aufhebung (optional)</label>
|
||||
<textarea id="gsItemReleaseText" rows="2" maxlength="2000"
|
||||
placeholder="Text der angezeigt wird, wenn die Sperre endet…"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;font-family:inherit;resize:vertical;outline:none;transition:border-color 0.2s;line-height:1.45;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Zeitstrafe: sperrt -->
|
||||
<div id="gsItemSperrtRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Sperrt</div>
|
||||
<div class="gs-check-group">
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemSperr_MUND"> Mund</label>
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemSperr_ANUS"> Anus</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zeitstrafe + Finisher: Temp. Öffnung -->
|
||||
<div id="gsItemUnlockRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Temp. Öffnung</div>
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;margin-bottom:0.35rem;cursor:pointer;">
|
||||
<input type="checkbox" id="gsItemBefore" style="accent-color:var(--color-primary);width:15px;height:15px;"> Vor der Maßnahme erforderlich
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;cursor:pointer;">
|
||||
<input type="checkbox" id="gsItemAfter" style="accent-color:var(--color-primary);width:15px;height:15px;"> Nach der Maßnahme erforderlich
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="gsItemError" style="color:#e74c3c;font-size:0.82rem;display:none;margin-bottom:0.5rem;"></div>
|
||||
<div style="display:flex;justify-content:flex-end;gap:0.75rem;margin-top:0.5rem;">
|
||||
<button onclick="closeGsItemModal()" style="background:var(--color-secondary);color:var(--color-text);border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;cursor:pointer;width:auto;">Abbrechen</button>
|
||||
<button onclick="saveGsItem()" style="background:var(--color-primary);color:#fff;border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;font-weight:600;cursor:pointer;width:auto;">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spiel-Set: Inhalt-Popup -->
|
||||
<div class="modal-backdrop" id="gsEditModal">
|
||||
<div class="modal-box" style="max-width:600px;width:calc(100% - 2rem);" onclick="event.stopPropagation()">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:0.75rem;margin-bottom:1.25rem;">
|
||||
<h2 id="gsEditModalTitle" style="margin:0;color:var(--color-primary);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></h2>
|
||||
<button onclick="closeGsEditModal()" style="background:none;border:none;color:var(--color-muted);font-size:1.3rem;line-height:1;cursor:pointer;padding:0;width:auto;flex-shrink:0;">✕</button>
|
||||
</div>
|
||||
<div id="gsEditModalContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/card-defs.js"></script>
|
||||
<script src="/js/card-display.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
@@ -935,8 +816,114 @@
|
||||
function syncMinMax(id, val) {
|
||||
if (id.startsWith('min_')) { const mx = document.getElementById('max_'+id.slice(4)); if (mx && val > (parseInt(mx.value)||0)) mx.value = val; }
|
||||
else if (id.startsWith('max_')) { const mn = document.getElementById('min_'+id.slice(4)); if (mn && val < (parseInt(mn.value)||0)) mn.value = val; }
|
||||
if (id === 'min_GAME_CARD' || id === 'max_GAME_CARD') {
|
||||
if (val > 0) {
|
||||
// Gegenseitiger Ausschluss: Task-Karten nullen
|
||||
const minT = document.getElementById('min_TASK'); if (minT) minT.value = 0;
|
||||
const maxT = document.getElementById('max_TASK'); if (maxT) maxT.value = 0;
|
||||
checkTaskCardSection();
|
||||
}
|
||||
checkGameCardSection();
|
||||
}
|
||||
if (id === 'min_TASK' || id === 'max_TASK') {
|
||||
if (val > 0) {
|
||||
// Gegenseitiger Ausschluss: Game-Karten nullen
|
||||
const minG = document.getElementById('min_GAME_CARD'); if (minG) minG.value = 0;
|
||||
const maxG = document.getElementById('max_GAME_CARD'); if (maxG) maxG.value = 0;
|
||||
checkGameCardSection();
|
||||
}
|
||||
checkTaskCardSection();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Spiel-Karte: Aufgaben-Set + Spieldauer ──
|
||||
const GAME_SPIELDAUER = [
|
||||
{ label: 'Sehr kurz' },
|
||||
{ label: 'Kurz' },
|
||||
{ label: 'Mittel' },
|
||||
{ label: 'Lang' },
|
||||
{ label: 'Sehr lang' },
|
||||
];
|
||||
let _gameSetSearchTimer = null;
|
||||
let _gameSetResults = [];
|
||||
|
||||
function checkGameCardSection() {
|
||||
const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0;
|
||||
const maxV = parseInt(document.getElementById('max_GAME_CARD')?.value) || 0;
|
||||
const sec = document.getElementById('sectionGameSetConfig');
|
||||
if (sec) sec.style.display = (minV > 0 || maxV > 0) ? '' : 'none';
|
||||
}
|
||||
|
||||
function updateGameSpieldauer(val) {
|
||||
document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[+val].label;
|
||||
}
|
||||
|
||||
function onGameSetSearchFocus() {
|
||||
if (!document.getElementById('fGameSetId').value) onGameSetSearch(document.getElementById('gameSetSearch').value);
|
||||
}
|
||||
|
||||
function onGameSetSearch(value) {
|
||||
clearTimeout(_gameSetSearchTimer);
|
||||
if (value.length === 0) {
|
||||
_gameSetSearchTimer = setTimeout(() => doGameSetSearch(''), 0);
|
||||
} else if (value.length < 2) {
|
||||
document.getElementById('gameSetDropdown').style.display = 'none';
|
||||
} else {
|
||||
_gameSetSearchTimer = setTimeout(() => doGameSetSearch(value), 300);
|
||||
}
|
||||
}
|
||||
|
||||
async function doGameSetSearch(search) {
|
||||
try {
|
||||
const url = '/gruppe/chastity' + (search ? '?search=' + encodeURIComponent(search) : '');
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
_gameSetResults = data.gruppen || [];
|
||||
renderGameSetDropdown();
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
function renderGameSetDropdown() {
|
||||
const dd = document.getElementById('gameSetDropdown');
|
||||
if (!dd) return;
|
||||
if (!_gameSetResults.length) { dd.style.display = 'none'; return; }
|
||||
dd.innerHTML = _gameSetResults.map(g => `
|
||||
<div class="gs-dropdown-item" onclick="selectGameSet('${esc(g.gruppenId)}','${esc(g.name).replace(/'/g, "\\'")}')">
|
||||
<div class="gs-item-name">${esc(g.name)}</div>
|
||||
${g.beschreibung ? `<div class="gs-item-desc">${esc(g.beschreibung)}</div>` : ''}
|
||||
</div>`).join('');
|
||||
dd.style.display = 'block';
|
||||
}
|
||||
|
||||
function selectGameSet(id, name, suppressDirty = false) {
|
||||
document.getElementById('fGameSetId').value = id;
|
||||
document.getElementById('gameSetSearch').value = '';
|
||||
document.getElementById('gameSetDropdown').style.display = 'none';
|
||||
document.getElementById('gameSetSelected').innerHTML =
|
||||
`<span style="flex:1;">${esc(name)}</span>
|
||||
<button type="button" onclick="clearGameSet()" title="Auswahl entfernen">✕</button>`;
|
||||
document.getElementById('gameSetSelected').style.display = 'flex';
|
||||
document.getElementById('errGameSet').style.display = 'none';
|
||||
if (!suppressDirty) markDirty();
|
||||
}
|
||||
|
||||
function clearGameSet() {
|
||||
document.getElementById('fGameSetId').value = '';
|
||||
document.getElementById('gameSetSearch').value = '';
|
||||
document.getElementById('gameSetSelected').style.display = 'none';
|
||||
document.getElementById('gameSetSelected').innerHTML = '';
|
||||
markDirty();
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const search = document.getElementById('gameSetSearch');
|
||||
const dd = document.getElementById('gameSetDropdown');
|
||||
if (dd && search && !search.contains(e.target) && !dd.contains(e.target)) {
|
||||
dd.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// ── Karten-Info ──
|
||||
function openCardInfo(cardId) {
|
||||
const c = CARD_DEFS.find(x => x.id === cardId); if (!c) return;
|
||||
@@ -1223,51 +1210,6 @@
|
||||
if (res.ok || res.status === 204) await loadTaskSets();
|
||||
}
|
||||
|
||||
const GS_TOOLS = [
|
||||
{ value: 'UMSCHNALLDILDO', label: 'Strap-on' },
|
||||
{ value: 'MUND', label: 'Oral' },
|
||||
{ value: 'ANUS', label: 'Anal' },
|
||||
];
|
||||
function gsGetChecked(prefix) {
|
||||
return GS_TOOLS.filter(t => document.getElementById(prefix + t.value)?.checked).map(t => t.value);
|
||||
}
|
||||
function gsSetChecked(prefix, values) {
|
||||
GS_TOOLS.forEach(t => {
|
||||
const el = document.getElementById(prefix + t.value);
|
||||
if (el) el.checked = (values || []).includes(t.value);
|
||||
});
|
||||
}
|
||||
|
||||
const GAME_SPIELDAUER = [
|
||||
{ label: 'Sehr kurz' },
|
||||
{ label: 'Kurz' },
|
||||
{ label: 'Mittel' },
|
||||
{ label: 'Lang' },
|
||||
{ label: 'Sehr lang' },
|
||||
];
|
||||
|
||||
function populateGameSetSelect() {
|
||||
const sel = document.getElementById('fGameSetId');
|
||||
if (!sel) return;
|
||||
const cur = sel.value;
|
||||
sel.innerHTML = '<option value="">Kein Spiel-Set</option>';
|
||||
_gameSets.forEach(s => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.id; opt.textContent = s.name;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
sel.value = cur;
|
||||
}
|
||||
|
||||
function onGameSetChange() {
|
||||
const val = document.getElementById('fGameSetId')?.value;
|
||||
document.getElementById('gameSetSpieldauerRow').style.display = val ? '' : 'none';
|
||||
}
|
||||
|
||||
function updateGameSpieldauer(val) {
|
||||
document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[val]?.label || '';
|
||||
}
|
||||
|
||||
function populateTaskSetSelects() {
|
||||
for (const selId of ['fCardTaskSetId', 'fTimelockTaskSetId']) {
|
||||
const sel = document.getElementById(selId);
|
||||
@@ -1424,6 +1366,19 @@
|
||||
tpFromMinutes('pe', template?.pickEveryMinute || 60);
|
||||
document.getElementById('fAccumulate').checked = template?.accumulatePicks || false;
|
||||
document.getElementById('fShowRemaining').checked = template?.showRemainingCards || false;
|
||||
|
||||
// Spiel-Karte
|
||||
clearGameSet();
|
||||
checkGameCardSection();
|
||||
const gsi = template?.gameSpieldauerIdx ?? 2;
|
||||
document.getElementById('sldGameSpieldauer').value = gsi;
|
||||
updateGameSpieldauer(gsi);
|
||||
if (template?.gameSetId) {
|
||||
fetch(`/gruppe/${template.gameSetId}`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(g => { if (g?.name) selectGameSet(template.gameSetId, g.name, true); })
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'TIMELOCK') {
|
||||
@@ -1476,14 +1431,6 @@
|
||||
onTaskSetChange('card');
|
||||
onTaskSetChange('timelock');
|
||||
|
||||
// Spiel-Set
|
||||
populateGameSetSelect();
|
||||
document.getElementById('fGameSetId').value = template?.gameSetId || '';
|
||||
onGameSetChange();
|
||||
const sdIdx = template?.gameSpieldauerIdx ?? 2;
|
||||
document.getElementById('sldGameSpieldauer').value = sdIdx;
|
||||
updateGameSpieldauer(sdIdx);
|
||||
|
||||
alignModalToContent();
|
||||
document.getElementById('modalBackdrop').classList.add('open');
|
||||
document.getElementById('modalDiscardConfirm').style.display = 'none';
|
||||
@@ -1527,13 +1474,7 @@
|
||||
document.getElementById('modalBackdrop').addEventListener('click', e => { if (e.target===e.currentTarget) tryCloseModal(); });
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (document.getElementById('gsItemModal').classList.contains('open')) {
|
||||
e.preventDefault(); closeGsItemModal();
|
||||
} else if (document.getElementById('gsSetModal').classList.contains('open')) {
|
||||
e.preventDefault(); closeGsSetModal();
|
||||
} else if (document.getElementById('gsEditModal').classList.contains('open')) {
|
||||
e.preventDefault(); closeGsEditModal();
|
||||
} else if (document.getElementById('taskSetModalBackdrop').classList.contains('open')) {
|
||||
if (document.getElementById('taskSetModalBackdrop').classList.contains('open')) {
|
||||
e.preventDefault(); tryCloseTaskSetModal();
|
||||
} else if (document.getElementById('modalBackdrop').classList.contains('open')) {
|
||||
e.preventDefault(); tryCloseModal();
|
||||
@@ -1589,8 +1530,13 @@
|
||||
const hasTaskCards = (parseInt(document.getElementById('min_TASK').value)||0)>0 || (parseInt(document.getElementById('max_TASK').value)||0)>0;
|
||||
if (hasTaskCards && !document.getElementById('fCardTaskSetId').value) { showModalError('Aufgaben-Karten konfiguriert, aber kein Aufgaben-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); }
|
||||
const hasGameCards = (parseInt(document.getElementById('min_GAME_CARD').value)||0)>0 || (parseInt(document.getElementById('max_GAME_CARD').value)||0)>0;
|
||||
if (hasGameCards && !document.getElementById('fGameSetId').value) { showModalError('Spiel-Karten konfiguriert, aber kein Spiel-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); }
|
||||
|
||||
if (hasGameCards && !document.getElementById('fGameSetId').value) {
|
||||
document.getElementById('errGameSet').style.display = '';
|
||||
showModalError('Spiel-Karten konfiguriert, aber kein Aufgaben-Set ausgewählt.');
|
||||
firstError = firstError || document.getElementById('errGameSet');
|
||||
} else {
|
||||
document.getElementById('errGameSet').style.display = 'none';
|
||||
}
|
||||
if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; }
|
||||
|
||||
const cardCountsMin={}, cardCountsMax={};
|
||||
@@ -1609,8 +1555,8 @@
|
||||
taskSetId: document.getElementById('fCardTaskSetId').value || null,
|
||||
requiresVerification: document.getElementById('fRequiresVerification').checked,
|
||||
taskMode: document.querySelector('input[name="modalCardTaskMode"]:checked')?.value||'RANDOM',
|
||||
gameSetId: document.getElementById('fGameSetId').value || null,
|
||||
gameSpieldauerIdx: parseInt(document.getElementById('sldGameSpieldauer').value) || 2,
|
||||
gameSetId: hasGameCards ? (document.getElementById('fGameSetId').value || null) : null,
|
||||
gameSpieldauerIdx: hasGameCards ? (parseInt(document.getElementById('sldGameSpieldauer').value) || 2) : null,
|
||||
};
|
||||
} else {
|
||||
// TimeLock
|
||||
@@ -1895,426 +1841,8 @@
|
||||
observer.observe(document.getElementById('scrollSentinel'));
|
||||
|
||||
resetList();
|
||||
loadGameSets();
|
||||
|
||||
// ════════════════════════════════════════════════
|
||||
// Spiel-Sets
|
||||
// ════════════════════════════════════════════════
|
||||
|
||||
let _gameSets = [];
|
||||
let _gsEditSetId = null; // set being renamed
|
||||
let _gsSetCaller = null; // 'template' when opened from the template modal
|
||||
let _gsOpenSetId = null; // set currently open in the content popup
|
||||
let _gsItemType = null; // 'aufgabe' | 'zeitstrafe' | 'finisher'
|
||||
let _gsItemSetId = null;
|
||||
let _gsItemIdx = null; // null = new, number = editing
|
||||
|
||||
async function loadGameSets() {
|
||||
try {
|
||||
const res = await fetch('/chastity/game-sets');
|
||||
if (!res.ok) return;
|
||||
_gameSets = await res.json();
|
||||
renderGameSetList();
|
||||
populateGameSetSelect();
|
||||
if (_gsOpenSetId) renderGsEditModalContent(_gsOpenSetId);
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
function renderGameSetList() {
|
||||
const list = document.getElementById('gameSetList');
|
||||
list.innerHTML = '';
|
||||
document.getElementById('gameSetEmpty').style.display = _gameSets.length ? 'none' : '';
|
||||
document.getElementById('btnNewGameSet').disabled = _gameSets.length >= 5;
|
||||
|
||||
_gameSets.forEach(s => {
|
||||
const aufgaben = s.aufgaben || [];
|
||||
const zeitstrafen = s.zeitstrafen || [];
|
||||
const finisher = s.finisher || [];
|
||||
const levelCounts = [1,2,3,4,5].map(l => aufgaben.filter(a => a.level === l).length);
|
||||
|
||||
const lvlBadges = levelCounts.map((c, i) => {
|
||||
const cls = c >= 3 ? 'gs-badge gs-badge-neutral' : 'gs-badge';
|
||||
return `<span class="${cls}">L${i+1}: ${c}</span>`;
|
||||
}).join('');
|
||||
const finBadgeCls = finisher.length >= 1 ? 'gs-badge gs-badge-neutral' : 'gs-badge';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'gs-card';
|
||||
card.id = 'gscard_' + s.id;
|
||||
card.addEventListener('click', () => openGsEditModal(s.id));
|
||||
card.innerHTML = `
|
||||
<div class="gs-card-header">
|
||||
<div class="gs-card-meta">
|
||||
<div class="gs-card-name">${esc(s.name)}</div>
|
||||
<div style="display:flex;gap:0.3rem;flex-wrap:wrap;margin-top:0.3rem;">
|
||||
${lvlBadges}
|
||||
<span class="gs-badge gs-badge-neutral">Zeitstrafen: ${zeitstrafen.length}</span>
|
||||
<span class="${finBadgeCls}">Finisher: ${finisher.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gs-card-header-actions" onclick="event.stopPropagation()">
|
||||
<button class="gs-btn-item-edit" onclick="openGsSetModal('${s.id}')">✎</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGameSet('${s.id}',${JSON.stringify(s.name)})">✕</button>
|
||||
</div>
|
||||
</div>`;
|
||||
list.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleGsListItem(id) {
|
||||
document.getElementById(id)?.classList.toggle('open');
|
||||
}
|
||||
|
||||
// ── Set content popup ──────────────────────────
|
||||
|
||||
function openGsEditModal(setId) {
|
||||
_gsOpenSetId = setId;
|
||||
renderGsEditModalContent(setId);
|
||||
document.getElementById('gsEditModal').classList.add('open');
|
||||
}
|
||||
|
||||
function closeGsEditModal() {
|
||||
document.getElementById('gsEditModal').classList.remove('open');
|
||||
_gsOpenSetId = null;
|
||||
}
|
||||
|
||||
function renderGsEditModalContent(setId) {
|
||||
const container = document.getElementById('gsEditModalContent');
|
||||
if (!container) return;
|
||||
const s = _gameSets.find(x => x.id === setId);
|
||||
if (!s) { closeGsEditModal(); return; }
|
||||
document.getElementById('gsEditModalTitle').textContent = s.name;
|
||||
const aufgaben = s.aufgaben || [];
|
||||
const zeitstrafen = s.zeitstrafen || [];
|
||||
const finisher = s.finisher || [];
|
||||
let html = '';
|
||||
for (let l = 1; l <= 5; l++) {
|
||||
const items = aufgaben.map((a, i) => ({...a, _gi: i})).filter(a => a.level === l);
|
||||
const warnCls = items.length < 3 ? ' gs-sub-warn' : '';
|
||||
const itemsHtml = items.map(a => gsAufgabeRowHtml(s.id, a._gi, a)).join('') ||
|
||||
'<div class="gs-sub-empty">–</div>';
|
||||
html += `<div class="gs-sub">
|
||||
<div class="gs-sub-header">
|
||||
<span class="gs-sub-title${warnCls}">Level ${l} <span style="font-weight:400;">(${items.length}/3+)</span></span>
|
||||
<button class="gs-btn-sub-add" onclick="openGsItemModal('aufgabe','${s.id}',null,${l})">+ Aufgabe</button>
|
||||
</div>
|
||||
<div class="gs-item-list">${itemsHtml}</div></div>`;
|
||||
}
|
||||
const zeitHtml = zeitstrafen.map((z, i) => gsZeitstrafeRowHtml(s.id, i, z)).join('') ||
|
||||
'<div class="gs-sub-empty">–</div>';
|
||||
html += `<div class="gs-sub">
|
||||
<div class="gs-sub-header">
|
||||
<span class="gs-sub-title">Zeitstrafen <span style="font-weight:400;">(${zeitstrafen.length})</span></span>
|
||||
<button class="gs-btn-sub-add" onclick="openGsItemModal('zeitstrafe','${s.id}',null,null)">+ Zeitstrafe</button>
|
||||
</div>
|
||||
<div class="gs-item-list">${zeitHtml}</div></div>`;
|
||||
const finWarnCls = finisher.length < 1 ? ' gs-sub-warn' : '';
|
||||
const finHtml = finisher.map((f, i) => gsFinisherRowHtml(s.id, i, f)).join('') ||
|
||||
'<div class="gs-sub-empty">–</div>';
|
||||
html += `<div class="gs-sub">
|
||||
<div class="gs-sub-header">
|
||||
<span class="gs-sub-title${finWarnCls}">Finisher <span style="font-weight:400;">(${finisher.length}/1+)</span></span>
|
||||
<button class="gs-btn-sub-add" onclick="openGsItemModal('finisher','${s.id}',null,null)">+ Finisher</button>
|
||||
</div>
|
||||
<div class="gs-item-list">${finHtml}</div></div>`;
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Row HTML helpers ───────────────────────────
|
||||
|
||||
function gsAufgabeRowHtml(setId, gi, a) {
|
||||
const toolLabels = (a.benoetigt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean);
|
||||
const badges = [
|
||||
a.minutes ? `<span class="gs-badge gs-badge-neutral">${a.minutes} Min.</span>` : '',
|
||||
...toolLabels.map(l => `<span class="gs-badge gs-badge-neutral">${l}</span>`),
|
||||
].join('');
|
||||
const desc = a.description ? `<div class="gs-detail-text">${esc(a.description)}</div>` : '';
|
||||
return `<div class="gs-list-item" id="gsli_${setId}_a_${gi}">
|
||||
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_a_${gi}')">
|
||||
<span class="gs-list-item-text">${esc(a.title)}</span>
|
||||
<div class="gs-list-item-badges">${badges}</div>
|
||||
</div>
|
||||
<div class="gs-list-item-detail">${desc}
|
||||
<div class="gs-detail-actions">
|
||||
<button class="gs-btn-item-edit" onclick="openGsItemModal('aufgabe','${setId}',${gi},null)">✎ Bearbeiten</button>
|
||||
<button class="gs-btn-item-edit" onclick="duplicateGsItem('aufgabe','${setId}',${gi})">⧉ Kopie</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGsItem('aufgabe','${setId}',${gi})">✕ Löschen</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
function gsZeitstrafeRowHtml(setId, idx, z) {
|
||||
const timeStr = (z.minMinutes != null ? z.minMinutes : '?') + '–' + (z.maxMinutes != null ? z.maxMinutes : '?') + ' Min.';
|
||||
const sperrtLabels = (z.sperrt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean);
|
||||
const badges = [
|
||||
z.level ? `<span class="gs-badge">L${z.level}</span>` : '',
|
||||
`<span class="gs-badge gs-badge-neutral">${timeStr}</span>`,
|
||||
...sperrtLabels.map(l => `<span class="gs-badge gs-badge-neutral">🔒 ${l}</span>`),
|
||||
z.releaseText ? `<span class="gs-badge gs-badge-neutral">📝 Aufhebung</span>` : '',
|
||||
z.tempUnlockBeforeRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Vorher</span>` : '',
|
||||
z.tempUnlockAfterRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Nachher</span>` : '',
|
||||
].join('');
|
||||
const releaseRow = z.releaseText ? `<div style="font-size:0.75rem;color:var(--color-muted);margin-bottom:0.15rem;">Bei Aufhebung:</div><div class="gs-detail-text">${esc(z.releaseText)}</div>` : '';
|
||||
const desc = z.description ? `<div class="gs-detail-text">${esc(z.description)}</div>` : '';
|
||||
return `<div class="gs-list-item" id="gsli_${setId}_z_${idx}">
|
||||
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_z_${idx}')">
|
||||
<span class="gs-list-item-text">${esc(z.title)}</span>
|
||||
<div class="gs-list-item-badges">${badges}</div>
|
||||
</div>
|
||||
<div class="gs-list-item-detail">${desc}${releaseRow}
|
||||
<div class="gs-detail-actions">
|
||||
<button class="gs-btn-item-edit" onclick="openGsItemModal('zeitstrafe','${setId}',${idx},null)">✎ Bearbeiten</button>
|
||||
<button class="gs-btn-item-edit" onclick="duplicateGsItem('zeitstrafe','${setId}',${idx})">⧉ Kopie</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGsItem('zeitstrafe','${setId}',${idx})">✕ Löschen</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
function gsFinisherRowHtml(setId, idx, f) {
|
||||
const badges = [
|
||||
f.tempUnlockBeforeRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Vorher</span>` : '',
|
||||
f.tempUnlockAfterRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Nachher</span>` : '',
|
||||
].join('');
|
||||
const desc = f.description ? `<div class="gs-detail-text">${esc(f.description)}</div>` : '';
|
||||
return `<div class="gs-list-item" id="gsli_${setId}_f_${idx}">
|
||||
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_f_${idx}')">
|
||||
<span class="gs-list-item-text">${esc(f.title)}</span>
|
||||
<div class="gs-list-item-badges">${badges}</div>
|
||||
</div>
|
||||
<div class="gs-list-item-detail">${desc}
|
||||
<div class="gs-detail-actions">
|
||||
<button class="gs-btn-item-edit" onclick="openGsItemModal('finisher','${setId}',${idx},null)">✎ Bearbeiten</button>
|
||||
<button class="gs-btn-item-edit" onclick="duplicateGsItem('finisher','${setId}',${idx})">⧉ Kopie</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGsItem('finisher','${setId}',${idx})">✕ Löschen</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
// ── Set create / rename modal ──────────────────
|
||||
|
||||
function openGsSetModal(id, caller) {
|
||||
_gsEditSetId = id || null;
|
||||
_gsSetCaller = caller || null;
|
||||
document.getElementById('gsSetModalTitle').textContent = id ? 'Spiel-Set umbenennen' : 'Neues Spiel-Set';
|
||||
document.getElementById('gsSetName').value = id ? (_gameSets.find(s => s.id === id)?.name || '') : '';
|
||||
document.getElementById('gsSetError').style.display = 'none';
|
||||
document.getElementById('gsSetModal').classList.add('open');
|
||||
setTimeout(() => document.getElementById('gsSetName').focus(), 50);
|
||||
}
|
||||
|
||||
function closeGsSetModal() {
|
||||
document.getElementById('gsSetModal').classList.remove('open');
|
||||
_gsEditSetId = _gsSetCaller = null;
|
||||
}
|
||||
|
||||
async function saveGsSet() {
|
||||
const name = document.getElementById('gsSetName').value.trim();
|
||||
const errEl = document.getElementById('gsSetError');
|
||||
if (!name) { errEl.textContent = 'Name ist ein Pflichtfeld.'; errEl.style.display = ''; return; }
|
||||
errEl.style.display = 'none';
|
||||
const set = _gsEditSetId ? _gameSets.find(s => s.id === _gsEditSetId) : null;
|
||||
const url = _gsEditSetId ? `/chastity/game-sets/${_gsEditSetId}` : '/chastity/game-sets';
|
||||
const method = _gsEditSetId ? 'PUT' : 'POST';
|
||||
const body = { name,
|
||||
aufgaben: set?.aufgaben || [],
|
||||
zeitstrafen: set?.zeitstrafen || [],
|
||||
finisher: set?.finisher || [] };
|
||||
try {
|
||||
const res = await fetch(url, { method, headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
|
||||
if (res.ok) {
|
||||
const saved = await res.json().catch(() => null);
|
||||
const caller = _gsSetCaller;
|
||||
closeGsSetModal();
|
||||
await loadGameSets();
|
||||
if (caller === 'template' && saved?.id) {
|
||||
document.getElementById('fGameSetId').value = saved.id;
|
||||
onGameSetChange();
|
||||
markDirty();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const b = await res.json().catch(() => ({}));
|
||||
errEl.textContent = b.error || 'Fehler.'; errEl.style.display = '';
|
||||
} catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; }
|
||||
}
|
||||
|
||||
async function deleteGameSet(id, name) {
|
||||
if (!confirm(`Spiel-Set „${name}" wirklich löschen?`)) return;
|
||||
const res = await fetch(`/chastity/game-sets/${id}`, { method: 'DELETE' });
|
||||
if (res.ok || res.status === 204) loadGameSets();
|
||||
}
|
||||
|
||||
// ── Item modal ─────────────────────────────────
|
||||
|
||||
function openGsItemModal(type, setId, itemIdx, contextLevel) {
|
||||
_gsItemType = type;
|
||||
_gsItemSetId = setId;
|
||||
_gsItemIdx = itemIdx !== null && itemIdx !== undefined ? itemIdx : null;
|
||||
|
||||
const titles = { aufgabe: 'Aufgabe', zeitstrafe: 'Zeitstrafe', finisher: 'Finisher' };
|
||||
document.getElementById('gsItemModalTitle').textContent =
|
||||
(_gsItemIdx !== null ? 'Bearbeiten: ' : 'Neu: ') + titles[type];
|
||||
|
||||
// Reset fields
|
||||
document.getElementById('gsItemTitle').value = '';
|
||||
document.getElementById('gsItemDesc').value = '';
|
||||
document.getElementById('gsItemMinutes').value = '';
|
||||
document.getElementById('gsItemMinMin').value = '';
|
||||
document.getElementById('gsItemMaxMin').value = '';
|
||||
document.getElementById('gsItemReleaseText').value = '';
|
||||
document.getElementById('gsItemBefore').checked = false;
|
||||
document.getElementById('gsItemAfter').checked = false;
|
||||
document.getElementById('gsItemAufgabeLevel').value = contextLevel || 1;
|
||||
document.getElementById('gsItemZeitstrafeLevel').value = 1;
|
||||
document.getElementById('gsItemError').style.display = 'none';
|
||||
gsSetChecked('gsItemBen_', []);
|
||||
gsSetChecked('gsItemSperr_', []);
|
||||
|
||||
// Show/hide type-specific rows
|
||||
document.getElementById('gsItemAufgabeRow').style.display = type === 'aufgabe' ? '' : 'none';
|
||||
document.getElementById('gsItemBenoetigtRow').style.display = type === 'aufgabe' ? '' : 'none';
|
||||
document.getElementById('gsItemZeitstrafeRow').style.display = type === 'zeitstrafe' ? '' : 'none';
|
||||
document.getElementById('gsItemSperrtRow').style.display = type === 'zeitstrafe' ? '' : 'none';
|
||||
document.getElementById('gsItemUnlockRow').style.display = (type === 'zeitstrafe' || type === 'finisher') ? '' : 'none';
|
||||
|
||||
// Pre-fill when editing
|
||||
if (_gsItemIdx !== null) {
|
||||
const set = _gameSets.find(s => s.id === setId);
|
||||
if (set) {
|
||||
let item;
|
||||
if (type === 'aufgabe') item = set.aufgaben[_gsItemIdx];
|
||||
if (type === 'zeitstrafe') item = set.zeitstrafen[_gsItemIdx];
|
||||
if (type === 'finisher') item = set.finisher[_gsItemIdx];
|
||||
if (item) {
|
||||
document.getElementById('gsItemTitle').value = item.title || '';
|
||||
document.getElementById('gsItemDesc').value = item.description || '';
|
||||
if (type === 'aufgabe') {
|
||||
document.getElementById('gsItemAufgabeLevel').value = item.level || 1;
|
||||
document.getElementById('gsItemMinutes').value = item.minutes || '';
|
||||
gsSetChecked('gsItemBen_', item.benoetigt || []);
|
||||
}
|
||||
if (type === 'zeitstrafe') {
|
||||
document.getElementById('gsItemZeitstrafeLevel').value = item.level || 1;
|
||||
document.getElementById('gsItemMinMin').value = item.minMinutes ?? '';
|
||||
document.getElementById('gsItemMaxMin').value = item.maxMinutes ?? '';
|
||||
document.getElementById('gsItemReleaseText').value = item.releaseText || '';
|
||||
gsSetChecked('gsItemSperr_', item.sperrt || []);
|
||||
}
|
||||
if (type === 'zeitstrafe' || type === 'finisher') {
|
||||
document.getElementById('gsItemBefore').checked = !!item.tempUnlockBeforeRequired;
|
||||
document.getElementById('gsItemAfter').checked = !!item.tempUnlockAfterRequired;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('gsItemModal').classList.add('open');
|
||||
setTimeout(() => document.getElementById('gsItemTitle').focus(), 50);
|
||||
}
|
||||
|
||||
function closeGsItemModal() {
|
||||
document.getElementById('gsItemModal').classList.remove('open');
|
||||
_gsItemType = _gsItemSetId = _gsItemIdx = null;
|
||||
}
|
||||
|
||||
async function saveGsItem() {
|
||||
const title = document.getElementById('gsItemTitle').value.trim();
|
||||
const errEl = document.getElementById('gsItemError');
|
||||
if (!title) { errEl.textContent = 'Titel ist ein Pflichtfeld.'; errEl.style.display = ''; return; }
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const set = _gameSets.find(s => s.id === _gsItemSetId);
|
||||
if (!set) return;
|
||||
const updated = {
|
||||
name: set.name,
|
||||
aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
|
||||
zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
|
||||
finisher: JSON.parse(JSON.stringify(set.finisher || [])),
|
||||
};
|
||||
|
||||
const desc = document.getElementById('gsItemDesc').value.trim() || null;
|
||||
let item;
|
||||
if (_gsItemType === 'aufgabe') {
|
||||
const min = parseInt(document.getElementById('gsItemMinutes').value);
|
||||
const ben = gsGetChecked('gsItemBen_');
|
||||
item = { title, description: desc,
|
||||
level: parseInt(document.getElementById('gsItemAufgabeLevel').value) || 1,
|
||||
minutes: isNaN(min) ? null : min,
|
||||
benoetigt: ben.length ? ben : null };
|
||||
if (_gsItemIdx !== null) updated.aufgaben[_gsItemIdx] = item;
|
||||
else updated.aufgaben.push(item);
|
||||
} else if (_gsItemType === 'zeitstrafe') {
|
||||
const minMin = parseInt(document.getElementById('gsItemMinMin').value);
|
||||
const maxMin = parseInt(document.getElementById('gsItemMaxMin').value);
|
||||
const sperrt = gsGetChecked('gsItemSperr_');
|
||||
const releaseText = document.getElementById('gsItemReleaseText').value.trim() || null;
|
||||
item = { title, description: desc,
|
||||
level: parseInt(document.getElementById('gsItemZeitstrafeLevel').value) || 1,
|
||||
minMinutes: isNaN(minMin) ? null : minMin,
|
||||
maxMinutes: isNaN(maxMin) ? null : maxMin,
|
||||
releaseText,
|
||||
tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked,
|
||||
tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked,
|
||||
sperrt: sperrt.length ? sperrt : null };
|
||||
if (_gsItemIdx !== null) updated.zeitstrafen[_gsItemIdx] = item;
|
||||
else updated.zeitstrafen.push(item);
|
||||
} else if (_gsItemType === 'finisher') {
|
||||
item = { title, description: desc,
|
||||
tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked,
|
||||
tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked };
|
||||
if (_gsItemIdx !== null) updated.finisher[_gsItemIdx] = item;
|
||||
else updated.finisher.push(item);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/chastity/game-sets/${_gsItemSetId}`, {
|
||||
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
|
||||
});
|
||||
if (res.ok) { closeGsItemModal(); await loadGameSets(); }
|
||||
else { const b = await res.json().catch(()=>({})); errEl.textContent = b.error||'Fehler.'; errEl.style.display = ''; }
|
||||
} catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; }
|
||||
}
|
||||
|
||||
async function deleteGsItem(type, setId, idx) {
|
||||
if (!confirm('Eintrag wirklich löschen?')) return;
|
||||
const set = _gameSets.find(s => s.id === setId);
|
||||
if (!set) return;
|
||||
const updated = {
|
||||
name: set.name,
|
||||
aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
|
||||
zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
|
||||
finisher: JSON.parse(JSON.stringify(set.finisher || [])),
|
||||
};
|
||||
if (type === 'aufgabe') updated.aufgaben.splice(idx, 1);
|
||||
if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx, 1);
|
||||
if (type === 'finisher') updated.finisher.splice(idx, 1);
|
||||
const res = await fetch(`/chastity/game-sets/${setId}`, {
|
||||
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
|
||||
});
|
||||
if (res.ok) loadGameSets();
|
||||
}
|
||||
|
||||
async function duplicateGsItem(type, setId, idx) {
|
||||
const set = _gameSets.find(s => s.id === setId);
|
||||
if (!set) return;
|
||||
const updated = {
|
||||
name: set.name,
|
||||
aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
|
||||
zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
|
||||
finisher: JSON.parse(JSON.stringify(set.finisher || [])),
|
||||
};
|
||||
if (type === 'aufgabe') updated.aufgaben.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.aufgaben[idx])));
|
||||
if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.zeitstrafen[idx])));
|
||||
if (type === 'finisher') updated.finisher.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.finisher[idx])));
|
||||
const res = await fetch(`/chastity/game-sets/${setId}`, {
|
||||
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
|
||||
});
|
||||
if (res.ok) loadGameSets();
|
||||
}
|
||||
|
||||
document.getElementById('gsSetModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsSetModal(); });
|
||||
document.getElementById('gsItemModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsItemModal(); });
|
||||
document.getElementById('gsEditModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsEditModal(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user