From f762dd8b7992727598b9b268bd54cc152acab38a Mon Sep 17 00:00:00 2001
From: Jen Breese-Kauth
Date: Wed, 20 May 2026 22:22:58 -0700
Subject: [PATCH 1/2] IFDM-173: working on inline errors for each calculator
---
.../retirement-calculator/page.tsx | 323 +++++++++++++-----
app/ui/globals.css | 5 +
2 files changed, 250 insertions(+), 78 deletions(-)
diff --git a/app/interactives/retirement-calculator/page.tsx b/app/interactives/retirement-calculator/page.tsx
index 878d8df..e5cbb28 100644
--- a/app/interactives/retirement-calculator/page.tsx
+++ b/app/interactives/retirement-calculator/page.tsx
@@ -37,6 +37,14 @@ export default function RetirementCalculator() {
const [inputs, setInputs] = useState(defaultInputs)
const [isCalculated, setIsCalculated] = useState(false)
const [frozenRequiredBalance, setFrozenRequiredBalance] = useState(0)
+ const [annualSpendingError, setAnnualSpendingError] = useState("");
+ const [retirementLengthError, setRetirementLengthError] =
+ useState("");
+ const [returnDuringError, setReturnDuringError] = useState("");
+ const [currentSavingsError, setCurrentSavingsError] = useState("");
+ const [yearsToRetirementError, setYearsToRetirementError] =
+ useState("");
+ const [returnBeforeError, setReturnBeforeError] = useState("");
const results = useMemo(() => {
const realReturnDuringRetirement = inputs.expectedReturnDuringRetirement / 100 // ← CHANGED
@@ -75,6 +83,7 @@ export default function RetirementCalculator() {
targetBalance: Math.round(targetBalance),
annualSavings: Math.round(annualSavings),
monthlySavings: Math.round(annualSavings / 12),
+ FV_currentSavings: Math.round(FV_currentSavings),
}
}, [inputs, activeTab, frozenRequiredBalance])
@@ -134,24 +143,23 @@ export default function RetirementCalculator() {
{/* Header */}
-
- Understanding retirement planning
-
+
Understanding retirement planning
{/* Tabs */}
-
{
- if (v === "savings" && !isCalculated) return
- setActiveTab(v as "balance" | "savings")
- if (v === "balance") {
- setIsCalculated(false) // This allows recalculation on the balance tab
- setFrozenRequiredBalance(0) // Clear frozen value when returning to balance tab
- }
- }} className="mb-10">
+ {
+ if (v === "savings" && !isCalculated) return;
+ setActiveTab(v as "balance" | "savings");
+ if (v === "balance") {
+ setIsCalculated(false); // This allows recalculation on the balance tab
+ setFrozenRequiredBalance(0); // Clear frozen value when returning to balance tab
+ }
+ }}
+ className="mb-10"
+ >
-
+
Required balance
- First calculate your required retirement balance before viewing the Annual savings tab.
+ First calculate your required retirement balance before
+ viewing the Annual savings tab.
)}
@@ -190,10 +199,31 @@ export default function RetirementCalculator() {
type="number"
min="1"
value={inputs.annualSpending || ""}
- onChange={(e) => updateInput("annualSpending", e.target.value)}
- className="w-full pl-4 pr-16 py-3 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
+ onChange={(e) => {
+ const val = parseFloat(e.target.value) || 0;
+ if (val <= 0) {
+ setAnnualSpendingError(
+ "Annual spending must be greater than 0.",
+ );
+ } else {
+ setAnnualSpendingError("");
+ }
+ updateInput("annualSpending", e.target.value);
+ }}
+ className={`w-full pl-8 pr-16 py-3 border-2 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${annualSpendingError ? "border-[var(--color-inline-error)]" : "border-gray-300"}`}
/>
+
+ $
+
+ {annualSpendingError && (
+
+ {annualSpendingError}
+
+ )}
How much you plan to withdraw each year.
@@ -210,12 +240,41 @@ export default function RetirementCalculator() {
updateInput("currentSavings", e.target.value)}
- className="w-full pl-4 pr-16 py-3 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
+ onChange={(e) => {
+ const val = parseFloat(e.target.value) || 0;
+ if (val < 0) {
+ setCurrentSavingsError(
+ "Current savings cannot be negative.",
+ );
+ } else {
+ setCurrentSavingsError("");
+ }
+ updateInput("currentSavings", e.target.value);
+ }}
+ className={`w-full pl-8 pr-16 py-3 border-2 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${currentSavingsError || (!currentSavingsError && inputs.currentSavings > 0 && inputs.yearsToRetirement > 0 && results.FV_currentSavings >= frozenRequiredBalance) ? "border-[var(--color-inline-error)]" : "border-gray-300"}`}
/>
+
+ $
+
+ {currentSavingsError && (
+
+ {currentSavingsError}
+
+ )}
+ {!currentSavingsError &&
+ inputs.currentSavings > 0 &&
+ inputs.yearsToRetirement > 0 &&
+ results.FV_currentSavings >= frozenRequiredBalance && (
+
+ Your current savings already meet your retirement goal.
+
+ )}
How much you have already saved for retirement.
@@ -232,10 +291,32 @@ export default function RetirementCalculator() {
min="0"
max="99"
value={inputs.yearsToRetirement || ""}
- onChange={(e) => updateInput("yearsToRetirement", e.target.value)}
- className="w-full pl-4 pr-16 py-3 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
+ onChange={(e) => {
+ const val = parseFloat(e.target.value) || 0;
+ if (val <= 0) {
+ setYearsToRetirementError(
+ "Years to retirement must be greater than 0.",
+ );
+ } else if (val > 99) {
+ setYearsToRetirementError(
+ "Years to retirement cannot exceed 99.",
+ );
+ } else {
+ setYearsToRetirementError("");
+ }
+ updateInput("yearsToRetirement", e.target.value);
+ }}
+ className={`w-full pl-4 pr-16 py-3 border-2 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${yearsToRetirementError ? "border-[var(--color-inline-error)]" : "border-gray-300"}`}
/>
+ {yearsToRetirementError && (
+
+ {yearsToRetirementError}
+
+ )}
How many years until you plan to retire.
@@ -244,29 +325,53 @@ export default function RetirementCalculator() {
)}
{/* Retirement Length Input */}
- {activeTab === "balance" &&
-
- Expected length of retirement (years)
-
-
-
updateInput("retirementLength", e.target.value)}
- className="w-full pl-4 pr-16 py-3 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
- />
+ {activeTab === "balance" && (
+
+
+ Expected length of retirement (years)
+
+
+ {
+ const val = parseFloat(e.target.value) || 0;
+ if (val <= 0) {
+ setRetirementLengthError(
+ "Retirement length must be greater than 0.",
+ );
+ } else if (val > 100) {
+ setRetirementLengthError(
+ "Retirement length cannot exceed 100 years.",
+ );
+ } else {
+ setRetirementLengthError("");
+ }
+ updateInput("retirementLength", e.target.value);
+ }}
+ className={`w-full pl-4 pr-16 py-3 border-2 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${retirementLengthError ? "border-[var(--color-inline-error)]" : "border-gray-300"}`}
+ />
-
- How many years your retirement will last.
-
-
}
+ {retirementLengthError && (
+
+ {retirementLengthError}
+
+ )}
+
+ How many years your retirement will last.
+
+
+ )}
{/* Expected Return Input - BALANCE TAB */}
{activeTab === "balance" && (
- Expected annual return during retirement (%)
+ Expected annual return during retirement
updateInput("expectedReturnDuringRetirement", e.target.value)}
- className="w-full pl-4 pr-16 py-3 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
+ onChange={(e) => {
+ const val = parseFloat(e.target.value) || 0;
+ if (val < 0) {
+ setReturnDuringError("Return rate cannot be negative.");
+ } else if (val > 100) {
+ setReturnDuringError("Return rate cannot exceed 100%.");
+ } else {
+ setReturnDuringError("");
+ }
+ updateInput(
+ "expectedReturnDuringRetirement",
+ e.target.value,
+ );
+ }}
+ className={`w-full pl-4 pr-16 py-3 border-2 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${returnDuringError ? "border-[var(--color-inline-error)]" : "border-gray-300"}`}
/>
+
+ %
+
+ {returnDuringError && (
+
+ {returnDuringError}
+
+ )}
Annual investment return rate during retirement.
)}
-{/* Expected Return Input - SAVINGS TAB */}
-{activeTab === "savings" && (
-
-
- Expected annual return before retirement (%)
-
-
- updateInput("expectedReturnBeforeRetirement", e.target.value)}
- className="w-full pl-4 pr-16 py-3 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
- />
-
-
- Annual investment return rate before retirement.
-
-
-)}
+ {/* Expected Return Input - SAVINGS TAB */}
+ {activeTab === "savings" && (
+
+
+ Expected annual return before retirement
+
+
+ {
+ const val = parseFloat(e.target.value) || 0;
+ if (val < 0) {
+ setReturnBeforeError("Return rate cannot be negative.");
+ } else if (val > 100) {
+ setReturnBeforeError("Return rate cannot exceed 100%.");
+ } else {
+ setReturnBeforeError("");
+ }
+ updateInput(
+ "expectedReturnBeforeRetirement",
+ e.target.value,
+ );
+ }}
+ className={`w-full pl-4 pr-16 py-3 border-2 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${returnBeforeError ? "border-[var(--color-inline-error)]" : "border-gray-300"}`}
+ />
+
+ %
+
+
+ {returnBeforeError && (
+
+ {returnBeforeError}
+
+ )}
+
+ Annual investment return rate before retirement.
+
+
+ )}
{/* Calculate and Reset Buttons */}
@@ -326,7 +485,6 @@ export default function RetirementCalculator() {
>
)}
-
{/* Right Column - Results */}
@@ -339,10 +497,15 @@ export default function RetirementCalculator() {
Required Retirement Balance
- {isCalculated ? formatCurrency(results.requiredBalance) : "—"}
+ {isCalculated
+ ? formatCurrency(results.requiredBalance)
+ : "—"}
- This estimates the lump sum needed at retirement to fund your annual spending for {inputs.retirementLength} years, assuming a {inputs.expectedReturnDuringRetirement}% annual return during retirement.
+ This estimates the lump sum needed at retirement to fund
+ your annual spending for {inputs.retirementLength} years,
+ assuming a {inputs.expectedReturnDuringRetirement}% annual
+ return during retirement.
>
@@ -350,25 +513,29 @@ export default function RetirementCalculator() {
<>
{/* Annual Savings Result */}
-
Required Retirement Balance
+
+ Required Retirement Balance
+
- {isCalculated ? formatCurrency(results.requiredBalance) : "—"}
+ {isCalculated
+ ? formatCurrency(results.requiredBalance)
+ : "—"}
- This estimates the lump sum needed at retirement to fund your annual spending
- for {inputs.retirementLength} years, assuming a {inputs.expectedReturnDuringRetirement}%
- annual return during retirement.
-
-
- Required Annual Savings
+ This estimates the lump sum needed at retirement to fund
+ your annual spending for {inputs.retirementLength} years,
+ assuming a {inputs.expectedReturnDuringRetirement}% annual
+ return during retirement.
+
Required Annual Savings
{formatCurrency(results.annualSavings)}
- Amount to save each year over {inputs.yearsToRetirement} years to reach your
- target balance, assuming a {inputs.expectedReturnBeforeRetirement}% annual
- return before retirement.
+ Amount to save each year over {inputs.yearsToRetirement}{" "}
+ years to reach your target balance, assuming a{" "}
+ {inputs.expectedReturnBeforeRetirement}% annual return
+ before retirement.
>
@@ -377,5 +544,5 @@ export default function RetirementCalculator() {
- )
+ );
}
diff --git a/app/ui/globals.css b/app/ui/globals.css
index f0c916f..2974625 100644
--- a/app/ui/globals.css
+++ b/app/ui/globals.css
@@ -119,6 +119,9 @@
--foreground: oklch(0.145 0 0);
--button-green: rgba(30, 113, 102, 1);
--button-berry: rgba(151, 24, 87, 1);
+ --color-teal: rgba(74, 203, 225, 1);
+ --color-symbols: #616877;
+ --color-inline-error: #8C1515;
}
.dark {
@@ -171,6 +174,8 @@
--additional-background: rgb(52, 51, 51);
--button-green: rgba(30, 113, 102, 1);
--button-berry: rgba(151, 24, 87, 1);
+ --color-symbols: oklch(0.985 0 0);
+ --color-inline-error: #F4795B;
}
@layer base {
From 70d5413ac0ebebe11c2191f5d85a9ee9591d6a08 Mon Sep 17 00:00:00 2001
From: Jen Breese-Kauth
Date: Wed, 20 May 2026 22:48:38 -0700
Subject: [PATCH 2/2] app
---
.../debt-payoff-calculator/page.tsx | 1012 +++++++++--------
1 file changed, 566 insertions(+), 446 deletions(-)
diff --git a/app/interactives/debt-payoff-calculator/page.tsx b/app/interactives/debt-payoff-calculator/page.tsx
index 2ddc66e..ce97bc8 100644
--- a/app/interactives/debt-payoff-calculator/page.tsx
+++ b/app/interactives/debt-payoff-calculator/page.tsx
@@ -34,6 +34,11 @@ export default function DebtPayoffCalculator() {
const [additionalPayment, setAdditionalPayment] = useState(0)
const [targetYears, setTargetYears] = useState(10)
const [targetMonths, setTargetMonths] = useState(11)
+ const [debtAmountError, setDebtAmountError] = useState("")
+ const [interestRateError, setInterestRateError] = useState("")
+ const [paymentError, setPaymentError] = useState("")
+ const [additionalPaymentError, setAdditionalPaymentError] = useState("")
+ const [targetTimeError, setTargetTimeError] = useState("")
const getCompoundingPeriodsPerYear = (frequency: CompoundingFrequency): number => {
switch (frequency) {
@@ -157,502 +162,617 @@ export default function DebtPayoffCalculator() {
Debt Payoff Calculator
{/* Mode Selection */}
-
-
-
- Calculate Time to Pay Off Debt
- Calculate Required Payment
-
-
- {/* Time to pay off tab. */}
-
- <>
-
-
-
-
-
- Debt amount
- This is your total balance owed or what you would like to pay off.
-
-
-
setDebtAmount(Number(e.target.value) || 0)}
- className="font-bold block w-full rounded-md shadow-sm py-2 px-3 border pr-10 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
- />
-
-
setDebtAmount((prev) => Math.max(0, prev + 1))}
- className="mb-[-5px] hover:text-grey-med-dark focus:outline-none"
- >
-
-
-
setDebtAmount((prev) => Math.max(0, prev - 1))}
- className="hover:text-grey-med-dark focus:outline-none"
- >
-
-
+
+
+
+ Calculate Time to Pay Off Debt
+
+
+ Calculate Required Payment
+
+
+
+ {/* Time to pay off tab. */}
+
+ <>
+
+
+
+
+
+
+ Debt amount
+
+
+ This is your total balance owed or what you would
+ like to pay off.
+
-
-
-
-
-
- Annual interest rate (%)
- This is the annual percentage rate (APR) charged by your lender.
-
-
-
setInterestRate(Number(e.target.value) || 0)}
- min="0.1"
- className="relative font-bold block w-full text-lagunita rounded-md shadow-sm py-2 px-3 border pr-10 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
- />
-
-
setInterestRate((prev) => Math.max(0, parseFloat((prev + 0.1).toFixed(1))))}
- className="mb-[-5px] hover:text-grey-med-dark focus:outline-none"
- >
-
-
-
setInterestRate((prev) => Math.max(0, parseFloat((prev - 0.1).toFixed(1))))}
- className="hover:text-grey-med-dark focus:outline-none"
- >
-
-
+
+
{
+ const val = Number(e.target.value) || 0;
+ if (val <= 0) {
+ setDebtAmountError(
+ "Debt amount must be greater than 0.",
+ );
+ } else {
+ setDebtAmountError("");
+ }
+ setDebtAmount(val);
+ }}
+ className={`block w-full rounded-md shadow-sm py-2 pl-8 pr-3 border [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${debtAmountError ? "border-[var(--color-inline-error)] border-2" : ""}`}
+ />
+
+ $
+
+ {debtAmountError && (
+
+ {debtAmountError}
+
+ )}
-
-
-
- Compounding frequency
- How often interest is applied and payments are made. Most loans compound monthly.
-
-
-
setCompoundingFrequency(e.target.value as CompoundingFrequency)}
- id="compounding-select"
- aria-describedby="compounding-desc"
- className="border-1 w-full rounded-md shadow-sm py-2 px-3 appearance-none"
- >
- Daily
- Weekly
- Bi-weekly
- Monthly
- Quarterly
- Semi-Annually
- Annually
-
-
-
-
-
-
- Current selection: {compoundingFrequency}
-
-
- The compounding frequency is equal to your payment frequency. For example, in the monthly case, you
- make 12 debt payments per year.
-
-
-
-
- Payment per compounding period
- This is your frequency of payment and how often the interest is applied. For example, each month or each year.
-
-
-
setPayment(Number(e.target.value) || 0)}
- className="font-bold block w-full rounded-md shadow-sm py-2 px-3 border pr-10 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
- />
-
-
setPayment((prev) => Math.max(0, prev + 1))}
- className="mb-[-5px] hover:text-grey-med-dark focus:outline-none"
- >
-
-
-
setPayment((prev) => Math.max(0, prev - 1))}
- className="hover:text-grey-med-dark focus:outline-none"
+
+
+
-
-
+ Annual interest rate
+
+
+ This is the annual percentage rate (APR) charged by
+ your lender.
+
+
+
+
{
+ const val = Number(e.target.value) || 0;
+ if (val <= 0) {
+ setInterestRateError(
+ "Interest rate must be greater than 0.",
+ );
+ } else if (val > 100) {
+ setInterestRateError(
+ "Interest rate cannot exceed 100%.",
+ );
+ } else {
+ setInterestRateError("");
+ }
+ setInterestRate(val);
+ }}
+ className={`relative block w-full rounded-md shadow-sm py-2 px-3 border pr-10 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${interestRateError ? "border-[var(--color-inline-error)] border-2" : ""}`}
+ />
+
+ %
+
+ {interestRateError && (
+
+ {interestRateError}
+
+ )}
-
-
-
+
- Additional payment per period (optional)
- Enter a fixed extra amount you plan to pay each month.
+
+ Compounding frequency
+
+
+ {" "}
+ How often interest is applied and payments are made.
+ Most loans compound monthly.
+
-
-
-
setAdditionalPayment(e.target.value === "" ? "" : Number(e.target.value))}
- onBlur={(e) => setAdditionalPayment(e.target.value === "" ? 0 : Number(e.target.value))}
- className="font-bold text-lagunita block w-full rounded-md shadow-sm py-2 px-3 border pr-10 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
- />
-
-
setAdditionalPayment((prev) => Math.max(0, (typeof prev === "number" ? prev : 0) + 1))}
- className="mb-[-5px] hover:text-grey-med-dark focus:outline-none"
- >
-
-
-
setAdditionalPayment((prev) => Math.max(0, (typeof prev === "number" ? prev : 0) - 1))}
- className="hover:text-grey-med-dark focus:outline-none"
+
+
+ setCompoundingFrequency(
+ e.target.value as CompoundingFrequency,
+ )
+ }
+ id="compounding-select"
+ aria-describedby="compounding-desc"
+ className="border-1 w-full rounded-md shadow-sm py-2 px-3 appearance-none"
>
-
-
+ Daily
+ Weekly
+ Bi-weekly
+ Monthly
+ Quarterly
+ Semi-Annually
+ Annually
+
+
+
+
+
+
+ Current selection: {compoundingFrequency}
+
+ The compounding frequency is equal to your payment
+ frequency. For example, in the monthly case, you make
+ 12 debt payments per year.
+
-
- Each steady extra payment reduces your total interest.
-
-
-
-
-
-
-
- Your payoff summary
-
-
-
-
Time to pay off
-
{formatTime(payoffResult.timeInMonths)}
-
Debt-free by {formatDate(payoffResult.payoffDate)}
-
-
-
-
-
- Total interest:
+
+
+
+ Payment per compounding period
+
+
+ This is your frequency of payment and how often the
+ interest is applied. For example, each month or each
+ year.
+
-
- {formatCurrency(payoffResult.totalInterest)}
+
+
{
+ const val = Number(e.target.value) || 0;
+ if (val <= 0) {
+ setPaymentError(
+ "Payment must be greater than 0.",
+ );
+ } else {
+ setPaymentError("");
+ }
+ setPayment(val);
+ }}
+ className={`block w-full rounded-md shadow-sm py-2 px-3 border pl-8 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${paymentError ? "border-[var(--color-inline-error)] border-2" : ""}`}
+ />
+
+ $
+
+ {paymentError && (
+
+ {paymentError}
+
+ )}
-
-
- Total amount paid:
+
+
+
+
+ Additional payment per period (optional)
+
+
+ Enter a fixed extra amount you plan to pay each
+ month.
+
+
-
- {formatCurrency(payoffResult.totalAmountPaid)}
+
+ {
+ const val =
+ e.target.value === ""
+ ? ""
+ : Number(e.target.value);
+ if (typeof val === "number" && val < 0) {
+ setAdditionalPaymentError(
+ "Additional payment cannot be negative.",
+ );
+ } else {
+ setAdditionalPaymentError("");
+ }
+ setAdditionalPayment(val);
+ }}
+ className={`block w-full rounded-md shadow-sm py-2 px-3 border pr-10 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${additionalPaymentError ? "border-[var(--color-inline-error)] border-2" : ""}`}
+ />
+
+ Each steady extra payment reduces your total interest.
+
+ {additionalPaymentError && (
+
+ {additionalPaymentError}
+
+ )}
+ {!isFinite(payoffResult.timeInMonths) &&
+ payment > 0 && (
+
+ Your payment doesn't cover the interest. Please
+ increase your payment amount.
+
+ )}
+
+
+
+
+
+
+
+ Your payoff summary
+
+
+
+
+
+ Time to pay off
+
+
+ {formatTime(payoffResult.timeInMonths)}
+
+
+ Debt-free by {formatDate(payoffResult.payoffDate)}
+
-
-
- Interest saved:
+
+
+
+ Total interest:
+
+
+ {formatCurrency(payoffResult.totalInterest)}
+
-
- {formatCurrency(payoffResult.interestSaved)}
+
+
+
+ Total amount paid:
+
+
+ {formatCurrency(payoffResult.totalAmountPaid)}
+
+
+
+
+
+ Interest saved:
+
+
+ {formatCurrency(payoffResult.interestSaved)}
+
-
-
- You're turning your loan into a plan. A little extra now means freedom sooner.
-
-
-
-
-
- >
-
-
- {/* Calculate required payment tab. */}
-
- <>
-
-
-
-
-
- Debt amount
- This is your total balance owed or what you would like to pay off.
-
-
-
setDebtAmount(Number(e.target.value) || 0)}
- min="1"
- className="font-bold block w-full rounded-md shadow-sm py-2 px-3 border pr-10 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
- />
-
-
setDebtAmount((prev) => Math.max(0, prev + 1))}
- className="mb-[-5px] hover:text-grey-med-dark focus:outline-none"
- >
-
-
-
setDebtAmount((prev) => Math.max(0, prev - 1))}
- className="hover:text-grey-med-dark focus:outline-none"
+
+ You're turning your loan into a plan. A little extra
+ now means freedom sooner.
+
+
+
+
+ >
+
+
+ {/* Calculate required payment tab. */}
+
+ <>
+
+
+
+
+
+
-
-
+ Debt amount
+
+
+ This is your total balance owed or what you would
+ like to pay off.
+
+
+
+
{
+ const val = Number(e.target.value) || 0;
+ if (val <= 0) {
+ setDebtAmountError(
+ "Debt amount must be greater than 0.",
+ );
+ } else {
+ setDebtAmountError("");
+ }
+ setDebtAmount(val);
+ }}
+ className={`block w-full rounded-md shadow-sm py-2 px-3 border pl-8 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${debtAmountError ? "border-[var(--color-inline-error)] border-2" : ""}`}
+ />
+
+ $
+
+ {debtAmountError && (
+
+ {debtAmountError}
+
+ )}
-
-
-
- Annual interest rate (%)
- This is the annual percentage rate (APR) charged by your lender.
-
-
-
setInterestRate(Number(e.target.value) || 0)}
- min="0.1"
- className="font-bold text-lagunita block w-full rounded-md shadow-sm py-2 px-3 border pr-10 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
- />
-
-
setInterestRate((prev) => Math.max(0, parseFloat((prev + 0.1).toFixed(1))))}
- className="mb-[-5px] hover:text-grey-med-dark focus:outline-none"
- >
-
-
-
setInterestRate((prev) => Math.max(0, parseFloat((prev - 0.1).toFixed(1))))}
- className="hover:text-grey-med-dark focus:outline-none"
+
+
+
-
-
+ Annual interest rate
+
+
+ This is the annual percentage rate (APR) charged by
+ your lender.
+
+
+
+
{
+ const val = Number(e.target.value) || 0;
+ if (val <= 0) {
+ setInterestRateError(
+ "Interest rate must be greater than 0.",
+ );
+ } else if (val > 100) {
+ setInterestRateError(
+ "Interest rate cannot exceed 100%.",
+ );
+ } else {
+ setInterestRateError("");
+ }
+ setInterestRate(val);
+ }}
+ className={`block w-full rounded-md shadow-sm py-2 px-3 border pr-10 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${interestRateError ? "border-[var(--color-inline-error)] border-2" : ""}`}
+ />
+
+ %
+
+ {interestRateError && (
+
+ {interestRateError}
+
+ )}
-
-
-
- Compounding frequency
- How often interest is applied and payments are made. Most loans compound monthly.
-
-
-
setCompoundingFrequency(e.target.value as CompoundingFrequency)}
- id="compounding-select-2"
- className="w-full border-1 rounded-md shadow-sm py-2 px-3 appearance-none"
- aria-describedby="compounding-desc-2"
- >
- Daily
- Weekly
- Bi-weekly
- Monthly
- Quarterly
- Semi-Annually
- Annually
-
-
-
+
+
+
+ Compounding frequency
+
+
+ How often interest is applied and payments are made.
+ Most loans compound monthly.
+
-
-
+
+
+ setCompoundingFrequency(
+ e.target.value as CompoundingFrequency,
+ )
+ }
+ id="compounding-select-2"
+ className="w-full border-1 rounded-md shadow-sm py-2 px-3 appearance-none"
+ aria-describedby="compounding-desc-2"
+ >
+ Daily
+ Weekly
+ Bi-weekly
+ Monthly
+ Quarterly
+ Semi-Annually
+ Annually
+
+
+
+
+
+
Current selection: {compoundingFrequency}
+
+
+ The compounding frequency is equal to your payment
+ frequency. For example, in the monthly case, you make
+ 12 debt payments per year.
+
-
- The compounding frequency is equal to your payment frequency. For example, in the monthly case, you
- make 12 debt payments per year.
-
-
-
-
- Target time to payoff
- How long do you want to take to pay off this debt?
-
-
-
-
-
setTargetYears(Number(e.target.value) || 0)}
- className="font-bold block w-full rounded-md shadow-sm py-2 px-3 border pr-10 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
- />
-
- setTargetYears((prev) => Math.max(0, prev + 1))}
- className="mb-[-5px] hover:text-grey-med-dark focus:outline-none"
- >
-
-
- setTargetYears((prev) => Math.max(0, prev - 1))}
- className="hover:text-grey-med-dark focus:outline-none"
- >
-
-
-
-
-
- Years
+
+
+
+ Target time to payoff
+
+ How long do you want to take to pay off this debt?
+
-
-
-
setTargetMonths(Math.min(11, Math.max(0, Number(e.target.value)) || 0))}
- className="font-bold block w-full rounded-md shadow-sm py-2 px-3 border pr-10 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
- />
-
-
setTargetMonths((prev) => Math.min(11, prev + 1))}
- className="mb-[-5px] hover:text-grey-med-dark focus:outline-none"
- >
-
-
-
setTargetMonths((prev) => Math.max(0, prev - 1))}
- className="hover:text-grey-med-dark focus:outline-none"
- >
-
-
+
+
+
+ {
+ const val = Number(e.target.value) || 0;
+ setTargetYears(val);
+ if (val === 0 && targetMonths === 0) {
+ setTargetTimeError(
+ "Target payoff time must be greater than 0.",
+ );
+ } else {
+ setTargetTimeError("");
+ }
+ }}
+ className={`block w-full rounded-md shadow-sm py-2 px-3 border pr-10 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${targetTimeError ? "border-[var(--color-inline-error)] border-2" : ""}`}
+ />
+
+ Years
+
-
- Months
-
+
+
+ {
+ const val = Math.min(
+ 11,
+ Math.max(0, Number(e.target.value)) || 0,
+ );
+ setTargetMonths(val);
+ if (targetYears === 0 && val === 0) {
+ setTargetTimeError(
+ "Target payoff time must be greater than 0.",
+ );
+ } else {
+ setTargetTimeError("");
+ }
+ }}
+ className={`block w-full rounded-md shadow-sm py-2 px-3 border pr-10 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${targetTimeError ? "border-[var(--color-inline-error)] border-2" : ""}`}
+ />
+
+
+ Months
+
+
+ {targetTimeError && (
+
+ {targetTimeError}
+
+ )}
+
+
+ Total: {targetYears} year
+ {targetYears !== 1 ? "s" : ""} {targetMonths} month
+ {targetMonths !== 1 ? "s" : ""}
-
- Total: {targetYears} year{targetYears !== 1 ? "s" : ""} {targetMonths} month{targetMonths !== 1 ? "s" : ""}
+
+
+
+
+
+
+ Required payment
+
+
+
+
+
+ Payment per period
+
+
+ {formatCurrency(
+ requiredPaymentResult.requiredPayment,
+ )}
+
+
+ To pay off in{" "}
+ {formatTime(targetYears * 12 + targetMonths)}
+
-
-
-
-
-
-
- Required payment
-
-
-
-
Payment per period
-
- {formatCurrency(requiredPaymentResult.requiredPayment)}
-
-
To pay off in {formatTime(targetYears * 12 + targetMonths)}
-
-
-
-
- Total interest:
-
-
- {formatCurrency(requiredPaymentResult.totalInterest)}
+
+
+
+ Total interest:
+
+
+ {formatCurrency(
+ requiredPaymentResult.totalInterest,
+ )}
+
-
-
-
- Total amount paid:
-
-
- {formatCurrency(requiredPaymentResult.totalAmountPaid)}
+
+
+ Total amount paid:
+
+
+ {formatCurrency(
+ requiredPaymentResult.totalAmountPaid,
+ )}
+
-
You're turning your loan into a plan.
-
-
-
-
- >
-
-
+
+
+
+ >
+
+
+
-
- )
+ );
}
\ No newline at end of file