Weiter am Chastity Game gearbeitet und Interaktionen zwischen Keyholder und Lockee hinzugefügt
This commit is contained in:
249
xxxthegame/src/main/resources/static/communityvotes.html
Normal file
249
xxxthegame/src/main/resources/static/communityvotes.html
Normal 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>
|
||||
Reference in New Issue
Block a user