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. */} - - <> -
- - -
-
- - 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" - /> -
- - + + + + Calculate Time to Pay Off Debt + + + Calculate Required Payment + + + + {/* Time to pay off tab. */} + + <> +
+ + +
+
+ + + This is your total balance owed or what you would + like to pay off. +
-
-
- -
-
- - 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" - /> -
- - +
+ { + 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} +

+ )}
-
-
-
- - How often interest is applied and payments are made. Most loans compound monthly. -
-
- -
- -
-
-
- 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. -

-
-
-
- - 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" - /> -
- - + 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} +

+ )}
-
-
-
+
- - Enter a fixed extra amount you plan to pay each month. + + + {" "} + 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" - /> -
- -
-

- 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: +
+
+ + + 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: +
+
+
+ + + 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. */} - - <> -
- - -
-
- - 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" - /> -
- -
+ + + + {/* Calculate required payment tab. */} + + <> +
+ + +
+
+ + + 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} +

+ )}
-
-
-
- - 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" - /> -
- - + 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} +

+ )}
-
-
-
- - How often interest is applied and payments are made. Most loans compound monthly. -
-
- -
- +
+
+ + + How often interest is applied and payments are made. + Most loans compound monthly. +
-
-
+
+ +
+ +
+
+
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. -

-
-
-
- - 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" - /> -
- - -
-
-
+ {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" &&
- -
- 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" && ( +
+ +
+ { + 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" && (
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" && ( -
- -
- 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" && ( +
+ +
+ { + 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 {