Show a draw animation and win/lose reveal on the user dashboard
When the round leaves "open" (closing/drawing/paying_out), the round card swaps its timer for a spinning "Estrazione del vincitore in corso…" state instead — bets are already rejected server-side once the round isn't open, this just reflects that visually. Once winner_user_id is set AND at least draw_animation_seconds has elapsed since the round started closing (client-tracked per round_id), it reveals "🎉 Hai vinto! +N PLM" (compared against the user's own id from /users/me) or "Non hai vinto questa volta.", with a success toast on a win. The result stays on screen through the cooldown gap and only clears once a genuinely new round opens (tracked via activeResultRoundId), not the instant the old round has no active status. Polling is now dynamic (setTimeout-chained, not setInterval): 3s while the round is closing/drawing/paying_out for a responsive reveal, 15s while open. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+106
-7
@@ -140,6 +140,19 @@
|
|||||||
|
|
||||||
.hidden { display: none !important; }
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
|
.draw-state { display: none; text-align: center; padding: 8px 0 4px; }
|
||||||
|
.draw-state.active { display: block; }
|
||||||
|
.draw-spinner {
|
||||||
|
width: 40px; height: 40px; margin: 0 auto 10px;
|
||||||
|
border: 3px solid var(--color-border); border-top-color: var(--color-primary);
|
||||||
|
border-radius: 50%; animation: spin 900ms linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
.draw-state .draw-label { font-weight: 600; font-size: 0.95rem; }
|
||||||
|
.draw-result { font-size: 1.05rem; font-weight: 700; padding: 6px 0; }
|
||||||
|
.draw-result.win { color: var(--color-success); }
|
||||||
|
.draw-result.lose { color: var(--color-muted-foreground); }
|
||||||
|
|
||||||
#toast-container {
|
#toast-container {
|
||||||
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
|
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
|
||||||
display: flex; flex-direction: column; gap: 8px; z-index: 100; width: calc(100% - 40px); max-width: 440px;
|
display: flex; flex-direction: column; gap: 8px; z-index: 100; width: calc(100% - 40px); max-width: 440px;
|
||||||
@@ -201,11 +214,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="row-between">
|
<div class="row-between" id="round-normal-row">
|
||||||
<h2 id="round-title">Round —</h2>
|
<h2 id="round-title">Round —</h2>
|
||||||
<span class="mono" id="round-timer" style="font-size:1.1rem;font-weight:700">--:--</span>
|
<span class="mono" id="round-timer" style="font-size:1.1rem;font-weight:700">--:--</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-between" style="margin-top:10px">
|
<div class="row-between" style="margin-top:10px" id="round-stats-row">
|
||||||
<div>
|
<div>
|
||||||
<div class="hint" style="margin-bottom:2px">Giocatori</div>
|
<div class="hint" style="margin-bottom:2px">Giocatori</div>
|
||||||
<span class="mono" id="round-players">—</span>
|
<span class="mono" id="round-players">—</span>
|
||||||
@@ -215,6 +228,13 @@
|
|||||||
<span class="mono" id="round-jackpot">—</span> <span class="balance-unit">PLM</span>
|
<span class="mono" id="round-jackpot">—</span> <span class="balance-unit">PLM</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="draw-state" id="draw-state">
|
||||||
|
<div class="draw-spinner"></div>
|
||||||
|
<div class="draw-label">Estrazione del vincitore in corso…</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hidden" id="draw-result"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="menu" aria-label="Sezioni">
|
<nav class="menu" aria-label="Sezioni">
|
||||||
@@ -337,7 +357,7 @@ function switchPanel(name) {
|
|||||||
|
|
||||||
let roundCloseAt = null;
|
let roundCloseAt = null;
|
||||||
let roundTimerInterval = null;
|
let roundTimerInterval = null;
|
||||||
let roundPollInterval = null;
|
let roundPollTimeout = null;
|
||||||
|
|
||||||
const ROUND_STATUS_LABELS = {
|
const ROUND_STATUS_LABELS = {
|
||||||
open: 'aperto',
|
open: 'aperto',
|
||||||
@@ -346,6 +366,15 @@ const ROUND_STATUS_LABELS = {
|
|||||||
paying_out: 'pagamento in corso',
|
paying_out: 'pagamento in corso',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DRAWING_STATUSES = ['closing', 'drawing', 'paying_out'];
|
||||||
|
|
||||||
|
// Per round_id: when we first saw it enter a drawing status (client clock), and
|
||||||
|
// whether the win/lose result has already been shown for it. Local-only state,
|
||||||
|
// not persisted — a page reload just re-derives it from the next poll.
|
||||||
|
const drawStartedAt = {};
|
||||||
|
const revealedRounds = new Set();
|
||||||
|
let activeResultRoundId = null; // round_id whose win/lose result is on screen, if any
|
||||||
|
|
||||||
function updateRoundTimer() {
|
function updateRoundTimer() {
|
||||||
const el = document.getElementById('round-timer');
|
const el = document.getElementById('round-timer');
|
||||||
if (!roundCloseAt) { el.textContent = '--:--'; return; }
|
if (!roundCloseAt) { el.textContent = '--:--'; return; }
|
||||||
@@ -355,6 +384,30 @@ function updateRoundTimer() {
|
|||||||
el.textContent = mm + ':' + ss;
|
el.textContent = mm + ':' + ss;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showDrawingState() {
|
||||||
|
document.getElementById('round-normal-row').classList.add('hidden');
|
||||||
|
document.getElementById('round-stats-row').classList.add('hidden');
|
||||||
|
document.getElementById('draw-state').classList.add('active');
|
||||||
|
document.getElementById('draw-result').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResultState(html, cls) {
|
||||||
|
document.getElementById('round-normal-row').classList.add('hidden');
|
||||||
|
document.getElementById('round-stats-row').classList.add('hidden');
|
||||||
|
document.getElementById('draw-state').classList.remove('active');
|
||||||
|
const el = document.getElementById('draw-result');
|
||||||
|
el.className = 'draw-result ' + cls;
|
||||||
|
el.innerHTML = html;
|
||||||
|
el.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNormalState() {
|
||||||
|
document.getElementById('round-normal-row').classList.remove('hidden');
|
||||||
|
document.getElementById('round-stats-row').classList.remove('hidden');
|
||||||
|
document.getElementById('draw-state').classList.remove('active');
|
||||||
|
document.getElementById('draw-result').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshRound() {
|
async function refreshRound() {
|
||||||
try {
|
try {
|
||||||
const data = await call('GET', '/rounds/current');
|
const data = await call('GET', '/rounds/current');
|
||||||
@@ -365,7 +418,48 @@ async function refreshRound() {
|
|||||||
document.getElementById('round-jackpot').textContent = data.jackpot_sats / SATS_PER_PLM;
|
document.getElementById('round-jackpot').textContent = data.jackpot_sats / SATS_PER_PLM;
|
||||||
roundCloseAt = data.closes_at ? new Date(data.closes_at) : null;
|
roundCloseAt = data.closes_at ? new Date(data.closes_at) : null;
|
||||||
updateRoundTimer();
|
updateRoundTimer();
|
||||||
} catch (e) {}
|
|
||||||
|
const isDrawing = data.round_id && DRAWING_STATUSES.includes(data.status);
|
||||||
|
|
||||||
|
if (isDrawing) {
|
||||||
|
if (!(data.round_id in drawStartedAt)) drawStartedAt[data.round_id] = Date.now();
|
||||||
|
const elapsedMs = Date.now() - drawStartedAt[data.round_id];
|
||||||
|
const minMs = data.draw_animation_seconds * 1000;
|
||||||
|
|
||||||
|
if (data.winner_user_id != null && elapsedMs >= minMs && !revealedRounds.has(data.round_id)) {
|
||||||
|
revealedRounds.add(data.round_id);
|
||||||
|
activeResultRoundId = data.round_id;
|
||||||
|
if (myUserId != null && data.winner_user_id === myUserId) {
|
||||||
|
const won = (data.winner_amount_sats / SATS_PER_PLM);
|
||||||
|
showResultState('🎉 Hai vinto! +' + won + ' PLM', 'win');
|
||||||
|
toast('Hai vinto il round #' + data.round_id + '! +' + won + ' PLM', 'success');
|
||||||
|
} else {
|
||||||
|
showResultState('Non hai vinto questa volta.', 'lose');
|
||||||
|
}
|
||||||
|
} else if (!revealedRounds.has(data.round_id)) {
|
||||||
|
showDrawingState();
|
||||||
|
}
|
||||||
|
} else if (data.round_id && data.round_id !== activeResultRoundId) {
|
||||||
|
// a genuinely new round is open — clear any previous result and go back to normal
|
||||||
|
activeResultRoundId = null;
|
||||||
|
showNormalState();
|
||||||
|
} else if (!data.round_id && activeResultRoundId == null) {
|
||||||
|
// nothing has ever been revealed and there's no active round — plain empty state
|
||||||
|
showNormalState();
|
||||||
|
}
|
||||||
|
// else: no active round right now, but we just revealed a result for the last
|
||||||
|
// one — keep it on screen through the cooldown gap instead of flashing back to
|
||||||
|
// "Nessun round attivo".
|
||||||
|
|
||||||
|
scheduleNextRoundPoll(isDrawing);
|
||||||
|
} catch (e) {
|
||||||
|
scheduleNextRoundPoll(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleNextRoundPoll(fast) {
|
||||||
|
clearTimeout(roundPollTimeout);
|
||||||
|
roundPollTimeout = setTimeout(refreshRound, fast ? 3000 : 15000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDashboard() {
|
function showDashboard() {
|
||||||
@@ -377,9 +471,7 @@ function showDashboard() {
|
|||||||
refreshMe();
|
refreshMe();
|
||||||
refreshRound();
|
refreshRound();
|
||||||
clearInterval(roundTimerInterval);
|
clearInterval(roundTimerInterval);
|
||||||
clearInterval(roundPollInterval);
|
|
||||||
roundTimerInterval = setInterval(updateRoundTimer, 1000);
|
roundTimerInterval = setInterval(updateRoundTimer, 1000);
|
||||||
roundPollInterval = setInterval(refreshRound, 15000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistSession(data, u) {
|
function persistSession(data, u) {
|
||||||
@@ -429,8 +521,12 @@ async function login() {
|
|||||||
function logout() {
|
function logout() {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
token = username = address = null;
|
token = username = address = null;
|
||||||
|
myUserId = null;
|
||||||
|
activeResultRoundId = null;
|
||||||
|
revealedRounds.clear();
|
||||||
|
for (const key of Object.keys(drawStartedAt)) delete drawStartedAt[key];
|
||||||
clearInterval(roundTimerInterval);
|
clearInterval(roundTimerInterval);
|
||||||
clearInterval(roundPollInterval);
|
clearTimeout(roundPollTimeout);
|
||||||
document.getElementById('dashboard-section').classList.add('hidden');
|
document.getElementById('dashboard-section').classList.add('hidden');
|
||||||
document.getElementById('auth-section').classList.remove('hidden');
|
document.getElementById('auth-section').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
@@ -444,11 +540,14 @@ async function copyAddress() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let myUserId = null;
|
||||||
|
|
||||||
async function refreshMe() {
|
async function refreshMe() {
|
||||||
const btn = document.getElementById('refresh-btn');
|
const btn = document.getElementById('refresh-btn');
|
||||||
await withLoading(btn, '…', async () => {
|
await withLoading(btn, '…', async () => {
|
||||||
try {
|
try {
|
||||||
const data = await call('GET', '/users/me');
|
const data = await call('GET', '/users/me');
|
||||||
|
myUserId = data.id;
|
||||||
document.getElementById('dash-balance').textContent = data.balance_sats / SATS_PER_PLM;
|
document.getElementById('dash-balance').textContent = data.balance_sats / SATS_PER_PLM;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(e.message, 'error');
|
toast(e.message, 'error');
|
||||||
|
|||||||
Reference in New Issue
Block a user