From f45d398d8a2313281e7ad4b392c13558b4f0c229 Mon Sep 17 00:00:00 2001 From: Jen Breese-Kauth Date: Mon, 18 May 2026 15:09:38 -0700 Subject: [PATCH 1/6] IDFM-183: Caps and design edits --- .../compounding-frequency-calculator/page.tsx | 111 +++++++++++------- 1 file changed, 70 insertions(+), 41 deletions(-) diff --git a/app/interactives/compounding-frequency-calculator/page.tsx b/app/interactives/compounding-frequency-calculator/page.tsx index a603dda..57d8a89 100644 --- a/app/interactives/compounding-frequency-calculator/page.tsx +++ b/app/interactives/compounding-frequency-calculator/page.tsx @@ -42,6 +42,9 @@ 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("") @@ -49,10 +52,10 @@ export default function CompoundInterestCalculator() { const [selectedCompounding, setSelectedCompounding] = useState("monthly") const principal = parseFloat(initialAmount.replace('$', '')) || 0 - const rate = (parseFloat(annualRate.replace('%', '')) || 0) / 100 + 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,97 @@ export default function CompoundInterestCalculator() { } }) }, [principal, rate, totalPeriods, selectedOption.periodsPerYear]) - + 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, ''); + const numericValue = parseFloat(numericPart); + if (!isNaN(numericValue) && numericValue > MAX_INITIAL_AMOUNT) return; 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" + 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" />
- +
{ 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) return; + 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" min="0" step="0.1" /> +
- - 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" + 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 +172,19 @@ 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,11 +203,12 @@ const getPeriodText = (compounding: CompoundingPeriod, periods: number): string {/* Results Section */} -

+

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

+

- {formatCurrency(selectedResult.finalAmount)}

+ {formatCurrency(selectedResult.finalAmount)} +

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

@@ -194,8 +221,9 @@ const getPeriodText = (compounding: CompoundingPeriod, periods: number): string
- {/* Comparison Table */} -
+ + {/* Comparison Table */} +

Comparison Across Compounding Frequencies

