Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
531 lines
27 KiB
HTML
531 lines
27 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<link rel="icon" href="/img/icon.png" type="image/png">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Keyholder finden – xXx Sphere</title>
|
||
<link rel="stylesheet" href="/css/variables.css">
|
||
<link rel="stylesheet" href="/css/style.css">
|
||
<style>
|
||
.offer-card {
|
||
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||
border-radius: 10px; padding: 1rem; margin-bottom: 0.75rem;
|
||
display: flex; align-items: center; gap: 0.85rem;
|
||
}
|
||
.offer-avatar {
|
||
width: 48px; height: 48px; border-radius: 50%;
|
||
background: var(--color-secondary); border: 1px solid rgba(255,255,255,0.08);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 1.3rem; flex-shrink: 0; overflow: hidden;
|
||
}
|
||
.offer-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||
.offer-body { flex: 1; min-width: 0; }
|
||
.offer-name { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.2rem;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.offer-sub { font-size: 0.78rem; color: var(--color-muted); margin-bottom: 0.3rem; }
|
||
.offer-tags { display: flex; flex-wrap: wrap; gap: 0.35rem; }
|
||
.offer-badge {
|
||
display: inline-block; font-size: 0.72rem; padding: 0.1rem 0.45rem;
|
||
border-radius: 4px; background: rgba(255,255,255,0.07); border: 1px solid var(--color-secondary);
|
||
}
|
||
.offer-badge.direct { background: rgba(46,204,113,0.12); border-color: rgba(46,204,113,0.3); color: #2ecc71; }
|
||
.offer-badge.confirm { background: rgba(230,126,34,0.12); border-color: rgba(230,126,34,0.3); color: #e67e22; }
|
||
.btn-join {
|
||
background: var(--color-primary); border: none; color: #fff;
|
||
border-radius: 7px; padding: 0.4rem 1rem; font-size: 0.85rem;
|
||
font-weight: 600; cursor: pointer; flex-shrink: 0; width: auto;
|
||
}
|
||
.btn-join:disabled { opacity: 0.45; cursor: default; }
|
||
|
||
/* Klickbarer Card-Bereich */
|
||
.offer-card-clickable { cursor: pointer; }
|
||
.offer-card-clickable:hover { background: var(--color-secondary); }
|
||
|
||
/* Detail-Dialog */
|
||
.detail-backdrop {
|
||
display: none; position: fixed; inset: 0;
|
||
background: rgba(0,0,0,0.65); z-index: 500;
|
||
align-items: flex-start; justify-content: center;
|
||
overflow-y: auto; padding: 2rem 1rem;
|
||
}
|
||
.detail-backdrop.open { display: flex; }
|
||
.detail-box {
|
||
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||
border-radius: 14px; padding: 1.5rem; max-width: 520px; width: 100%;
|
||
display: flex; flex-direction: column; gap: 1rem; position: relative;
|
||
}
|
||
.detail-section { margin-bottom: 0.25rem; }
|
||
.detail-section-title {
|
||
font-size: 0.72rem; font-weight: 700; color: var(--color-primary);
|
||
text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 0.5rem;
|
||
}
|
||
.detail-row {
|
||
display: flex; justify-content: space-between; gap: 1rem;
|
||
padding: 0.25rem 0; border-bottom: 1px solid rgba(255,255,255,0.05);
|
||
font-size: 0.88rem;
|
||
}
|
||
.detail-row:last-child { border-bottom: none; }
|
||
.detail-row-label { color: var(--color-muted); flex-shrink: 0; }
|
||
.detail-row-val { color: var(--color-text); text-align: right; }
|
||
.detail-task-item {
|
||
font-size: 0.88rem; padding: 0.35rem 0;
|
||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||
}
|
||
.detail-task-item:last-child { border-bottom: none; }
|
||
.detail-wheel-entry {
|
||
display: inline-block; font-size: 0.8rem; padding: 0.15rem 0.5rem;
|
||
border-radius: 4px; background: rgba(255,255,255,0.07);
|
||
border: 1px solid var(--color-secondary); margin: 0.15rem 0.2rem 0.15rem 0;
|
||
}
|
||
.detail-footer {
|
||
display: flex; gap: 0.6rem; justify-content: flex-end;
|
||
border-top: 1px solid var(--color-secondary); padding-top: 1rem; margin-top: 0.25rem;
|
||
}
|
||
.btn-close-detail {
|
||
background: none; border: 1px solid var(--color-secondary);
|
||
color: var(--color-muted); padding: 0.5rem 1.1rem;
|
||
border-radius: 7px; cursor: pointer; font-size: 0.88rem; width: auto;
|
||
}
|
||
.btn-join-detail {
|
||
background: var(--color-primary); border: none; color: #fff;
|
||
border-radius: 7px; padding: 0.5rem 1.25rem; font-size: 0.88rem;
|
||
font-weight: 600; cursor: pointer; width: auto;
|
||
}
|
||
.btn-join-detail:disabled { opacity: 0.45; cursor: default; }
|
||
.detail-author-avatar {
|
||
width: 52px; height: 52px; border-radius: 50%;
|
||
background: var(--color-secondary); border: 1px solid rgba(255,255,255,0.1);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 1.4rem; overflow: hidden; flex-shrink: 0;
|
||
}
|
||
.detail-author-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||
|
||
/* Join-Dialog */
|
||
.join-modal-backdrop {
|
||
display: none; position: fixed; inset: 0;
|
||
background: rgba(0,0,0,0.65); z-index: 500;
|
||
align-items: center; justify-content: center;
|
||
}
|
||
.join-modal-backdrop.open { display: flex; }
|
||
.join-modal-box {
|
||
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||
border-radius: 14px; padding: 1.5rem; max-width: 400px; width: 92%;
|
||
display: flex; flex-direction: column; gap: 1rem; position: relative;
|
||
}
|
||
.form-group { display: flex; flex-direction: column; gap: 0.35rem; }
|
||
.form-label { font-size: 0.72rem; font-weight: 700; color: var(--color-primary);
|
||
text-transform: uppercase; letter-spacing: 0.06em; }
|
||
</style>
|
||
</head>
|
||
<body class="app">
|
||
<div class="main">
|
||
<div class="content">
|
||
<h2 style="margin-bottom:1.25rem;">🔍 Keyholder finden</h2>
|
||
<p style="font-size:0.88rem;color:var(--color-muted);margin-bottom:1.25rem;line-height:1.5;">
|
||
Hier findest du Nutzer*innen, die sich als Keyholder für ein bestimmtes Lock-Template anbieten.
|
||
Die beliebtesten Angebote erscheinen ganz oben.
|
||
</p>
|
||
|
||
<div id="offerList"></div>
|
||
<p id="listEmpty" style="display:none;color:var(--color-muted);">Keine Keyholder-Angebote gefunden.</p>
|
||
<p id="listLoading" style="color:var(--color-muted);">Wird geladen…</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Detail-Dialog -->
|
||
<div class="detail-backdrop" id="detailModal" onclick="closeDetail()">
|
||
<div class="detail-box" onclick="event.stopPropagation()">
|
||
<button onclick="closeDetail()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;">✕</button>
|
||
<div style="display:flex;align-items:flex-start;gap:0.85rem;">
|
||
<div class="detail-author-avatar" id="detailAvatar" style="display:none;">◉</div>
|
||
<div>
|
||
<h2 id="detailTitle" style="margin:0 0 0.25rem;font-size:1.2rem;"></h2>
|
||
<div id="detailMeta" style="font-size:0.82rem;color:var(--color-muted);"></div>
|
||
</div>
|
||
</div>
|
||
<div id="detailBody"></div>
|
||
<div class="detail-footer">
|
||
<button class="btn-close-detail" onclick="closeDetail()">Schließen</button>
|
||
<button class="btn-join-detail" id="detailJoinBtn" onclick="detailJoin()">🔒 Beitreten</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Join-Dialog -->
|
||
<div class="join-modal-backdrop" id="joinModal">
|
||
<div class="join-modal-box">
|
||
<button onclick="closeJoinModal()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;">✕</button>
|
||
<h3 style="margin:0;font-size:1.05rem;">🔒 Angebot annehmen</h3>
|
||
<p id="joinModalDesc" style="margin:0;font-size:0.85rem;color:var(--color-muted);line-height:1.5;"></p>
|
||
|
||
<div class="form-group">
|
||
<div class="form-label">Schloss-Steuerung</div>
|
||
<select id="joinControllType" style="padding:0.5rem 0.75rem;border-radius:7px;border:1px solid var(--color-secondary);background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;">
|
||
<option value="">– Bitte wählen –</option>
|
||
<option value="UNLOCK_CODE">🔢 Entsperrcode (Standard)</option>
|
||
<option value="TRUST">🤝 Trust (kein Code)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group" id="codeLenGroup">
|
||
<div class="form-label">Code-Länge</div>
|
||
<input type="number" id="joinCodeLen" value="5" min="1" max="10"
|
||
style="padding:0.5rem 0.75rem;border-radius:7px;border:1px solid var(--color-secondary);background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;width:100%;box-sizing:border-box;">
|
||
</div>
|
||
|
||
<div id="joinError" style="display:none;font-size:0.85rem;color:#e74c3c;"></div>
|
||
|
||
<div style="display:flex;gap:0.6rem;justify-content:flex-end;margin-top:0.25rem;">
|
||
<button onclick="closeJoinModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;width:auto;">Abbrechen</button>
|
||
<button id="joinConfirmBtn" onclick="confirmJoin()" style="padding:0.5rem 1.25rem;border-radius:7px;font-size:0.88rem;font-weight:600;width:auto;">Beitreten</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ergebnis-Dialog (nach erfolgreichem Join) -->
|
||
<div class="join-modal-backdrop" id="joinResultModal">
|
||
<div class="join-modal-box" style="align-items:center;text-align:center;">
|
||
<div style="font-size:2.5rem;line-height:1;" id="joinResultIcon">🔒</div>
|
||
<h3 style="margin:0;" id="joinResultTitle"></h3>
|
||
<p style="margin:0;font-size:0.88rem;color:var(--color-muted);line-height:1.5;" id="joinResultText"></p>
|
||
<div id="joinResultCode" style="display:none;font-family:monospace;font-size:1.6rem;font-weight:700;letter-spacing:0.18em;padding:0.6rem 1.25rem;background:rgba(255,255,255,0.06);border-radius:8px;"></div>
|
||
<div style="display:flex;gap:0.6rem;justify-content:center;margin-top:0.5rem;flex-wrap:wrap;">
|
||
<button onclick="closeJoinResultModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;width:auto;">Schließen</button>
|
||
<button id="btnGoToLock" onclick="goToActiveLock()" style="padding:0.5rem 1.25rem;border-radius:7px;font-size:0.88rem;font-weight:600;width:auto;display:none;">Zum aktiven Lock</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/shared.js"></script>
|
||
<script src="/js/icons.js"></script>
|
||
<script src="/js/nav.js"></script>
|
||
<script>
|
||
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
||
|
||
const GENDER_LABELS = { WEIBLICH: 'Weiblich', MAENNLICH: 'Männlich', DIVERS: 'Divers' };
|
||
|
||
let _joinOfferId = null;
|
||
let _lastJoinOfferId = null;
|
||
let _joinLockId = null;
|
||
let _detailOfferId = null;
|
||
let _detailOffer = null;
|
||
let _allOffers = [];
|
||
|
||
// ── Laden ──────────────────────────────────────────────────────────────────
|
||
async function loadOffers() {
|
||
const res = await fetch('/keyholder-offers/public');
|
||
document.getElementById('listLoading').style.display = 'none';
|
||
if (!res.ok) return;
|
||
_allOffers = await res.json();
|
||
const list = document.getElementById('offerList');
|
||
if (_allOffers.length === 0) { document.getElementById('listEmpty').style.display = ''; return; }
|
||
_allOffers.forEach(o => list.appendChild(buildCard(o)));
|
||
}
|
||
|
||
function buildCard(o) {
|
||
const av = o.offererProfilePic
|
||
? `<div class="offer-avatar"><img src="data:image/png;base64,${o.offererProfilePic}" alt=""></div>`
|
||
: `<div class="offer-avatar">👤</div>`;
|
||
|
||
const genderTags = (o.targetGenders && o.targetGenders.length > 0)
|
||
? o.targetGenders.map(g => `<span class="offer-badge">${esc(GENDER_LABELS[g] || g)}</span>`).join('')
|
||
: '<span class="offer-badge">Alle</span>';
|
||
|
||
const modeBadge = o.directStart
|
||
? '<span class="offer-badge direct">Direktstart</span>'
|
||
: '<span class="offer-badge confirm">Mit Bestätigung</span>';
|
||
|
||
const typeBadge = o.templateType === 'TIMELOCK'
|
||
? '<span class="offer-badge">⏱ Zeit-Lock</span>'
|
||
: '<span class="offer-badge">🃏 Karten-Lock</span>';
|
||
|
||
const authorLine = o.offererName
|
||
? `von ${esc(o.offererName)} · `
|
||
: '';
|
||
|
||
const joinBtn = o.isOwn
|
||
? `<button class="btn-join" disabled title="Eigenes Angebot">Eigenes</button>`
|
||
: `<button class="btn-join" onclick="openJoinModal('${o.id}', event)">Beitreten</button>`;
|
||
|
||
const div = document.createElement('div');
|
||
div.className = 'offer-card';
|
||
div.dataset.offerId = o.id;
|
||
div.innerHTML = `
|
||
${av}
|
||
<div class="offer-body offer-card-clickable" onclick="openDetail('${o.id}')">
|
||
<div class="offer-name">${esc(o.templateName || 'Unbenannt')}</div>
|
||
<div class="offer-sub">${authorLine}✓ ${o.acceptanceCount}× angenommen</div>
|
||
<div class="offer-tags">${typeBadge} ${modeBadge} ${genderTags}</div>
|
||
</div>
|
||
${joinBtn}`;
|
||
return div;
|
||
}
|
||
|
||
// ── Join-Dialog ────────────────────────────────────────────────────────────
|
||
function openJoinModal(offerId, e) {
|
||
if (e) e.stopPropagation();
|
||
_joinOfferId = offerId;
|
||
const card = document.querySelector(`[data-offer-id="${offerId}"]`);
|
||
const name = card ? card.querySelector('.offer-name')?.textContent : 'dieses Lock';
|
||
const direct = card?.querySelector('.offer-badge.direct') != null;
|
||
|
||
document.getElementById('joinModalDesc').textContent = direct
|
||
? `Das Lock „${name}" wird sofort für dich gestartet. Bitte wähle deine bevorzugte Schloss-Steuerung.`
|
||
: `Du sendest eine Einladung an den Keyholder für das Lock „${name}". Nach Annahme kannst du loslegen.`;
|
||
|
||
document.getElementById('joinError').style.display = 'none';
|
||
document.getElementById('joinControllType').value = '';
|
||
document.getElementById('joinCodeLen').value = '5';
|
||
document.getElementById('joinConfirmBtn').disabled = true;
|
||
updateCodeLenVisibility();
|
||
document.getElementById('joinModal').classList.add('open');
|
||
}
|
||
|
||
function closeJoinModal() {
|
||
document.getElementById('joinModal').classList.remove('open');
|
||
_joinOfferId = null;
|
||
}
|
||
|
||
document.getElementById('joinControllType').addEventListener('change', function() {
|
||
updateCodeLenVisibility();
|
||
document.getElementById('joinConfirmBtn').disabled = !this.value;
|
||
});
|
||
function updateCodeLenVisibility() {
|
||
const val = document.getElementById('joinControllType').value;
|
||
document.getElementById('codeLenGroup').style.display = val === 'TRUST' ? 'none' : '';
|
||
}
|
||
|
||
async function confirmJoin() {
|
||
if (!_joinOfferId) return;
|
||
const controllType = document.getElementById('joinControllType').value;
|
||
if (!controllType) return;
|
||
const btn = document.getElementById('joinConfirmBtn');
|
||
btn.disabled = true;
|
||
const unlockCodeLength = parseInt(document.getElementById('joinCodeLen').value) || 5;
|
||
|
||
const res = await fetch(`/keyholder-offers/${_joinOfferId}/join`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ controllType, unlockCodeLength })
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const d = await res.json().catch(() => ({}));
|
||
let msg = 'Fehler beim Beitreten.';
|
||
if (d.error === 'active_lock_exists') msg = 'Du hast bereits ein aktives Lock.';
|
||
else if (d.error === 'own_offer') msg = 'Du kannst nicht deinem eigenen Angebot beitreten.';
|
||
else if (d.error === 'template_gone') msg = 'Die Vorlage existiert nicht mehr.';
|
||
document.getElementById('joinError').textContent = msg;
|
||
document.getElementById('joinError').style.display = '';
|
||
btn.disabled = false;
|
||
return;
|
||
}
|
||
|
||
const data = await res.json();
|
||
_lastJoinOfferId = _joinOfferId;
|
||
closeJoinModal();
|
||
showJoinResult(data);
|
||
}
|
||
|
||
function showJoinResult(data) {
|
||
_joinLockId = data.lockId;
|
||
const direct = !data.invitationSent;
|
||
|
||
document.getElementById('joinResultIcon').textContent = direct ? '🔒' : '✉️';
|
||
document.getElementById('joinResultTitle').textContent = direct ? 'Lock gestartet!' : 'Einladung gesendet';
|
||
document.getElementById('btnGoToLock').style.display = direct ? '' : 'none';
|
||
|
||
if (direct && data.unlockCode) {
|
||
document.getElementById('joinResultText').textContent = 'Dein aktueller Entsperrcode:';
|
||
document.getElementById('joinResultCode').textContent = data.unlockCode;
|
||
document.getElementById('joinResultCode').style.display = '';
|
||
} else if (direct) {
|
||
document.getElementById('joinResultText').textContent = 'Das Lock wurde erfolgreich gestartet.';
|
||
document.getElementById('joinResultCode').style.display = 'none';
|
||
} else {
|
||
document.getElementById('joinResultText').textContent =
|
||
'Die Einladung wurde an den Keyholder gesendet. Sobald dieser annimmt, startet das Lock.';
|
||
document.getElementById('joinResultCode').style.display = 'none';
|
||
}
|
||
|
||
document.getElementById('joinResultModal').classList.add('open');
|
||
}
|
||
|
||
function closeJoinResultModal() {
|
||
document.getElementById('joinResultModal').classList.remove('open');
|
||
_joinLockId = null;
|
||
}
|
||
|
||
function goToActiveLock() {
|
||
if (!_joinLockId) return;
|
||
const isTimelock = _allOffers.find(o => o.id === _lastJoinOfferId)?.templateType === 'TIMELOCK';
|
||
const page = isTimelock ? '/games/chastity/activetimelock.html' : '/games/chastity/activelock.html';
|
||
window.location.href = page + '?lockId=' + _joinLockId;
|
||
}
|
||
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape') {
|
||
closeDetail();
|
||
closeJoinModal();
|
||
closeJoinResultModal();
|
||
}
|
||
});
|
||
|
||
// ── Detail-Dialog ──────────────────────────────────────────────────────────
|
||
async function openDetail(offerId) {
|
||
_detailOfferId = offerId;
|
||
const card = document.querySelector(`[data-offer-id="${offerId}"]`);
|
||
|
||
// Offer-Objekt aus den geladenen Daten holen
|
||
_detailOffer = _allOffers.find(o => o.id === offerId);
|
||
if (!_detailOffer) return;
|
||
|
||
// Autoren-Avatar
|
||
const avatarEl = document.getElementById('detailAvatar');
|
||
if (_detailOffer.offererProfilePic) {
|
||
avatarEl.innerHTML = `<img src="data:image/png;base64,${_detailOffer.offererProfilePic}" alt="">`;
|
||
avatarEl.style.display = '';
|
||
} else {
|
||
avatarEl.style.display = 'none';
|
||
}
|
||
|
||
const typeTxt = _detailOffer.templateType === 'TIMELOCK' ? '⏱ Zeit-Lock' : '🃏 Karten-Lock';
|
||
const modeTxt = _detailOffer.directStart ? 'Direktstart' : 'Mit Bestätigung';
|
||
const authorTxt = _detailOffer.offererName ? ' · von ' + _detailOffer.offererName : '';
|
||
document.getElementById('detailTitle').textContent = _detailOffer.templateName || 'Unbenannt';
|
||
document.getElementById('detailMeta').textContent = typeTxt + ' · ' + modeTxt + authorTxt;
|
||
|
||
// Join-Button ein/ausblenden
|
||
const joinBtn = document.getElementById('detailJoinBtn');
|
||
joinBtn.style.display = _detailOffer.isOwn ? 'none' : '';
|
||
|
||
// Template-Details laden
|
||
document.getElementById('detailBody').innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Lädt…</p>';
|
||
document.getElementById('detailModal').classList.add('open');
|
||
|
||
try {
|
||
const res = await fetch('/templates/' + _detailOffer.templateId + '/public');
|
||
if (res.ok) {
|
||
const tpl = await res.json();
|
||
document.getElementById('detailBody').innerHTML = buildDetailBody(tpl);
|
||
} else {
|
||
document.getElementById('detailBody').innerHTML = '';
|
||
}
|
||
} catch { document.getElementById('detailBody').innerHTML = ''; }
|
||
}
|
||
|
||
function closeDetail() {
|
||
document.getElementById('detailModal').classList.remove('open');
|
||
_detailOfferId = null;
|
||
_detailOffer = null;
|
||
}
|
||
|
||
function detailJoin() {
|
||
const id = _detailOfferId;
|
||
if (!id) return;
|
||
closeDetail();
|
||
openJoinModal(id, null);
|
||
}
|
||
|
||
// ── Detail-Body ────────────────────────────────────────────────────────────
|
||
function fmtMinutes(min) {
|
||
if (!min) return '0 Min.';
|
||
const d = Math.floor(min / 1440), h = Math.floor((min % 1440) / 60), m = min % 60;
|
||
return [d ? d + 'd' : '', h ? h + 'h' : '', m ? m + 'min' : ''].filter(Boolean).join(' ') || '0 Min.';
|
||
}
|
||
|
||
function buildSection(title, rows) {
|
||
const rowsHtml = rows.map(([l, v]) =>
|
||
`<div class="detail-row"><span class="detail-row-label">${esc(l)}</span><span class="detail-row-val">${v}</span></div>`
|
||
).join('');
|
||
return `<div class="detail-section"><div class="detail-section-title">${title}</div>${rowsHtml}</div>`;
|
||
}
|
||
|
||
function buildDetailBody(t) {
|
||
const sections = [];
|
||
|
||
if (t.lockType === 'TIMELOCK') {
|
||
sections.push(buildSection('⏱ Zeit-Einstellungen', [
|
||
['Mindestdauer', fmtMinutes(t.minTimeInMinutes)],
|
||
['Maximaldauer', fmtMinutes(t.maxTimeInMinutes)],
|
||
['Endzeit sichtbar', t.endTimeVisible ? 'Ja' : 'Nein'],
|
||
]));
|
||
|
||
if (t.spinningWheelEntries && t.spinningWheelEntries.length) {
|
||
const WHEEL_LABELS = {
|
||
ADD_TIME: '+ Zeit', REMOVE_TIME: '− Zeit', FREEZE_TIME: '❄ Einfrieren für',
|
||
FREEZE: '🧊 Einfrieren (∞)', UNFREEZE: '🌊 Auftauen', TASK: '🎯 Aufgabe', TEXT: '💬 Text',
|
||
};
|
||
const entries = t.spinningWheelEntries.map(e => {
|
||
const label = WHEEL_LABELS[e.type] || e.type;
|
||
const extra = e.intVal ? ' ' + fmtMinutes(e.intVal) : (e.stringVal ? ' «' + e.stringVal + '»' : '');
|
||
return `<span class="detail-wheel-entry">${label}${extra}</span>`;
|
||
}).join('');
|
||
sections.push(`<div class="detail-section">
|
||
<div class="detail-section-title">🎡 Glücksrad (${t.spinningWheelEntries.length} Einträge${t.spinsEveryMinutes ? ', alle ' + fmtMinutes(t.spinsEveryMinutes) : ''})</div>
|
||
<div>${entries}</div>
|
||
</div>`);
|
||
}
|
||
|
||
if (t.penaltyType) {
|
||
const penaltyLabels = { ADD: 'Zeit hinzufügen', FREEZE: 'Einfrieren', PILLORY: 'Pranger' };
|
||
sections.push(buildSection('⚠ Strafmaß', [
|
||
['Typ', penaltyLabels[t.penaltyType] || t.penaltyType],
|
||
['Wert', t.penaltyValue ? fmtMinutes(t.penaltyValue) : '–'],
|
||
]));
|
||
}
|
||
|
||
if (t.taskEveryMinutes || t.minTasksPerDay) {
|
||
sections.push(buildSection('🎯 Aufgaben-Timing', [
|
||
['Intervall', t.taskEveryMinutes ? fmtMinutes(t.taskEveryMinutes) : '–'],
|
||
['Min./Tag', t.minTasksPerDay ? t.minTasksPerDay + ' Aufgabe(n)' : '–'],
|
||
]));
|
||
}
|
||
}
|
||
|
||
if (t.lockType === 'CARDLOCK') {
|
||
const allKeys = new Set([
|
||
...Object.keys(t.cardCountsMin || {}),
|
||
...Object.keys(t.cardCountsMax || {}),
|
||
]);
|
||
const rows = [];
|
||
allKeys.forEach(k => {
|
||
const mn = (t.cardCountsMin || {})[k] ?? 0;
|
||
const mx = (t.cardCountsMax || {})[k] ?? 0;
|
||
if (mn > 0 || mx > 0) rows.push([k, `${mn} – ${mx}`]);
|
||
});
|
||
if (rows.length) sections.push(buildSection('🃏 Karten', rows));
|
||
|
||
sections.push(buildSection('⚙ Karten-Einstellungen', [
|
||
['Zieh-Intervall', t.pickEveryMinute ? fmtMinutes(t.pickEveryMinute) : '–'],
|
||
['Picks kumulieren', t.accumulatePicks ? 'Ja' : 'Nein'],
|
||
['Verbl. Karten zeigen', t.showRemainingCards ? 'Ja' : 'Nein'],
|
||
]));
|
||
}
|
||
|
||
sections.push(buildSection('⚙ Allgemein', [
|
||
['Hygiene-Öffnung', t.hygieneEnabled ? `alle ${fmtMinutes(t.hygineOpeningEveryMinites)}, ${fmtMinutes(t.hygineOpeningDurationMinutes)} offen` : 'Keine'],
|
||
['Verifikation', t.requiresVerification ? 'Erforderlich' : 'Keine'],
|
||
['Aufgaben-Modus', t.taskMode === 'KEYHOLDER' ? 'Keyholder' : t.taskMode === 'COMMUNITY' ? 'Community' : 'Zufällig'],
|
||
]));
|
||
|
||
if (t.tasks && t.tasks.length) {
|
||
const taskItems = t.tasks.map(task => {
|
||
const dur = task.durationMinutes ? ` <span style="color:var(--color-muted);font-size:0.8rem;">(${fmtMinutes(task.durationMinutes)})</span>` : '';
|
||
const desc = task.description ? `<div style="font-size:0.78rem;color:var(--color-muted);margin-top:0.1rem;">${esc(task.description)}</div>` : '';
|
||
return `<div class="detail-task-item">${esc(task.title || task.name || '–')}${dur}${desc}</div>`;
|
||
}).join('');
|
||
sections.push(`<div class="detail-section">
|
||
<div class="detail-section-title">🎯 Aufgaben (${t.tasks.length})</div>${taskItems}
|
||
</div>`);
|
||
}
|
||
|
||
return sections.join('');
|
||
}
|
||
|
||
loadOffers();
|
||
</script>
|
||
</body>
|
||
</html>
|