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

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:
Cauê Faleiros
2026-09-24 16:56:58 -03:00
parent 177882e77b
commit b81ff8d03d
12 changed files with 212 additions and 72 deletions

View File

@@ -48,6 +48,11 @@ server {
} }
# Always revalidate HTML/JS/CSS after a deployment. Without this, a browser # 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. # 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 / { location / {
expires -1; expires -1;
try_files $uri $uri/ =404; try_files $uri $uri/ =404;

View File

@@ -19,6 +19,10 @@ server {
proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-For $remote_addr;
client_max_body_size 2m; 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; } location / { try_files $uri $uri/ =404; }
} }
server { server {

View File

@@ -63,6 +63,10 @@ try{
await site.call('Page.reload'); await site.call('Page.reload');
await waitFor(()=>site.eval('typeof window.dtfCheckout === "function"'),'reload after security probe'); await waitFor(()=>site.eval('typeof window.dtfCheckout === "function"'),'reload after security probe');
await site.click('[data-modo="file"]'); 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"]'); await site.click('[data-cam="tabela"]');
const root=await site.call('DOM.getDocument'); const root=await site.call('DOM.getDocument');
const input=await site.call('DOM.querySelector',{nodeId:root.root.nodeId,selector:'#inp'}); const input=await site.call('DOM.querySelector',{nodeId:root.root.nodeId,selector:'#inp'});
@@ -77,6 +81,10 @@ try{
await pause(800); await pause(800);
await site.call('Page.reload'); await site.call('Page.reload');
await waitFor(()=>site.eval('typeof pedido!=="undefined" && pedido.length===1'),'persistent cart recovery'); 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].localFiles[0].name'),'local-test.cdr');
assert.equal(await site.eval('pedido[0].total'),21.89); assert.equal(await site.eval('pedido[0].total'),21.89);
assert.equal(await site.eval('document.getElementById("bPagar").disabled'),false); assert.equal(await site.eval('document.getElementById("bPagar").disabled'),false);

View File

@@ -29,6 +29,10 @@ def run():
assert script_src == "script-src 'self'", script_src assert script_src == "script-src 'self'", script_src
assert "object-src 'none'" in policy assert "object-src 'none'" in policy
assert 'cdnjs' not in policy, 'pdf.js is vendored; no CDN belongs in the 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/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':'https://attacker.invalid'},b'{}')
raw('/api/account/logout',403,{'Content-Type':'application/json','Origin':'http://localhost:9999'},b'{}') raw('/api/account/logout',403,{'Content-Type':'application/json','Origin':'http://localhost:9999'},b'{}')

View File

@@ -167,7 +167,7 @@
} else if (quote.status==='paid') { } 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'}; 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)+'.'); 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 { } else {
message('Cotação expirada. Envie o carrinho para uma nova revisão.'); message('Cotação expirada. Envie o carrinho para uma nova revisão.');
button('Nova cotação',clearDraft); button('Nova cotação',clearDraft);

View File

