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 — 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