Add a marketing landing hero and a live chain/round status strip

The user page's login screen was a bare test dashboard with no explanation
of how the lottery works; it now leads with a hero (3-step explainer, trust
pills) shown only while logged out, plus a bento-style nav and a glowing
round card during the draw.

Both the user and admin pages now show the current chain tip height and
lottery status (open / drawing / waiting for next round) via a small status
strip, polled from /rounds/current (extended with chain_tip_height sourced
from ElectrumListener.tip_height).
This commit is contained in:
2026-07-22 10:16:51 +02:00
parent 8627f3fa0c
commit 78109aa4c4
3 changed files with 295 additions and 15 deletions
+9 -3
View File
@@ -1,6 +1,6 @@
from datetime import timedelta, timezone from datetime import timedelta, timezone
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, Request
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -24,16 +24,21 @@ class CurrentRoundResponse(BaseModel):
draw_animation_seconds: int draw_animation_seconds: int
winner_user_id: int | None = None winner_user_id: int | None = None
winner_amount_sats: int | None = None winner_amount_sats: int | None = None
chain_tip_height: int | None = None
@router.get("/current", response_model=CurrentRoundResponse) @router.get("/current", response_model=CurrentRoundResponse)
async def current_round(session: AsyncSession = Depends(get_session)) -> CurrentRoundResponse: async def current_round(request: Request, session: AsyncSession = Depends(get_session)) -> CurrentRoundResponse:
config = await get_round_config(session) config = await get_round_config(session)
round_ = await get_active_round(session) round_ = await get_active_round(session)
listener = request.app.state.electrum_listener
chain_tip_height = listener.tip_height or None
if round_ is None: if round_ is None:
await session.commit() await session.commit()
return CurrentRoundResponse( return CurrentRoundResponse(
bet_amount_sats=config.bet_amount_sats, draw_animation_seconds=config.draw_animation_seconds bet_amount_sats=config.bet_amount_sats,
draw_animation_seconds=config.draw_animation_seconds,
chain_tip_height=chain_tip_height,
) )
participant_count = await session.scalar( participant_count = await session.scalar(
@@ -54,4 +59,5 @@ async def current_round(session: AsyncSession = Depends(get_session)) -> Current
draw_animation_seconds=config.draw_animation_seconds, draw_animation_seconds=config.draw_animation_seconds,
winner_user_id=round_.winner_user_id, winner_user_id=round_.winner_user_id,
winner_amount_sats=round_.winner_amount_sats, winner_amount_sats=round_.winner_amount_sats,
chain_tip_height=chain_tip_height,
) )
+64 -1
View File
@@ -57,6 +57,26 @@
.navbar .nav-tab:hover { color: var(--color-foreground); } .navbar .nav-tab:hover { color: var(--color-foreground); }
.navbar .spacer { flex: 1; } .navbar .spacer { flex: 1; }
.chain-status-pill { display: inline-flex; align-items: center; gap: 7px; font-weight: 600; font-size: 0.82rem; white-space: nowrap; }
.status-dot {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
background: var(--color-muted-foreground);
}
.status-dot.status-open {
background: var(--color-success);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-success) 18%, transparent);
}
.status-dot.status-drawing {
background: var(--color-primary);
animation: status-dot-pulse 1400ms ease-in-out infinite;
}
.status-dot.status-waiting { background: var(--color-muted-foreground); }
@keyframes status-dot-pulse {
0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 45%, transparent); }
50% { box-shadow: 0 0 0 5px transparent; }
}
.chain-block { color: var(--color-muted-foreground); font-size: 0.82rem; white-space: nowrap; }
main { max-width: 960px; margin: 0 auto; padding: 24px 20px 80px; } main { max-width: 960px; margin: 0 auto; padding: 24px 20px 80px; }
.view { display: none; } .view { display: none; }
@@ -187,7 +207,12 @@
<span class="nav-tab" id="nav-pending" onclick="switchView('pending')">Transazioni pendenti</span> <span class="nav-tab" id="nav-pending" onclick="switchView('pending')">Transazioni pendenti</span>
<span class="nav-tab" id="nav-audit" onclick="switchView('audit')">Audit log</span> <span class="nav-tab" id="nav-audit" onclick="switchView('audit')">Audit log</span>
<span class="spacer"></span> <span class="spacer"></span>
<button class="secondary" style="margin:8px 0" onclick="adminLogout()">Esci</button> <span class="chain-status-pill">
<span class="status-dot" id="chain-status-dot"></span>
<span id="chain-status-label">Connessione…</span>
</span>
<span class="chain-block mono" id="chain-block">Blocco —</span>
<button class="secondary" style="margin:8px 0 8px 14px" onclick="adminLogout()">Esci</button>
</nav> </nav>
<main> <main>
@@ -342,6 +367,42 @@ function fmtDate(iso) {
return new Date(iso).toLocaleString('it-IT'); return new Date(iso).toLocaleString('it-IT');
} }
const DRAWING_STATUSES = ['closing', 'drawing', 'paying_out'];
const CHAIN_STATUS_LABELS = {
waiting: 'In attesa del prossimo round',
open: 'Round aperto',
drawing: 'Estrazione in corso',
};
let chainStatusInterval = null;
async function refreshChainStatus() {
try {
const res = await fetch('/rounds/current');
const data = await res.json();
let statusKey;
if (!data.round_id) statusKey = 'waiting';
else if (DRAWING_STATUSES.includes(data.status)) statusKey = 'drawing';
else statusKey = 'open';
document.getElementById('chain-status-dot').className = 'status-dot status-' + statusKey;
document.getElementById('chain-status-label').textContent = CHAIN_STATUS_LABELS[statusKey];
document.getElementById('chain-block').textContent =
'Blocco ' + (data.chain_tip_height != null ? '#' + data.chain_tip_height : '—');
} catch (e) {
// leave the last-known status on screen rather than blanking it out
}
}
function startChainStatusPolling() {
refreshChainStatus();
clearInterval(chainStatusInterval);
chainStatusInterval = setInterval(refreshChainStatus, 15000);
}
function stopChainStatusPolling() {
clearInterval(chainStatusInterval);
chainStatusInterval = null;
}
const VIEWS = ['parametri', 'utenti', 'round', 'pending', 'audit']; const VIEWS = ['parametri', 'utenti', 'round', 'pending', 'audit'];
function switchView(name) { function switchView(name) {
@@ -356,6 +417,7 @@ function switchView(name) {
function showDashboard() { function showDashboard() {
document.getElementById('login-section').classList.add('hidden'); document.getElementById('login-section').classList.add('hidden');
document.getElementById('dashboard-section').classList.remove('hidden'); document.getElementById('dashboard-section').classList.remove('hidden');
startChainStatusPolling();
} }
async function loadDashboard() { async function loadDashboard() {
@@ -379,6 +441,7 @@ async function adminLogin() {
} }
function adminLogout() { function adminLogout() {
stopChainStatusPolling();
sessionStorage.removeItem('plm_admin_token'); sessionStorage.removeItem('plm_admin_token');
adminToken = null; adminToken = null;
document.getElementById('admin-token').value = ''; document.getElementById('admin-token').value = '';
+222 -11
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>PLM Lottery — Test</title> <title>PLM Lottery</title>
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@500;600&family=Fira+Sans:wght@400;500;600;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@500;600&family=Fira+Sans:wght@400;500;600;700&display=swap');
@@ -167,6 +167,123 @@
.toast.error { background: var(--color-destructive-bg); color: var(--color-destructive); } .toast.error { background: var(--color-destructive-bg); color: var(--color-destructive); }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } @keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
/* --- landing hero (shown only when logged out) --- */
body {
position: relative;
isolation: isolate;
}
body::before {
content: '';
position: fixed;
inset: 0;
z-index: -1;
background:
radial-gradient(600px circle at 20% -10%, color-mix(in srgb, var(--color-primary) 16%, transparent), transparent 60%),
radial-gradient(500px circle at 90% 10%, color-mix(in srgb, var(--color-accent) 12%, transparent), transparent 60%);
}
.hero { text-align: center; padding: 8px 0 28px; }
.hero .eyebrow {
display: inline-flex; align-items: center; gap: 6px;
font-size: 0.75rem; font-weight: 600; letter-spacing: 0.02em;
color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--color-primary) 30%, transparent);
padding: 4px 10px; border-radius: 999px; margin-bottom: 14px;
}
.hero h1 {
font-size: 1.75rem; font-weight: 700; letter-spacing: -0.02em; margin: 0 0 8px;
background: linear-gradient(135deg, var(--color-foreground), var(--color-accent) 120%);
-webkit-background-clip: text; background-clip: text; color: transparent;
}
.hero p.lead { color: var(--color-muted-foreground); font-size: 0.95rem; margin: 0 auto; max-width: 360px; }
.hero-steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 22px 0; }
.hero-step {
background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius);
padding: 14px 8px; transition: transform 150ms, border-color 150ms;
}
.hero-step:hover { transform: translateY(-2px); border-color: var(--color-ring); }
.hero-step .step-icon {
width: 32px; height: 32px; margin: 0 auto 8px; border-radius: 999px;
background: color-mix(in srgb, var(--color-primary) 14%, transparent); color: var(--color-primary);
display: flex; align-items: center; justify-content: center;
}
.hero-step .step-icon .icon { width: 16px; height: 16px; }
.hero-step .step-title { font-size: 0.8rem; font-weight: 600; margin-bottom: 2px; }
.hero-step .step-hint { font-size: 0.72rem; color: var(--color-muted-foreground); line-height: 1.35; }
.trust-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-bottom: 24px; }
.trust-pill {
font-size: 0.72rem; font-weight: 500; color: var(--color-muted-foreground);
background: var(--color-surface); border: 1px solid var(--color-border);
padding: 5px 10px; border-radius: 999px; display: inline-flex; align-items: center; gap: 5px;
}
.trust-pill .icon { width: 13px; height: 13px; color: var(--color-success); flex-shrink: 0; }
/* --- bento nav for the dashboard menu --- */
nav.menu.bento {
display: grid; grid-template-columns: 1fr 1fr; grid-template-areas: "deposit deposit" "bet withdraw";
gap: 8px;
}
nav.menu.bento button.nav-item {
width: 100%; align-items: flex-start; text-align: left; flex-direction: row; gap: 10px;
min-height: 64px; padding: 12px 14px; border-radius: 14px;
}
nav.menu.bento button#nav-deposit { grid-area: deposit; }
nav.menu.bento button#nav-bet { grid-area: bet; }
nav.menu.bento button#nav-withdraw { grid-area: withdraw; }
nav.menu.bento button.nav-item .icon { width: 20px; height: 20px; margin-top: 2px; }
nav.menu.bento button.nav-item .nav-item-text { display: flex; flex-direction: column; gap: 2px; }
nav.menu.bento button.nav-item .nav-item-title { font-size: 0.85rem; }
nav.menu.bento button.nav-item .nav-item-hint {
font-size: 0.7rem; font-weight: 400; color: inherit; opacity: 0.75;
}
/* --- glowing card while a round is drawing --- */
.card.drawing-glow {
border-color: color-mix(in srgb, var(--color-primary) 55%, var(--color-border));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary) 25%, transparent),
0 0 24px color-mix(in srgb, var(--color-primary) 22%, transparent);
animation: glow-pulse 2200ms ease-in-out infinite;
}
@keyframes glow-pulse {
0%, 100% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary) 25%, transparent), 0 0 16px color-mix(in srgb, var(--color-primary) 16%, transparent); }
50% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary) 45%, transparent), 0 0 28px color-mix(in srgb, var(--color-primary) 30%, transparent); }
}
.jackpot-bump { animation: jackpot-bump 420ms ease-out; }
@keyframes jackpot-bump {
0% { transform: scale(1); }
30% { transform: scale(1.12); color: var(--color-primary); }
100% { transform: scale(1); }
}
/* --- network / lottery status strip, shown on every screen --- */
.chain-bar {
display: flex; align-items: center; justify-content: space-between; gap: 10px;
font-size: 0.78rem; padding: 10px 2px 16px; margin-bottom: 8px;
border-bottom: 1px solid var(--color-border);
}
.chain-status-pill { display: inline-flex; align-items: center; gap: 7px; font-weight: 600; color: var(--color-foreground); }
.status-dot {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
background: var(--color-muted-foreground);
}
.status-dot.status-open {
background: var(--color-success);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-success) 18%, transparent);
}
.status-dot.status-drawing {
background: var(--color-primary);
animation: status-dot-pulse 1400ms ease-in-out infinite;
}
.status-dot.status-waiting { background: var(--color-muted-foreground); }
@keyframes status-dot-pulse {
0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 45%, transparent); }
50% { box-shadow: 0 0 0 5px transparent; }
}
.chain-block { color: var(--color-muted-foreground); white-space: nowrap; }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
* { animation: none !important; transition: none !important; } * { animation: none !important; transition: none !important; }
} }
@@ -174,10 +291,42 @@
</head> </head>
<body> <body>
<header> <div class="chain-bar" id="chain-bar">
<span class="chain-status-pill">
<span class="status-dot" id="chain-status-dot"></span>
<span id="chain-status-label">Connessione…</span>
</span>
<span class="chain-block mono" id="chain-block">Blocco —</span>
</div>
<section id="landing-hero" class="hero">
<h1>PLM Lottery</h1> <h1>PLM Lottery</h1>
<p>Dashboard di test — mainnet reale</p> <p class="lead">Deposita PLM, entra nel round con una quota fissa, e se viene estratto il tuo numero vinci il montepremi.</p>
</header>
<div class="hero-steps">
<div class="hero-step">
<div class="step-icon"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 21V5a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v16"/></svg></div>
<div class="step-title">1. Deposita</div>
<div class="step-hint">Ricevi un indirizzo PLM personale, tuo per sempre</div>
</div>
<div class="hero-step">
<div class="step-icon"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 12h.01M12 12h.01M18 12h.01"/></svg></div>
<div class="step-title">2. Gioca</div>
<div class="step-hint">Una bet a quota fissa per entrare nel round corrente</div>
</div>
<div class="hero-step">
<div class="step-icon"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 21h8M12 17v4M7 4h10v4a5 5 0 0 1-10 0V4Z"/><path d="M7 5H4a1 1 0 0 0-1 1v1a4 4 0 0 0 4 4M17 5h3a1 1 0 0 1 1 1v1a4 4 0 0 1-4 4"/></svg></div>
<div class="step-title">3. Vinci</div>
<div class="step-hint">Estrazione dal blocco, montepremi accreditato subito</div>
</div>
</div>
<div class="trust-row">
<span class="trust-pill"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>Quota fissa dichiarata</span>
<span class="trust-pill"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>Estrazione da hash di blocco</span>
<span class="trust-pill"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>Prelievo libero in ogni momento</span>
</div>
</section>
<section id="auth-section" class="card"> <section id="auth-section" class="card">
<div class="tabs"> <div class="tabs">
@@ -213,7 +362,7 @@
</div> </div>
</div> </div>
<div class="card"> <div class="card" id="round-card">
<div class="row-between" id="round-normal-row"> <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>
@@ -237,18 +386,18 @@
<div class="hidden" id="draw-result"></div> <div class="hidden" id="draw-result"></div>
</div> </div>
<nav class="menu" aria-label="Sezioni"> <nav class="menu bento" aria-label="Sezioni">
<button class="nav-item active" id="nav-deposit" onclick="switchPanel('deposit')"> <button class="nav-item active" id="nav-deposit" onclick="switchPanel('deposit')">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 21V5a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v16"/></svg> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 21V5a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v16"/></svg>
Deposito <span class="nav-item-text"><span class="nav-item-title">Deposito</span><span class="nav-item-hint">Indirizzo e saldo</span></span>
</button> </button>
<button class="nav-item" id="nav-bet" onclick="switchPanel('bet')"> <button class="nav-item" id="nav-bet" onclick="switchPanel('bet')">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 12h.01M12 12h.01M18 12h.01"/></svg> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 12h.01M12 12h.01M18 12h.01"/></svg>
Bet <span class="nav-item-text"><span class="nav-item-title">Bet</span><span class="nav-item-hint">Entra nel round</span></span>
</button> </button>
<button class="nav-item" id="nav-withdraw" onclick="switchPanel('withdraw')"> <button class="nav-item" id="nav-withdraw" onclick="switchPanel('withdraw')">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>
Prelievo <span class="nav-item-text"><span class="nav-item-title">Prelievo</span><span class="nav-item-hint">Verso indirizzo esterno</span></span>
</button> </button>
</nav> </nav>
@@ -368,12 +517,56 @@ const ROUND_STATUS_LABELS = {
const DRAWING_STATUSES = ['closing', 'drawing', 'paying_out']; const DRAWING_STATUSES = ['closing', 'drawing', 'paying_out'];
const CHAIN_STATUS_LABELS = {
waiting: 'In attesa del prossimo round',
open: 'Round aperto',
drawing: 'Estrazione in corso',
};
function updateChainStatusBar(data) {
const dot = document.getElementById('chain-status-dot');
const label = document.getElementById('chain-status-label');
const block = document.getElementById('chain-block');
let statusKey;
if (!data.round_id) statusKey = 'waiting';
else if (DRAWING_STATUSES.includes(data.status)) statusKey = 'drawing';
else statusKey = 'open';
dot.className = 'status-dot status-' + statusKey;
label.textContent = CHAIN_STATUS_LABELS[statusKey];
block.textContent = 'Blocco ' + (data.chain_tip_height != null ? '#' + data.chain_tip_height : '—');
}
let chainOnlyInterval = null;
async function refreshChainStatusOnly() {
try {
const data = await call('GET', '/rounds/current');
updateChainStatusBar(data);
} catch (e) {
// leave the last-known status on screen rather than blanking it out
}
}
function startChainOnlyPolling() {
refreshChainStatusOnly();
clearInterval(chainOnlyInterval);
chainOnlyInterval = setInterval(refreshChainStatusOnly, 15000);
}
function stopChainOnlyPolling() {
clearInterval(chainOnlyInterval);
chainOnlyInterval = null;
}
// Per round_id: when we first saw it enter a drawing status (client clock), and // 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, // 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. // not persisted — a page reload just re-derives it from the next poll.
const drawStartedAt = {}; const drawStartedAt = {};
const revealedRounds = new Set(); const revealedRounds = new Set();
let activeResultRoundId = null; // round_id whose win/lose result is on screen, if any let activeResultRoundId = null; // round_id whose win/lose result is on screen, if any
let lastJackpotValue = null;
function updateRoundTimer() { function updateRoundTimer() {
const el = document.getElementById('round-timer'); const el = document.getElementById('round-timer');
@@ -411,15 +604,25 @@ function showNormalState() {
async function refreshRound() { async function refreshRound() {
try { try {
const data = await call('GET', '/rounds/current'); const data = await call('GET', '/rounds/current');
updateChainStatusBar(data);
document.getElementById('round-title').textContent = data.round_id document.getElementById('round-title').textContent = data.round_id
? 'Round #' + data.round_id + ' — ' + (ROUND_STATUS_LABELS[data.status] || data.status) ? 'Round #' + data.round_id + ' — ' + (ROUND_STATUS_LABELS[data.status] || data.status)
: 'Nessun round attivo'; : 'Nessun round attivo';
document.getElementById('round-players').textContent = data.participant_count; document.getElementById('round-players').textContent = data.participant_count;
document.getElementById('round-jackpot').textContent = data.jackpot_sats / SATS_PER_PLM; const jackpotEl = document.getElementById('round-jackpot');
const jackpotValue = data.jackpot_sats / SATS_PER_PLM;
jackpotEl.textContent = jackpotValue;
if (lastJackpotValue !== null && jackpotValue !== lastJackpotValue) {
jackpotEl.classList.remove('jackpot-bump');
void jackpotEl.offsetWidth; // restart the animation
jackpotEl.classList.add('jackpot-bump');
}
lastJackpotValue = jackpotValue;
roundCloseAt = data.closes_at ? new Date(data.closes_at) : null; roundCloseAt = data.closes_at ? new Date(data.closes_at) : null;
updateRoundTimer(); updateRoundTimer();
const isDrawing = data.round_id && DRAWING_STATUSES.includes(data.status); const isDrawing = data.round_id && DRAWING_STATUSES.includes(data.status);
document.getElementById('round-card').classList.toggle('drawing-glow', !!isDrawing);
if (isDrawing) { if (isDrawing) {
if (!(data.round_id in drawStartedAt)) drawStartedAt[data.round_id] = Date.now(); if (!(data.round_id in drawStartedAt)) drawStartedAt[data.round_id] = Date.now();
@@ -463,6 +666,8 @@ function scheduleNextRoundPoll(fast) {
} }
function showDashboard() { function showDashboard() {
stopChainOnlyPolling();
document.getElementById('landing-hero').classList.add('hidden');
document.getElementById('auth-section').classList.add('hidden'); document.getElementById('auth-section').classList.add('hidden');
document.getElementById('dashboard-section').classList.remove('hidden'); document.getElementById('dashboard-section').classList.remove('hidden');
document.getElementById('dash-username').textContent = username; document.getElementById('dash-username').textContent = username;
@@ -529,6 +734,8 @@ function logout() {
clearTimeout(roundPollTimeout); 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');
document.getElementById('landing-hero').classList.remove('hidden');
startChainOnlyPolling();
} }
async function copyAddress() { async function copyAddress() {
@@ -585,7 +792,11 @@ async function withdraw() {
refreshMe(); refreshMe();
} }
if (token) showDashboard(); if (token) {
showDashboard();
} else {
startChainOnlyPolling();
}
</script> </script>
</body> </body>