Projeto: a calculadora de frete da lancha
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:
- Formulário: peso, destino e tipo de carga
- Cálculo: em funções puras, separadas da tela
- Histórico: lista dos cálculos, guardada entre visitas
- 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
-
localStoragebloqueado não quebra a calculadora - nada usa
innerHTMLcom 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
-
constpor padrão,letsó onde precisa mudar -
===em todas as comparações - dinheiro em centavos inteiros
- nenhum
console.logesquecido
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.jsonpela API de verdade da escola.
Referências
localStorage- MDN - guardar dados no navegador- Guia de JavaScript - MDN - a referência completa, em português
- JavaScript.info - o tutorial mais completo da web, com exercícios
Testa o que você leu
5 perguntas sobre esta aula. Errar aqui não custa nada - a explicação vem junto da correção.