Pular para o conteúdo

A primeira escola especializada em tecnologia do Alto Solimões, com corpo técnico formado na área.

Módulo 5: JavaScript na página

Projeto: a calculadora de frete da lancha

Juntar tudo numa página que funciona

O que você vai construir

Uma página que calcula o frete de uma encomenda pela lancha, guarda o histórico e mostra a tabela de preços vinda de um arquivo.

Quatro partes:

  1. Formulário: peso, destino e tipo de carga
  2. Cálculo: em funções puras, separadas da tela
  3. Histórico: lista dos cálculos, guardada entre visitas
  4. Tabela de preços; carregada com fetch, com os quatro estados

Parte 1: o HTML

<main class="conteudo">
<h1>Calculadora de frete</h1>

<form id="frete">
<fieldset>
<legend>Dados da encomenda</legend>

<label for="peso">Peso em quilos</label>
<input type="number" id="peso" name="peso" min="0.1" step="0.1" required />

<label for="destino">Destino</label>
<select id="destino" name="destino" required>
<option value="">Selecione</option>
<option value="tabatinga">Tabatinga</option>
<option value="atalaia">Atalaia do Norte</option>
<option value="manaus">Manaus</option>
</select>
</fieldset>

<fieldset>
<legend>Tipo de carga</legend>

<input type="radio" id="comum" name="tipo" value="comum" checked />
<label for="comum">Comum</label>

<input type="radio" id="fragil" name="tipo" value="fragil" />
<label for="fragil">Frágil (+30%)</label>
</fieldset>

<button type="submit">Calcular</button>
</form>

<p id="resultado" role="status"></p>

<section>
<h2>Últimos cálculos</h2>
<ul id="historico"></ul>
<button type="button" id="limpar">Limpar histórico</button>
</section>

<section>
<h2>Tabela de preços</h2>
<p id="tabela-carregando" hidden>Carregando…</p>
<p id="tabela-erro" hidden role="alert"></p>
<table id="tabela"></table>
</section>
</main>

Parte 2: o cálculo, em funções puras

const PRECO_POR_KG_EM_CENTAVOS = {
tabatinga: 350,
atalaia: 250,
manaus: 1200,
};

const TAXA_FIXA_EM_CENTAVOS = 500;
const ACRESCIMO_FRAGIL = 0.3;

export function calcularFrete({ pesoEmKg, destino, tipo = "comum" }) {
const precoPorKg = PRECO_POR_KG_EM_CENTAVOS[destino];

if (precoPorKg === undefined) return null;
if (!Number.isFinite(pesoEmKg) || pesoEmKg <= 0) return null;

// O frete cobra por quilo cheio: 8,3 kg paga 9.
const quilosCobrados = Math.ceil(pesoEmKg);
const base = quilosCobrados * precoPorKg + TAXA_FIXA_EM_CENTAVOS;

if (tipo === "fragil") return Math.round(base * (1 + ACRESCIMO_FRAGIL));

return base;
}

export function formatarPreco(centavos) {
return (centavos / 100).toLocaleString("pt-BR", {
style: "currency",
currency: "BRL",
});
}

Estas funções não conhecem a página. Você pode testá-las no console, chamar de outra tela, ou levá-las para o servidor um dia.

Parte 3: a tela

const formulario = document.querySelector("#frete");
const resultado = document.querySelector("#resultado");

formulario?.addEventListener("submit", (event) => {
event.preventDefault();

const dados = new FormData(formulario);
const entrada = {
pesoEmKg: Number(dados.get("peso")),
destino: String(dados.get("destino")),
tipo: String(dados.get("tipo")),
};

const freteEmCentavos = calcularFrete(entrada);

if (freteEmCentavos === null) {
resultado.textContent = "Confira o peso e o destino.";
return;
}

resultado.textContent = `Frete: ${formatarPreco(freteEmCentavos)}`;
guardarNoHistorico({ ...entrada, freteEmCentavos, quando: new Date().toISOString() });
renderizarHistorico();
});

O ouvinte faz três coisas e mais nada: lê a entrada, chama a função, mostra o resultado. Toda a regra está fora dele.

Parte 4: o histórico

const CHAVE = "frete:historico";

