Verschiebung nach anderem RePo - nun pro Projekt getrennt
This commit is contained in:
413
bin/main/static/games/chastity/communityvotes.html
Normal file
413
bin/main/static/games/chastity/communityvotes.html
Normal file
@@ -0,0 +1,413 @@
|
||||
<!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 & 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
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>
|
||||
Reference in New Issue
Block a user