Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
308 lines
12 KiB
HTML
308 lines
12 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>Lock-Einladung – xXx Sphere</title>
|
||
<link rel="stylesheet" href="/css/variables.css">
|
||
<link rel="stylesheet" href="/css/style.css">
|
||
<style>
|
||
.invite-card {
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 12px;
|
||
padding: 2rem 1.5rem;
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
.invite-icon { font-size: 3rem; margin-bottom: 1rem; }
|
||
.invite-title { font-size: 1.3rem; font-weight: 700; margin-bottom: 0.4rem; }
|
||
.invite-sub { color: var(--color-muted); font-size: 0.9rem; margin-bottom: 1.5rem; }
|
||
.invite-detail {
|
||
background: var(--color-secondary);
|
||
border-radius: 8px;
|
||
padding: 0.75rem 1rem;
|
||
margin-bottom: 1.5rem;
|
||
text-align: left;
|
||
font-size: 0.9rem;
|
||
}
|
||
.invite-detail dt { color: var(--color-muted); font-size: 0.78rem; margin-bottom: 0.1rem; }
|
||
.invite-detail dd { font-weight: 600; margin: 0 0 0.5rem 0; }
|
||
.invite-detail dd:last-child { margin-bottom: 0; }
|
||
.invite-actions { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; }
|
||
.invite-actions button { width: auto; padding: 0.65rem 1.5rem; }
|
||
.btn-danger { background: #c0392b !important; }
|
||
.btn-danger:hover { background: #a93226 !important; }
|
||
|
||
.code-lines-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
justify-content: center;
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
.code-lines-row input { width: 80px; text-align: center; }
|
||
.code-lines-row span { color: var(--color-text); font-size: 0.9rem; }
|
||
|
||
/* Unlock-Code-Modal */
|
||
.unlock-modal-bg {
|
||
display: none;
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 400;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.unlock-modal-bg.open { display: flex; }
|
||
.unlock-modal-overlay {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: rgba(0,0,0,0.55);
|
||
}
|
||
.unlock-modal-box {
|
||
position: relative;
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 12px;
|
||
padding: 1.5rem 1.5rem 1.25rem;
|
||
max-width: 380px;
|
||
width: 90%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
z-index: 1;
|
||
text-align: center;
|
||
}
|
||
.unlock-code-display {
|
||
font-family: monospace;
|
||
font-size: 2rem;
|
||
letter-spacing: 0.3em;
|
||
background: var(--color-secondary);
|
||
border-radius: 8px;
|
||
padding: 1rem 1.5rem;
|
||
color: var(--color-primary);
|
||
line-height: 1.8;
|
||
word-break: break-all;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
#stateLoading { display: none; }
|
||
#stateError { display: none; }
|
||
#stateAlready { display: none; }
|
||
#stateDeclined { display: none; }
|
||
</style>
|
||
</head>
|
||
<body class="app">
|
||
<div class="main">
|
||
<div class="content">
|
||
|
||
<div id="stateLoading" style="text-align:center;padding:3rem 1rem;color:var(--color-muted);">Lade Einladung…</div>
|
||
|
||
<div id="stateError" style="text-align:center;padding:3rem 1rem;">
|
||
<div style="font-size:2.5rem;margin-bottom:1rem;">⚠️</div>
|
||
<h2 style="margin-bottom:0.5rem;">Einladung nicht gefunden</h2>
|
||
<p style="color:var(--color-muted);">Diese Einladung existiert nicht oder wurde bereits bearbeitet.</p>
|
||
<a href="/games/common/einladungen.html" style="display:inline-block;margin-top:1.5rem;padding:0.65rem 1.5rem;background:var(--color-primary);color:#fff;border-radius:8px;text-decoration:none;font-weight:600;">Zu meinen Einladungen</a>
|
||
</div>
|
||
|
||
<div id="stateAlready" style="text-align:center;padding:3rem 1rem;">
|
||
<div style="font-size:2.5rem;margin-bottom:1rem;">🔒</div>
|
||
<h2 style="margin-bottom:0.5rem;">Lock bereits aktiv</h2>
|
||
<p style="color:var(--color-muted);">Diese Einladung wurde bereits angenommen.</p>
|
||
</div>
|
||
|
||
<div id="stateDeclined" style="text-align:center;padding:3rem 1rem;">
|
||
<div style="font-size:2.5rem;margin-bottom:1rem;">✕</div>
|
||
<h2 style="margin-bottom:0.5rem;">Einladung abgelehnt</h2>
|
||
<p style="color:var(--color-muted);">Du hast die Einladung abgelehnt. Der Keyholder wurde benachrichtigt.</p>
|
||
<a href="/games/common/einladungen.html" style="display:inline-block;margin-top:1.5rem;padding:0.65rem 1.5rem;background:var(--color-primary);color:#fff;border-radius:8px;text-decoration:none;font-weight:600;">Zu meinen Einladungen</a>
|
||
</div>
|
||
|
||
<div id="stateInvite" style="display:none;">
|
||
<div class="invite-card">
|
||
<div class="invite-icon">🔒</div>
|
||
<div class="invite-title">Lock-Einladung</div>
|
||
<div class="invite-sub" id="invSubtitle"></div>
|
||
<dl class="invite-detail" id="invDetail"></dl>
|
||
|
||
<div id="acceptSection">
|
||
<p style="font-size:0.88rem;color:var(--color-muted);margin-bottom:0.75rem;">
|
||
Wie viele Ziffern soll dein Entsperrcode haben?
|
||
</p>
|
||
<div class="code-lines-row">
|
||
<input type="number" id="codeLines" min="1" max="20" value="5">
|
||
<span>Ziffern</span>
|
||
</div>
|
||
<div class="invite-actions">
|
||
<button class="btn-danger" onclick="declineInvitation()">✕ Ablehnen</button>
|
||
<button onclick="acceptInvitation()">✓ Annehmen</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="errorMsg" style="color:#e74c3c;font-size:0.85rem;margin-top:0.5rem;display:none;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Unlock-Code-Modal -->
|
||
<div class="unlock-modal-bg" id="unlockModal">
|
||
<div class="unlock-modal-overlay"></div>
|
||
<div class="unlock-modal-box">
|
||
<div style="font-size:2rem;">🔒</div>
|
||
<h3 id="unlockModalTitle" style="margin:0;">Dein Entsperrcode</h3>
|
||
<p id="unlockModalHint" style="color:var(--color-muted);font-size:0.85rem;margin:0;">
|
||
Stelle die Kombination deines Tresors auf den folgenden Code ein und verschließe deinen Schlüssel in diesem.
|
||
</p>
|
||
<div class="unlock-code-display" id="unlockCodeDisplay"></div>
|
||
<div id="unlockModalCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);font-family:monospace;"></div>
|
||
<button id="unlockModalBtn" style="width:100%;margin-top:0.25rem;">Weiter</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/icons.js"></script>
|
||
<script src="/js/nav.js"></script>
|
||
<script src="/js/social-sidebar.js"></script>
|
||
<script>
|
||
const params = new URLSearchParams(window.location.search);
|
||
const token = params.get('token');
|
||
let lockId = null;
|
||
|
||
document.getElementById('stateLoading').style.display = '';
|
||
|
||
async function load() {
|
||
if (!token) { showState('stateError'); return; }
|
||
try {
|
||
const res = await fetch('/lockee/invitation/' + encodeURIComponent(token));
|
||
if (res.status === 409) { showState('stateAlready'); return; }
|
||
if (!res.ok) { showState('stateError'); return; }
|
||
const inv = await res.json();
|
||
lockId = inv.lockId;
|
||
|
||
document.getElementById('invSubtitle').textContent =
|
||
inv.keyholderName + ' hat dich als Lockee eingeladen';
|
||
const dl = document.getElementById('invDetail');
|
||
dl.innerHTML = `
|
||
<dt>Lock-Name</dt><dd>${esc(inv.lockName)}</dd>
|
||
<dt>Keyholder</dt><dd>${esc(inv.keyholderName)}</dd>`;
|
||
showState('stateInvite');
|
||
} catch(e) {
|
||
showState('stateError');
|
||
}
|
||
}
|
||
|
||
function showState(id) {
|
||
document.getElementById('stateLoading').style.display = 'none';
|
||
['stateError','stateAlready','stateInvite','stateDeclined'].forEach(s => {
|
||
document.getElementById(s).style.display = s === id ? '' : 'none';
|
||
});
|
||
}
|
||
|
||
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
||
|
||
async function acceptInvitation() {
|
||
const lines = parseInt(document.getElementById('codeLines').value);
|
||
if (!lines || lines < 1) { showError('Bitte eine Ziffernanzahl eingeben.'); return; }
|
||
|
||
const btn = document.querySelector('#acceptSection button:last-child');
|
||
btn.disabled = true;
|
||
document.getElementById('errorMsg').style.display = 'none';
|
||
|
||
try {
|
||
const res = await fetch('/lockee/invitation/' + encodeURIComponent(token) + '/accept', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ unlockCodeLines: lines })
|
||
});
|
||
if (!res.ok) { btn.disabled = false; showError('Fehler beim Annehmen der Einladung.'); return; }
|
||
const data = await res.json();
|
||
showUnlockCodeModal(data.unlockCode, data.lockId);
|
||
} catch(e) {
|
||
btn.disabled = false;
|
||
showError('Fehler beim Annehmen der Einladung.');
|
||
}
|
||
}
|
||
|
||
async function declineInvitation() {
|
||
if (!confirm('Bist du sicher, dass du diese Einladung ablehnen möchtest? Das Lock wird gelöscht.')) return;
|
||
const btn = document.querySelector('.btn-danger');
|
||
btn.disabled = true;
|
||
try {
|
||
const res = await fetch('/lockee/invitation/' + encodeURIComponent(token), { method: 'DELETE' });
|
||
if (res.ok || res.status === 204) {
|
||
showState('stateDeclined');
|
||
} else {
|
||
btn.disabled = false;
|
||
showError('Fehler beim Ablehnen der Einladung.');
|
||
}
|
||
} catch(e) {
|
||
btn.disabled = false;
|
||
showError('Fehler beim Ablehnen der Einladung.');
|
||
}
|
||
}
|
||
|
||
function showError(msg) {
|
||
const el = document.getElementById('errorMsg');
|
||
el.textContent = msg;
|
||
el.style.display = '';
|
||
}
|
||
|
||
function showUnlockCodeModal(code, lid) {
|
||
document.getElementById('unlockCodeDisplay').textContent = code;
|
||
const url = '/games/chastity/activelock.html?lockId=' + lid;
|
||
const btn = document.getElementById('unlockModalBtn');
|
||
btn.onclick = () => startCodeScramble(code, url);
|
||
document.getElementById('unlockModal').classList.add('open');
|
||
}
|
||
|
||
function startCodeScramble(realCode, url) {
|
||
const display = document.getElementById('unlockCodeDisplay');
|
||
const btn = document.getElementById('unlockModalBtn');
|
||
const hint = document.getElementById('unlockModalHint');
|
||
const countdown = document.getElementById('unlockModalCountdown');
|
||
|
||
const len = realCode.length;
|
||
const DURATION = 3 * 60;
|
||
let remaining = DURATION;
|
||
let stopped = false;
|
||
|
||
function randomCode() {
|
||
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
|
||
}
|
||
function finish() {
|
||
stopped = true;
|
||
clearInterval(scrambleInterval);
|
||
clearInterval(countdownInterval);
|
||
window.location.href = url;
|
||
}
|
||
|
||
if (hint) hint.style.display = 'none';
|
||
countdown.style.display = '';
|
||
document.getElementById('unlockModalTitle').textContent = 'Nun vergessen wir den Code…';
|
||
btn.textContent = 'Abbrechen';
|
||
btn.onclick = finish;
|
||
|
||
function updateCountdown() {
|
||
const m = Math.floor(remaining / 60);
|
||
const s = remaining % 60;
|
||
countdown.textContent = `${m}:${String(s).padStart(2, '0')}`;
|
||
}
|
||
updateCountdown();
|
||
|
||
const scrambleInterval = setInterval(() => { if (!stopped) display.textContent = randomCode(); }, 1000);
|
||
const countdownInterval = setInterval(() => {
|
||
if (stopped) return;
|
||
remaining--;
|
||
updateCountdown();
|
||
if (remaining <= 0) finish();
|
||
}, 1000);
|
||
}
|
||
|
||
load();
|
||
</script>
|
||
</body>
|
||
</html>
|