From 2b313a1cc8f46f48272de37bca3c4b45f71797a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Tue, 29 Sep 2026 15:34:57 -0300 Subject: [PATCH] feat: show upload progress with a bar, time left and a header badge Uploads of several GB ran behind one grey line in the cart. The order summary now has a progress bar with the percentage, size and an estimate of the time left from the recent speed; the header shows "enviando 45%" on every page while it runs; the payment button says what it is waiting for; and leaving the page mid-upload asks first. Co-Authored-By: Claude Opus 5.5 --- web/checkout.js | 49 +++++++++++++++++++++++++++++++++++++++++-------- web/index.html | 2 +- web/site-v2.css | 7 ++++++- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/web/checkout.js b/web/checkout.js index cb2d335..d09fa59 100644 --- a/web/checkout.js +++ b/web/checkout.js @@ -116,24 +116,57 @@ const arquivosDoCarrinho=()=>[...pedido,...(busy&&itemAtual?[itemAtual]:[])].flatMap(it=>it.localFiles||[]); const gb=n=>(n/1073741824).toLocaleString('pt-BR',{maximumFractionDigits:1})+' GB'; const mb=n=>n>=1073741824 ? gb(n) : Math.round(n/1048576)+' MB'; - function textoEnvio() { - const files=arquivosDoCarrinho(); if(!files.length) return ''; + // Where the uploads stand, and roughly how long is left from the recent speed. + const amostras=[]; + function estadoEnvio() { + const files=arquivosDoCarrinho(); if(!files.length) return null; let total=0, sent=0, pendentes=0; for (const f of files) { const e=envios.get(chaveArquivo(f)); total+=f.size; sent+=e ? Math.min(e.sent,f.size) : 0; if (!e || !e.done) pendentes++; } - if (!pendentes) return 'Arquivos enviados.'; - if (sent>=total) return 'Arquivos enviados · verificando a segurança…'; - return 'Enviando seus arquivos: '+Math.floor(sent/total*100)+'% ('+mb(sent)+' de '+mb(total)+')'; + const agora=Date.now(); + amostras.push([agora,sent]); while(amostras.length>2 && agora-amostras[0][0]>15000) amostras.shift(); + const [t0,s0]=amostras[0], taxa=agora>t0 ? (sent-s0)/((agora-t0)/1000) : 0; + const fase=!pendentes ? 'pronto' : sent>=total ? 'verificando' : 'enviando'; + return {total, sent, fase, pct:Math.floor(sent/total*100), + restante: fase==='enviando' && taxa>0 ? (total-sent)/taxa : null}; + } + const tempo=s=>s<60 ? 'menos de 1 min' : 'cerca de '+Math.ceil(s/60)+' min'; + function textoEnvio(e) { + if (!e) return ''; + if (e.fase==='pronto') return 'Arquivos enviados ✓'; + if (e.fase==='verificando') return 'Arquivos enviados · verificando a segurança…'; + return 'Enviando seus arquivos: '+e.pct+'% · '+mb(e.sent)+' de '+mb(e.total)+ + (e.restante!=null ? ' · '+tempo(e.restante) : ''); } function pintaEnvio() { + const e=estadoEnvio(), texto=textoEnvio(e); const el=document.getElementById('envioArq'); - const texto=textoEnvio(); - if (el) el.textContent=texto; - if (busy && texto) message(texto); + if (el) { + el.replaceChildren(); + if (e && e.fase!=='pronto') { + const barra=document.createElement('div'); barra.className='envioBarra'; + const i=document.createElement('i'); i.style.width=(e.fase==='verificando'?100:e.pct)+'%'; barra.append(i); + el.append(barra); + } + if (texto) el.append(document.createTextNode(texto)); + el.classList.toggle('ok', e?.fase==='pronto'); + } + // Visible on every page while it runs. + const topo=document.getElementById('cartEnvio'); + if (topo) topo.textContent = !e || e.fase==='pronto' ? '' : e.fase==='verificando' ? '· verificando' : '· enviando '+e.pct+'%'; + if (busy && texto) { + message(texto); + $('bPagar').textContent = e && e.fase!=='pronto' ? (e.fase==='verificando' ? 'Verificando os arquivos…' : 'Enviando arquivos… '+e.pct+'%') : $('bPagar').textContent; + } } + // Leaving the page pauses an upload; it resumes, but the customer is warned. + window.addEventListener('beforeunload',event=>{ + const e=estadoEnvio(); + if (e && e.fase==='enviando') { event.preventDefault(); event.returnValue=''; } + }); // Only what is in the cart: the item on the product page may still change. window.addEventListener('dtf-cart-changed',()=>{ arquivosDoCarrinho().forEach(f=>{ enviar(f).catch(()=>{}); }); pintaEnvio(); }); async function upload(file) { diff --git a/web/index.html b/web/index.html index 2f2f0ac..51c37bb 100644 --- a/web/index.html +++ b/web/index.html @@ -765,7 +765,7 @@ footer a:hover{color:var(--laranja2)} diff --git a/web/site-v2.css b/web/site-v2.css index 1fe33a6..bce35fc 100644 --- a/web/site-v2.css +++ b/web/site-v2.css @@ -229,7 +229,12 @@ section+section{border-top:0} /* The payment pages: paying on the left, the order summary on the right */ html:is([data-rota="pagamento"],[data-rota="pix"]) .checkout{max-width:1040px;margin-top:8px} html:is([data-rota="pagamento"],[data-rota="pix"]) .pagGrid{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:32px;align-items:start} -.envioArq{font-size:13px;color:var(--texto2);margin-top:12px;line-height:1.5} +.envioArq{font-size:13px;color:var(--texto2);margin-top:14px;line-height:1.5} +.envioArq.ok{color:var(--verde)} +.envioBarra{height:6px;border-radius:999px;background:#EDEDED;overflow:hidden;margin-bottom:6px} +.envioBarra i{display:block;height:100%;background:var(--laranja);border-radius:999px;transition:width .4s} +.cartEnvio{font-size:13px;font-weight:700;color:var(--laranja-escuro);margin-left:6px} +.cartEnvio:empty{display:none} .envioArq:empty{display:none} .dicaEnd{font-size:13px;color:var(--texto2);margin-top:12px} .dicaEnd:empty{display:none}