diff --git a/app/interactives/compounding-frequency-calculator/page.tsx b/app/interactives/compounding-frequency-calculator/page.tsx index a603dda..fe836b4 100644 --- a/app/interactives/compounding-frequency-calculator/page.tsx +++ b/app/interactives/compounding-frequency-calculator/page.tsx @@ -42,17 +42,20 @@ function calculateCompoundInterest( return { finalAmount, interestEarned, totalPeriods } } +const MAX_INITIAL_AMOUNT = 50_000_000_000_000 // 50 trillion +const MAX_ANNUAL_RATE = 1000 // 1,000% + export default function CompoundInterestCalculator() { const [initialAmount, setInitialAmount] = useState("") const [annualRate, setAnnualRate] = useState("") const [periods, setPeriods] = useState("") const [selectedCompounding, setSelectedCompounding] = useState("monthly") - const principal = parseFloat(initialAmount.replace('$', '')) || 0 - const rate = (parseFloat(annualRate.replace('%', '')) || 0) / 100 + const principal = parseFloat(initialAmount) || 0 + const rate = (parseFloat(annualRate) || 0) / 100 const totalPeriods = parseFloat(periods) || 0 - const selectedOption = useMemo(() => + const selectedOption = useMemo(() => compoundingOptions.find((o) => o.value === selectedCompounding)!, [selectedCompounding] ) @@ -71,80 +74,166 @@ export default function CompoundInterestCalculator() { } }) }, [principal, rate, totalPeriods, selectedOption.periodsPerYear]) - + + const [initialAmountError, setInitialAmountError] = useState("") + const [annualRateError, setAnnualRateError] = useState("") + type CompoundingPeriod = 'daily' | 'weekly' | 'biweekly' | 'monthly' | 'quarterly' | 'semi-annually' | 'annually'; -const getPeriodText = (compounding: CompoundingPeriod, periods: number): string => { - const periodMap: Record = { - daily: ['day', 'days'], - weekly: ['week', 'weeks'], - biweekly: ['bi-weekly period', 'bi-weekly periods'], - monthly: ['month', 'months'], - quarterly: ['quarter', 'quarters'], - 'semi-annually': ['semi-annual period', 'semi-annual periods'], - annually: ['year', 'years'] - }; + const getPeriodText = (compounding: CompoundingPeriod, periods: number): string => { + const periodMap: Record = { + daily: ['day', 'days'], + weekly: ['week', 'weeks'], + biweekly: ['bi-weekly period', 'bi-weekly periods'], + monthly: ['month', 'months'], + quarterly: ['quarter', 'quarters'], + 'semi-annually': ['semi-annual period', 'semi-annual periods'], + annually: ['year', 'years'] + }; - const [singular, plural] = periodMap[compounding]; - return periods === 1 ? singular : plural; -}; + const [singular, plural] = periodMap[compounding]; + return periods === 1 ? singular : plural; + }; return ( -
+
{/* Header */} -

Compounding Frequency Calculator

+

Compounding Frequency Calculator

