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; }
|
||||
|
||||
.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 {
|
||||
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;
|
||||
@@ -201,11 +214,11 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row-between">
|
||||
<div class="row-between" id="round-normal-row">
|
||||
<h2 id="round-title">Round —</h2>
|
||||
<span class="mono" id="round-timer" style="font-size:1.1rem;font-weight:700">--:--</span>
|
||||
</div>
|
||||
<div class="row-between" style="margin-top:10px">
|
||||
<div class="row-between" style="margin-top:10px" id="round-stats-row">
|
||||
<div>
|
||||
<div class="hint" style="margin-bottom:2px">Giocatori</div>
|
||||
<span class="mono" id="round-players">—</span>
|
||||
@@ -215,6 +228,13 @@
|
||||
<span class="mono" id="round-jackpot">—</span> <span class="balance-unit">PLM</span>
|
||||
</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>
|
||||
|
||||
<nav class="menu" aria-label="Sezioni">
|
||||
@@ -337,7 +357,7 @@ function switchPanel(name) {
|
||||
|
||||
let roundCloseAt = null;
|
||||
let roundTimerInterval = null;
|
||||
let roundPollInterval = null;
|
||||
let roundPollTimeout = null;
|
||||
|
||||
const ROUND_STATUS_LABELS = {
|
||||
open: 'aperto',
|
||||
@@ -346,6 +366,15 @@ const ROUND_STATUS_LABELS = {
|
||||
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() {
|
||||
const el = document.getElementById('round-timer');
|
||||
if (!roundCloseAt) { el.textContent = '--:--'; return; }
|
||||
@@ -355,6 +384,30 @@ function updateRoundTimer() {
|
||||
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() {
|
||||
try {
|
||||
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;
|
||||
roundCloseAt = data.closes_at ? new Date(data.closes_at) : null;
|
||||
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() {
|
||||
@@ -377,9 +471,7 @@ function showDashboard() {
|
||||
refreshMe();
|
||||
refreshRound();
|
||||
clearInterval(roundTimerInterval);
|
||||
clearInterval(roundPollInterval);
|
||||
roundTimerInterval = setInterval(updateRoundTimer, 1000);
|
||||
roundPollInterval = setInterval(refreshRound, 15000);
|
||||
}
|
||||
|
||||
function persistSession(data, u) {
|
||||
@@ -429,8 +521,12 @@ async function login() {
|
||||
function logout() {
|
||||
localStorage.clear();
|
||||
token = username = address = null;
|
||||
myUserId = null;
|
||||
activeResultRoundId = null;
|
||||
revealedRounds.clear();
|
||||
for (const key of Object.keys(drawStartedAt)) delete drawStartedAt[key];
|
||||
clearInterval(roundTimerInterval);
|
||||
clearInterval(roundPollInterval);
|
||||
clearTimeout(roundPollTimeout);
|
||||
document.getElementById('dashboard-section').classList.add('hidden');
|
||||
document.getElementById('auth-section').classList.remove('hidden');
|
||||
}
|
||||
@@ -444,11 +540,14 @@ async function copyAddress() {
|
||||
}
|
||||
}
|
||||
|
||||
let myUserId = null;
|
||||
|
||||
async function refreshMe() {
|
||||
const btn = document.getElementById('refresh-btn');
|
||||
await withLoading(btn, '…', async () => {
|
||||
try {
|
||||
const data = await call('GET', '/users/me');
|
||||
myUserId = data.id;
|
||||
document.getElementById('dash-balance').textContent = data.balance_sats / SATS_PER_PLM;
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
|
||||
Reference in New Issue
Block a user