Pular para o conteúdo

Cap. 19 · Ferramenta

Calculadora de parcelas

A calculadora do Cap. 19, funcionando aqui mesmo. Abaixo dela, o código-fonte completo do componente — esta página é a demo e o template ao mesmo tempo.

Ferramenta ao vivo

Demonstração ao vivo

Parcela mensal
R$ 528,11
Total pago
R$ 12.674,69
Juros totais
R$ 2.674,69

O componente é renderizado como é entregue no módulo (tema claro), sobre papel. Os resultados mudam a cada campo.

Código-fonte

Código-fonte do componente

É exatamente o componente renderizado acima. Copie ou baixe o .zip.

Calculator.tsx
// Calculator.tsx — installment / financing calculator (Chapter 19)
// Self-contained React + TypeScript component. Only dependency is React.
// Styling uses neutral Tailwind utility classes (works in light and dark).
//
// This is the SAME component rendered live at eduardomendes.com.br/repo/calculadora.
// The optional lead-capture form NEVER gates the calculation — the result is
// always visible first (Chapter 19's rule: value before the ask).
import { useMemo, useState, type FormEvent } from "react";
/**
* Standard PRICE-table monthly installment.
* P = principal, i = monthly rate (decimal), n = term in months
* installment = P * i / (1 - (1 + i)^-n) (for i > 0)
* installment = P / n (for i == 0)
*/
export function monthlyInstallment(principal: number, monthlyRatePct: number, months: number): number {
if (months <= 0) return 0;
const i = monthlyRatePct / 100;
if (i === 0) return principal / months;
return (principal * i) / (1 - Math.pow(1 + i, -months));
}
const brl = (n: number) =>
n.toLocaleString("pt-BR", { style: "currency", currency: "BRL", maximumFractionDigits: 2 });
interface Errors {
amount?: string;
rate?: string;
term?: string;
}
// TODO: point this at your own endpoint to receive optional leads.
const LEAD_ENDPOINT = "/api/leads";
export default function Calculator() {
const [amount, setAmount] = useState("10000");
const [rate, setRate] = useState("1.99");
const [term, setTerm] = useState("24");
const [email, setEmail] = useState("");
const [sent, setSent] = useState(false);
const errors: Errors = useMemo(() => {
const e: Errors = {};
const a = Number(amount);
const r = Number(rate);
const t = Number(term);
if (!amount || isNaN(a) || a <= 0) e.amount = "Informe um valor maior que zero.";
if (rate === "" || isNaN(r) || r < 0) e.rate = "Informe uma taxa válida (>= 0).";
if (!term || isNaN(t) || t <= 0 || !Number.isInteger(t)) e.term = "Informe o número de meses (inteiro > 0).";
return e;
}, [amount, rate, term]);
const valid = Object.keys(errors).length === 0;
const result = useMemo(() => {
if (!valid) return null;
const p = Number(amount);
const inst = monthlyInstallment(p, Number(rate), Number(term));
const total = inst * Number(term);
return { inst, total, interest: total - p };
}, [valid, amount, rate, term]);
async function submitLead(e: FormEvent) {
e.preventDefault();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return;
try {
await fetch(LEAD_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, source: "calculadora" }),
});
} catch {
/* optional — ignore network errors */
}
setSent(true);
}
const field = "w-full rounded-lg border px-3 py-2 text-sm outline-none bg-white dark:bg-slate-900 border-slate-300 dark:border-slate-700 text-slate-900 dark:text-slate-100 focus:border-slate-500";
return (
<div className="rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-6 shadow-sm">
<div className="grid gap-4 sm:grid-cols-3">
<label className="block">
<span className="text-xs font-semibold text-slate-600 dark:text-slate-300">Valor financiado (R$)</span>
<input inputMode="decimal" value={amount} onChange={(e) => setAmount(e.target.value)} className={`mt-1 ${field}`} />
{errors.amount && <span className="mt-1 block text-xs text-red-600">{errors.amount}</span>}
</label>
<label className="block">
<span className="text-xs font-semibold text-slate-600 dark:text-slate-300">Juros ao mês (%)</span>
<input inputMode="decimal" value={rate} onChange={(e) => setRate(e.target.value)} className={`mt-1 ${field}`} />
{errors.rate && <span className="mt-1 block text-xs text-red-600">{errors.rate}</span>}
</label>
<label className="block">
<span className="text-xs font-semibold text-slate-600 dark:text-slate-300">Prazo (meses)</span>
<input inputMode="numeric" value={term} onChange={(e) => setTerm(e.target.value)} className={`mt-1 ${field}`} />
{errors.term && <span className="mt-1 block text-xs text-red-600">{errors.term}</span>}
</label>
</div>
{result && (
<div className="mt-6 grid gap-3 sm:grid-cols-3">
<div className="rounded-xl bg-slate-900 dark:bg-slate-800 p-4 text-center text-white">
<div className="text-[11px] uppercase tracking-wider text-slate-300">Parcela mensal</div>
<div className="mt-1 text-2xl font-extrabold">{brl(result.inst)}</div>
</div>
<div className="rounded-xl border border-slate-200 dark:border-slate-700 p-4 text-center">
<div className="text-[11px] uppercase tracking-wider text-slate-500">Total pago</div>
<div className="mt-1 text-2xl font-extrabold text-slate-900 dark:text-slate-100">{brl(result.total)}</div>
</div>
<div className="rounded-xl border border-slate-200 dark:border-slate-700 p-4 text-center">
<div className="text-[11px] uppercase tracking-wider text-slate-500">Juros totais</div>
<div className="mt-1 text-2xl font-extrabold text-slate-900 dark:text-slate-100">{brl(result.interest)}</div>
</div>
</div>
)}
{/* Optional lead capture — appears AFTER the result, never blocks it. */}
{result && !sent && (
<form onSubmit={submitLead} className="mt-6 flex flex-col sm:flex-row gap-2">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Receber a simulação por e-mail (opcional)"
className={`flex-1 ${field}`}
/>
<button type="submit" className="rounded-lg bg-slate-900 dark:bg-slate-100 px-4 py-2 text-sm font-bold text-white dark:text-slate-900">
Enviar
</button>
</form>
)}
{sent && <p className="mt-4 text-sm font-semibold text-green-700 dark:text-green-400">Enviado! ✔</p>}
</div>
);
}

Dúvidas

Perguntas frequentes

Que fórmula ela usa?

A Tabela PRICE (parcelas fixas): parcela = P·i / (1 − (1+i)^−n), com i = juros ao mês e n = prazo. Para juros zero, divide o valor pelo número de meses.

O e-mail é obrigatório?

Não. O resultado aparece antes de qualquer captura — o campo de e-mail é opcional e nunca bloqueia o cálculo.

Posso usar no meu site?

Sim. Baixe o .zip, é um componente React autocontido (só depende de React) com utilitários Tailwind. Ajuste o LEAD_ENDPOINT se quiser receber os leads.

Implementação

Prefere que eu implemente?

Conte o seu caso numa conversa e a gente define o escopo juntos.

Agendar conversa