- {/* Input Fields */} -
+
- +
{ const input = e.target.value; - const numericPart = input.replace(/^\$/, '').replace(/[^0-9.]/g, ''); - setInitialAmount('$' + numericPart); + const numericPart = input.replace(/[^0-9.]/g, ""); + const numericValue = parseFloat(numericPart); + if ( + !isNaN(numericValue) && + numericValue > MAX_INITIAL_AMOUNT + ) { + setInitialAmountError( + "Initial amount cannot exceed $50,000,000,000,000.", + ); + return; + } + setInitialAmountError(""); + setInitialAmount(numericPart); }} - 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" min="0" + className={`block w-full pl-8 rounded-md shadow-sm border [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${initialAmountError ? "border-[var(--color-inline-error)] border-2" : ""}`} /> + + $ +
+ {initialAmountError && ( +

+ {initialAmountError} +

+ )}
- +
{ const input = e.target.value; - const numericPart = input.replace(/^%/, '').replace(/[^0-9.]/g, ''); - setAnnualRate(numericPart + '%'); + const numericPart = input.replace(/[^0-9.]/g, ""); + const numericValue = parseFloat(numericPart); + if ( + !isNaN(numericValue) && + numericValue > MAX_ANNUAL_RATE + ) { + setAnnualRateError( + "Annual interest rate cannot exceed 1,000%.", + ); + return; + } + setAnnualRateError(""); + setAnnualRate(numericPart); }} - 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" + 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 ${annualRateError ? "border-[var(--color-inline-error)] border-2" : ""}`} min="0" step="0.1" /> +
+ {annualRateError && ( +

+ {annualRateError} +

+ )}
- - Periods are counted based on the selected compounding frequency. For monthly compounding, 60 periods equals 60 months. + + + Periods are counted based on the selected compounding + frequency. For monthly compounding, 60 periods equals 60 + months. +
setPeriods(e.target.value)} - 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" + onChange={(e) => { + const val = e.target.value; + if (val === "" || Number(val) >= 0) setPeriods(val); + }} + onKeyDown={(e) => { + if (e.key === "-" || e.key === "e") e.preventDefault(); + }} + onBlur={() => { + if (periods.startsWith(".")) { + setPeriods("0" + periods); + } + }} + aria-describedby="periods-info" + 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" min="0" />
@@ -152,13 +241,26 @@ const getPeriodText = (compounding: CompoundingPeriod, periods: number): string
- - Compounding frequency is how often interest is calculated and added to the balance. For example, monthly compounding applies interest once each month. + + + Compounding frequency is how often interest is calculated and + added to the balance. For example, monthly compounding applies + interest once each month. +
-
+
@@ -177,59 +282,113 @@ const getPeriodText = (compounding: CompoundingPeriod, periods: number): string {/* Results Section */} -

- Balance after {periods} {getPeriodText(selectedCompounding, Number(periods))} -

+

+ Balance after {periods}{" "} + {getPeriodText(selectedCompounding, Number(periods))} +

- {formatCurrency(selectedResult.finalAmount)}

+ {formatCurrency(selectedResult.finalAmount)} +

- Interest accrued over {periods} {getPeriodText(selectedCompounding, Number(periods))} + Interest accrued over {periods}{" "} + {getPeriodText(selectedCompounding, Number(periods))}

{formatCurrency(selectedResult.interestEarned)}

- With {selectedCompounding === 'semi-annually' ? 'semi-annual' : selectedCompounding === 'annually' ? 'annual' : selectedCompounding} compounding + With{" "} + + {selectedCompounding === "semi-annually" + ? "semi-annual" + : selectedCompounding === "annually" + ? "annual" + : selectedCompounding} + {" "} + compounding

- {/* Comparison Table */} -
-

Comparison Across Compounding Frequencies

+ + {/* Comparison Table */} +
+

+ Comparison Across Compounding Frequencies +

- See how compounding frequency affects returns over the same time period. + See how compounding frequency affects returns over the same time + period.

- - - - + + + + {comparisonResults.map((result) => ( - - - + + + ))}
Compounding FrequencyNumber of
Compounding Periods
Final AmountInterest Accrued + Compounding Frequency + + Number of
+ Compounding Periods +
+ Final Amount + + Interest Accrued +
{result.label}{Number(result.totalPeriods.toFixed(2))}{formatCurrency(result.finalAmount)}{formatCurrency(result.interestEarned)} + {result.totalPeriods % 1 === 0 + ? result.totalPeriods.toFixed(0) + : result.totalPeriods.toFixed(2)} + + {formatCurrency(result.finalAmount)} + + {formatCurrency(result.interestEarned)} +
-

Over the same time period, more frequent compounding generally results in more interest accrued, assuming the annual interest rate stays the same.

+

+ Over the same time period, more frequent compounding results in + more interest accrued, assuming the annual interest rate stays the + same. +

-
- ) + ); } \ No newline at end of file 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 {