From b81ff8d03d9b064db09d81d4505783a0e7129433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Thu, 24 Sep 2026 16:56:58 -0300 Subject: [PATCH] feat: give each Site product and the cart its own page 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 --- deploy/nginx.conf.template | 5 ++ infra/nginx.conf.template | 4 ++ tests/browser_test.mjs | 8 +++ tests/security_test.py | 4 ++ web/checkout.js | 2 +- web/index.html | 36 +++++----- web/portal.js | 2 +- web/site-flow.js | 41 +++++++----- web/site-modes.js | 17 +++-- web/site-pages.js | 19 ++++++ web/site-steps.js | 133 +++++++++++++++++++++++++++---------- web/site-v2.css | 13 ++++ 12 files changed, 212 insertions(+), 72 deletions(-) create mode 100644 web/site-pages.js diff --git a/deploy/nginx.conf.template b/deploy/nginx.conf.template index 9a0f5b9..a911dd2 100644 --- a/deploy/nginx.conf.template +++ b/deploy/nginx.conf.template @@ -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; diff --git a/infra/nginx.conf.template b/infra/nginx.conf.template index 25b609c..17e520e 100644 --- a/infra/nginx.conf.template +++ b/infra/nginx.conf.template @@ -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 { diff --git a/tests/browser_test.mjs b/tests/browser_test.mjs index 5f49600..1024ba6 100644 --- a/tests/browser_test.mjs +++ b/tests/browser_test.mjs @@ -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); diff --git a/tests/security_test.py b/tests/security_test.py index 7492c5e..b8a85a4 100644 --- a/tests/security_test.py +++ b/tests/security_test.py @@ -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'{}') diff --git a/web/checkout.js b/web/checkout.js index 622d5b6..a0436f4 100644 --- a/web/checkout.js +++ b/web/checkout.js @@ -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); diff --git a/web/index.html b/web/index.html index c39060a..26a0bd7 100644 --- a/web/index.html +++ b/web/index.html @@ -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}} - + + @@ -764,7 +765,7 @@ footer a:hover{color:var(--laranja2)} @@ -785,10 +786,10 @@ footer a:hover{color:var(--laranja2)}
@@ -831,7 +832,7 @@ footer a:hover{color:var(--laranja2)}

A gente encaixa sua folha e você paga menos metro

Mande a arte, veja a nota e a folha montada antes de pagar. Revisão de 5 pontos e reencaixe por nossa conta.

- Enviar minha arte + Enviar minha arte Sem cadastro · preço em segundos
@@ -856,7 +857,7 @@ footer a:hover{color:var(--laranja2)}
-
1
Envie a arte

PNG, JPG ou PDF. Sem cadastro.

+
1
Envie a arte

PNG, JPG ou PDF, conforme o produto. Sem cadastro.

2
Veja nota e montagem

A folha e o preço aparecem na hora.

3
Confira e pague

Conferimos a metragem e liberamos o pagamento.

4
Acompanhe

Aviso no WhatsApp a cada etapa.

@@ -865,11 +866,11 @@ footer a:hover{color:var(--laranja2)}
@@ -1097,7 +1098,7 @@ footer a:hover{color:var(--laranja2)}
- +
@@ -1191,7 +1192,7 @@ footer a:hover{color:var(--laranja2)}
TotalR$ 0,00
- +

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)}

+
@@ -1235,7 +1241,7 @@ footer a:hover{color:var(--laranja2)}