From f15a8af644ba247987e2030629743472ffaadb6c Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:04:22 -0500 Subject: [PATCH 01/19] feat: Update button label for inflation adjustment in AccumulationStrategy component --- .gitignore | 1 + src/components/features/AccumulationStrategy.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 435c512..ec298ae 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ deploy.sh todo.md .agent/* .github/copilot-instructions.md +improvements.md .agent/* diff --git a/src/components/features/AccumulationStrategy.tsx b/src/components/features/AccumulationStrategy.tsx index 4b07b8d..5158c5f 100644 --- a/src/components/features/AccumulationStrategy.tsx +++ b/src/components/features/AccumulationStrategy.tsx @@ -113,7 +113,7 @@ const AccumulationStrategy: React.FC = ({ profile, se
- +
From 3b95044fa3ff36edbda3c7adac479a17012e6d3c Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:06:25 -0500 Subject: [PATCH 02/19] feat: Refactor InputSection styles for improved layout and spacing --- src/components/features/InputSection.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/features/InputSection.tsx b/src/components/features/InputSection.tsx index c793174..b9eb9ef 100644 --- a/src/components/features/InputSection.tsx +++ b/src/components/features/InputSection.tsx @@ -86,8 +86,9 @@ const InputSection: React.FC = ({ profile, setProfile, onRest const inputClass = "w-full rounded-md border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2 transition-colors"; const labelClass = "flex items-center gap-1 text-sm font-medium text-slate-600 dark:text-slate-300 mb-1"; const iconClass = "absolute left-3 top-2 text-slate-400 dark:text-slate-500"; - const containerClass = "bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-800 p-6 space-y-8 transition-colors"; - const headerClass = "text-xl font-bold text-slate-800 dark:text-white flex items-center gap-2 mb-4"; + const containerClass = "bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-800 p-6 space-y-4 transition-colors"; + const sectionClass = "rounded-xl p-5 bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700"; + const headerClass = "text-xl font-bold text-slate-800 dark:text-white flex items-center gap-2 mb-5"; // const isFutureScenario = (Number(profile.age) || 0) !== profile.baseAge; const [activeModal, setActiveModal] = useState<'accumulation' | 'retirement' | null>(null); @@ -99,8 +100,8 @@ const InputSection: React.FC = ({ profile, setProfile, onRest return (
{/* Personal Details */} -
-
+
+