See how compounding frequency affects returns over the same time period. @@ -205,16 +233,17 @@ const getPeriodText = (compounding: CompoundingPeriod, periods: number): string - - - - + + + + {comparisonResults.map((result) => ( @@ -228,7 +257,7 @@ const getPeriodText = (compounding: CompoundingPeriod, periods: number): string

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

- + ) From 86252b7b2ec00ecf8c217c2e6ab0ce1cedd30090 Mon Sep 17 00:00:00 2001 From: Jen Breese-Kauth Date: Mon, 18 May 2026 20:28:28 -0700 Subject: [PATCH 2/6] New feedback from Excel sheet --- .../compounding-frequency-calculator/page.tsx | 167 ++++++++++++++---- app/ui/globals.css | 1 + 2 files changed, 133 insertions(+), 35 deletions(-) diff --git a/app/interactives/compounding-frequency-calculator/page.tsx b/app/interactives/compounding-frequency-calculator/page.tsx index 57d8a89..aed37e6 100644 --- a/app/interactives/compounding-frequency-calculator/page.tsx +++ b/app/interactives/compounding-frequency-calculator/page.tsx @@ -99,11 +99,16 @@ export default function CompoundInterestCalculator() {

Compounding Frequency Calculator

- {/* Input Fields */} -
+
-
Compounding FrequencyNumber of
Compounding Periods
Final AmountInterest AccruedCompounding FrequencyNumber of
Compounding Periods
Final AmountInterest Accrued
{result.label}
- - - - + + + + {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..b76729b 100644 --- a/app/ui/globals.css +++ b/app/ui/globals.css @@ -119,6 +119,7 @@ --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); } .dark { From ca3b1262aacb366869c2c5a7ba234193b7ace5b9 Mon Sep 17 00:00:00 2001 From: Jen Breese-Kauth Date: Tue, 19 May 2026 16:22:36 -0700 Subject: [PATCH 3/6] fixup --- .../compounding-frequency-calculator/page.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/interactives/compounding-frequency-calculator/page.tsx b/app/interactives/compounding-frequency-calculator/page.tsx index aed37e6..6d58f8b 100644 --- a/app/interactives/compounding-frequency-calculator/page.tsx +++ b/app/interactives/compounding-frequency-calculator/page.tsx @@ -51,7 +51,7 @@ export default function CompoundInterestCalculator() { const [periods, setPeriods] = useState("") const [selectedCompounding, setSelectedCompounding] = useState("monthly") - const principal = parseFloat(initialAmount.replace('$', '')) || 0 + const principal = parseFloat(initialAmount) || 0 const rate = (parseFloat(annualRate) || 0) / 100 const totalPeriods = parseFloat(periods) || 0 @@ -118,20 +118,21 @@ export default function CompoundInterestCalculator() { value={initialAmount} onChange={(e) => { const input = e.target.value; - const numericPart = input - .replace(/^\$/, "") - .replace(/[^0-9.]/g, ""); + const numericPart = input.replace(/[^0-9.]/g, ""); const numericValue = parseFloat(numericPart); if ( !isNaN(numericValue) && numericValue > MAX_INITIAL_AMOUNT ) return; - setInitialAmount("$" + numericPart); + setInitialAmount(numericPart); }} - 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" + className="w-full pl-4 pl-8 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" /> + + $ +
From c6c9c7bde0b1a35dbb478a428e4e81b8c24c56c7 Mon Sep 17 00:00:00 2001 From: Jen Breese-Kauth Date: Wed, 20 May 2026 14:40:01 -0700 Subject: [PATCH 4/6] fixup --- app/interactives/compounding-frequency-calculator/page.tsx | 6 +++--- app/ui/globals.css | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/interactives/compounding-frequency-calculator/page.tsx b/app/interactives/compounding-frequency-calculator/page.tsx index 6d58f8b..720deb6 100644 --- a/app/interactives/compounding-frequency-calculator/page.tsx +++ b/app/interactives/compounding-frequency-calculator/page.tsx @@ -128,9 +128,9 @@ export default function CompoundInterestCalculator() { setInitialAmount(numericPart); }} min="0" - className="w-full pl-4 pl-8 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" + 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" /> - + $ @@ -162,7 +162,7 @@ export default function CompoundInterestCalculator() { /> diff --git a/app/ui/globals.css b/app/ui/globals.css index b76729b..3b7890a 100644 --- a/app/ui/globals.css +++ b/app/ui/globals.css @@ -120,6 +120,7 @@ --button-green: rgba(30, 113, 102, 1); --button-berry: rgba(151, 24, 87, 1); --color-teal: rgba(74, 203, 225, 1); + --color-symbols: #616877; } .dark { @@ -172,6 +173,7 @@ --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); } @layer base { From 7bb3661e1106e3cdcea2cbf3ba49bdd73cbd2a11 Mon Sep 17 00:00:00 2001 From: Jen Breese-Kauth Date: Wed, 20 May 2026 15:12:24 -0700 Subject: [PATCH 5/6] added errors --- .../compounding-frequency-calculator/page.tsx | 42 ++++++++++++++++--- app/ui/globals.css | 1 + 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/app/interactives/compounding-frequency-calculator/page.tsx b/app/interactives/compounding-frequency-calculator/page.tsx index 720deb6..352b3de 100644 --- a/app/interactives/compounding-frequency-calculator/page.tsx +++ b/app/interactives/compounding-frequency-calculator/page.tsx @@ -75,6 +75,9 @@ 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 => { @@ -123,17 +126,30 @@ export default function CompoundInterestCalculator() { if ( !isNaN(numericValue) && numericValue > MAX_INITIAL_AMOUNT - ) + ) { + setInitialAmountError( + "Initial amount cannot exceed $50,000,000,000,000.", + ); return; + } + setInitialAmountError(""); setInitialAmount(numericPart); }} min="0" - 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" + 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-error border-2" : ""}`} /> $ + {initialAmountError && ( +

+ {initialAmountError} +

+ )}
@@ -152,11 +168,19 @@ export default function CompoundInterestCalculator() { const input = e.target.value; const numericPart = input.replace(/[^0-9.]/g, ""); const numericValue = parseFloat(numericPart); - if (!isNaN(numericValue) && numericValue > MAX_ANNUAL_RATE) + if ( + !isNaN(numericValue) && + numericValue > MAX_ANNUAL_RATE + ) { + setAnnualRateError( + "Annual interest rate cannot exceed 1,000%.", + ); return; + } + setAnnualRateError(""); setAnnualRate(numericPart); }} - 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" + 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-error border-2" : ""}`} min="0" step="0.1" /> @@ -167,6 +191,14 @@ export default function CompoundInterestCalculator() { %
+ {annualRateError && ( +

+ {annualRateError} +

+ )}
@@ -195,7 +227,7 @@ export default function CompoundInterestCalculator() { onKeyDown={(e) => { if (e.key === "-" || e.key === "e") e.preventDefault(); }} - onBlur={(e) => { + onBlur={() => { if (periods.startsWith(".")) { setPeriods("0" + periods); } diff --git a/app/ui/globals.css b/app/ui/globals.css index 3b7890a..1c66738 100644 --- a/app/ui/globals.css +++ b/app/ui/globals.css @@ -45,6 +45,7 @@ --color-sky-dark: rgba(14, 59, 79, 1); --color-berry: rgba(195, 31, 112, 1); --color-berry-light: rgba(255, 237, 246, 1); + --color-error: #8C1515; --color-palo-verde: rgba(39, 153, 137, 1); --color-palo-verde-light: rgba(239, 247, 239, 1); --color-lagunita: rgba(0, 124, 146, 1); From 895a7926a785e496e13e4bf9122a8fae7a49261d Mon Sep 17 00:00:00 2001 From: Jen Breese-Kauth Date: Wed, 20 May 2026 15:36:16 -0700 Subject: [PATCH 6/6] fixup --- .../compounding-frequency-calculator/page.tsx | 8 ++++---- app/ui/globals.css | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/interactives/compounding-frequency-calculator/page.tsx b/app/interactives/compounding-frequency-calculator/page.tsx index 352b3de..fe836b4 100644 --- a/app/interactives/compounding-frequency-calculator/page.tsx +++ b/app/interactives/compounding-frequency-calculator/page.tsx @@ -136,7 +136,7 @@ export default function CompoundInterestCalculator() { setInitialAmount(numericPart); }} 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-error border-2" : ""}`} + 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" : ""}`} /> $ @@ -145,7 +145,7 @@ export default function CompoundInterestCalculator() { {initialAmountError && (

{initialAmountError}

@@ -180,7 +180,7 @@ export default function CompoundInterestCalculator() { setAnnualRateError(""); setAnnualRate(numericPart); }} - 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-error border-2" : ""}`} + 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" /> @@ -194,7 +194,7 @@ export default function CompoundInterestCalculator() { {annualRateError && (

{annualRateError}

diff --git a/app/ui/globals.css b/app/ui/globals.css index 1c66738..2974625 100644 --- a/app/ui/globals.css +++ b/app/ui/globals.css @@ -45,7 +45,6 @@ --color-sky-dark: rgba(14, 59, 79, 1); --color-berry: rgba(195, 31, 112, 1); --color-berry-light: rgba(255, 237, 246, 1); - --color-error: #8C1515; --color-palo-verde: rgba(39, 153, 137, 1); --color-palo-verde-light: rgba(239, 247, 239, 1); --color-lagunita: rgba(0, 124, 146, 1); @@ -122,6 +121,7 @@ --button-berry: rgba(151, 24, 87, 1); --color-teal: rgba(74, 203, 225, 1); --color-symbols: #616877; + --color-inline-error: #8C1515; } .dark { @@ -175,6 +175,7 @@ --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 {