function lerHistorico() {
try {
const texto = localStorage.getItem(CHAVE);
if (!texto) return [];
return JSON.parse(texto);
} catch {
// Dado corrompido ou armazenamento bloqueado: comece do zero em vez de
// deixar a página inteira quebrada por causa do histórico.
return [];
}
}

function guardarNoHistorico(item) {
const historico = lerHistorico();
const atualizado = [item, ...historico].slice(0, 10);

try {
localStorage.setItem(CHAVE, JSON.stringify(atualizado));
} catch {
// Navegação anônima ou cota cheia. O cálculo continua funcionando.
}
}

function renderizarHistorico() {
const lista = document.querySelector("#historico");
if (!lista) return;

const historico = lerHistorico();

if (historico.length === 0) {
lista.replaceChildren();
return;
}

const fragmento = document.createDocumentFragment();

for (const item of historico) {
const li = document.createElement("li");
li.textContent = `${item.pesoEmKg} kg → ${item.destino}: ${formatarPreco(item.freteEmCentavos)}`;
fragmento.append(li);
}

lista.replaceChildren(fragmento);
}

document.querySelector("#limpar")?.addEventListener("click", () => {
localStorage.removeItem(CHAVE);
renderizarHistorico();
});

renderizarHistorico();

localStorage guarda texto, no navegador daquela pessoa. Objetos passam por JSON.stringify na ida e JSON.parse na volta.

Parte 5: a tabela por fetch

async function carregarTabela() {
const carregando = document.querySelector("#tabela-carregando");
const erro = document.querySelector("#tabela-erro");
const tabela = document.querySelector("#tabela");

carregando.hidden = false;
erro.hidden = true;

try {
const response = await fetch("precos.json");
if (!response.ok) throw new Error(`Resposta ${response.status}`);

const precos = await response.json();
const fragmento = document.createDocumentFragment();

const cabecalho = document.createElement("tr");
for (const texto of ["Destino", "Preço por kg"]) {
const th = document.createElement("th");
th.scope = "col";
th.textContent = texto;
cabecalho.append(th);
}
fragmento.append(cabecalho);

for (const [destino, centavos] of Object.entries(precos)) {
const linha = document.createElement("tr");

const nome = document.createElement("td");
nome.textContent = destino;

const valor = document.createElement("td");
valor.textContent = formatarPreco(centavos);

linha.append(nome, valor);
fragmento.append(linha);
}

tabela.replaceChildren(fragmento);
} catch (falha) {
console.error(falha);
erro.textContent = "Não foi possível carregar a tabela. Os preços do formulário continuam valendo.";
erro.hidden = false;
} finally {
carregando.hidden = true;
}
}

carregarTabela();

Repare na mensagem de erro: ela diz o que ainda funciona. "Erro ao carregar" deixa a pessoa sem saber se pode continuar.

A lista de conferência

Funciona

  • o cálculo está certo para os três destinos
  • frágil acrescenta 30%
  • peso zero ou negativo mostra aviso, não NaN
  • o histórico sobrevive a recarregar a página
  • limpar histórico funciona

Teclado e leitor de tela

  • dá para preencher e enviar só com o teclado
  • o foco é visível em todo campo e botão
  • o resultado tem role="status" e é anunciado
  • a mensagem de erro tem role="alert"
  • todo campo tem <label for>

Falhas

  • a tabela falha com elegância e diz o que ainda funciona
  • localStorage bloqueado não quebra a calculadora
  • nada usa innerHTML com dado que veio de fora

Sem JavaScript

  • o formulário ainda aparece e é legível
  • o HTML faz sentido sozinho

Código

  • o cálculo está em funções puras, separado da tela
  • const por padrão, let só onde precisa mudar
  • === em todas as comparações
  • dinheiro em centavos inteiros
  • nenhum console.log esquecido

Para ir além

  • Some vários itens no mesmo cálculo e mostre o total.
  • Ordene o histórico por valor.
  • Mostre o total gasto no histórico com reduce.
  • Exporte o histórico em CSV.
  • Troque precos.json pela API de verdade da escola.

Referências

Testa o que você leu

5 perguntas sobre esta aula. Errar aqui não custa nada - a explicação vem junto da correção.

Pergunta 1 de 5
Onde deve morar a regra de negócio de um projeto?