Files
xxx-sphere-web/bin/main/static/games/chastity/communityvotes.html

414 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>Community Votes xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.page-title {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 0.35rem;
}
.page-subtitle {
font-size: 0.88rem;
color: var(--color-muted);
margin-bottom: 1.5rem;
}
#feed {
display: flex;
flex-direction: column;
gap: 1rem;
}
/* ── Verifikations-Karte ── */
.vote-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
overflow: hidden;
}
.vote-card-media { position: relative; }
.vote-card-img {
width: 100%;
max-height: 420px;
object-fit: contain;
display: block;
background: #000;
}
.vote-card-code {
position: absolute;
top: 1rem;
left: 50%;
transform: translateX(-50%);
font-family: monospace;
font-size: 2rem;
font-weight: 700;
letter-spacing: 0.25em;
color: #fff;
background: rgba(0,0,0,0.55);
padding: 0.4rem 1.1rem;
border-radius: 8px;
pointer-events: none;
white-space: nowrap;
}
.vote-card-body {
padding: 0.85rem 1rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.vote-meta { font-size: 0.8rem; color: var(--color-muted); }
.vote-actions { display: flex; align-items: center; gap: 0.75rem; }
.vote-btn {
display: flex;
align-items: center;
gap: 0.35rem;
background: none;
border: 1px solid var(--color-secondary);
border-radius: 6px;
padding: 0.4rem 0.85rem;
font-size: 0.9rem;
cursor: pointer;
color: var(--color-text);
margin: 0;
width: auto;
transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.vote-btn:hover:not(:disabled) {
border-color: var(--color-primary);
color: var(--color-primary);
background: none;
}
.vote-btn.voted-up { border-color: #2ecc71; color: #2ecc71; background: rgba(46,204,113,0.08); }
.vote-btn.voted-down { border-color: #e74c3c; color: #e74c3c; background: rgba(231,76,60,0.08); }
.vote-btn:disabled { opacity: 0.55; cursor: not-allowed; pointer-events: none; }
.vote-count { font-weight: 600; font-size: 0.88rem; }
/* ── Aufgaben-Abstimmungs-Karte ── */
.task-vote-card {
background: var(--color-card);
border: 1px solid rgba(52,152,219,0.35);
border-radius: 10px;
padding: 0.85rem 1rem;
}
.task-vote-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
flex-wrap: wrap;
gap: 0.4rem;
}
.task-vote-lockee { font-weight: 600; font-size: 0.92rem; }
.task-vote-expires { font-size: 0.78rem; color: var(--color-muted); }
.task-vote-options { display: flex; flex-direction: column; gap: 0.35rem; margin-top: 0.5rem; }
.task-vote-btn {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(52,152,219,0.08);
border: 1px solid rgba(52,152,219,0.25);
border-radius: 7px;
padding: 0.45rem 0.7rem;
cursor: pointer;
color: var(--color-text);
text-align: left;
font-size: 0.85rem;
transition: background 0.15s, border-color 0.15s;
width: 100%;
margin: 0;
}
.task-vote-btn:hover:not(:disabled) {
background: rgba(52,152,219,0.22);
border-color: rgba(52,152,219,0.5);
}
.task-vote-btn.my-vote {
border-color: var(--color-primary);
background: rgba(52,152,219,0.18);
}
.task-vote-btn:disabled { cursor: default; }
.task-vote-count {
font-size: 0.78rem;
color: var(--color-muted);
white-space: nowrap;
margin-left: 0.5rem;
flex-shrink: 0;
}
.task-vote-own-hint {
font-size: 0.78rem;
color: var(--color-muted);
font-style: italic;
margin-top: 0.4rem;
text-align: center;
}
/* ── Pranger-Karte ── */
.pillory-card {
background: var(--color-card);
border: 1px solid rgba(231,76,60,0.35);
border-radius: 10px;
padding: 0.85rem 1rem;
}
.pillory-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.4rem;
flex-wrap: wrap;
gap: 0.4rem;
}
.pillory-lockee { font-weight: 600; font-size: 0.92rem; }
.pillory-date { font-size: 0.78rem; color: var(--color-muted); }
.pillory-reason {
font-size: 0.82rem;
color: #e74c3c;
margin-bottom: 0.25rem;
}
.pillory-message { font-size: 0.88rem; }
.empty-hint {
color: var(--color-muted);
font-size: 0.9rem;
text-align: center;
padding: 2rem 0;
}
.load-spinner {
text-align: center;
color: var(--color-muted);
font-size: 0.85rem;
padding: 1rem 0;
}
.sentinel { height: 1px; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<div class="page-title">Community Votes</div>
<div class="page-subtitle">Verifikationen, Aufgaben-Abstimmungen &amp; Pranger</div>
<div id="feed"></div>
<div class="load-spinner" id="loadSpinner" style="display:none;">Lädt…</div>
<div class="empty-hint" id="emptyHint" style="display:none;">Noch keine Community-Abstimmungen vorhanden.</div>
<div class="sentinel" id="sentinel"></div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/social-sidebar.js"></script>
<script>
function esc(s) {
return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function fmtDateTime(isoStr) {
return new Date(isoStr).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
}
// ── Verifikations-Karte ────────────────────────────────────────────────────
function buildVerCard(base, detail) {
const voted = detail.isOwnLock || detail.myVote !== null && detail.myVote !== undefined;
const votedUp = !detail.isOwnLock && detail.myVote === true;
const votedDn = !detail.isOwnLock && detail.myVote === false;
const id = base.displayId;
const card = document.createElement('div');
card.className = 'vote-card';
card.innerHTML = `
<div class="vote-card-media">
<img class="vote-card-img" src="data:image/jpeg;base64,${detail.image}" alt="Verifikationsbild">
<div class="vote-card-code">${esc(detail.code)}</div>
</div>
<div class="vote-card-body">
<div class="vote-meta">Verifikation · ${esc(base.lockeeName)} · ${fmtDateTime(base.createdAt)}</div>
<div class="vote-actions">
<button class="vote-btn ${votedUp ? 'voted-up' : ''}" id="up-${id}"
${voted ? 'disabled' : ''}
onclick="castVerVote('${id}', true)">
👍 <span class="vote-count" id="upcount-${id}">${detail.upvotes}</span>
</button>
<button class="vote-btn ${votedDn ? 'voted-down' : ''}" id="dn-${id}"
${voted ? 'disabled' : ''}
onclick="castVerVote('${id}', false)">
👎 <span class="vote-count" id="dncount-${id}">${detail.downvotes}</span>
</button>
</div>
</div>`;
return card;
}
async function castVerVote(displayId, upvote) {
document.getElementById('up-' + displayId).disabled = true;
document.getElementById('dn-' + displayId).disabled = true;
const res = await fetch(`/games/chastity/community/verification/${displayId}/vote/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ upvote })
});
if (res.ok || res.status === 202) {
const countEl = document.getElementById(upvote ? 'upcount-' + displayId : 'dncount-' + displayId);
countEl.textContent = parseInt(countEl.textContent) + 1;
document.getElementById((upvote ? 'up-' : 'dn-') + displayId)
.classList.add(upvote ? 'voted-up' : 'voted-down');
}
}
// ── Aufgaben-Abstimmungs-Karte ─────────────────────────────────────────────
function buildTaskVoteCard(base, detail) {
const isOwn = detail.isOwnLock;
const alreadyVoted = detail.entries.some(e => e.ownVote);
const id = base.displayId;
let optionsHtml = '';
(detail.entries || []).forEach((e, i) => {
const desc = e.description
? `<div style="font-size:0.75rem;color:var(--color-muted);margin-top:0.1rem;">${esc(e.description)}</div>`
: '';
const mins = e.minutes > 0
? ` <span style="font-size:0.75rem;color:var(--color-muted);">⏱ ${e.minutes} Min.</span>`
: '';
optionsHtml += `<button class="task-vote-btn ${e.ownVote ? 'my-vote' : ''}"
id="tvbtn-${id}-${i}"
${(alreadyVoted || isOwn) ? 'disabled' : ''}
onclick="castTaskVote('${id}', ${i})">
<div style="flex:1;min-width:0;">
<div style="font-weight:600;">${esc(e.title)}${mins}</div>
${desc}
</div>
<span class="task-vote-count" id="tvcount-${id}-${i}">${e.votes} Stimme${e.votes !== 1 ? 'n' : ''}</span>
</button>`;
});
const ownHint = isOwn
? `<div class="task-vote-own-hint">Das ist dein eigenes Lock du kannst hier nicht abstimmen.</div>`
: '';
const card = document.createElement('div');
card.className = 'task-vote-card';
card.innerHTML = `
<div class="task-vote-header">
<span class="task-vote-lockee">🃏 ${esc(base.lockeeName)}</span>
<span class="task-vote-expires">Endet: ${fmtDateTime(detail.expiresAt)}</span>
</div>
<div class="task-vote-options">${optionsHtml}</div>
${ownHint}`;
return card;
}
async function castTaskVote(displayId, taskIndex) {
document.querySelectorAll(`[id^="tvbtn-${displayId}-"]`).forEach(btn => btn.disabled = true);
const res = await fetch(`/games/chastity/community/taskvote/${displayId}/vote/${taskIndex}`, { method: 'POST' });
if (res.ok || res.status === 204) {
const countEl = document.getElementById(`tvcount-${displayId}-${taskIndex}`);
if (countEl) {
const next = (parseInt(countEl.textContent) || 0) + 1;
countEl.textContent = `${next} Stimme${next !== 1 ? 'n' : ''}`;
}
document.getElementById(`tvbtn-${displayId}-${taskIndex}`)?.classList.add('my-vote');
}
}
// ── Pranger-Karte ──────────────────────────────────────────────────────────
const PILLORY_LABELS = {
HYGIENE_OPENING_EXEEDED: 'Hygiene-Öffnung überschritten',
KEYHOLDER_DESCESSION: 'Keyholder hat aufgegeben'
};
function buildPilloryCard(base, detail) {
const card = document.createElement('div');
card.className = 'pillory-card';
card.innerHTML = `
<div class="pillory-header">
<span class="pillory-lockee">🔒 ${esc(base.lockeeName)}</span>
<span class="pillory-date">${fmtDateTime(base.createdAt)}</span>
</div>
<div class="pillory-reason">⚠️ ${esc(PILLORY_LABELS[detail.reason] || detail.reason)}</div>
${detail.message ? `<div class="pillory-message">${esc(detail.message)}</div>` : ''}`;
return card;
}
// ── Unified feed mit Paging ────────────────────────────────────────────────
let page = 0;
let exhausted = false;
let loading = false;
let rendered = 0;
async function fetchDetail(base) {
const urls = {
VERIFICATION: `/games/chastity/community/verification/${base.displayId}`,
TASK_VOTE: `/games/chastity/community/taskvote/${base.displayId}`,
PILLORY: `/games/chastity/community/pillory/${base.displayId}`
};
const url = urls[base.type];
if (!url) return null;
try {
const r = await fetch(url);
return r.ok ? await r.json() : null;
} catch(e) { return null; }
}
function buildCard(base, detail) {
if (!detail) return null;
if (base.type === 'VERIFICATION') return buildVerCard(base, detail);
if (base.type === 'TASK_VOTE') return buildTaskVoteCard(base, detail);
if (base.type === 'PILLORY') return buildPilloryCard(base, detail);
return null;
}
async function loadMore() {
if (loading || exhausted) return;
loading = true;
document.getElementById('loadSpinner').style.display = '';
let pageData;
try {
const r = await fetch(`/games/chastity/community?page=${page}&sort=createdAt,desc`);
if (!r.ok) { loading = false; document.getElementById('loadSpinner').style.display = 'none'; return; }
pageData = await r.json();
} catch(e) { loading = false; document.getElementById('loadSpinner').style.display = 'none'; return; }
const items = pageData.content || [];
if (pageData.last) exhausted = true;
page++;
const details = await Promise.all(items.map(fetchDetail));
document.getElementById('loadSpinner').style.display = 'none';
loading = false;
const feed = document.getElementById('feed');
items.forEach((base, i) => {
const card = buildCard(base, details[i]);
if (card) { feed.appendChild(card); rendered++; }
});
if (rendered === 0 && exhausted) {
document.getElementById('emptyHint').style.display = '';
}
}
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) loadMore();
}, { rootMargin: '200px' });
observer.observe(document.getElementById('sentinel'));
loadMore();
</script>
</body>
</html>