@@ -748,7 +748,8 @@ footer a:hover{color:var(--laranja2)}
justify-content:space-between;gap:12px;flex-wrap:wrap;font-size:11px;color:#5A6570} 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}} @media(prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto}}
</style> </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> </head>
<body> <body>
@@ -764,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="#carr" id="cartLink">Carrinho (0)</a> <a href="/carrinho" id="cartLink">Carrinho (0)</a>
</div> </div>
</div></header> </div></header>
@@ -785,10 +786,10 @@ footer a:hover{color:var(--laranja2)}
</div> </div>
</div> </div>
<div class="mi on"> <div class="mi on">
<a href="#envio">Impressão DTF</a> <a href="/#envio">Impressão DTF</a>
<div class="sub"> <div class="sub">
<a href="#envio">DTF Têxtil · 57 cm</a> <a href="/#envio">DTF Têxtil · 57 cm</a>
<a href="#envio">DTF UV · 28,5 cm</a> <a href="/#envio">DTF UV · 28,5 cm</a>
</div> </div>
</div> </div>
<div class="mi"> <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> <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> <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"> <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> <span>Sem cadastro · preço em segundos</span>
</div> </div>
</div> </div>
@@ -856,7 +857,7 @@ footer a:hover{color:var(--laranja2)}
</header> </header>
<section class="etapas2"><div class="w"> <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>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>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> <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 --> <!-- ENVIO -->
<section id="envio"> <section id="envio">
<nav class="passos" id="passos" aria-label="Etapas do pedido" hidden> <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> <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> <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> </nav>
<div class="w"> <div class="w">
@@ -1097,7 +1098,7 @@ footer a:hover{color:var(--laranja2)}
<div class="pinfo" id="pInfo"></div> <div class="pinfo" id="pInfo"></div>
</div> </div>
<div class="pcta"> <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="pMais">Adicionar mais artes</button>
<button class="pb" id="pVolta">Trocar o arquivo</button> <button class="pb" id="pVolta">Trocar o arquivo</button>
</div> </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="tot"><span>Total</span><b id="carrTot">R$ 0,00</b></div>
<div class="cbts"> <div class="cbts">
<button id="bPagar">Ir para o pagamento</button> <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> </div>
<p class="obs">A nota fiscal sai no CNPJ informado. O arquivo fica guardado por 30 dias; <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 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> <p id="checkoutStatus" class="avisoE" role="status" aria-live="polite"></p>
<div id="checkoutActions"></div> <div id="checkoutActions"></div>
</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> </div></section>
<!-- PREÇO E VANTAGENS --> <!-- PREÇO E VANTAGENS -->
@@ -1235,7 +1241,7 @@ footer a:hover{color:var(--laranja2)}
<div class="barraM" id="barraM" hidden> <div class="barraM" id="barraM" hidden>
<div><span id="barraMSub">Seu pedido</span><b id="barraMTot">R$ 0,00</b></div> <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> </div>
<footer><div class="w"> <footer><div class="w">
@@ -1276,8 +1282,8 @@ footer a:hover{color:var(--laranja2)}
<script src="/privacy.js"></script> <script src="/privacy.js"></script>
<script src="/upload.js"></script> <script src="/upload.js"></script>
<script src="/checkout.js"></script> <script src="/checkout.js"></script>
<script src="/cart.js?v=2"></script> <script src="/cart.js"></script>
<script src="/site-steps.js?v=1"></script> <script src="/site-steps.js"></script>
</body> </body>
</html> </html>

View File

