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 is
7// 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 months
13 * 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: 2
25 });
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 term
47 ]);
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 - p
58 };
59 }, [
60 valid,
61 amount,
62 rate,
63 term
64 ]);
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.amount
106 })
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.rate
125 })
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.term
144 })
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.