feat: give each Site product and the cart its own page
All checks were successful
Build and deploy / Validate source (push) Successful in 5s
Build and deploy / Integration suite on a real stack (push) Successful in 2m28s
Build and deploy / Secret scan and release gate (push) Successful in 8s
Build and deploy / Publish images (push) Successful in 1m52s
All checks were successful
Build and deploy / Validate source (push) Successful in 5s
Build and deploy / Integration suite on a real stack (push) Successful in 2m28s
Build and deploy / Secret scan and release gate (push) Successful in 8s
Build and deploy / Publish images (push) Successful in 1m52s
The home, each product's Montagem and the cart now have their own addresses (/artes-avulsas, /arquivo-por-metro, /uv-artes-avulsas, /uv-arquivo-por-metro, /carrinho) and show only their own content, with Back, Forward, reload and direct links working as in any store. They stay one document so uploaded artworks survive moving between pages; nginx serves index.html for these addresses. "Adicionar ao carrinho" puts the item in the cart and opens it, and an empty cart says so. Portal quote links open in the cart. Also fixes the "57 cm" line break on the ready-sheet option, returns "Novo pedido" to the home, and says PDF depends on the product. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,11 @@ server {
|
||||
}
|
||||
# Always revalidate HTML/JS/CSS after a deployment. Without this, a browser
|
||||
# can pair a new Kanban page with a cached older script after a rollout.
|
||||
# The Site's product pages and cart are addresses of the same page (web/site-pages.js).
|
||||
location ~ ^/(arquivo-por-metro|artes-avulsas|uv-arquivo-por-metro|uv-artes-avulsas|carrinho)/?$ {
|
||||
expires -1;
|
||||
try_files /index.html =404;
|
||||
}
|
||||
location / {
|
||||
expires -1;
|
||||
try_files $uri $uri/ =404;
|
||||
|
||||
@@ -19,6 +19,10 @@ server {
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
client_max_body_size 2m;
|
||||
}
|
||||
# The Site's product pages and cart are addresses of the same page (web/site-pages.js).
|
||||
location ~ ^/(arquivo-por-metro|artes-avulsas|uv-arquivo-por-metro|uv-artes-avulsas|carrinho)/?$ {
|
||||
try_files /index.html =404;
|
||||
}
|
||||
location / { try_files $uri $uri/ =404; }
|
||||
}
|
||||
server {
|
||||
|
||||
@@ -63,6 +63,10 @@ try{
|
||||
await site.call('Page.reload');
|
||||
await waitFor(()=>site.eval('typeof window.dtfCheckout === "function"'),'reload after security probe');
|
||||
await site.click('[data-modo="file"]');
|
||||
// Each product has its own page; the home's parts are not on it.
|
||||
assert.deepEqual(await site.eval(`({path:location.pathname,page:document.documentElement.dataset.rota,
|
||||
cards:getComputedStyle($('cards')).display,cart:getComputedStyle($('carr')).display})`),
|
||||
{path:'/arquivo-por-metro',page:'produto',cards:'none',cart:'none'});
|
||||
await site.click('[data-cam="tabela"]');
|
||||
const root=await site.call('DOM.getDocument');
|
||||
const input=await site.call('DOM.querySelector',{nodeId:root.root.nodeId,selector:'#inp'});
|
||||
@@ -77,6 +81,10 @@ try{
|
||||
await pause(800);
|
||||
await site.call('Page.reload');
|
||||
await waitFor(()=>site.eval('typeof pedido!=="undefined" && pedido.length===1'),'persistent cart recovery');
|
||||
// Reloading a product page reopens that product; the cart is its own page.
|
||||
assert.equal(await site.eval('modo'),'file');
|
||||
await site.click('#cartLink');
|
||||
await waitFor(()=>site.eval(`location.pathname==='/carrinho' && getComputedStyle($('carr')).display!=='none' && getComputedStyle($('foco')).display==='none'`),'cart page');
|
||||
assert.equal(await site.eval('pedido[0].localFiles[0].name'),'local-test.cdr');
|
||||
assert.equal(await site.eval('pedido[0].total'),21.89);
|
||||
assert.equal(await site.eval('document.getElementById("bPagar").disabled'),false);
|
||||
|
||||
@@ -29,6 +29,10 @@ def run():
|
||||
assert script_src == "script-src 'self'", script_src
|
||||
assert "object-src 'none'" in policy
|
||||
assert 'cdnjs' not in policy, 'pdf.js is vendored; no CDN belongs in the policy'
|
||||
# Product pages and the cart are addresses of the Site's page, under the same policy.
|
||||
for page in ('/arquivo-por-metro','/artes-avulsas','/uv-arquivo-por-metro','/uv-artes-avulsas','/carrinho'):
|
||||
assert raw(page,200)['Content-Security-Policy']==policy,page
|
||||
raw('/carrinho/outra-coisa',404)
|
||||
raw('/api/health',400,{'Host':'attacker.invalid'})
|
||||
raw('/api/account/logout',403,{'Content-Type':'application/json','Origin':'https://attacker.invalid'},b'{}')
|
||||
raw('/api/account/logout',403,{'Content-Type':'application/json','Origin':'http://localhost:9999'},b'{}')
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
} else if (quote.status==='paid') {
|
||||
const STAGES={rec:'arte recebida',tra:'arte tratada',fil:'na fila de impressão',imp:'imprimindo',cor:'aguardando correção',fin:'finalizado'};
|
||||
message('Pedido #'+quote.order.number+' · '+(STAGES[quote.order.state]||quote.order.state)+'.');
|
||||
button('Novo pedido',()=>{clearDraft();location.reload();});
|
||||
button('Novo pedido',()=>{clearDraft();location.assign('/');});
|
||||
} else {
|
||||
message('Cotação expirada. Envie o carrinho para uma nova revisão.');
|
||||
button('Nova cotação',clearDraft);
|
||||
|
||||
@@ -748,7 +748,8 @@ footer a:hover{color:var(--laranja2)}
|
||||
justify-content:space-between;gap:12px;flex-wrap:wrap;font-size:11px;color:#5A6570}
|
||||
@media(prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto}}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/site-v2.css?v=3">
|
||||
<link rel="stylesheet" href="/site-v2.css?v=4">
|
||||
<script src="/site-pages.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -764,7 +765,7 @@ footer a:hover{color:var(--laranja2)}
|
||||
</div>
|
||||
<div class="acts">
|
||||
<a href="/portal.html">Minha conta</a>
|
||||
<a href="#carr" id="cartLink">Carrinho (0)</a>
|
||||
<a href="/carrinho" id="cartLink">Carrinho (0)</a>
|
||||
</div>
|
||||
</div></header>
|
||||
|
||||
@@ -785,10 +786,10 @@ footer a:hover{color:var(--laranja2)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mi on">
|
||||
<a href="#envio">Impressão DTF</a>
|
||||
<a href="/#envio">Impressão DTF</a>
|
||||
<div class="sub">
|
||||
<a href="#envio">DTF Têxtil · 57 cm</a>
|
||||
<a href="#envio">DTF UV · 28,5 cm</a>
|
||||
<a href="/#envio">DTF Têxtil · 57 cm</a>
|
||||
<a href="/#envio">DTF UV · 28,5 cm</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mi">
|
||||
@@ -831,7 +832,7 @@ footer a:hover{color:var(--laranja2)}
|
||||
<h1>A gente encaixa sua folha e você paga <em>menos metro</em></h1>
|
||||
<p>Mande a arte, veja a nota e a folha montada antes de pagar. Revisão de 5 pontos e reencaixe por nossa conta.</p>
|
||||
<div class="hero2-cta">
|
||||
<a href="#envio" class="btn-laranja">Enviar minha arte <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
|
||||
<a href="/#envio" class="btn-laranja">Enviar minha arte <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
|
||||
<span>Sem cadastro · preço em segundos</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -856,7 +857,7 @@ footer a:hover{color:var(--laranja2)}
|
||||
</header>
|
||||
|
||||
<section class="etapas2"><div class="w">
|
||||
<div><span>1</span><div><b>Envie a arte</b><p>PNG, JPG ou PDF. Sem cadastro.</p></div></div>
|
||||
<div><span>1</span><div><b>Envie a arte</b><p>PNG, JPG ou PDF, conforme o produto. Sem cadastro.</p></div></div>
|
||||
<div><span>2</span><div><b>Veja nota e montagem</b><p>A folha e o preço aparecem na hora.</p></div></div>
|
||||
<div><span>3</span><div><b>Confira e pague</b><p>Conferimos a metragem e liberamos o pagamento.</p></div></div>
|
||||
<div><span>4</span><div><b>Acompanhe</b><p>Aviso no WhatsApp a cada etapa.</p></div></div>
|
||||
@@ -865,11 +866,11 @@ footer a:hover{color:var(--laranja2)}
|
||||
<!-- ENVIO -->
|
||||
<section id="envio">
|
||||
<nav class="passos" id="passos" aria-label="Etapas do pedido" hidden>
|
||||
<a href="#foco" data-passo="1"><span>1</span>Montagem</a>
|
||||
<a href="/#envio" data-passo="1"><span>1</span>Montagem</a>
|
||||
<i aria-hidden="true"></i>
|
||||
<a href="#carr" data-passo="2"><span>2</span>Dados e entrega</a>
|
||||
<a href="/carrinho" data-passo="2"><span>2</span>Dados e entrega</a>
|
||||
<i aria-hidden="true"></i>
|
||||
<a href="#checkoutStatus" data-passo="3"><span>3</span>Pagamento</a>
|
||||
<a href="/carrinho#checkoutStatus" data-passo="3"><span>3</span>Pagamento</a>
|
||||
</nav>
|
||||
<div class="w">
|
||||
|
||||
@@ -1097,7 +1098,7 @@ footer a:hover{color:var(--laranja2)}
|
||||
<div class="pinfo" id="pInfo"></div>
|
||||
</div>
|
||||
<div class="pcta">
|
||||
<button class="pb p" id="pOk">Está certo, fechar o pedido</button>
|
||||
<button class="pb p" id="pOk">Adicionar ao carrinho</button>
|
||||
<button class="pb" id="pMais">Adicionar mais artes</button>
|
||||
<button class="pb" id="pVolta">Trocar o arquivo</button>
|
||||
</div>
|
||||
@@ -1191,7 +1192,7 @@ footer a:hover{color:var(--laranja2)}
|
||||
<div class="tot"><span>Total</span><b id="carrTot">R$ 0,00</b></div>
|
||||
<div class="cbts">
|
||||
<button id="bPagar">Ir para o pagamento</button>
|
||||
<button id="bMais" class="sec">Adicionar outro produto</button>
|
||||
<button id="bMais" class="sec">Continuar comprando</button>
|
||||
</div>
|
||||
<p class="obs">A nota fiscal sai no CNPJ informado. O arquivo fica guardado por 30 dias;
|
||||
depois disso, um novo pedido precisa do arquivo de novo. O histórico do pedido
|
||||
@@ -1205,6 +1206,11 @@ footer a:hover{color:var(--laranja2)}
|
||||
<p id="checkoutStatus" class="avisoE" role="status" aria-live="polite"></p>
|
||||
<div id="checkoutActions"></div>
|
||||
</div>
|
||||
<div class="carrVazio" id="carrVazio" hidden>
|
||||
<h2>Seu carrinho está vazio</h2>
|
||||
<p>Escolha um produto, envie a arte e veja o preço na hora.</p>
|
||||
<a href="/#envio" class="btn-laranja">Escolher produto</a>
|
||||
</div>
|
||||
</div></section>
|
||||
|
||||
<!-- PREÇO E VANTAGENS -->
|
||||
@@ -1235,7 +1241,7 @@ footer a:hover{color:var(--laranja2)}
|
||||
|
||||
<div class="barraM" id="barraM" hidden>
|
||||
<div><span id="barraMSub">Seu pedido</span><b id="barraMTot">R$ 0,00</b></div>
|
||||
<a href="#carr" id="barraMBtn">Continuar</a>
|
||||
<a href="/carrinho" id="barraMBtn">Continuar</a>
|
||||
</div>
|
||||
|
||||
<footer><div class="w">
|
||||
@@ -1276,8 +1282,8 @@ footer a:hover{color:var(--laranja2)}
|
||||
<script src="/privacy.js"></script>
|
||||
<script src="/upload.js"></script>
|
||||
<script src="/checkout.js"></script>
|
||||
<script src="/cart.js?v=2"></script>
|
||||
<script src="/site-steps.js?v=1"></script>
|
||||
<script src="/cart.js"></script>
|
||||
<script src="/site-steps.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -18,7 +18,7 @@ async function load(){
|
||||
for(const quote of data.quotes){
|
||||
const card=node('article');card.className='card';card.append(node('h3','Cotação '+quote.id.slice(0,8)));
|
||||
card.append(node('p',quote.approved?'Total aprovado: '+money(quote.approved.total_cents):'Aguardando conferência de metragem e nota.'));
|
||||
const link=node('a','Abrir cotação no Site');link.href='/?quote='+quote.id;card.append(link);$('quotes').append(card);
|
||||
const link=node('a','Abrir cotação no Site');link.href='/carrinho?quote='+quote.id;card.append(link);$('quotes').append(card);
|
||||
}
|
||||
$('orders').replaceChildren();
|
||||
if(!data.orders.length)$('orders').append(node('p','Nenhum pedido pago nesta conta ou sessão.'));
|
||||
|
||||
@@ -7,15 +7,36 @@
|
||||
// Por isso a nota NÃO muda aqui. E a metragem nova só existe depois que o motor
|
||||
// roda na folha de verdade, no servidor: o navegador recebe a folha fechada,
|
||||
// não as artes separadas, então não há como recalcular aqui sem inventar número.
|
||||
// Loose artworks still show the mounted sheet below; a ready sheet goes
|
||||
// straight to the cart.
|
||||
$('qOk').addEventListener('click',()=>{
|
||||
if($('qOk').disabled) return;
|
||||
if(!$('prev').classList.contains('on')){ adicionaAoCarrinho(); return; }
|
||||
const ok=$('ciente').classList.contains('on');
|
||||
qm(ok? '<b>Registrado.</b> Você confirmou que viu a ressalva de resolução.'
|
||||
: '<b>Certo.</b> Seu pedido está montado abaixo.','var(--tx)');
|
||||
$('carr').scrollIntoView({behavior:'smooth',block:'nearest'});
|
||||
: '<b>Certo.</b> Confira a folha montada abaixo.','var(--tx)');
|
||||
$('prev').scrollIntoView({behavior:'smooth',block:'start'});
|
||||
});
|
||||
|
||||
$('pOk').addEventListener('click',()=>$('carr').scrollIntoView({behavior:'smooth'}));
|
||||
// The finished item joins the order and the product page starts over empty.
|
||||
function guardaItem(){
|
||||
if(itemAtual && !itemPodeEnviar(itemAtual)){
|
||||
$('qual').scrollIntoView({behavior:'smooth',block:'nearest'});
|
||||
return false;
|
||||
}
|
||||
if(itemAtual){ pedido.push(itemAtual); itemAtual=null; }
|
||||
$('atual').style.display='none'; $('carrLin').innerHTML='';
|
||||
$('foco').classList.remove('on');
|
||||
['qual','prev'].forEach(id=>$(id).classList.remove('on'));
|
||||
modo=null; artes=[]; folhas=[]; metros=0; nota=0; montagemCm=0;
|
||||
pintaPedido();
|
||||
return true;
|
||||
}
|
||||
function adicionaAoCarrinho(){
|
||||
if(!itemAtual){ $('qual').scrollIntoView({behavior:'smooth',block:'nearest'}); return; }
|
||||
if(guardaItem()) vaiPara(CARRINHO);
|
||||
}
|
||||
$('pOk').addEventListener('click',adicionaAoCarrinho);
|
||||
$('pMais').addEventListener('click',()=>$('foco').scrollIntoView({behavior:'smooth'}));
|
||||
$('pVolta').addEventListener('click',()=>{
|
||||
folhas=[]; artes=[]; reencaixado=false; pintaFolha(); pintaArtes();
|
||||
@@ -165,19 +186,7 @@ $('bCep').addEventListener('click',async()=>{
|
||||
pintaEntrega();
|
||||
});
|
||||
|
||||
$('bMais').addEventListener('click',()=>{
|
||||
if(itemAtual && !itemPodeEnviar(itemAtual)){
|
||||
$('qual').scrollIntoView({behavior:'smooth',block:'nearest'});
|
||||
return;
|
||||
}
|
||||
if(itemAtual){ pedido.push(itemAtual); itemAtual=null; }
|
||||
$('atual').style.display='none'; $('carrLin').innerHTML='';
|
||||
$('foco').classList.remove('on'); $('cards').style.display='';
|
||||
['qual','prev'].forEach(id=>$(id).classList.remove('on'));
|
||||
modo=null; artes=[]; folhas=[]; metros=0; nota=0; montagemCm=0;
|
||||
pintaPedido();
|
||||
document.getElementById('envio').scrollIntoView({behavior:'smooth'});
|
||||
});
|
||||
$('bMais').addEventListener('click',()=>{ if(guardaItem()) vaiPara('/#envio'); });
|
||||
|
||||
$('bPagar').addEventListener('click',()=>{
|
||||
if(window.dtfCheckout) window.dtfCheckout();
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
Loaded as classic scripts in the order listed there: they share one global
|
||||
scope and run top to bottom, exactly as the original did. */
|
||||
// ── escolha
|
||||
document.querySelectorAll('.ec').forEach(b=>b.addEventListener('click',()=>abrir(b.dataset.modo)));
|
||||
document.querySelectorAll('.ec').forEach(b=>b.addEventListener('click',()=>escolheProduto(b.dataset.modo)));
|
||||
// Back on the home and into the same product: the artworks sent are still there.
|
||||
function escolheProduto(m){
|
||||
if(modo===m && $('foco').classList.contains('on')) vaiPara(PAGINAS[m]);
|
||||
else abrir(m);
|
||||
}
|
||||
function abrir(m){
|
||||
modo=m; artes=[]; folhas=[]; metros=0; nota=0; reencaixado=false;
|
||||
montagemCm=0;
|
||||
@@ -13,7 +18,7 @@ function abrir(m){
|
||||
recemChegada=null; caminho='auto'; avisoCam(''); pintaCaminhos();
|
||||
$('vMt').textContent='—'; previaAoVivo(); agendaAvaliacao();
|
||||
limpaPaineis(); // o carrinho só some se o pedido estiver vazio
|
||||
$('foco').scrollIntoView({behavior:'smooth',block:'start'});
|
||||
vaiPara(PAGINAS[m]);
|
||||
}
|
||||
function pintaModo(){
|
||||
const m=modo;
|
||||
@@ -29,6 +34,7 @@ function pintaModo(){
|
||||
$('larguraFolhaPronta').textContent=String(c.larg).replace('.',',');
|
||||
$('foco2').classList.remove('sofila'); // a caixa de requisitos vale nos 4 modos
|
||||
$('montcabTit').textContent = ehFolha(m) ? 'Sua folha' : 'Montagem ao vivo';
|
||||
$('qOk').textContent = ehFolha(m) ? 'Adicionar ao carrinho' : 'Ver a folha montada';
|
||||
pintaTipoEnvio();
|
||||
}
|
||||
// O par folha montada / artes separadas fica visível e com preço nos dois lados,
|
||||
@@ -81,15 +87,16 @@ function trocaTipo(destino){
|
||||
if(!destino || modo===destino) return;
|
||||
folhas=[]; artes=[]; recemChegada=null; metros=0; nota=0; montagemCm=0;
|
||||
modo=destino;
|
||||
history.replaceState(history.state,'',PAGINAS[modo]); document.title=tituloPagina();
|
||||
$('lista').innerHTML=''; $('rA').style.display='none'; recusa([]);
|
||||
caminho='auto'; avisoCam('');
|
||||
pintaModo(); pintaCaminhos(); limpaPaineis(); agendaAvaliacao(); previaAoVivo();
|
||||
}
|
||||
$('bVoltar').addEventListener('click',()=>{
|
||||
itemAtual=null; $('atual').style.display='none'; $('carrLin').innerHTML=''; pintaPedido();
|
||||
$('foco').classList.remove('on'); $('cards').style.display='';
|
||||
itemAtual=null; modo=null; $('atual').style.display='none'; $('carrLin').innerHTML=''; pintaPedido();
|
||||
$('foco').classList.remove('on');
|
||||
['qual','prev'].forEach(id=>$(id).classList.remove('on'));
|
||||
if(!pedido.length) $('carr').classList.remove('on');
|
||||
document.getElementById('envio').scrollIntoView({behavior:'smooth'});
|
||||
vaiPara('/#envio');
|
||||
});
|
||||
|
||||
|
||||
19
web/site-pages.js
Normal file
19
web/site-pages.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/* Site DTF — the Site's pages: the home, one page per product and the cart.
|
||||
They share this document, so artworks already loaded in the browser survive
|
||||
moving between them; each page has its own address, so Back, Forward, reload
|
||||
and shared links behave as in any store. nginx serves index.html for these
|
||||
addresses. Loaded in <head> so the right page is the first one painted;
|
||||
site-steps.js does the navigation. */
|
||||
const PAGINAS={file:'/arquivo-por-metro', avulsa:'/artes-avulsas',
|
||||
uvfile:'/uv-arquivo-por-metro', uv:'/uv-artes-avulsas'};
|
||||
const CARRINHO='/carrinho';
|
||||
function paginaDe(caminho){
|
||||
caminho=caminho.replace(/\/+$/,'')||'/';
|
||||
if(caminho===CARRINHO) return {pagina:'carrinho'};
|
||||
const m=Object.keys(PAGINAS).find(k=>PAGINAS[k]===caminho);
|
||||
return m ? {pagina:'produto',modo:m} : {pagina:'inicio'};
|
||||
}
|
||||
// Older portal links open a quote on the home address; quotes live in the cart.
|
||||
if(location.pathname==='/' && new URLSearchParams(location.search).has('quote'))
|
||||
history.replaceState(null,'',CARRINHO+location.search);
|
||||
document.documentElement.dataset.rota=paginaDe(location.pathname).pagina;
|
||||
@@ -1,52 +1,112 @@
|
||||
/* Site DTF — the order progress bar, the product card prices and the phone
|
||||
total bar. This file only reads the page's state (which panels are open,
|
||||
the cart total, the checkout status); it never changes an order, a price or
|
||||
a file, so the flow in the other site-*.js files behaves exactly as before. */
|
||||
(function(){
|
||||
// Card prices come from the same table the checkout uses, so they cannot drift.
|
||||
document.querySelectorAll('[data-de]').forEach(el=>{ el.textContent=rs(pisoEscada(el.dataset.de)); });
|
||||
document.querySelectorAll('[data-ate]').forEach(el=>{ el.textContent=rs(TABELA[el.dataset.ate]); });
|
||||
/* Site DTF — moving between the Site's pages (addresses in site-pages.js), the
|
||||
order progress bar, the product card prices and the phone total bar. It only
|
||||
switches which page is shown and reads the page's state (the open product,
|
||||
the cart, the checkout status); orders, prices and files stay with the other
|
||||
site-*.js files. */
|
||||
// Card prices come from the same table the checkout uses, so they cannot drift.
|
||||
document.querySelectorAll('[data-de]').forEach(el=>{ el.textContent=rs(pisoEscada(el.dataset.de)); });
|
||||
document.querySelectorAll('[data-ate]').forEach(el=>{ el.textContent=rs(TABELA[el.dataset.ate]); });
|
||||
|
||||
const TITULO_INICIO=document.title;
|
||||
function tituloPagina(){
|
||||
const p=paginaDe(location.pathname);
|
||||
if(p.pagina==='carrinho') return 'Carrinho · DTF Dropstar';
|
||||
if(p.pagina==='produto') return MODOS[p.modo].tit+' · DTF Dropstar';
|
||||
return TITULO_INICIO;
|
||||
}
|
||||
|
||||
// Shows the page for the current address; `ancora` scrolls to a section,
|
||||
// otherwise the page opens at `y` (the saved position on Back and Forward).
|
||||
function mostraPagina(ancora,y,suave){
|
||||
const p=paginaDe(location.pathname);
|
||||
if(p.pagina==='produto' && modo!==p.modo) abrir(p.modo);
|
||||
document.documentElement.dataset.rota=p.pagina;
|
||||
$('cards').style.display='';
|
||||
document.title=tituloPagina();
|
||||
window.dispatchEvent(new Event('dtf-page-changed'));
|
||||
const el=ancora && document.getElementById(ancora);
|
||||
if(el) el.scrollIntoView({behavior:suave?'smooth':'auto',block:'start'});
|
||||
else window.scrollTo(0,y||0);
|
||||
}
|
||||
function vaiPara(url){
|
||||
const u=new URL(url,location.href);
|
||||
const ancora=u.hash.slice(1)||null;
|
||||
if(u.pathname+u.search===location.pathname+location.search){
|
||||
mostraPagina(ancora,0,true);
|
||||
return;
|
||||
}
|
||||
try{ history.replaceState({...(history.state||{}),y:scrollY},''); }catch(e){}
|
||||
history.pushState({y:0},'',u.pathname+u.search+u.hash);
|
||||
mostraPagina(ancora,0,false);
|
||||
}
|
||||
|
||||
(function(){
|
||||
if('scrollRestoration' in history) history.scrollRestoration='manual';
|
||||
addEventListener('popstate',e=>mostraPagina(e.state?null:location.hash.slice(1)||null,e.state?.y));
|
||||
|
||||
// Links to the Site's own pages move inside this document; everything else
|
||||
// (the account portal, the store, e-mail) is a normal link.
|
||||
document.addEventListener('click',e=>{
|
||||
const a=e.target.closest && e.target.closest('a[href]');
|
||||
if(!a || e.defaultPrevented || e.button || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || a.target) return;
|
||||
const u=new URL(a.href,location.href);
|
||||
if(u.origin!==location.origin) return;
|
||||
const caminho=u.pathname.replace(/\/+$/,'')||'/';
|
||||
if(caminho!=='/' && caminho!=='/index.html' && paginaDe(caminho).pagina==='inicio') return;
|
||||
e.preventDefault(); vaiPara(u.pathname+u.search+u.hash);
|
||||
});
|
||||
|
||||
const passos=$('passos'), barra=$('barraM'), vazio=$('carrVazio');
|
||||
const foco=$('foco'), carr=$('carr'), status=$('checkoutStatus'), acoes=$('checkoutActions');
|
||||
const rota=()=>document.documentElement.dataset.rota;
|
||||
const temCheckout=()=>!!(status.textContent||'').trim() || acoes.children.length>0;
|
||||
|
||||
const passos=$('passos'), barra=$('barraM');
|
||||
// The store header is sticky; the progress bar and the side panels sit below it.
|
||||
function alturas(){
|
||||
const topo=document.querySelector('.topo')?.offsetHeight||0;
|
||||
document.documentElement.style.setProperty('--topo',topo+'px');
|
||||
document.documentElement.style.setProperty('--topo-passos',(topo+(passos.hidden?0:passos.offsetHeight)+16)+'px');
|
||||
}
|
||||
const foco=$('foco'), carr=$('carr'), status=$('checkoutStatus'), acoes=$('checkoutActions');
|
||||
|
||||
function naTela(el){
|
||||
const r=el.getBoundingClientRect();
|
||||
return r.top < innerHeight*0.55 && r.bottom > innerHeight*0.2;
|
||||
return r.height>0 && r.top<innerHeight && r.bottom>0;
|
||||
}
|
||||
function etapa(){
|
||||
// Once the cart is sent, the quote, the payment and the order live in the
|
||||
// checkout status: that is the payment step.
|
||||
if((status.textContent||'').trim() || acoes.children.length) return 3;
|
||||
if(carr.classList.contains('on') && (naTela(carr) || !foco.classList.contains('on'))) return 2;
|
||||
return 1;
|
||||
// Where the phone bar's button leads: the button that finishes the item on a
|
||||
// product page, the order summary in the cart.
|
||||
function alvoBarra(){
|
||||
if(rota()==='produto' && itemAtual)
|
||||
return $('prev').classList.contains('on') ? $('pOk') : $('qOk');
|
||||
if(rota()==='carrinho' && (pedido.length || itemAtual) && !temCheckout())
|
||||
return document.querySelector('.resumo');
|
||||
return null;
|
||||
}
|
||||
let pendente=false;
|
||||
function pinta(){
|
||||
pendente=false;
|
||||
const ativo=foco.classList.contains('on') || carr.classList.contains('on') || (status.textContent||'').trim();
|
||||
passos.hidden=!ativo; alturas();
|
||||
if(ativo){
|
||||
const atual=etapa();
|
||||
const r=rota();
|
||||
// With nothing in it, the cart page says so instead of showing an empty form.
|
||||
const semItens = !pedido.length && !itemAtual;
|
||||
const carrinhoVazio = r==='carrinho' && semItens && !temCheckout();
|
||||
document.documentElement.toggleAttribute('data-sem-itens', semItens);
|
||||
vazio.hidden = !carrinhoVazio;
|
||||
passos.hidden = r==='inicio' || carrinhoVazio;
|
||||
alturas();
|
||||
if(!passos.hidden){
|
||||
const atual = r==='produto' ? 1 : temCheckout() ? 3 : 2;
|
||||
passos.querySelectorAll('[data-passo]').forEach(a=>{
|
||||
const n=Number(a.dataset.passo);
|
||||
a.classList.toggle('atual',n===atual); a.classList.toggle('feito',n<atual);
|
||||
if(n===atual) a.setAttribute('aria-current','step'); else a.removeAttribute('aria-current');
|
||||
});
|
||||
passos.querySelector('[data-passo="1"]').href =
|
||||
modo && foco.classList.contains('on') ? PAGINAS[modo] : '/#envio';
|
||||
}
|
||||
// Phone bar: the running total while the cart itself is off screen.
|
||||
const temPedido=carr.classList.contains('on');
|
||||
const mostrar=temPedido && !naTela(carr) && !(status.textContent||'').trim();
|
||||
const alvo=alvoBarra();
|
||||
const mostrar=!!alvo && !naTela(alvo);
|
||||
barra.hidden=!mostrar;
|
||||
if(mostrar){
|
||||
$('barraMTot').textContent=$('carrTot').textContent;
|
||||
$('barraMSub').textContent=foco.classList.contains('on')?'Total até agora':'Seu pedido';
|
||||
$('barraMSub').textContent= r==='produto' ? 'Total até agora' : 'Seu pedido';
|
||||
}
|
||||
}
|
||||
function agenda(){ if(!pendente){ pendente=true; requestAnimationFrame(pinta); } }
|
||||
@@ -54,19 +114,24 @@
|
||||
const obs=new MutationObserver(agenda);
|
||||
obs.observe(foco,{attributes:true,attributeFilter:['class']});
|
||||
obs.observe(carr,{attributes:true,attributeFilter:['class']});
|
||||
obs.observe($('prev'),{attributes:true,attributeFilter:['class']});
|
||||
obs.observe(status,{childList:true,characterData:true,subtree:true});
|
||||
obs.observe(acoes,{childList:true});
|
||||
obs.observe($('carrTot'),{childList:true,characterData:true,subtree:true});
|
||||
addEventListener('scroll',agenda,{passive:true});
|
||||
addEventListener('resize',agenda);
|
||||
addEventListener('dtf-cart-changed',agenda);
|
||||
addEventListener('dtf-page-changed',agenda);
|
||||
|
||||
// Steps are links; step 3 only makes sense once there is something to pay.
|
||||
passos.querySelectorAll('a').forEach(a=>a.addEventListener('click',e=>{
|
||||
const destino=document.querySelector(a.getAttribute('href'));
|
||||
if(!destino || destino.offsetParent===null){ e.preventDefault(); return; }
|
||||
e.preventDefault(); destino.scrollIntoView({behavior:'smooth',block:'start'});
|
||||
}));
|
||||
$('barraMBtn').addEventListener('click',e=>{ e.preventDefault(); carr.scrollIntoView({behavior:'smooth',block:'start'}); });
|
||||
pinta();
|
||||
// Payment is a step only once the order has been sent.
|
||||
passos.querySelector('[data-passo="3"]').addEventListener('click',e=>{
|
||||
if(!temCheckout()) e.preventDefault();
|
||||
});
|
||||
$('barraMBtn').addEventListener('click',e=>{
|
||||
e.preventDefault();
|
||||
alvoBarra()?.scrollIntoView({behavior:'smooth',block:'center'});
|
||||
});
|
||||
|
||||
try{ history.replaceState({...(history.state||{}),y:0},''); }catch(e){}
|
||||
mostraPagina(location.hash.slice(1)||null,0);
|
||||
})();
|
||||
|
||||
@@ -12,6 +12,18 @@ body{background:#fff}
|
||||
h1,h2,h3,h4{font-family:"Inter",sans-serif}
|
||||
.et{font-family:"Inter",sans-serif;font-weight:800;font-size:12.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--laranja-escuro)}
|
||||
|
||||
/* Pages (site-pages.js): the home, a product page and the cart show only their own parts. */
|
||||
html[data-rota="inicio"] :is(#passos,#foco,#qual,#prev,#carr,.checkout,#carrVazio),
|
||||
html[data-rota="produto"] :is(.hero2,.etapas2,#cards,#carr,.checkout,#carrVazio,#info),
|
||||
html[data-rota="carrinho"] :is(.hero2,.etapas2,#cards,#foco,#qual,#prev,#info){display:none!important}
|
||||
html[data-sem-itens] #carr{display:none!important}
|
||||
html:not([data-rota="inicio"]) #envio{padding-top:0;padding-bottom:64px;min-height:70vh}
|
||||
#envio,#foco,#qual,#prev,#checkoutStatus{scroll-margin-top:var(--topo-passos,128px)}
|
||||
.carrVazio{max-width:520px;margin:48px auto 0;padding:0 16px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:14px}
|
||||
.carrVazio[hidden]{display:none}
|
||||
.carrVazio h2{font-size:28px;font-weight:800;letter-spacing:-.02em}
|
||||
.carrVazio p{color:var(--fraco);font-size:16px}
|
||||
|
||||
/* Hero */
|
||||
.hero2{background:var(--preto);color:#fff;padding:64px 0}
|
||||
.hero2-grid{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,1fr);gap:56px;align-items:center}
|
||||
@@ -98,6 +110,7 @@ section+section{border-top:0}
|
||||
.zona:hover,.zona.drag{border-color:var(--laranja2)}
|
||||
.cam{border-radius:var(--raio2)}
|
||||
.cam.on{border-color:var(--laranja);background:var(--aviso-fundo)}
|
||||
#larguraFolhaPronta{display:inline}
|
||||
.mont{border-radius:var(--raio)}
|
||||
.req{border-radius:var(--raio)}
|
||||
.qual,.prev{border-radius:16px;border-color:var(--linha)}
|
||||
|
||||
Reference in New Issue
Block a user