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.
Demonstração ao vivo
Parcela mensal
R$ 528,11
Total pago
R$ 12.674,69
Juros totais
R$ 2.674,69
Código-fonte do componente
É exatamente o componente renderizado acima. Copie ou baixe o .zip.
Calculator.tsx
1// Calculator.tsx — installment / financing calculator (Chapter 19)2// Self-contained React + TypeScript component. Only dependency is React.3// Styling uses neutral Tailwind utility classes (works in light and dark).4//5// This is the SAME component rendered live at eduardomendes.com.br/repo/calculadora.6// The optional lead-capture form NEVER gates the calculation — the result is7// always visible first (Chapter 19's rule: value before the ask).8import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";9import { useMemo, useState } from "react";10/**11 * Standard PRICE-table monthly installment.12 * P = principal, i = monthly rate (decimal), n = term in months13 * installment = P * i / (1 - (1 + i)^-n) (for i > 0)14 * installment = P / n (for i == 0)15 */ export function monthlyInstallment(principal, monthlyRatePct, months) {16 if (months <= 0) return 0;17 const i = monthlyRatePct / 100;18 if (i === 0) return principal / months;19 return principal * i / (1 - Math.pow(1 + i, -months));20}21const brl = (n)=>n.toLocaleString("pt-BR", {22 style: "currency",23 currency: "BRL",24 maximumFractionDigits: 225 });26// TODO: point this at your own endpoint to receive optional leads.27const LEAD_ENDPOINT = "/api/leads";28export default function Calculator() {29 const [amount, setAmount] = useState("10000");30 const [rate, setRate] = useState("1.99");31 const [term, setTerm] = useState("24");32 const [email, setEmail] = useState("");33 const [sent, setSent] = useState(false);34 const errors = useMemo(()=>{35 const e = {};36 const a = Number(amount);37 const r = Number(rate);38 const t = Number(term);39 if (!amount || isNaN(a) || a <= 0) e.amount = "Informe um valor maior que zero.";40 if (rate === "" || isNaN(r) || r < 0) e.rate = "Informe uma taxa válida (>= 0).";41 if (!term || isNaN(t) || t <= 0 || !Number.isInteger(t)) e.term = "Informe o número de meses (inteiro > 0).";42 return e;43 }, [44 amount,45 rate,46 term47 ]);48 const valid = Object.keys(errors).length === 0;49 const result = useMemo(()=>{50 if (!valid) return null;51 const p = Number(amount);52 const inst = monthlyInstallment(p, Number(rate), Number(term));53 const total = inst * Number(term);54 return {55 inst,56 total,57 interest: total - p58 };59 }, [60 valid,61 amount,62 rate,63 term64 ]);65 async function submitLead(e) {66 e.preventDefault();67 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return;68 try {69 await fetch(LEAD_ENDPOINT, {70 method: "POST",71 headers: {72 "Content-Type": "application/json"73 },74 body: JSON.stringify({75 email,76 source: "calculadora"77 })78 });79 } catch {80 /* optional — ignore network errors */ }81 setSent(true);82 }83 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";84 return /*#__PURE__*/ _jsxs("div", {85 className: "rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-6 shadow-sm",86 children: [87 /*#__PURE__*/ _jsxs("div", {88 className: "grid gap-4 sm:grid-cols-3",89 children: [90 /*#__PURE__*/ _jsxs("label", {91 className: "block",92 children: [93 /*#__PURE__*/ _jsx("span", {94 className: "text-xs font-semibold text-slate-600 dark:text-slate-300",95 children: "Valor financiado (R$)"96 }),97 /*#__PURE__*/ _jsx("input", {98 inputMode: "decimal",99 value: amount,100 onChange: (e)=>setAmount(e.target.value),101 className: `mt-1 ${field}`102 }),103 errors.amount && /*#__PURE__*/ _jsx("span", {104 className: "mt-1 block text-xs text-red-600",105 children: errors.amount106 })107 ]108 }),109 /*#__PURE__*/ _jsxs("label", {110 className: "block",111 children: [112 /*#__PURE__*/ _jsx("span", {113 className: "text-xs font-semibold text-slate-600 dark:text-slate-300",114 children: "Juros ao m\xeas (%)"115 }),116 /*#__PURE__*/ _jsx("input", {117 inputMode: "decimal",118 value: rate,119 onChange: (e)=>setRate(e.target.value),120 className: `mt-1 ${field}`121 }),122 errors.rate && /*#__PURE__*/ _jsx("span", {123 className: "mt-1 block text-xs text-red-600",124 children: errors.rate125 })126 ]127 }),128 /*#__PURE__*/ _jsxs("label", {129 className: "block",130 children: [131 /*#__PURE__*/ _jsx("span", {132 className: "text-xs font-semibold text-slate-600 dark:text-slate-300",133 children: "Prazo (meses)"134 }),135 /*#__PURE__*/ _jsx("input", {136 inputMode: "numeric",137 value: term,138 onChange: (e)=>setTerm(e.target.value),139 className: `mt-1 ${field}`140 }),141 errors.term && /*#__PURE__*/ _jsx("span", {142 className: "mt-1 block text-xs text-red-600",143 children: errors.term144 })145 ]146 })147 ]148 }),149 result && /*#__PURE__*/ _jsxs("div", {150 className: "mt-6 grid gap-3 sm:grid-cols-3",151 children: [152 /*#__PURE__*/ _jsxs("div", {153 className: "rounded-xl bg-slate-900 dark:bg-slate-800 p-4 text-center text-white",154 children: [155 /*#__PURE__*/ _jsx("div", {156 className: "text-[11px] uppercase tracking-wider text-slate-300",157 children: "Parcela mensal"158 }),159 /*#__PURE__*/ _jsx("div", {160 className: "mt-1 text-2xl font-extrabold",161 children: brl(result.inst)162 })163 ]164 }),165 /*#__PURE__*/ _jsxs("div", {166 className: "rounded-xl border border-slate-200 dark:border-slate-700 p-4 text-center",167 children: [168 /*#__PURE__*/ _jsx("div", {169 className: "text-[11px] uppercase tracking-wider text-slate-500",170 children: "Total pago"171 }),172 /*#__PURE__*/ _jsx("div", {173 className: "mt-1 text-2xl font-extrabold text-slate-900 dark:text-slate-100",174 children: brl(result.total)175 })176 ]177 }),178 /*#__PURE__*/ _jsxs("div", {179 className: "rounded-xl border border-slate-200 dark:border-slate-700 p-4 text-center",180 children: [181 /*#__PURE__*/ _jsx("div", {182 className: "text-[11px] uppercase tracking-wider text-slate-500",183 children: "Juros totais"184 }),185 /*#__PURE__*/ _jsx("div", {186 className: "mt-1 text-2xl font-extrabold text-slate-900 dark:text-slate-100",187 children: brl(result.interest)188 })189 ]190 })191 ]192 }),193 result && !sent && /*#__PURE__*/ _jsxs("form", {194 onSubmit: submitLead,195 className: "mt-6 flex flex-col sm:flex-row gap-2",196 children: [197 /*#__PURE__*/ _jsx("input", {198 type: "email",199 value: email,200 onChange: (e)=>setEmail(e.target.value),201 placeholder: "Receber a simula\xe7\xe3o por e-mail (opcional)",202 className: `flex-1 ${field}`203 }),204 /*#__PURE__*/ _jsx("button", {205 type: "submit",206 className: "rounded-lg bg-slate-900 dark:bg-slate-100 px-4 py-2 text-sm font-bold text-white dark:text-slate-900",207 children: "Enviar"208 })209 ]210 }),211 sent && /*#__PURE__*/ _jsx("p", {212 className: "mt-4 text-sm font-semibold text-green-700 dark:text-green-400",213 children: "Enviado! ✔"214 })215 ]216 });217}
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.