feat: show upload progress with a bar, time left and a header badge
All checks were successful
Build and deploy / Validate source (push) Successful in 9s
Build and deploy / Integration suite on a real stack (push) Successful in 2m27s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images (push) Successful in 45s

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 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-29 15:34:57 -03:00
parent 7116ebe50a
commit 2b313a1cc8
3 changed files with 48 additions and 10 deletions

View File

@@ -116,24 +116,57 @@
const arquivosDoCarrinho=()=>[...pedido,...(busy&&itemAtual?[itemAtual]:[])].flatMap(it=>it.localFiles||[]); const arquivosDoCarrinho=()=>[...pedido,...(busy&&itemAtual?[itemAtual]:[])].flatMap(it=>it.localFiles||[]);
const gb=n=>(n/1073741824).toLocaleString('pt-BR',{maximumFractionDigits:1})+' GB'; const gb=n=>(n/1073741824).toLocaleString('pt-BR',{maximumFractionDigits:1})+' GB';
const mb=n=>n>=1073741824 ? gb(n) : Math.round(n/1048576)+' MB'; const mb=n=>n>=1073741824 ? gb(n) : Math.round(n/1048576)+' MB';
function textoEnvio() { // Where the uploads stand, and roughly how long is left from the recent speed.
const files=arquivosDoCarrinho(); if(!files.length) return ''; const amostras=[];
function estadoEnvio() {
const files=arquivosDoCarrinho(); if(!files.length) return null;
let total=0, sent=0, pendentes=0; let total=0, sent=0, pendentes=0;
for (const f of files) { for (const f of files) {
const e=envios.get(chaveArquivo(f)); const e=envios.get(chaveArquivo(f));
total+=f.size; sent+=e ? Math.min(e.sent,f.size) : 0; total+=f.size; sent+=e ? Math.min(e.sent,f.size) : 0;
if (!e || !e.done) pendentes++; if (!e || !e.done) pendentes++;
} }
if (!pendentes) return 'Arquivos enviados.'; const agora=Date.now();
if (sent>=total) return 'Arquivos enviados · verificando a segurança…'; amostras.push([agora,sent]); while(amostras.length>2 && agora-amostras[0][0]>15000) amostras.shift();
return 'Enviando seus arquivos: '+Math.floor(sent/total*100)+'% ('+mb(sent)+' de '+mb(total)+')'; 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() { function pintaEnvio() {
const e=estadoEnvio(), texto=textoEnvio(e);
const el=document.getElementById('envioArq'); const el=document.getElementById('envioArq');
const texto=textoEnvio(); if (el) {
if (el) el.textContent=texto; el.replaceChildren();
if (busy && texto) message(texto); 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. // 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(); }); window.addEventListener('dtf-cart-changed',()=>{ arquivosDoCarrinho().forEach(f=>{ enviar(f).catch(()=>{}); }); pintaEnvio(); });
async function upload(file) { async function upload(file) {

View File

@@ -765,7 +765,7 @@ footer a:hover{color:var(--laranja2)}
</div> </div>
<div class="acts"> <div class="acts">
<a href="/portal.html">Minha conta</a> <a href="/portal.html">Minha conta</a>
<a href="/carrinho" id="cartLink">Carrinho (0)</a> <a href="/carrinho" id="cartLink">Carrinho (0)</a><span class="cartEnvio" id="cartEnvio" aria-live="polite"></span>
</div> </div>
</div></header> </div></header>

View File

@@ -229,7 +229,12 @@ section+section{border-top:0}
/* The payment pages: paying on the left, the order summary on the right */ /* 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"]) .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} 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} .envioArq:empty{display:none}
.dicaEnd{font-size:13px;color:var(--texto2);margin-top:12px} .dicaEnd{font-size:13px;color:var(--texto2);margin-top:12px}
.dicaEnd:empty{display:none} .dicaEnd:empty{display:none}