Amounts were rendered by bare sats/SATS_PER_PLM division, so binary floating-point artefacts reached the UI — a 0.7 PLM jackpot could display as 0.7000000000000001 (B-22). formatPlm() in app.js and fmtPlm() in admin.js route every display site through Intl.NumberFormat with the already-resolved language. Input fields deliberately keep the raw value: a grouped, localized string would break parseFloat, and amounts sent to the server still go through Math.round(x * SATS_PER_PLM). withLoading kept a snapshot of the button's markup and restored it in finally, but refreshMe() is fired from the SSE handler, the poll chain, placeBet, withdraw and showDashboard, all sharing #refresh-btn. Two overlapping calls made the second snapshot the *loading* label and then restore it permanently, leaving the button stuck on "Aggiornamento…" (B-23). The in-flight promise now lives in a WeakMap keyed by the button, so a nested call awaits the existing one and only the outermost call touches the markup. The registration form mirrors the constraints the server now enforces (minlength/pattern/required) and register() pre-checks the password length, so the failure is immediate and translated instead of a generic 422 (B-12). Five new error codes are translated in all 7 languages — broadcast_failed, amount_below_dust_limit, withdrawal_to_own_address, internal_error, guide_unavailable — keeping the key sets identical, as the i18n contract in CLAUDE.md requires (verified: 123 keys per language). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
262 lines
16 KiB
HTML
262 lines
16 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>PLM Lottery</title>
|
|
<link rel="icon" type="image/svg+xml" href="/logo.svg">
|
|
<link rel="stylesheet" href="/style.css">
|
|
</head>
|
|
<body>
|
|
|
|
<nav class="hidden" id="app-navbar" data-i18n-aria-label="nav.ariaSections" aria-label="Sezioni">
|
|
<div class="app-navbar-top">
|
|
<div class="app-navbar-top-inner">
|
|
<span class="brand">
|
|
<img class="brand-mark" src="/logo.svg" alt="">
|
|
PLM Lottery
|
|
</span>
|
|
<div class="app-navbar-account">
|
|
<span class="navbar-username" id="dash-username"></span>
|
|
<span class="navbar-balance mono">
|
|
<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>
|
|
<span id="navbar-balance">— PLM</span>
|
|
</span>
|
|
<a class="link icon-link" href="/guida" target="_blank" rel="noopener" data-i18n-title="nav.guideTitle" title="Guida" data-i18n-aria-label="nav.guideAria" aria-label="Apri la guida utente">
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 2-3 4"/><path d="M12 17h.01"/></svg>
|
|
</a>
|
|
<a class="link icon-link" href="https://github.com/REPLACE_ME/plm-lottery/issues/new" target="_blank" rel="noopener" data-i18n-title="nav.bugReport" title="Segnala un bug" data-i18n-aria-label="nav.bugReport" aria-label="Segnala un bug su GitHub">
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v3M16 2v3M12 12v-2a2 2 0 1 1 2 2h-2Z"/><rect x="6" y="10" width="12" height="10" rx="4"/><path d="M6 15H3M21 15h-3M9 20v-3M15 20v-3"/></svg>
|
|
</a>
|
|
<button class="link icon-link" onclick="logout()" data-i18n-title="nav.logoutTitle" title="Esci" data-i18n-aria-label="nav.logoutAria" aria-label="Esci dall'account">
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5M21 12H9"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="app-navbar-tabs">
|
|
<button class="navbar-tab 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>
|
|
<span data-i18n="nav.deposit">Deposito</span>
|
|
</button>
|
|
<button class="navbar-tab" 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>
|
|
<span data-i18n="nav.bet">Bet</span>
|
|
</button>
|
|
<button class="navbar-tab" 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>
|
|
<span data-i18n="nav.withdraw">Prelievo</span>
|
|
</button>
|
|
<button class="navbar-tab" id="nav-profile" onclick="switchPanel('profile')">
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
|
<span data-i18n="nav.profile">Profilo</span>
|
|
</button>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="app-shell">
|
|
|
|
<div class="chain-bar" id="chain-bar">
|
|
<span class="chain-status-pill">
|
|
<span class="status-dot" id="chain-status-dot"></span>
|
|
<!-- No data-i18n on this one or on #draw-label below: both are written by
|
|
app.js from live state, and letting applyStaticTranslations() also own
|
|
them made a language switch flash (or, here, assert) a stale value. -->
|
|
<span id="chain-status-label">Connecting…</span>
|
|
</span>
|
|
<span class="chain-bar-right">
|
|
<span class="chain-block mono" id="chain-block">—</span>
|
|
<!-- Deliberately here and not in the navbar: the navbar is hidden until login,
|
|
which would leave the landing page and the login form untranslatable for
|
|
anyone who can't read the browser-detected default. -->
|
|
<select id="lang-switcher" class="lang-switcher" onchange="setLanguage(this.value)" aria-label="Language">
|
|
<option value="en">English</option>
|
|
<option value="it">Italiano</option>
|
|
<option value="es">Español</option>
|
|
<option value="fr">Français</option>
|
|
<option value="de">Deutsch</option>
|
|
<option value="ru">Русский</option>
|
|
<option value="zh">中文</option>
|
|
</select>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="maintenance-banner hidden" id="maintenance-banner">
|
|
<span>⚠️</span>
|
|
<span data-i18n="maintenance.banner">Manutenzione in programma: il round in corso viene completato regolarmente (vincitore incluso), ma il round successivo non si aprirà finché la manutenzione non sarà terminata.</span>
|
|
</div>
|
|
|
|
<section id="landing-hero" class="hero">
|
|
<h1>PLM Lottery</h1>
|
|
<p class="lead" data-i18n="hero.lead">Deposita PLM, entra nel round con una quota fissa, e se viene estratto il tuo numero vinci il montepremi.</p>
|
|
|
|
<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" data-i18n="hero.step1.title">1. Deposita</div>
|
|
<div class="step-hint" data-i18n="hero.step1.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" data-i18n="hero.step2.title">2. Gioca</div>
|
|
<div class="step-hint" data-i18n="hero.step2.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" data-i18n="hero.step3.title">3. Vinci</div>
|
|
<div class="step-hint" data-i18n="hero.step3.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><span data-i18n="trust.fixedRate">Quota fissa dichiarata</span></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><span data-i18n="trust.blockHash">Estrazione da hash di blocco</span></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><span data-i18n="trust.freeWithdraw">Prelievo libero in ogni momento</span></span>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="auth-section" class="card">
|
|
<div class="tabs">
|
|
<div class="tab active" id="tab-login" onclick="switchTab('login')" data-i18n="auth.tabLogin">Login</div>
|
|
<div class="tab" id="tab-register" onclick="switchTab('register')" data-i18n="auth.tabRegister">Registrati</div>
|
|
</div>
|
|
|
|
<div class="tab-panel active" id="panel-login">
|
|
<label for="login-username" data-i18n="auth.username">Username</label>
|
|
<input id="login-username" autocomplete="username">
|
|
<label for="login-password" data-i18n="auth.password">Password</label>
|
|
<input id="login-password" type="password" autocomplete="current-password">
|
|
<button onclick="login()" id="login-btn" data-i18n="auth.loginBtn">Accedi</button>
|
|
</div>
|
|
|
|
<div class="tab-panel" id="panel-register">
|
|
<label for="reg-username" data-i18n="auth.username">Username</label>
|
|
<input id="reg-username" autocomplete="username" minlength="3" maxlength="32" pattern="[A-Za-z0-9_.\-]+" required>
|
|
<label for="reg-password" data-i18n="auth.password">Password</label>
|
|
<input id="reg-password" type="password" autocomplete="new-password" minlength="8" required>
|
|
<label for="reg-password-confirm" data-i18n="auth.passwordConfirm">Conferma password</label>
|
|
<input id="reg-password-confirm" type="password" autocomplete="new-password" minlength="8" required>
|
|
<button onclick="register()" id="register-btn" data-i18n="auth.registerBtn">Crea account</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="dashboard-section" class="hidden">
|
|
|
|
<div class="card round-card" id="round-card">
|
|
<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" id="round-stats-row">
|
|
<div>
|
|
<div class="hint" style="margin-bottom:2px" data-i18n="round.players">Giocatori</div>
|
|
<span class="mono" id="round-players">—</span>
|
|
</div>
|
|
<div style="text-align:right">
|
|
<div class="hint" style="margin-bottom:2px" data-i18n="round.jackpot">Jackpot</div>
|
|
<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" id="draw-label">Drawing the winner…</div>
|
|
</div>
|
|
|
|
<div class="hidden" id="draw-result"></div>
|
|
</div>
|
|
|
|
<div class="dash-panel active" id="panel-deposit">
|
|
<div class="card">
|
|
<h2 data-i18n="deposit.balanceTitle">Saldo interno</h2>
|
|
<p class="hint" data-i18n="deposit.balanceHint">Aggiornato dopo 1 conferma sulla rete</p>
|
|
<div class="row-between">
|
|
<div><span class="balance-value mono" id="dash-balance">—</span> <span class="balance-unit">PLM</span></div>
|
|
<button class="secondary" onclick="refreshMe()" id="refresh-btn" data-i18n-aria-label="deposit.refreshAria" aria-label="Aggiorna saldo">
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36M21 3v6h-6"/></svg>
|
|
<span data-i18n="deposit.refreshBtn">Aggiorna</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2 data-i18n="deposit.addressTitle">Indirizzo di deposito</h2>
|
|
<p class="hint" data-i18n="deposit.addressHint">È anche l'indirizzo su cui ricevi eventuali vincite</p>
|
|
<div class="address-box">
|
|
<span class="mono" id="dash-address"></span>
|
|
<button class="secondary" onclick="copyAddress()" data-i18n-aria-label="deposit.copyAria" aria-label="Copia indirizzo">
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
</button>
|
|
</div>
|
|
<div class="qr-box">
|
|
<img id="dash-qr" data-i18n-alt="deposit.qrAlt" alt="QR code dell'indirizzo di deposito">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dash-panel" id="panel-bet">
|
|
<div class="card">
|
|
<h2 data-i18n="bet.title">Bet</h2>
|
|
<p class="hint" data-i18n="bet.hint">Ingresso fisso al round corrente</p>
|
|
<!-- No data-i18n here: the label carries the live bet amount, which is
|
|
admin-configurable, so it's rendered by renderBetButton() in app.js. -->
|
|
<button onclick="placeBet()" id="bet-btn">Place bet</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dash-panel" id="panel-withdraw">
|
|
<div class="card">
|
|
<h2 data-i18n="withdraw.title">Withdrawal</h2>
|
|
<p class="hint" data-i18n="withdraw.hint">Invia fondi a un indirizzo PLM esterno</p>
|
|
<label for="wd-address" data-i18n="withdraw.addressLabel">Indirizzo esterno</label>
|
|
<input id="wd-address" class="mono" placeholder="plm1q...">
|
|
<p class="hint" data-i18n-html="withdraw.addressHint">Solo indirizzi P2WPKH bech32 (quelli che iniziano con <code>plm1q...</code>). Indirizzi legacy (<code>P...</code>) o P2SH non sono supportati.</p>
|
|
<label for="wd-amount" data-i18n="withdraw.amountLabel">Importo (PLM)</label>
|
|
<input id="wd-amount" inputmode="decimal" data-i18n-placeholder="withdraw.amountPlaceholder" placeholder="es. 2">
|
|
<label class="checkbox-row">
|
|
<input type="checkbox" id="wd-full-amount" onchange="toggleWithdrawFullAmount()">
|
|
<span data-i18n="withdraw.fullAmountPrefix">Preleva l'intero importo (</span><span class="mono" id="wd-full-amount-value">—</span><span data-i18n="withdraw.fullAmountSuffix"> PLM)</span>
|
|
</label>
|
|
<button onclick="withdraw()" id="withdraw-btn" data-i18n="withdraw.button">Preleva</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dash-panel" id="panel-profile">
|
|
<div class="card">
|
|
<h2 data-i18n="profile.title">Profilo</h2>
|
|
<p class="hint" data-i18n="profile.hint">Le tue informazioni account</p>
|
|
<label data-i18n="profile.usernameLabel">Username</label>
|
|
<div class="address-box"><span id="profile-username">—</span></div>
|
|
<label data-i18n="profile.addressLabel">Indirizzo di deposito</label>
|
|
<div class="address-box"><span class="mono" id="profile-address">—</span></div>
|
|
<label data-i18n="profile.balanceLabel">Saldo interno</label>
|
|
<div class="address-box"><span class="mono" id="profile-balance">—</span> <span class="balance-unit">PLM</span></div>
|
|
<label data-i18n="profile.createdLabel">Utente dal</label>
|
|
<div class="address-box"><span id="profile-created-at">—</span></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2 data-i18n="settings.title">Impostazioni</h2>
|
|
<p class="hint" data-i18n="settings.hint">Cambia la password del tuo account</p>
|
|
<label for="settings-current-password" data-i18n="settings.currentPasswordLabel">Password attuale</label>
|
|
<input id="settings-current-password" type="password" autocomplete="current-password">
|
|
<label for="settings-new-password" data-i18n="settings.newPasswordLabel">Nuova password</label>
|
|
<input id="settings-new-password" type="password" autocomplete="new-password">
|
|
<label for="settings-new-password-confirm" data-i18n="settings.newPasswordConfirmLabel">Conferma nuova password</label>
|
|
<input id="settings-new-password-confirm" type="password" autocomplete="new-password">
|
|
<button onclick="changePassword()" id="change-password-btn" data-i18n="settings.updateBtn">Aggiorna password</button>
|
|
</div>
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</div>
|
|
|
|
<div id="toast-container" aria-live="polite"></div>
|
|
|
|
<script src="/i18n.js"></script>
|
|
<script src="/app.js"></script>
|
|
|
|
</body>
|
|
</html>
|