529 lines
24 KiB
HTML
529 lines
24 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>Vorlagen entdecken – xXx Sphere</title>
|
||
<link rel="stylesheet" href="/css/variables.css">
|
||
<link rel="stylesheet" href="/css/style.css">
|
||
<style>
|
||
/* ── Suche ── */
|
||
.search-bar {
|
||
display: flex; gap: 0.6rem; margin-bottom: 1.5rem; align-items: center;
|
||
}
|
||
.search-bar input {
|
||
flex: 1; padding: 0.55rem 0.85rem; border-radius: 8px;
|
||
border: 1px solid var(--color-secondary); background: var(--color-card);
|
||
color: var(--color-text); font-size: 0.95rem;
|
||
}
|
||
.search-bar button {
|
||
width: auto; padding: 0.55rem 1.2rem; font-size: 0.9rem;
|
||
}
|
||
|
||
/* ── Template-Karte ── */
|
||
.tpl-card {
|
||
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||
border-radius: 10px; padding: 1rem; margin-bottom: 0.75rem;
|
||
cursor: pointer; transition: border-color 0.15s;
|
||
}
|
||
.tpl-card:hover { border-color: var(--color-primary); }
|
||
.tpl-card.own-template { border-left: 3px solid #3498db; }
|
||
.tpl-card-header {
|
||
display: flex; align-items: flex-start;
|
||
justify-content: space-between; gap: 0.75rem;
|
||
}
|
||
.tpl-icon {
|
||
width: 2.4rem; height: 2.4rem; flex-shrink: 0;
|
||
border-radius: 8px; background: var(--color-secondary);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 1.2rem;
|
||
}
|
||
.tpl-name { font-weight: 700; font-size: 1rem; margin-bottom: 0.2rem; }
|
||
.tpl-meta { font-size: 0.78rem; color: var(--color-muted); line-height: 1.5; }
|
||
.tpl-badges {
|
||
display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.6rem;
|
||
}
|
||
.tpl-badge {
|
||
font-size: 0.7rem; border-radius: 5px; padding: 0.18rem 0.55rem;
|
||
border: 1px solid var(--color-secondary); color: var(--color-muted);
|
||
background: var(--color-secondary);
|
||
}
|
||
.tpl-badge.blue { background: rgba(52,152,219,0.12); border-color: rgba(52,152,219,0.35); color: #3498db; }
|
||
.tpl-badge.green { background: rgba(46,204,113,0.12); border-color: rgba(46,204,113,0.35); color: #2ecc71; }
|
||
.tpl-badge.orange { background: rgba(231,152,52,0.12); border-color: rgba(231,152,52,0.35); color: #e67e22; }
|
||
.tpl-badge.own { background: rgba(52,152,219,0.15); border-color: rgba(52,152,219,0.5); color: #3498db; font-weight: 600; }
|
||
|
||
/* ── Abonnieren-Button ── */
|
||
.btn-sub {
|
||
white-space: nowrap; width: auto; padding: 0.4rem 0.9rem; font-size: 0.82rem;
|
||
font-weight: 600; border-radius: 7px; cursor: pointer; flex-shrink: 0;
|
||
border: 1px solid var(--color-secondary);
|
||
background: none; color: var(--color-muted);
|
||
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||
}
|
||
.btn-sub:hover:not(:disabled) {
|
||
background: rgba(52,152,219,0.12); border-color: rgba(52,152,219,0.45); color: #3498db;
|
||
}
|
||
.btn-sub.subscribed {
|
||
background: rgba(46,204,113,0.12); border-color: rgba(46,204,113,0.4); color: #2ecc71;
|
||
}
|
||
.btn-sub.subscribed:hover:not(:disabled) {
|
||
background: rgba(231,76,60,0.1); border-color: rgba(231,76,60,0.35); color: #e74c3c;
|
||
}
|
||
.btn-sub:disabled { opacity: 0.45; cursor: not-allowed; }
|
||
|
||
/* ── Detail-Modal ── */
|
||
.detail-backdrop {
|
||
display: none; position: fixed; inset: 0;
|
||
background: rgba(0,0,0,0.65); z-index: 400;
|
||
align-items: flex-start; justify-content: center;
|
||
padding: 2rem 1rem; overflow-y: auto;
|
||
}
|
||
.detail-backdrop.open { display: flex; }
|
||
.detail-box {
|
||
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||
border-radius: 14px; padding: 1.75rem 1.5rem 1.5rem;
|
||
max-width: 500px; width: 100%; position: relative;
|
||
display: flex; flex-direction: column; gap: 1rem;
|
||
}
|
||
.detail-section {
|
||
background: var(--color-secondary); border-radius: 8px;
|
||
padding: 0.85rem 1rem;
|
||
}
|
||
.detail-section-title {
|
||
font-size: 0.72rem; font-weight: 700; color: var(--color-muted);
|
||
text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 0.5rem;
|
||
}
|
||
.detail-row {
|
||
display: flex; justify-content: space-between; align-items: baseline;
|
||
font-size: 0.88rem; padding: 0.2rem 0; gap: 1rem;
|
||
}
|
||
.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.85rem; color: var(--color-text); padding: 0.3rem 0;
|
||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||
}
|
||
.detail-task-item:last-child { border-bottom: none; }
|
||
.detail-wheel-entry {
|
||
display: inline-flex; align-items: center; gap: 0.3rem;
|
||
font-size: 0.78rem; background: var(--color-card);
|
||
border: 1px solid var(--color-secondary); border-radius: 5px;
|
||
padding: 0.2rem 0.55rem; margin: 0.2rem;
|
||
}
|
||
.detail-footer {
|
||
display: flex; gap: 0.75rem; justify-content: flex-end; flex-wrap: wrap;
|
||
border-top: 1px solid var(--color-secondary); padding-top: 1rem;
|
||
}
|
||
.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-subscribe-detail {
|
||
padding: 0.5rem 1.25rem; border-radius: 7px; cursor: pointer;
|
||
font-size: 0.88rem; font-weight: 600; width: auto; border: none;
|
||
background: var(--color-primary); color: #fff;
|
||
}
|
||
.btn-subscribe-detail.subscribed {
|
||
background: rgba(46,204,113,0.15); border: 1px solid rgba(46,204,113,0.4); color: #2ecc71;
|
||
}
|
||
.detail-author-avatar {
|
||
width: 52px; height: 52px; border-radius: 50%;
|
||
border: 2px solid var(--color-secondary);
|
||
background: var(--color-secondary);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 1.5rem; color: var(--color-muted);
|
||
overflow: hidden; flex-shrink: 0;
|
||
}
|
||
.detail-author-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||
</style>
|
||
</head>
|
||
<body class="app">
|
||
<div class="main">
|
||
<div class="content">
|
||
|
||
<h2 style="margin-bottom:1.25rem;">🔍 Vorlagen entdecken</h2>
|
||
|
||
<!-- Suchleiste -->
|
||
<div class="search-bar">
|
||
<input type="text" id="searchInput" placeholder="Nach Namen suchen…"
|
||
onkeydown="if(event.key==='Enter') doSearch()">
|
||
<button onclick="doSearch()">Suchen</button>
|
||
</div>
|
||
|
||
<!-- Ergebnisliste -->
|
||
<div id="templateList"></div>
|
||
<div id="scrollSentinel" style="height:1px;"></div>
|
||
<p id="listLoading" style="display:none;text-align:center;color:var(--color-muted);padding:1rem;">Laden…</p>
|
||
<p id="listEmpty" style="display:none;color:var(--color-muted);">Keine öffentlichen Vorlagen gefunden.</p>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Detail-Modal -->
|
||
<div class="detail-backdrop" id="detailModal" onclick="closeDetail()">
|
||
<div class="detail-box" onclick="event.stopPropagation()">
|
||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;">
|
||
<div style="display:flex;align-items:flex-start;gap:0.85rem;">
|
||
<div class="detail-author-avatar" id="detailAuthorAvatar" 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>
|
||
<button onclick="closeDetail()" style="background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0;flex-shrink:0;">✕</button>
|
||
</div>
|
||
|
||
<div id="detailBody"></div>
|
||
|
||
<div class="detail-footer">
|
||
<button class="btn-close-detail" onclick="closeDetail()">Schließen</button>
|
||
<button class="btn-subscribe-detail" id="detailSubscribeBtn" onclick="toggleSubscribeDetail()"></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/shared.js"></script>
|
||
<script src="/js/icons.js"></script>
|
||
<script src="/js/sidebar.js"></script>
|
||
<script>
|
||
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
||
|
||
let page = 0;
|
||
let isLastPage = false;
|
||
let isLoading = false;
|
||
let currentSearch = '';
|
||
let _detailTemplate = null;
|
||
|
||
function fmtMinutes(min) {
|
||
if (!min) return '–';
|
||
const d = Math.floor(min / 1440), h = Math.floor((min % 1440) / 60), m = min % 60;
|
||
return [d && d + 'T', h && h + 'Std', m && m + 'Min'].filter(Boolean).join(' ') || '0Min';
|
||
}
|
||
|
||
// ── Laden ──────────────────────────────────────────────────────────────────
|
||
|
||
async function loadNextPage() {
|
||
if (isLoading || isLastPage) return;
|
||
isLoading = true;
|
||
document.getElementById('listLoading').style.display = '';
|
||
try {
|
||
const q = encodeURIComponent(currentSearch);
|
||
const res = await fetch(`/templates/public?page=${page}&size=10&q=${q}`);
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
data.content.forEach(t => appendCard(t));
|
||
isLastPage = !data.hasMore;
|
||
page = data.page + 1;
|
||
if (page === 1 && data.content.length === 0) {
|
||
document.getElementById('listEmpty').style.display = '';
|
||
}
|
||
} catch(e) { console.error(e); }
|
||
finally {
|
||
isLoading = false;
|
||
document.getElementById('listLoading').style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function resetList() {
|
||
page = 0; isLastPage = false; isLoading = false;
|
||
document.getElementById('templateList').innerHTML = '';
|
||
document.getElementById('listEmpty').style.display = 'none';
|
||
loadNextPage();
|
||
}
|
||
|
||
function doSearch() {
|
||
currentSearch = document.getElementById('searchInput').value.trim();
|
||
resetList();
|
||
}
|
||
|
||
// ── Karte ─────────────────────────────────────────────────────────────────
|
||
|
||
function appendCard(t) {
|
||
const list = document.getElementById('templateList');
|
||
const isCard = t.lockType === 'CARDLOCK';
|
||
|
||
const card = document.createElement('div');
|
||
card.className = 'tpl-card' + (t.isOwnTemplate ? ' own-template' : '');
|
||
card.dataset.templateId = t.templateId;
|
||
|
||
const metaParts = [
|
||
isCard ? '🃏 Karten-Lock' : '⏱ Zeit-Lock',
|
||
t.authorName ? 'von ' + esc(t.authorName) : null,
|
||
t.subscriberCount + ' Abo(s)',
|
||
].filter(Boolean);
|
||
|
||
const badges = buildBadges(t);
|
||
const subBtnCls = t.isOwnTemplate ? '' : (t.isSubscribed ? 'subscribed' : '');
|
||
const subBtnLabel = t.isOwnTemplate ? 'Eigene' : (t.isSubscribed ? '✓ Abonniert' : '+ Abonnieren');
|
||
|
||
card.innerHTML = `
|
||
<div class="tpl-card-header">
|
||
<div class="tpl-icon">${isCard ? '🃏' : '⏱'}</div>
|
||
<div style="flex:1;min-width:0;">
|
||
<div class="tpl-name">${esc(t.name || 'Ohne Namen')}</div>
|
||
<div class="tpl-meta">${metaParts.join(' · ')}</div>
|
||
</div>
|
||
<button class="btn-sub ${subBtnCls}" ${t.isOwnTemplate ? 'disabled' : ''}
|
||
onclick="event.stopPropagation();toggleSubscribe('${t.templateId}',this)">
|
||
${subBtnLabel}
|
||
</button>
|
||
</div>
|
||
<div class="tpl-badges">${badges}</div>`;
|
||
|
||
card.addEventListener('click', () => openDetail(t));
|
||
list.appendChild(card);
|
||
}
|
||
|
||
function buildBadges(t) {
|
||
const b = [];
|
||
if (t.lockType === 'TIMELOCK') {
|
||
if (t.minTimeInMinutes || t.maxTimeInMinutes) {
|
||
const min = fmtMinutes(t.minTimeInMinutes), max = fmtMinutes(t.maxTimeInMinutes);
|
||
b.push(`<span class="tpl-badge blue">⏱ ${min} – ${max}</span>`);
|
||
}
|
||
if (t.spinningWheelEntries && t.spinningWheelEntries.length)
|
||
b.push(`<span class="tpl-badge orange">🎡 Glücksrad (${t.spinningWheelEntries.length})</span>`);
|
||
if (t.penaltyType)
|
||
b.push(`<span class="tpl-badge orange">⚠ Strafe</span>`);
|
||
}
|
||
if (t.taskCount > 0)
|
||
b.push(`<span class="tpl-badge">🎯 ${t.taskCount} Aufgabe(n)</span>`);
|
||
if (t.hygieneEnabled)
|
||
b.push(`<span class="tpl-badge">🚿 Hygiene</span>`);
|
||
if (t.requiresVerification)
|
||
b.push(`<span class="tpl-badge">📷 Verifikation</span>`);
|
||
if (t.isOwnTemplate)
|
||
b.push(`<span class="tpl-badge own">Meine Vorlage</span>`);
|
||
return b.join('');
|
||
}
|
||
|
||
// ── Abonnieren (Listenansicht) ─────────────────────────────────────────────
|
||
|
||
async function toggleSubscribe(id, btn) {
|
||
const isSubscribed = btn.classList.contains('subscribed');
|
||
btn.disabled = true;
|
||
try {
|
||
const method = isSubscribed ? 'DELETE' : 'POST';
|
||
const res = await fetch(`/templates/${id}/subscribe`, { method });
|
||
if (res.ok || res.status === 204) {
|
||
if (isSubscribed) {
|
||
btn.classList.remove('subscribed');
|
||
btn.textContent = '+ Abonnieren';
|
||
// Update card data
|
||
const card = btn.closest('.tpl-card');
|
||
updateCardSubscriberCount(card, -1);
|
||
} else {
|
||
btn.classList.add('subscribed');
|
||
btn.textContent = '✓ Abonniert';
|
||
const card = btn.closest('.tpl-card');
|
||
updateCardSubscriberCount(card, +1);
|
||
}
|
||
}
|
||
} catch(e) { /* ignore */ }
|
||
btn.disabled = false;
|
||
}
|
||
|
||
function updateCardSubscriberCount(card, delta) {
|
||
// Update the meta text - find the "X Abo(s)" part
|
||
const meta = card.querySelector('.tpl-meta');
|
||
if (!meta) return;
|
||
meta.innerHTML = meta.innerHTML.replace(/(\d+) Abo\(s\)/, (_, n) => `${Math.max(0, parseInt(n) + delta)} Abo(s)`);
|
||
}
|
||
|
||
// ── Detail-Modal ───────────────────────────────────────────────────────────
|
||
|
||
function openDetail(t) {
|
||
_detailTemplate = t;
|
||
document.getElementById('detailTitle').textContent = t.name || 'Ohne Namen';
|
||
|
||
const avatarEl = document.getElementById('detailAuthorAvatar');
|
||
if (t.authorProfilePicture) {
|
||
avatarEl.innerHTML = `<img src="data:image/png;base64,${t.authorProfilePicture}" alt="${esc(t.authorName || '')}">`;
|
||
avatarEl.style.display = '';
|
||
} else {
|
||
avatarEl.innerHTML = '◉';
|
||
avatarEl.style.display = 'none';
|
||
}
|
||
|
||
const metaParts = [
|
||
t.lockType === 'CARDLOCK' ? '🃏 Karten-Lock' : '⏱ Zeit-Lock',
|
||
t.authorName ? 'von ' + t.authorName : null,
|
||
t.subscriberCount + ' Abonnent(en)',
|
||
].filter(Boolean);
|
||
document.getElementById('detailMeta').textContent = metaParts.join(' · ');
|
||
|
||
document.getElementById('detailBody').innerHTML = buildDetailBody(t);
|
||
|
||
const btn = document.getElementById('detailSubscribeBtn');
|
||
if (t.isOwnTemplate) {
|
||
btn.style.display = 'none';
|
||
} else {
|
||
btn.style.display = '';
|
||
btn.className = 'btn-subscribe-detail' + (t.isSubscribed ? ' subscribed' : '');
|
||
btn.textContent = t.isSubscribed ? '✓ Abonniert' : '+ Abonnieren';
|
||
}
|
||
|
||
document.getElementById('detailModal').classList.add('open');
|
||
}
|
||
|
||
function closeDetail() {
|
||
document.getElementById('detailModal').classList.remove('open');
|
||
_detailTemplate = null;
|
||
}
|
||
|
||
async function toggleSubscribeDetail() {
|
||
if (!_detailTemplate) return;
|
||
const t = _detailTemplate;
|
||
const btn = document.getElementById('detailSubscribeBtn');
|
||
btn.disabled = true;
|
||
const isSubscribed = t.isSubscribed;
|
||
try {
|
||
const method = isSubscribed ? 'DELETE' : 'POST';
|
||
const res = await fetch(`/templates/${t.templateId}/subscribe`, { method });
|
||
if (res.ok || res.status === 204) {
|
||
t.isSubscribed = !isSubscribed;
|
||
t.subscriberCount = Math.max(0, (t.subscriberCount || 0) + (isSubscribed ? -1 : 1));
|
||
if (isSubscribed) {
|
||
btn.className = 'btn-subscribe-detail';
|
||
btn.textContent = '+ Abonnieren';
|
||
} else {
|
||
btn.className = 'btn-subscribe-detail subscribed';
|
||
btn.textContent = '✓ Abonniert';
|
||
}
|
||
// Update card in list
|
||
const card = document.querySelector(`.tpl-card[data-template-id="${t.templateId}"]`);
|
||
if (card) {
|
||
const subBtn = card.querySelector('.btn-sub');
|
||
if (subBtn) {
|
||
if (isSubscribed) { subBtn.classList.remove('subscribed'); subBtn.textContent = '+ Abonnieren'; }
|
||
else { subBtn.classList.add('subscribed'); subBtn.textContent = '✓ Abonniert'; }
|
||
}
|
||
updateCardSubscriberCount(card, isSubscribed ? -1 : 1);
|
||
}
|
||
}
|
||
} catch(e) { /* ignore */ }
|
||
btn.disabled = false;
|
||
}
|
||
|
||
// ── Detail-Body aufbauen ───────────────────────────────────────────────────
|
||
|
||
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 rows = [];
|
||
const allKeys = new Set([
|
||
...Object.keys(t.cardCountsMin || {}),
|
||
...Object.keys(t.cardCountsMax || {}),
|
||
]);
|
||
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'],
|
||
]));
|
||
}
|
||
|
||
// Gemeinsame Einstellungen
|
||
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('');
|
||
}
|
||
|
||
function buildSection(title, rows) {
|
||
const rowsHtml = rows.map(([label, val]) =>
|
||
`<div class="detail-row">
|
||
<span class="detail-row-label">${label}</span>
|
||
<span class="detail-row-val">${val}</span>
|
||
</div>`
|
||
).join('');
|
||
return `<div class="detail-section">
|
||
<div class="detail-section-title">${title}</div>
|
||
${rowsHtml}
|
||
</div>`;
|
||
}
|
||
|
||
// ── Infinite Scroll ────────────────────────────────────────────────────────
|
||
|
||
const observer = new IntersectionObserver(entries => {
|
||
if (entries[0].isIntersecting) loadNextPage();
|
||
}, { rootMargin: '200px' });
|
||
observer.observe(document.getElementById('scrollSentinel'));
|
||
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape') closeDetail();
|
||
});
|
||
|
||
fetch('/login/me').then(r => r.ok ? r.json() : null).then(user => {
|
||
if (!user) { window.location.href = '/login.html'; return; }
|
||
loadNextPage();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|