git / moonbite-network / templates/wall.html · raw
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"><meta name="theme-color" content="#0B0D12"><title>The Wall — MoonBite</title><meta name="description" content="Every miner who found a block, in the order they found it. Chain-verified, numbered, permanent."><link rel="icon" type="image/svg+xml" href="/static/moonbite-logo.svg"><link rel="stylesheet" href="/static/fonts/moonbite-fonts.css"><style>/* Doc 04: three roles. A heavy grotesque for display, a quiet sans for reading, a monospace for anything that is a number. Archivo Black and Inter are self-hosted from /static/fonts (latin subset, OFL-licensed). They are deliberately not loaded from Google Fonts: /privacy states the site loads nothing that phones home, and a webfont CDN sends every visitor's IP to Google on every page load. Every figure that is money, a hash, an address or a block height is monospace with tabular figures — amounts that shift width as they update look untrustworthy. */:root{ --font-display:'Archivo Black','Helvetica Neue','Arial Black',sans-serif; --font-body:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; --font-data:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}h1,h2,.big,.reveal .big,.balance-amount,.elapsed,.mort-big{ font-family:var(--font-display); font-weight:400; letter-spacing:-.02em;}.num,.mono,.addr,.cert .amt,.cl .v,.tot .v,.mcell .v,.odds .v,.loss .v,.count .v,.zero-band .v,.total .v,.vault-v,td.n,th.n{ font-family:var(--font-data); font-variant-numeric:tabular-nums;}* { margin:0; padding:0; box-sizing:border-box; }body { background:#0B0D12; color:#F5F2EA; font-family:var(--font-body); overflow-x:hidden; -webkit-font-smoothing:antialiased; }::selection { background:#D9A441; color:#0B0D12; }a,button,input,select { -webkit-tap-highlight-color:transparent; font-family:inherit; }#sky { position:fixed; inset:0; z-index:0; }.wrap { position:relative; z-index:1; max-width:560px; margin:0 auto; padding:0 18px 70px; }.num { font-variant-numeric:tabular-nums; }.kicker { letter-spacing:.3em; font-size:.62rem; color:#D9A441; text-transform:uppercase; font-weight:800; }.head { text-align:center; padding-top:5vh; }h1 { font-size:clamp(2.1rem,9vw,3.3rem); font-weight:800; line-height:1.02; letter-spacing:-.025em; margin:13px 0 11px; }h1 .g { color:#D9A441; }.lede { color:#8A8580; font-size:.96rem; line-height:1.62; max-width:430px; margin:0 auto; }.verified { display:inline-flex; align-items:center; gap:7px; margin-top:16px; background:rgba(46,158,107,.12); border:1px solid rgba(46,158,107,.4); border-radius:999px; padding:7px 15px; font-size:.68rem; font-weight:800; color:#2E9E6B; letter-spacing:.08em; }.count { margin-top:22px; }.count .v { font-size:clamp(2.6rem,13vw,4rem); font-weight:800; color:#D9A441; line-height:1; letter-spacing:-.03em; }.count .k { font-size:.58rem; letter-spacing:.24em; color:#8A8580; text-transform:uppercase; margin-top:8px; font-weight:800; }/* ------------------------------------------------------------- CLAIM */.claim { background:linear-gradient(150deg,rgba(217,164,65,.14),rgba(22,26,34,.7)); border:1px solid rgba(217,164,65,.4); border-radius:20px; padding:22px 18px; margin-top:22px; display:none; box-shadow:0 22px 66px rgba(0,0,0,.5); }.claim.on { display:block; }.claim-t { font-size:.6rem; letter-spacing:.2em; color:#D9A441; text-transform:uppercase; font-weight:800; }.claim-h { font-size:1.15rem; font-weight:800; margin-top:8px; }.claim-m { color:#8A8580; font-size:.84rem; margin-top:6px; line-height:1.55; }.frow { display:flex; gap:9px; margin-top:15px; flex-wrap:wrap; }input[type=text], select { background:#0B0D12; color:#F5F2EA; font-size:.95rem; border:1px solid rgba(217,164,65,.3); border-radius:10px; padding:13px 12px; min-height:48px; }input[type=text] { flex:2; min-width:150px; }select { flex:1; min-width:110px; }input:focus, select:focus { outline:none; border-color:#D9A441; }.btn { border:none; border-radius:10px; padding:14px 22px; min-height:48px; font-weight:800; font-size:.9rem; cursor:pointer; text-decoration:none; display:inline-flex; align-items:center; justify-content:center; gap:8px; letter-spacing:.05em; }.btn-gold { background:#D9A441; color:#0B0D12; }.btn-gold:disabled { opacity:.5; }.btn-ghost { background:transparent; color:#F5F2EA; border:1px solid rgba(245,242,234,.24); }.claim .btn-gold { width:100%; margin-top:11px; }.err { color:#C8402F; font-size:.82rem; margin-top:10px; font-weight:700; display:none; }.err.on { display:block; }/* -------------------------------------------------------------- CERTS */.grid { display:grid; grid-template-columns:1fr; gap:11px; margin-top:22px; }@media (min-width:620px){ .grid { grid-template-columns:1fr 1fr; } }.cert { position:relative; background:linear-gradient(160deg,#161A22,#12151C); border:1px solid rgba(217,164,65,.22); border-radius:20px; padding:17px 16px; overflow:hidden; }.cert.mine { border-color:rgba(217,164,65,.65); box-shadow:0 0 34px rgba(217,164,65,.14); }.cert .no { position:absolute; top:12px; right:14px; font-size:.72rem; font-weight:800; color:#D9A441; letter-spacing:.06em; }.cert .hd { display:flex; align-items:center; gap:10px; }.cert .fl { font-size:1.3rem; line-height:1; }.cert .who { font-size:1.02rem; font-weight:800; color:#F5F2EA; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:70%; }.cert .amt { font-size:1.45rem; font-weight:800; color:#D9A441; margin-top:11px; line-height:1; }.cert .meta { color:#8A8580; font-size:.75rem; margin-top:7px; letter-spacing:.03em; }.cert .ribbon { position:absolute; left:0; top:0; bottom:0; width:3px; background:#D9A441; opacity:.5; }.cert.first .ribbon { opacity:1; }.cert.first::after { content:'FIRST EVER'; position:absolute; bottom:14px; right:14px; font-size:.52rem; letter-spacing:.18em; font-weight:800; color:#0B0D12; background:#D9A441; padding:3px 7px; border-radius:4px; }.empty { text-align:center; color:#8A8580; padding:34px 14px; line-height:1.8; font-size:.92rem; border:1px dashed rgba(217,164,65,.28); border-radius:20px; margin-top:22px; }.empty b { color:#D9A441; }.actions { display:flex; gap:10px; justify-content:center; flex-wrap:wrap; margin-top:24px; }.fine { font-size:.73rem; color:#8A8580; line-height:1.72; text-align:center; margin-top:22px; }.toast { position:fixed; left:50%; bottom:24px; transform:translateX(-50%) translateY(16px); background:#2E9E6B; color:#04160E; font-weight:800; padding:12px 22px; border-radius:10px; opacity:0; transition:all .3s; z-index:60; pointer-events:none; font-size:.88rem; }.toast.show { opacity:1; transform:translateX(-50%) translateY(0); }.no-flags .fl { color:#D9A441; font-size:.8rem; font-weight:800; }/* Doc 17: visible keyboard focus on every interactive element. */a:focus-visible, button:focus-visible, input:focus-visible,select:focus-visible, [tabindex]:focus-visible { outline:2px solid #D9A441; outline-offset:3px; border-radius:6px;}</style></head><body><canvas id="sky"></canvas>{% include 'partials/nav.html' %}<div class="wrap"> <section class="head"> <div class="kicker">THE WALL</div> <h1>Everyone who<br>found a <span class="g">block</span>.</h1> <p class="lede">In the order they found it. Your number is issued once and never reissued — it cannot be bought, only taken earlier than someone else.</p> <div class="verified"> EVERY ENTRY VERIFIED AGAINST THE CHAIN</div> <div class="count"> <div class="v num" id="total">0</div> <div class="k" id="totalK">certificates issued, of forever</div> </div> </section> <!-- claim --> <section class="claim" id="claim"> <div class="claim-t">YOU MINED A BLOCK</div> <div class="claim-h" id="claimH">Put it on the Wall.</div> <div class="claim-m" id="claimM">—</div> <div class="frow"> <input type="text" id="handle" maxlength="24" placeholder="Your name or handle" aria-label="Handle"> <select id="country" aria-label="Country"></select> </div> <button class="btn btn-gold" id="btnClaim">CLAIM MY CERTIFICATE</button> <div class="err" id="err"></div> </section> <div class="grid" id="grid"></div> <div class="empty" id="empty"> No certificate has been issued yet.<br> <b>#1 is still available.</b><br> Mine a block and it is yours, permanently. </div> <div class="actions"> <a class="btn btn-gold" href="/start">Mine a block</a> <a class="btn btn-ghost" href="/world-cup">World Cup</a> </div> <p class="fine"> Entries are accepted only for addresses the chain has actually paid a coinbase — a claim that never happened is refused by the server. One certificate per address. Handles are chosen by miners.<br> MBITE has no market value and is not an investment. </p></div>{% include 'partials/footer.html' %}<div class="toast" id="toast"></div><script nonce="{{ csp_nonce }}">(function () { 'use strict'; var REWARD = {{ consensus.current_reward_coins }}; function $(id){ return document.getElementById(id); } function fmt(n){ return (n||0).toLocaleString('en-US'); } /* Doc 11: block heights are never thousands-separated — only amounts are. */ function ht(n){ return String(Math.round(n||0)); } function coin(n){ return n.toLocaleString('en-US',{maximumFractionDigits:8}); } function toast(m){ var t=$('toast'); t.textContent=m; t.classList.add('show'); setTimeout(function(){ t.classList.remove('show'); },2600); } /* Handles are user-supplied and rendered client-side, so every value that reaches innerHTML is escaped here. */ function esc(s){ return String(s==null?'':s).replace(/[&<>"']/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); } function flag(code){ if(!code || code.length!==2) return ''; return code.toUpperCase().replace(/./g, function(ch){ return String.fromCodePoint(0x1F1E6 + ch.charCodeAt(0) - 65); }); } (function(){ try { var c=document.createElement('canvas').getContext('2d'); c.font='40px serif'; if(!(c.measureText('').width < c.measureText('').width*1.6)) document.body.classList.add('no-flags'); } catch(e){} })(); var CODES = ('AF Afghanistan|AR Argentina|AU Australia|AT Austria|BD Bangladesh|BE Belgium|'+ 'BR Brazil|CA Canada|CL Chile|CN China|CO Colombia|CZ Czechia|DK Denmark|EG Egypt|'+ 'ET Ethiopia|FI Finland|FR France|DE Germany|GH Ghana|GR Greece|HK Hong Kong|HU Hungary|'+ 'IN India|ID Indonesia|IR Iran|IQ Iraq|IE Ireland|IL Israel|IT Italy|JP Japan|JO Jordan|'+ 'KZ Kazakhstan|KE Kenya|KW Kuwait|MY Malaysia|MX Mexico|MA Morocco|NP Nepal|NL Netherlands|'+ 'NZ New Zealand|NG Nigeria|NO Norway|OM Oman|PK Pakistan|PS Palestine|PE Peru|PH Philippines|'+ 'PL Poland|PT Portugal|QA Qatar|RO Romania|RU Russia|SA Saudi Arabia|RS Serbia|SG Singapore|'+ 'ZA South Africa|KR South Korea|ES Spain|LK Sri Lanka|SE Sweden|CH Switzerland|TW Taiwan|'+ 'TZ Tanzania|TH Thailand|TR Turkey|UG Uganda|UA Ukraine|AE United Arab Emirates|'+ 'GB United Kingdom|US United States|UZ Uzbekistan|VN Vietnam|YE Yemen|ZW Zimbabwe').split('|'); var NAME={}; var sel=$('country'); var o0=document.createElement('option'); o0.value=''; o0.textContent='No country'; sel.appendChild(o0); CODES.forEach(function(s){ var c=s.slice(0,2); NAME[c]=s.slice(3); var o=document.createElement('option'); o.value=c; o.textContent=flag(c)+' '+NAME[c]; sel.appendChild(o); }); (function(){ var L=navigator.languages||[navigator.language||'']; for(var i=0;i<L.length;i++){ var m=/[-_]([A-Za-z]{2})$/.exec(L[i]||''); if(m && NAME[m[1].toUpperCase()]){ sel.value=m[1].toUpperCase(); return; } } })(); /* ------------------------------------------------------------- sky */ var sky=$('sky'), sg=sky.getContext('2d'), W,H,stars=[]; var DPR=Math.min(window.devicePixelRatio||1,2); var reduced=window.matchMedia('(prefers-reduced-motion: reduce)').matches; function resize(){ W=innerWidth; H=innerHeight; sky.width=W*DPR; sky.height=H*DPR; sky.style.width=W+'px'; sky.style.height=H+'px'; sg.setTransform(DPR,0,0,DPR,0,0); stars=[]; for(var i=0,n=Math.min(130,(W*H/11000)|0);i<n;i++) stars.push({x:Math.random()*W,y:Math.random()*H,r:Math.random()*1.1+.25,tw:Math.random()*6.28}); } addEventListener('resize',resize); resize(); var t=0; (function loop(){ t+=reduced?0:.008; sg.fillStyle='#0B0D12'; sg.fillRect(0,0,W,H); for(var i=0;i<stars.length;i++){ var s=stars[i]; sg.globalAlpha=.14+.3*Math.abs(Math.sin(t+s.tw)); sg.fillStyle='#F5F2EA'; sg.beginPath(); sg.arc(s.x,s.y,s.r,0,6.2832); sg.fill(); } sg.globalAlpha=1; requestAnimationFrame(loop); })(); /* ------------------------------------------------------------ state */ var myAddr=null, myCert=null; try { var lb=JSON.parse(localStorage.getItem('mb_last_block')||'null'); if(lb && lb.address) myAddr=lb.address; if(!myAddr) myAddr=localStorage.getItem('mb_start_address'); } catch(e){} function short(a){ /* Doc 18: first and last six always visible. */ return a && a.length > 14 ? a.slice(0,6)+'…'+a.slice(-6) : (a||''); } function when(ts){ var d=new Date((ts||0)*1000); return d.toUTCString().slice(5,16); } function render(d){ $('total').textContent = fmt(d.total); $('totalK').textContent = (d.total===1 ? 'certificate issued, of forever' : 'certificates issued, of forever'); var list = d.certificates||[]; var mineId = myCert ? myCert.id : (d.you ? d.you.id : null); $('grid').innerHTML = list.map(function(c){ var isMine = mineId && c.id===mineId; return '<div class="cert'+(isMine?' mine':'')+(c.id===1?' first':'')+'">'+ '<div class="ribbon"></div>'+ '<div class="no">#'+c.id+'</div>'+ '<div class="hd"><span class="fl">'+(c.country?flag(c.country):'')+'</span>'+ '<span class="who">'+esc(c.handle)+'</span></div>'+ '<div class="amt">+'+coin(c.reward)+' MBITE</div>'+ '<div class="meta">Block #'+ht(c.height)+' · '+esc(when(c.created_at))+'</div>'+ '</div>'; }).join(''); $('empty').style.display = list.length ? 'none' : ''; /* the claim panel only makes sense for an address with no certificate yet */ var already = !!(d.you || myCert); if (myAddr && !already){ $('claimM').textContent = 'Address '+short(myAddr)+ ' will be issued certificate #'+ (d.total+1) + '. That number is permanent.'; $('claim').classList.add('on'); } else if (already){ var mine = myCert || d.you; $('claim').classList.add('on'); $('claimH').textContent = 'You are certificate #'+mine.id+'.'; $('claimM').textContent = 'Issued to '+short(myAddr)+'. It cannot be reissued.'; $('handle').style.display='none'; $('country').style.display='none'; $('btnClaim').style.display='none'; } } function load(){ var q = '/api/wall?limit=60' + (myAddr ? '&address='+encodeURIComponent(myAddr) : ''); fetch(q).then(function(r){return r.json();}).then(function(d){ if(d.you) myCert=d.you; render(d); }).catch(function(){ render({total:0,certificates:[]}); }); } load(); $('btnClaim').addEventListener('click', function(){ if(!myAddr) return; var b=this; b.disabled=true; b.textContent='…'; $('err').classList.remove('on'); var lb={}; try { lb=JSON.parse(localStorage.getItem('mb_last_block')||'{}'); } catch(e){} fetch('/api/wall',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({ address:myAddr, handle:$('handle').value, country:$('country').value, height:lb.height||0, reward:REWARD })}) .then(function(r){ return r.json().then(function(j){ return {ok:r.ok, j:j}; }); }) .then(function(res){ if(!res.ok || res.j.status!=='success') throw new Error(res.j.message||'Could not claim'); myCert=res.j.you; render(res.j); toast(res.j.you.new ? 'Certificate #'+res.j.you.id+' is yours' : 'You already hold #'+res.j.you.id); }) .catch(function(e){ b.disabled=false; b.textContent='CLAIM MY CERTIFICATE'; $('err').textContent = e.message || 'Could not claim'; $('err').classList.add('on'); }); });})();</script></body></html>