Weiter am Chastity Game gearbeitet und Interaktionen zwischen Keyholder und Lockee hinzugefügt

This commit is contained in:
2026-03-16 23:16:45 +01:00
parent 57a7c78037
commit 97c6f0a131
399 changed files with 34194 additions and 2272 deletions

View File

@@ -0,0 +1,249 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Community Votes XXX The Game</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;
}
.vote-grid {
display: flex;
flex-direction: column;
gap: 1rem;
}
.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;
}
.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 - stimme ab</div>
<div class="vote-grid" id="voteGrid"></div>
<div class="load-spinner" id="loadSpinner" style="display:none;">Lädt…</div>
<div class="empty-hint" id="emptyHint" style="display:none;">Heute gibt es noch keine Verifikationen ohne Keyholder.</div>
<div class="sentinel" id="sentinel"></div>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script src="/js/social-sidebar.js"></script>
<script>
let page = 0;
let loading = false;
let exhausted = false;
function fmtTime(isoStr) {
const d = new Date(isoStr);
return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
}
function buildCard(item) {
const isOwn = item.myVote === 'own';
const voted = isOwn || (item.myVote !== null && item.myVote !== undefined);
const votedUp = !isOwn && item.myVote === true;
const votedDn = !isOwn && item.myVote === false;
const card = document.createElement('div');
card.className = 'vote-card';
card.dataset.id = item.verificationId;
card.innerHTML = `
<div class="vote-card-media">
<img class="vote-card-img" src="data:image/jpeg;base64,${item.image}" alt="Verifikationsbild">
<div class="vote-card-code">${item.code}</div>
</div>
<div class="vote-card-body">
<div class="vote-meta">Erstellt um ${fmtTime(item.verificationTime)}</div>
<div class="vote-actions">
<button class="vote-btn ${votedUp ? 'voted-up' : ''}" id="up-${item.verificationId}"
${voted ? 'disabled' : ''}
onclick="castVote('${item.verificationId}', true)">
👍 <span class="vote-count" id="upcount-${item.verificationId}">${item.upvotes}</span>
</button>
<button class="vote-btn ${votedDn ? 'voted-down' : ''}" id="dn-${item.verificationId}"
${voted ? 'disabled' : ''}
onclick="castVote('${item.verificationId}', false)">
👎 <span class="vote-count" id="dncount-${item.verificationId}">${item.downvotes}</span>
</button>
</div>
</div>`;
return card;
}
async function loadPage() {
if (loading || exhausted) return;
loading = true;
document.getElementById('loadSpinner').style.display = '';
const res = await fetch('/verification/community?page=' + page);
document.getElementById('loadSpinner').style.display = 'none';
loading = false;
if (!res.ok) return;
const items = await res.json();
const grid = document.getElementById('voteGrid');
if (items.length === 0 && page === 0) {
document.getElementById('emptyHint').style.display = '';
exhausted = true;
return;
}
items.forEach(item => grid.appendChild(buildCard(item)));
if (items.length < 10 || (items.length > 0 && !items[items.length - 1].hasMore)) {
exhausted = true;
} else {
page++;
}
}
async function castVote(verificationId, upvote) {
const upBtn = document.getElementById('up-' + verificationId);
const dnBtn = document.getElementById('dn-' + verificationId);
upBtn.disabled = true;
dnBtn.disabled = true;
const res = await fetch('/verification/' + verificationId + '/vote/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ upvote })
});
if (res.ok || res.status === 202) {
const countEl = document.getElementById(upvote ? 'upcount-' + verificationId : 'dncount-' + verificationId);
countEl.textContent = parseInt(countEl.textContent) + 1;
(upvote ? upBtn : dnBtn).classList.add(upvote ? 'voted-up' : 'voted-down');
} else {
// Doppelter Vote oder Fehler Buttons trotzdem disabled lassen
}
}
// Infinite Scroll via IntersectionObserver
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) loadPage();
}, { rootMargin: '200px' });
observer.observe(document.getElementById('sentinel'));
loadPage();
</script>
</body>
</html>