@@ -210,7 +211,7 @@ const InputSection: React.FC = ({ profile, setProfile, onRest

{/* Assets */} -
+

Assets (Portfolio) @@ -243,7 +244,7 @@ const InputSection: React.FC = ({ profile, setProfile, onRest

{/* Annual Contributions */} -
+

Annual Contributions @@ -273,7 +274,7 @@ const InputSection: React.FC = ({ profile, setProfile, onRest

{/* Income Sources */} -
+

Income (Annual) While in Retirement @@ -337,7 +338,7 @@ const InputSection: React.FC = ({ profile, setProfile, onRest

{/* Market Assumptions (Accumulation) */} -
+

Market Assumptions (Accumulation) @@ -372,7 +373,7 @@ const InputSection: React.FC = ({ profile, setProfile, onRest

{/* Market Assumptions (Retirement) */} -
+

Market Assumptions (Retirement) From 39ab0e7e641fceff733d96229c137e5f10c61249 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:09:57 -0500 Subject: [PATCH 03/19] feat: Adjust container padding and spacing in InputSection for improved layout --- src/components/features/InputSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/features/InputSection.tsx b/src/components/features/InputSection.tsx index b9eb9ef..8907187 100644 --- a/src/components/features/InputSection.tsx +++ b/src/components/features/InputSection.tsx @@ -86,7 +86,7 @@ const InputSection: React.FC = ({ profile, setProfile, onRest const inputClass = "w-full rounded-md border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2 transition-colors"; const labelClass = "flex items-center gap-1 text-sm font-medium text-slate-600 dark:text-slate-300 mb-1"; const iconClass = "absolute left-3 top-2 text-slate-400 dark:text-slate-500"; - const containerClass = "bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-800 p-6 space-y-4 transition-colors"; + const containerClass = "bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-800 p-3 space-y-3 transition-colors"; const sectionClass = "rounded-xl p-5 bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700"; const headerClass = "text-xl font-bold text-slate-800 dark:text-white flex items-center gap-2 mb-5"; From 5572d3dc8fea21a551af53314356e5e4bceb9458 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:10:45 -0500 Subject: [PATCH 04/19] feat: Enhance layout of Annual Contributions section with improved styling and spacing --- src/components/features/InputSection.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/features/InputSection.tsx b/src/components/features/InputSection.tsx index 8907187..94c00de 100644 --- a/src/components/features/InputSection.tsx +++ b/src/components/features/InputSection.tsx @@ -245,11 +245,13 @@ const InputSection: React.FC = ({ profile, setProfile, onRest {/* Annual Contributions */}
-

- - Annual Contributions -

-

For accumulation phase

+
+

+ + Annual Contributions +

+

For accumulation phase

+
{[ { label: 'Traditional IRA / 401k', key: 'traditionalIRA' as const }, From 298cb0804903ae3555bb97ba27f0a68e61525244 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:13:02 -0500 Subject: [PATCH 05/19] feat: Update tooltip content in InputSection for clarity on spending inputs --- src/components/features/InputSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/features/InputSection.tsx b/src/components/features/InputSection.tsx index 94c00de..0b67c0b 100644 --- a/src/components/features/InputSection.tsx +++ b/src/components/features/InputSection.tsx @@ -192,7 +192,7 @@ const InputSection: React.FC = ({ profile, setProfile, onRest className={`px-3 py-1.5 rounded-md min-h-[32px] transition-colors ${!profile.isSpendingReal ? 'bg-white dark:bg-slate-600 text-blue-600 dark:text-blue-300 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`} >Future $
- +
From fb9f835cf03cdb3e816be3cbecd951dbf7a256a5 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:15:51 -0500 Subject: [PATCH 06/19] feat: Adjust grid layout in InputSection for improved spacing and responsiveness --- src/components/features/InputSection.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/features/InputSection.tsx b/src/components/features/InputSection.tsx index 0b67c0b..4932679 100644 --- a/src/components/features/InputSection.tsx +++ b/src/components/features/InputSection.tsx @@ -216,7 +216,7 @@ const InputSection: React.FC = ({ profile, setProfile, onRest Assets (Portfolio)

-
+
{[ { label: 'Traditional IRA / 401k', key: 'traditionalIRA' as const }, { label: 'Roth IRA / 401k', key: 'rothIRA' as const }, @@ -252,7 +252,7 @@ const InputSection: React.FC = ({ profile, setProfile, onRest

For accumulation phase

-
+
{[ { label: 'Traditional IRA / 401k', key: 'traditionalIRA' as const }, { label: 'Roth IRA / Roth 401k', key: 'rothIRA' as const }, @@ -306,7 +306,7 @@ const InputSection: React.FC = ({ profile, setProfile, onRest />
-
+
$ From 54030f8e1a97d74a998e930592eeceb2675fac2e Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:20:38 -0500 Subject: [PATCH 07/19] feat: Add saved timestamp functionality to InputSection and display confirmation message --- src/App.tsx | 4 +++- src/components/features/InputSection.tsx | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index a988c40..7701a9a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -42,6 +42,7 @@ const App: React.FC = () => { const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [apiKey, setApiKey] = useState(''); const [isLoaded, setIsLoaded] = useState(false); + const [savedAt, setSavedAt] = useState(null); // Computed Retirement Profile // If the user is currently 55 but retiring at 65, we must project their assets @@ -160,7 +161,7 @@ const App: React.FC = () => { useEffect(() => { if (!isLoaded) return; const timer = setTimeout(() => { - db.profiles.put({ ...profile, id: 1 }).catch(e => console.error("Save failed:", e)); + db.profiles.put({ ...profile, id: 1 }).then(() => setSavedAt(new Date())).catch(e => console.error("Save failed:", e)); }, 1000); return () => clearTimeout(timer); }, [profile, isLoaded]); @@ -243,6 +244,7 @@ const App: React.FC = () => { profile={profile} setProfile={setProfile} onRestartWizard={() => setIsWizardOpen(true)} + savedAt={savedAt} />
diff --git a/src/components/features/InputSection.tsx b/src/components/features/InputSection.tsx index 4932679..836a68d 100644 --- a/src/components/features/InputSection.tsx +++ b/src/components/features/InputSection.tsx @@ -8,6 +8,7 @@ interface InputSectionProps { profile: UserProfile; setProfile: (profile: UserProfile) => void; onRestartWizard: () => void; + savedAt?: Date | null; } const FormattedNumberInput = ({ value, onChange, className, id }: { value: number; onChange: (val: number) => void; className?: string; id?: string }) => { @@ -73,7 +74,15 @@ const PercentageInput = ({ value, onChange, className, step = 0.1, id }: { value return ; }; -const InputSection: React.FC = ({ profile, setProfile, onRestartWizard }) => { +const InputSection: React.FC = ({ profile, setProfile, onRestartWizard, savedAt }) => { + const [showSaved, setShowSaved] = useState(false); + + useEffect(() => { + if (!savedAt) return; + setShowSaved(true); + const timer = setTimeout(() => setShowSaved(false), 3000); + return () => clearTimeout(timer); + }, [savedAt]); const handleChange = (field: keyof UserProfile, value: any) => setProfile({ ...profile, [field]: value }); const handleAssetChange = (field: keyof UserProfile['assets'], value: number) => setProfile({ ...profile, assets: { ...profile.assets, [field]: value } }); const handleContributionChange = (field: keyof UserProfile['contributions'], value: number) => setProfile({ ...profile, contributions: { ...profile.contributions, [field]: value } }); @@ -99,6 +108,11 @@ const InputSection: React.FC = ({ profile, setProfile, onRest return (
+
+ + ✓ Saved + +
{/* Personal Details */}
From 4b9cface77b9559f5e4748994727c2989ad8a6c6 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:23:04 -0500 Subject: [PATCH 08/19] feat: Add inflation callout display in InputSection based on spending real status --- src/components/features/InputSection.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/components/features/InputSection.tsx b/src/components/features/InputSection.tsx index 836a68d..50c09c2 100644 --- a/src/components/features/InputSection.tsx +++ b/src/components/features/InputSection.tsx @@ -76,6 +76,8 @@ const PercentageInput = ({ value, onChange, className, step = 0.1, id }: { value const InputSection: React.FC = ({ profile, setProfile, onRestartWizard, savedAt }) => { const [showSaved, setShowSaved] = useState(false); + const [showInflationCallout, setShowInflationCallout] = useState(false); + const prevIsSpendingReal = React.useRef(profile.isSpendingReal); useEffect(() => { if (!savedAt) return; @@ -83,6 +85,16 @@ const InputSection: React.FC = ({ profile, setProfile, onRest const timer = setTimeout(() => setShowSaved(false), 3000); return () => clearTimeout(timer); }, [savedAt]); + + useEffect(() => { + if (prevIsSpendingReal.current !== profile.isSpendingReal) { + setShowInflationCallout(true); + const timer = setTimeout(() => setShowInflationCallout(false), 3000); + prevIsSpendingReal.current = profile.isSpendingReal; + return () => clearTimeout(timer); + } + }, [profile.isSpendingReal]); + const handleChange = (field: keyof UserProfile, value: any) => setProfile({ ...profile, [field]: value }); const handleAssetChange = (field: keyof UserProfile['assets'], value: number) => setProfile({ ...profile, assets: { ...profile.assets, [field]: value } }); const handleContributionChange = (field: keyof UserProfile['contributions'], value: number) => setProfile({ ...profile, contributions: { ...profile.contributions, [field]: value } }); @@ -220,6 +232,13 @@ const InputSection: React.FC = ({ profile, setProfile, onRest className={`${inputClass} pl-8 font-semibold text-lg`} />
+ {profile.age > profile.baseAge && ( +

+ {profile.isSpendingReal + ? `→ ~$${Math.round(profile.spendingNeed * Math.pow(1 + profile.assumptions.inflationRate, profile.age - profile.baseAge)).toLocaleString()}/yr at retirement (${profile.assumptions.inflationRate * 100}% inflation over ${profile.age - profile.baseAge} yrs)` + : 'Using nominal (future) dollar amount as entered'} +

+ )}
From 113d57dc35f4c3f574ed26f14f6e7008cb7630e4 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:26:52 -0500 Subject: [PATCH 09/19] feat: Add Tax Reference modal and update active tab management in App component --- src/App.tsx | 46 +++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7701a9a..a726513 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import AccumulationStrategy from './components/features/AccumulationStrategy'; import TaxReference from './components/features/TaxReference'; import FireAnalysis from './components/features/FireAnalysis'; import { calculateStrategy, calculateLongevity } from './services/calculationEngine'; -import { TrendingUp, Calculator, AlertTriangle, BookOpen, Sun, Moon, PiggyBank, Settings, Flame, RefreshCw } from 'lucide-react'; +import { TrendingUp, Calculator, AlertTriangle, Sun, Moon, PiggyBank, Settings, Flame, RefreshCw, HelpCircle, X } from 'lucide-react'; import Footer from './components/layout/Footer'; import WizardModal from './components/features/wizard/WizardModal'; import SettingsModal from './components/features/SettingsModal'; @@ -36,7 +36,8 @@ const App: React.FC = () => { const [profile, setProfile] = useState(INITIAL_PROFILE); const [strategyResult, setStrategyResult] = useState(null); const [longevityResult, setLongevityResult] = useState(null); - const [activeTab, setActiveTab] = useState<'withdrawal' | 'accumulation' | 'longevity' | 'reference' | 'fire' | 'scenarios'>('accumulation'); + const [activeTab, setActiveTab] = useState<'withdrawal' | 'accumulation' | 'longevity' | 'fire' | 'scenarios'>('accumulation'); + const [isReferenceOpen, setIsReferenceOpen] = useState(false); const [isDarkMode, setIsDarkMode] = useState(false); const [isWizardOpen, setIsWizardOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false); @@ -212,6 +213,24 @@ const App: React.FC = () => { setApiKey={setApiKey} onReset={handleReset} /> + {isReferenceOpen && ( +
+
+
+

+ + Tax Reference +

+ +
+
+ +
+
+
+ )}
@@ -228,12 +247,17 @@ const App: React.FC = () => {

Tax-Efficient Planner

- - +
+ + + +
@@ -257,8 +281,7 @@ const App: React.FC = () => { { id: 'fire', icon: Flame, label: 'FIRE Analysis' }, { id: 'withdrawal', icon: Calculator, label: 'Withdrawal' }, { id: 'longevity', icon: TrendingUp, label: 'Longevity' }, - { id: 'scenarios', icon: RefreshCw, label: 'Scenarios' }, - { id: 'reference', icon: BookOpen, label: 'Reference' } + { id: 'scenarios', icon: RefreshCw, label: 'Scenarios' } ].map(tab => (
) :
Loading strategy...
}
From c9e8244750534bde7d2d402a83eb28f7766f907d Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:30:54 -0500 Subject: [PATCH 10/19] feat: Enhance LongevityAnalysis component with outcome tier styling and updated messaging --- src/components/features/LongevityAnalysis.tsx | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/components/features/LongevityAnalysis.tsx b/src/components/features/LongevityAnalysis.tsx index aef1629..0971d07 100644 --- a/src/components/features/LongevityAnalysis.tsx +++ b/src/components/features/LongevityAnalysis.tsx @@ -14,6 +14,15 @@ interface LongevityAnalysisProps { const LongevityAnalysis: React.FC = ({ longevity, profile, isDarkMode }) => { const { projection, depletionAge, initialWithdrawalRate, sustainable } = longevity; + // Determine outcome tier for depletion age styling + const GOOD_AGE_THRESHOLD = 90; + const WARNING_AGE_THRESHOLD = 85; + const outcomeLevel: 'good' | 'caution' | 'danger' = !depletionAge || depletionAge >= GOOD_AGE_THRESHOLD + ? 'good' + : depletionAge >= WARNING_AGE_THRESHOLD + ? 'caution' + : 'danger'; + // Chart styling colors const axisColor = isDarkMode ? '#94a3b8' : '#64748b'; const gridColor = isDarkMode ? '#334155' : '#e2e8f0'; @@ -42,28 +51,43 @@ const LongevityAnalysis: React.FC = ({ longevity, profil

-

Projected Outcome

- {depletionAge ? ( + {outcomeLevel === 'good' ? ( <> - +
- Depleted at Age {depletionAge} -

Money runs out in {depletionAge - profile.age} years.

+ + {depletionAge ? `Lasts to Age ${depletionAge}` : 'Sustainable'} + +

+ {depletionAge ? `Portfolio supports ${depletionAge - profile.age} years of retirement.` : 'Portfolio lasts to age 100+.'} +

+
+ + ) : outcomeLevel === 'caution' ? ( + <> + +
+ Depleted at Age {depletionAge} +

Money runs out in {depletionAge! - profile.age} years. Consider adjustments.

) : ( <> - +
- Sustainable -

Portfolio lasts to age 100+.

+ Depleted at Age {depletionAge} +

Money runs out in {depletionAge! - profile.age} years.

)} From 45b98110bace82fb145be1bb3bffe0d1a7810c83 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:38:49 -0500 Subject: [PATCH 11/19] feat: Add shortfall and risk guidance calculations in StrategyResults component --- src/components/features/StrategyResults.tsx | 52 +++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/components/features/StrategyResults.tsx b/src/components/features/StrategyResults.tsx index ed24cf2..de7dc50 100644 --- a/src/components/features/StrategyResults.tsx +++ b/src/components/features/StrategyResults.tsx @@ -92,6 +92,20 @@ const StrategyResults: React.FC = ({ result, profile, isDa if (!result.gapFilled) feasibility = 'Shortfall'; else if (withdrawalRate > 0.05) feasibility = 'Risk'; + // Guidance calculations + const shortfallAmount = feasibility === 'Shortfall' + ? Math.max(0, (result.nominalSpendingNeeded + result.estimatedFederalTax) - result.totalWithdrawal) + : 0; + const annualContributions = profile.contributions.traditionalIRA + profile.contributions.rothIRA + profile.contributions.brokerage + profile.contributions.hsa; + const delayYears = annualContributions > 0 && shortfallAmount > 0 + ? Math.ceil(shortfallAmount / (annualContributions * (1 + profile.assumptions.rateOfReturn))) + : null; + + // Risk guidance: how much to reduce spending to reach a safe 4% withdrawal rate + const safeWithdrawalTarget = totalPortfolio * 0.04; + const excessDraw = portfolioDraw - safeWithdrawalTarget; + const spendingReduction = feasibility === 'Risk' ? Math.max(0, Math.round(excessDraw)) : 0; + const feasibilityStyles = { Safe: 'bg-green-50 dark:bg-green-950/30 border-green-200 dark:border-green-900 text-green-700 dark:text-green-400', Risk: 'bg-amber-50 dark:bg-amber-950/30 border-amber-200 dark:border-amber-900 text-amber-700 dark:text-amber-400', @@ -199,6 +213,44 @@ const StrategyResults: React.FC = ({ result, profile, isDa
+ {/* Shortfall Guidance */} + {feasibility === 'Shortfall' && shortfallAmount > 0 && ( +
+ +
+

Your plan has an annual shortfall of {formatCurrency(shortfallAmount)}

+

Consider one or more of these adjustments:

+
    +
  • Reduce spending by at least {formatCurrency(shortfallAmount)}/yr to close the gap.
  • + {delayYears !== null && delayYears > 0 && ( +
  • Delay retirement by ~{delayYears} year{delayYears > 1 ? 's' : ''} to build a larger portfolio with continued contributions.
  • + )} +
  • Increase contributions now — even small additional savings compound significantly over time.
  • +
+
+
+ )} + + {/* Risk Guidance */} + {feasibility === 'Risk' && ( +
+ +
+

+ Withdrawal rate of {(withdrawalRate * 100).toFixed(1)}% exceeds the safe 4% guideline +

+

Consider one or more of these adjustments:

+
    + {spendingReduction > 0 && ( +
  • Reduce spending by ~{formatCurrency(spendingReduction)}/yr to bring your withdrawal rate closer to 4%.
  • + )} +
  • Delay retirement to allow your portfolio more time to grow and reduce the draw-down percentage.
  • +
  • Increase contributions now to build a larger portfolio base before retirement.
  • +
+
+
+ )} + {/* Tax Engine Explanation */}
From 27d17371d8ac9a466f17184f9335bf5f3dd59251 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:39:10 -0500 Subject: [PATCH 12/19] feat: Remove unnecessary flex styling from header in App component --- src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index a726513..cf726cd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -243,7 +243,7 @@ const App: React.FC = () => {
FiscalSunset Logo
-

FiscalSunset.

+

FiscalSunset.

Tax-Efficient Planner

From 73cefcacf7af130d6aa7da2f0c21f9e3469268e2 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:41:47 -0500 Subject: [PATCH 13/19] feat: Implement ErrorBoundary component for handling calculation errors in App --- src/App.tsx | 77 ++++++++++++++----------- src/components/common/ErrorBoundary.tsx | 64 ++++++++++++++++++++ 2 files changed, 107 insertions(+), 34 deletions(-) create mode 100644 src/components/common/ErrorBoundary.tsx diff --git a/src/App.tsx b/src/App.tsx index cf726cd..43958f1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import TaxReference from './components/features/TaxReference'; import FireAnalysis from './components/features/FireAnalysis'; import { calculateStrategy, calculateLongevity } from './services/calculationEngine'; import { TrendingUp, Calculator, AlertTriangle, Sun, Moon, PiggyBank, Settings, Flame, RefreshCw, HelpCircle, X } from 'lucide-react'; +import ErrorBoundary from './components/common/ErrorBoundary'; import Footer from './components/layout/Footer'; import WizardModal from './components/features/wizard/WizardModal'; import SettingsModal from './components/features/SettingsModal'; @@ -96,11 +97,17 @@ const App: React.FC = () => { useEffect(() => { - // Run strategy on the computed RETIREMENT profile - const sResult = calculateStrategy(retirementProfile); - const lResult = calculateLongevity(retirementProfile, sResult); - setStrategyResult(sResult); - setLongevityResult(lResult); + try { + // Run strategy on the computed RETIREMENT profile + const sResult = calculateStrategy(retirementProfile); + const lResult = calculateLongevity(retirementProfile, sResult); + setStrategyResult(sResult); + setLongevityResult(lResult); + } catch (error) { + console.error('Calculation error:', error); + setStrategyResult(null); + setLongevityResult(null); + } }, [retirementProfile]); // Depend on retirementProfile instead of profile useEffect(() => { @@ -299,36 +306,38 @@ const App: React.FC = () => {
- {strategyResult && longevityResult ? ( -
-
- setIsSettingsOpen(true)} - /> -
-
- setActiveTab('withdrawal')} - /> -
-
- + setProfile(p => ({ ...p }))}> + {strategyResult && longevityResult ? ( +
+
+ setIsSettingsOpen(true)} + /> +
+
+ setActiveTab('withdrawal')} + /> +
+
+ +
+
+ +
+
+ +
-
- -
-
- -
-
- ) :
Loading strategy...
} + ) :
Loading strategy...
} +
diff --git a/src/components/common/ErrorBoundary.tsx b/src/components/common/ErrorBoundary.tsx new file mode 100644 index 0000000..5841d9f --- /dev/null +++ b/src/components/common/ErrorBoundary.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { AlertTriangle, RefreshCw } from 'lucide-react'; + +interface Props { + children: React.ReactNode; + onReset?: () => void; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +class ErrorBoundary extends React.Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, info: React.ErrorInfo) { + console.error('ErrorBoundary caught:', error, info.componentStack); + } + + handleReset = () => { + this.setState({ hasError: false, error: null }); + this.props.onReset?.(); + }; + + render() { + if (this.state.hasError) { + return ( +
+ +
+

Something went wrong

+

+ A calculation error occurred. This can happen with unusual input combinations. +

+ {this.state.error && ( +

+ {this.state.error.message} +

+ )} +
+ +
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; From 72cb266ac3f5589f2fe16519241b6f1899a3bd30 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:43:32 -0500 Subject: [PATCH 14/19] feat: Update button text and layout for transitioning to withdrawal phase in AccumulationStrategy component --- src/components/features/AccumulationStrategy.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/features/AccumulationStrategy.tsx b/src/components/features/AccumulationStrategy.tsx index 5158c5f..9dc5bdd 100644 --- a/src/components/features/AccumulationStrategy.tsx +++ b/src/components/features/AccumulationStrategy.tsx @@ -149,12 +149,15 @@ const AccumulationStrategy: React.FC = ({ profile, se

Move over to the withdrawal tab to see how should you withdraw your money, and the strategy to pay the least amount of taxes, values are in Nominal Dollars

- +
+ + You can return to accumulation view anytime. +
{/* Portfolio Path Chart (Full Width) */} From f39c1e8df6e49f25d2b70e5bbf125c302954b1e2 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:45:41 -0500 Subject: [PATCH 15/19] feat: Add profile validation and error handling in App component --- src/App.tsx | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 43958f1..5586dda 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -95,8 +95,19 @@ const App: React.FC = () => { }, [profile]); + // Profile validation + const profileErrors: string[] = []; + if (profile.age < profile.baseAge) profileErrors.push('Retirement age must be ≥ current age.'); + if (profile.spendingNeed <= 0) profileErrors.push('Annual spending need must be greater than $0.'); + if (profile.baseAge <= 0) profileErrors.push('Current age must be greater than 0.'); + const isProfileValid = profileErrors.length === 0; useEffect(() => { + if (!isProfileValid) { + setStrategyResult(null); + setLongevityResult(null); + return; + } try { // Run strategy on the computed RETIREMENT profile const sResult = calculateStrategy(retirementProfile); @@ -108,7 +119,7 @@ const App: React.FC = () => { setStrategyResult(null); setLongevityResult(null); } - }, [retirementProfile]); // Depend on retirementProfile instead of profile + }, [retirementProfile, isProfileValid]); // Depend on retirementProfile instead of profile useEffect(() => { const loadData = async () => { @@ -238,7 +249,7 @@ const App: React.FC = () => {
)} -
+
Educational purposes only. No professional financial or tax advice intended. @@ -336,6 +347,16 @@ const App: React.FC = () => {
+ ) : !isProfileValid ? ( +
+
+ +

Fix input errors to see results

+
    + {profileErrors.map((err, i) =>
  • {err}
  • )} +
+
+
) :
Loading strategy...
}
From dd63ca13ecce93b6037167151d1ec77a60ff7ec8 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:47:29 -0500 Subject: [PATCH 16/19] feat: Enhance range inputs with labels for spending need, rate of return, target retirement age, and consulting income in FireAnalysis component --- src/components/features/FireAnalysis.tsx | 94 ++++++++++++++---------- 1 file changed, 55 insertions(+), 39 deletions(-) diff --git a/src/components/features/FireAnalysis.tsx b/src/components/features/FireAnalysis.tsx index 9445811..bba1910 100644 --- a/src/components/features/FireAnalysis.tsx +++ b/src/components/features/FireAnalysis.tsx @@ -2,7 +2,7 @@ import React, { useState, useMemo } from 'react'; import { UserProfile } from '../../types'; import { calculateFireMilestones } from '../../services/fireCalculations'; import { FireInputs } from '../../types/fire'; -import { Flame, TrendingUp, DollarSign, Calendar, RefreshCw } from 'lucide-react'; +import { Flame, TrendingUp, DollarSign, Calendar, RefreshCw, ChevronDown, ChevronUp } from 'lucide-react'; import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip as RechartsTooltip } from 'recharts'; import Tooltip from '../common/Tooltip'; @@ -82,9 +82,9 @@ const FireAnalysis: React.FC = ({ profile, isDarkMode }) => {

How is this calculated?

{isOpen ? ( - + ) : ( - + )} {isOpen && ( @@ -163,15 +163,19 @@ const FireAnalysis: React.FC = ({ profile, isDarkMode }) => { ${spendingNeed.toLocaleString()}
- setSpendingNeed(Number(e.target.value))} - className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-indigo-600" - /> +
+ $20k + setSpendingNeed(Number(e.target.value))} + className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-indigo-600" + /> + $200k +

Impacts FIRE Number directly.

@@ -183,15 +187,19 @@ const FireAnalysis: React.FC = ({ profile, isDarkMode }) => { {(rateOfReturn * 100).toFixed(1)}%
- setRateOfReturn(Number(e.target.value))} - className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-600" - /> +
+ 1% + setRateOfReturn(Number(e.target.value))} + className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-600" + /> + 12% +

Higher returns accelerate timeline.

@@ -203,15 +211,19 @@ const FireAnalysis: React.FC = ({ profile, isDarkMode }) => { {targetRetirementAge} - setTargetRetirementAge(Number(e.target.value))} - className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-blue-600" - /> +
+ {profile.baseAge + 1} + setTargetRetirementAge(Number(e.target.value))} + className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-blue-600" + /> + 80 +

Affects Coast FIRE target.

@@ -223,15 +235,19 @@ const FireAnalysis: React.FC = ({ profile, isDarkMode }) => { {formatCurrency(consultingIncome)}/yr - setConsultingIncome(Number(e.target.value))} - className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-purple-600" - /> +
+ $0 + setConsultingIncome(Number(e.target.value))} + className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-purple-600" + /> + $100k +

Side income for Barista FIRE.

From 81cda84f6e2aafe83e9ab10c935b22c7a029f5c8 Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:50:15 -0500 Subject: [PATCH 17/19] feat: Enhance modal components with click-to-close functionality and improve accessibility --- src/App.tsx | 4 ++-- src/components/features/AccumulationStrategy.tsx | 1 + src/components/features/SettingsModal.tsx | 4 ++-- src/components/modals/PortfolioSelectorModal.tsx | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 5586dda..80730d4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -232,8 +232,8 @@ const App: React.FC = () => { onReset={handleReset} /> {isReferenceOpen && ( -
-
+
setIsReferenceOpen(false)}> +
e.stopPropagation()}>

diff --git a/src/components/features/AccumulationStrategy.tsx b/src/components/features/AccumulationStrategy.tsx index 9dc5bdd..0bda8d3 100644 --- a/src/components/features/AccumulationStrategy.tsx +++ b/src/components/features/AccumulationStrategy.tsx @@ -195,6 +195,7 @@ const AccumulationStrategy: React.FC = ({ profile, se labelFormatter={(label) => `Age ${label}`} labelStyle={{ fontWeight: 'bold', color: tooltipText }} /> + diff --git a/src/components/features/SettingsModal.tsx b/src/components/features/SettingsModal.tsx index 521ea6e..953650d 100644 --- a/src/components/features/SettingsModal.tsx +++ b/src/components/features/SettingsModal.tsx @@ -36,8 +36,8 @@ const SettingsModal: React.FC = ({ isOpen, onClose, apiKey, }; return ( -
-
+
+
e.stopPropagation()}> {/* Header */}
diff --git a/src/components/modals/PortfolioSelectorModal.tsx b/src/components/modals/PortfolioSelectorModal.tsx index 1078793..ad63e9c 100644 --- a/src/components/modals/PortfolioSelectorModal.tsx +++ b/src/components/modals/PortfolioSelectorModal.tsx @@ -74,8 +74,8 @@ const PortfolioSelectorModal: React.FC = ({ if (!isOpen) return null; return ( -
-
+
+
e.stopPropagation()}> {/* Header */}
From 8516080b5f39bae3629b2b16656b787c849202aa Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:50:33 -0500 Subject: [PATCH 18/19] feat: Add click-to-close functionality to WizardModal for improved user experience --- src/components/features/wizard/WizardModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/features/wizard/WizardModal.tsx b/src/components/features/wizard/WizardModal.tsx index 11ada36..bb19d56 100644 --- a/src/components/features/wizard/WizardModal.tsx +++ b/src/components/features/wizard/WizardModal.tsx @@ -93,8 +93,8 @@ const WizardModal: React.FC = ({ isOpen, onClose, onComplete, const progress = (effectiveCurrentStep / effectiveTotalSteps) * 100; return ( -
-
+
+
e.stopPropagation()}> {/* Header */}
From 7754d18cf060cc512491a9b41a3869c17939988a Mon Sep 17 00:00:00 2001 From: Miguel Velasco Date: Mon, 23 Feb 2026 21:53:15 -0500 Subject: [PATCH 19/19] feat: Enhance input components with additional props for accessibility and flexibility --- src/components/features/InputSection.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/features/InputSection.tsx b/src/components/features/InputSection.tsx index 50c09c2..778dac5 100644 --- a/src/components/features/InputSection.tsx +++ b/src/components/features/InputSection.tsx @@ -11,7 +11,7 @@ interface InputSectionProps { savedAt?: Date | null; } -const FormattedNumberInput = ({ value, onChange, className, id }: { value: number; onChange: (val: number) => void; className?: string; id?: string }) => { +const FormattedNumberInput = ({ value, onChange, className, id, ...rest }: { value: number; onChange: (val: number) => void; className?: string; id?: string } & React.InputHTMLAttributes) => { const [displayValue, setDisplayValue] = useState(value.toLocaleString()); const lastExternalValue = React.useRef(value); @@ -33,11 +33,11 @@ const FormattedNumberInput = ({ value, onChange, className, id }: { value: numbe onChange(newVal); }; - return ; + return ; }; // Handles percentage inputs (stored as decimal, displayed as percentage) -const PercentageInput = ({ value, onChange, className, step = 0.1, id }: { value: number; onChange: (val: number) => void; className?: string; step?: number; id?: string }) => { +const PercentageInput = ({ value, onChange, className, step = 0.1, id, ...rest }: { value: number; onChange: (val: number) => void; className?: string; step?: number; id?: string } & React.InputHTMLAttributes) => { const [displayValue, setDisplayValue] = useState((value * 100).toFixed(1)); useEffect(() => { @@ -71,7 +71,7 @@ const PercentageInput = ({ value, onChange, className, step = 0.1, id }: { value } }; - return ; + return ; }; const InputSection: React.FC = ({ profile, setProfile, onRestartWizard, savedAt }) => { @@ -225,11 +225,11 @@ const InputSection: React.FC = ({ profile, setProfile, onRest
$ handleChange('spendingNeed', val)} className={`${inputClass} pl-8 font-semibold text-lg`} + aria-label="Annual Spending Need in Retirement" />
{profile.age > profile.baseAge && ( @@ -269,6 +269,7 @@ const InputSection: React.FC = ({ profile, setProfile, onRest value={profile.assets[item.key] || 0} onChange={(val) => handleAssetChange(item.key, val)} className={`${inputClass} pl-8`} + aria-label={item.tooltip ? `${item.label}: ${item.tooltip}` : item.label} />