@@ -18,7 +18,7 @@ async function load(){
for(const quote of data.quotes){ for(const quote of data.quotes){
const card=node('article');card.className='card';card.append(node('h3','Cotação '+quote.id.slice(0,8))); 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.')); 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(); $('orders').replaceChildren();
if(!data.orders.length)$('orders').append(node('p','Nenhum pedido pago nesta conta ou sessão.')); if(!data.orders.length)$('orders').append(node('p','Nenhum pedido pago nesta conta ou sessão.'));

View File

@@ -7,15 +7,36 @@
// Por isso a nota NÃO muda aqui. E a metragem nova só existe depois que o motor // 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, // 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. // 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',()=>{ $('qOk').addEventListener('click',()=>{
if($('qOk').disabled) return; if($('qOk').disabled) return;
if(!$('prev').classList.contains('on')){ adicionaAoCarrinho(); return; }
const ok=$('ciente').classList.contains('on'); const ok=$('ciente').classList.contains('on');
qm(ok? '<b>Registrado.</b> Você confirmou que viu a ressalva de resolução.' qm(ok? '<b>Registrado.</b> Você confirmou que viu a ressalva de resolução.'
: '<b>Certo.</b> Seu pedido está montado abaixo.','var(--tx)'); : '<b>Certo.</b> Confira a folha montada abaixo.','var(--tx)');
$('carr').scrollIntoView({behavior:'smooth',block:'nearest'}); $('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'})); $('pMais').addEventListener('click',()=>$('foco').scrollIntoView({behavior:'smooth'}));
$('pVolta').addEventListener('click',()=>{ $('pVolta').addEventListener('click',()=>{
folhas=[]; artes=[]; reencaixado=false; pintaFolha(); pintaArtes(); folhas=[]; artes=[]; reencaixado=false; pintaFolha(); pintaArtes();
@@ -165,19 +186,7 @@ $('bCep').addEventListener('click',async()=>{
pintaEntrega(); pintaEntrega();
}); });
$('bMais').addEventListener('click',()=>{ $('bMais').addEventListener('click',()=>{ if(guardaItem()) vaiPara('/#envio'); });
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'});
});
$('bPagar').addEventListener('click',()=>{ $('bPagar').addEventListener('click',()=>{
if(window.dtfCheckout) window.dtfCheckout(); if(window.dtfCheckout) window.dtfCheckout();

View File

@@ -3,7 +3,12 @@
Loaded as classic scripts in the order listed there: they share one global Loaded as classic scripts in the order listed there: they share one global
scope and run top to bottom, exactly as the original did. */ scope and run top to bottom, exactly as the original did. */
// ── escolha // ── 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){ function abrir(m){
modo=m; artes=[]; folhas=[]; metros=0; nota=0; reencaixado=false; modo=m; artes=[]; folhas=[]; metros=0; nota=0; reencaixado=false;
montagemCm=0; montagemCm=0;
@@ -13,7 +18,7 @@ function abrir(m){
recemChegada=null; caminho='auto'; avisoCam(''); pintaCaminhos(); recemChegada=null; caminho='auto'; avisoCam(''); pintaCaminhos();
$('vMt').textContent='—'; previaAoVivo(); agendaAvaliacao(); $('vMt').textContent='—'; previaAoVivo(); agendaAvaliacao();
limpaPaineis(); // o carrinho só some se o pedido estiver vazio limpaPaineis(); // o carrinho só some se o pedido estiver vazio
$('foco').scrollIntoView({behavior:'smooth',block:'start'}); vaiPara(PAGINAS[m]);
} }
function pintaModo(){ function pintaModo(){
const m=modo; const m=modo;
@@ -29,6 +34,7 @@ function pintaModo(){
$('larguraFolhaPronta').textContent=String(c.larg).replace('.',','); $('larguraFolhaPronta').textContent=String(c.larg).replace('.',',');
$('foco2').classList.remove('sofila'); // a caixa de requisitos vale nos 4 modos $('foco2').classList.remove('sofila'); // a caixa de requisitos vale nos 4 modos
$('montcabTit').textContent = ehFolha(m) ? 'Sua folha' : 'Montagem ao vivo'; $('montcabTit').textContent = ehFolha(m) ? 'Sua folha' : 'Montagem ao vivo';
$('qOk').textContent = ehFolha(m) ? 'Adicionar ao carrinho' : 'Ver a folha montada';
pintaTipoEnvio(); pintaTipoEnvio();
} }
// O par folha montada / artes separadas fica visível e com preço nos dois lados, // 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; if(!destino || modo===destino) return;
folhas=[]; artes=[]; recemChegada=null; metros=0; nota=0; montagemCm=0; folhas=[]; artes=[]; recemChegada=null; metros=0; nota=0; montagemCm=0;
modo=destino; modo=destino;
history.replaceState(history.state,'',PAGINAS[modo]); document.title=tituloPagina();
$('lista').innerHTML=''; $('rA').style.display='none'; recusa([]); $('lista').innerHTML=''; $('rA').style.display='none'; recusa([]);
caminho='auto'; avisoCam(''); caminho='auto'; avisoCam('');
pintaModo(); pintaCaminhos(); limpaPaineis(); agendaAvaliacao(); previaAoVivo(); pintaModo(); pintaCaminhos(); limpaPaineis(); agendaAvaliacao(); previaAoVivo();
} }
$('bVoltar').addEventListener('click',()=>{ $('bVoltar').addEventListener('click',()=>{
itemAtual=null; $('atual').style.display='none'; $('carrLin').innerHTML=''; pintaPedido(); itemAtual=null; modo=null; $('atual').style.display='none'; $('carrLin').innerHTML=''; pintaPedido();
$('foco').classList.remove('on'); $('cards').style.display=''; $('foco').classList.remove('on');
['qual','prev'].forEach(id=>$(id).classList.remove('on')); ['qual','prev'].forEach(id=>$(id).classList.remove('on'));
if(!pedido.length) $('carr').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
View 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;

View File

@@ -1,52 +1,112 @@
/* Site DTF — the order progress bar, the product card prices and the phone /* Site DTF — moving between the Site's pages (addresses in site-pages.js), the
total bar. This file only reads the page's state (which panels are open, order progress bar, the product card prices and the phone total bar. It only
the cart total, the checkout status); it never changes an order, a price or switches which page is shown and reads the page's state (the open product,
a file, so the flow in the other site-*.js files behaves exactly as before. */ the cart, the checkout status); orders, prices and files stay with the other
(function(){ site-*.js files. */
// Card prices come from the same table the checkout uses, so they cannot drift. // 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-de]').forEach(el=>{ el.textContent=rs(pisoEscada(el.dataset.de)); });
document.querySelectorAll('[data-ate]').forEach(el=>{ el.textContent=rs(TABELA[el.dataset.ate]); }); 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. // The store header is sticky; the progress bar and the side panels sit below it.
function alturas(){ function alturas(){
const topo=document.querySelector('.topo')?.offsetHeight||0; const topo=document.querySelector('.topo')?.offsetHeight||0;
document.documentElement.style.setProperty('--topo',topo+'px'); document.documentElement.style.setProperty('--topo',topo+'px');
document.documentElement.style.setProperty('--topo-passos',(topo+(passos.hidden?0:passos.offsetHeight)+16)+'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){ function naTela(el){
const r=el.getBoundingClientRect(); 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(){ // Where the phone bar's button leads: the button that finishes the item on a
// Once the cart is sent, the quote, the payment and the order live in the // product page, the order summary in the cart.
// checkout status: that is the payment step. function alvoBarra(){
if((status.textContent||'').trim() || acoes.children.length) return 3; if(rota()==='produto' && itemAtual)
if(carr.classList.contains('on') && (naTela(carr) || !foco.classList.contains('on'))) return 2; return $('prev').classList.contains('on') ? $('pOk') : $('qOk');
return 1; if(rota()==='carrinho' && (pedido.length || itemAtual) && !temCheckout())
return document.querySelector('.resumo');
return null;
} }
let pendente=false; let pendente=false;
function pinta(){ function pinta(){
pendente=false; pendente=false;
const ativo=foco.classList.contains('on') || carr.classList.contains('on') || (status.textContent||'').trim(); const r=rota();
passos.hidden=!ativo; alturas(); // With nothing in it, the cart page says so instead of showing an empty form.
if(ativo){ const semItens = !pedido.length && !itemAtual;
const atual=etapa(); 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=>{ passos.querySelectorAll('[data-passo]').forEach(a=>{
const n=Number(a.dataset.passo); const n=Number(a.dataset.passo);
a.classList.toggle('atual',n===atual); a.classList.toggle('feito',n<atual); 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'); 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 alvo=alvoBarra();
const temPedido=carr.classList.contains('on'); const mostrar=!!alvo && !naTela(alvo);
const mostrar=temPedido && !naTela(carr) && !(status.textContent||'').trim();
barra.hidden=!mostrar; barra.hidden=!mostrar;
if(mostrar){ if(mostrar){
$('barraMTot').textContent=$('carrTot').textContent; $('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); } } function agenda(){ if(!pendente){ pendente=true; requestAnimationFrame(pinta); } }
@@ -54,19 +114,24 @@
const obs=new MutationObserver(agenda); const obs=new MutationObserver(agenda);
obs.observe(foco,{attributes:true,attributeFilter:['class']}); obs.observe(foco,{attributes:true,attributeFilter:['class']});
obs.observe(carr,{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(status,{childList:true,characterData:true,subtree:true});
obs.observe(acoes,{childList:true}); obs.observe(acoes,{childList:true});
obs.observe($('carrTot'),{childList:true,characterData:true,subtree:true}); obs.observe($('carrTot'),{childList:true,characterData:true,subtree:true});
addEventListener('scroll',agenda,{passive:true}); addEventListener('scroll',agenda,{passive:true});
addEventListener('resize',agenda); addEventListener('resize',agenda);
addEventListener('dtf-cart-changed',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. // Payment is a step only once the order has been sent.
passos.querySelectorAll('a').forEach(a=>a.addEventListener('click',e=>{ passos.querySelector('[data-passo="3"]').addEventListener('click',e=>{
const destino=document.querySelector(a.getAttribute('href')); if(!temCheckout()) e.preventDefault();
if(!destino || destino.offsetParent===null){ e.preventDefault(); return; } });
e.preventDefault(); destino.scrollIntoView({behavior:'smooth',block:'start'}); $('barraMBtn').addEventListener('click',e=>{
})); e.preventDefault();
$('barraMBtn').addEventListener('click',e=>{ e.preventDefault(); carr.scrollIntoView({behavior:'smooth',block:'start'}); }); alvoBarra()?.scrollIntoView({behavior:'smooth',block:'center'});
pinta(); });
try{ history.replaceState({...(history.state||{}),y:0},''); }catch(e){}
mostraPagina(location.hash.slice(1)||null,0);
})(); })();

View File

@@ -12,6 +12,18 @@ body{background:#fff}
h1,h2,h3,h4{font-family:"Inter",sans-serif} 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)} .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 */ /* Hero */
.hero2{background:var(--preto);color:#fff;padding:64px 0} .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} .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)} .zona:hover,.zona.drag{border-color:var(--laranja2)}
.cam{border-radius:var(--raio2)} .cam{border-radius:var(--raio2)}
.cam.on{border-color:var(--laranja);background:var(--aviso-fundo)} .cam.on{border-color:var(--laranja);background:var(--aviso-fundo)}
#larguraFolhaPronta{display:inline}
.mont{border-radius:var(--raio)} .mont{border-radius:var(--raio)}
.req{border-radius:var(--raio)} .req{border-radius:var(--raio)}
.qual,.prev{border-radius:16px;border-color:var(--linha)} .qual,.prev{border-radius:16px;border-color:var(--linha)}