diff --git a/src/app/components/FireCalculatorForm.tsx b/src/app/components/FireCalculatorForm.tsx index bbd7dc7..64f3a0b 100644 --- a/src/app/components/FireCalculatorForm.tsx +++ b/src/app/components/FireCalculatorForm.tsx @@ -12,20 +12,27 @@ import { Label } from "@/components/ui/label"; import { Form, FormControl, - FormDescription, FormField, FormItem, FormLabel, FormMessage, -} from "../../components/ui/form"; +} from "@/components/ui/form"; import { Card, CardContent, CardDescription, - CardFooter, CardHeader, CardTitle, -} from "../../components/ui/card"; +} from "@/components/ui/card"; +import { ChartContainer, ChartTooltip } from "@/components/ui/chart"; +import { + Area, + AreaChart, + CartesianGrid, + XAxis, + YAxis, + ReferenceLine, +} from "recharts"; // Schema for form validation const formSchema = z.object({ @@ -57,10 +64,17 @@ interface CalculationResult { inflationAdjustedAllowance: number | null; retirementYears: number | null; error?: string; + yearlyData?: Array<{ + age: number; + year: number; + balance: number; + phase: "accumulation" | "retirement"; + }>; } export default function FireCalculatorForm() { const [result, setResult] = useState(null); + const currentYear = new Date().getFullYear(); // Initialize form with default values const form = useForm({ @@ -72,7 +86,7 @@ export default function FireCalculatorForm() { cagr: 7, desiredMonthlyAllowance: 2000, inflationRate: 2, - lifeExpectancy: 90, + lifeExpectancy: 84, }, }); @@ -105,6 +119,17 @@ export default function FireCalculatorForm() { let monthlyAllowance = initialMonthlyAllowance; let iterations = 0; + // Array to store yearly data for the chart + const yearlyData: CalculationResult["yearlyData"] = []; + + // Add starting point + yearlyData.push({ + age: currentAge, + year: currentYear, + balance: startingCapital, + phase: "accumulation", + }); + // Accumulation phase simulation while (age < lifeExpectancy && iterations < maxIterations) { // Simulate one year of saving and growth @@ -117,6 +142,14 @@ export default function FireCalculatorForm() { age++; iterations++; + // Record yearly data + yearlyData.push({ + age: age, + year: currentYear + (age - currentAge), + balance: Math.round(currentCapital), + phase: "accumulation", + }); + // Check each possible retirement capital target through binary search const mid = (low + high) / 2; if (high - low < 1) { @@ -178,6 +211,14 @@ export default function FireCalculatorForm() { age++; iterations++; + // Record yearly data + yearlyData.push({ + age: age, + year: currentYear + (age - currentAge), + balance: Math.round(currentCapital), + phase: "accumulation", + }); + // Test with current capital let testCapital = currentCapital; let testAge = age; @@ -209,12 +250,46 @@ export default function FireCalculatorForm() { } } + // If retirement is possible, simulate the retirement phase for the chart + if (canRetire) { + // Update the phase for all years after retirement + yearlyData.forEach((data) => { + if (data.age >= retirementAge) { + data.phase = "retirement"; + } + }); + + // Continue simulation for retirement phase if needed + let simulationCapital = currentCapital; + let simulationAllowance = monthlyAllowance; + let simulationAge = age; + + // If we haven't simulated up to life expectancy, continue + while (simulationAge < lifeExpectancy) { + for (let month = 0; month < 12; month++) { + simulationCapital -= simulationAllowance; + simulationCapital *= 1 + monthlyGrowthRate; + simulationAllowance *= 1 + monthlyInflationRate; + } + simulationAge++; + + // Record yearly data + yearlyData.push({ + age: simulationAge, + year: currentYear + (simulationAge - currentAge), + balance: Math.round(simulationCapital), + phase: "retirement", + }); + } + } + if (canRetire) { setResult({ fireNumber: requiredCapital, retirementAge: retirementAge, inflationAdjustedAllowance: finalInflationAdjustedAllowance, retirementYears: lifeExpectancy - retirementAge, + yearlyData: yearlyData, error: undefined, }); } else { @@ -223,6 +298,7 @@ export default function FireCalculatorForm() { retirementAge: null, inflationAdjustedAllowance: null, retirementYears: null, + yearlyData: yearlyData, error: iterations >= maxIterations ? "Calculation exceeded maximum iterations." @@ -231,8 +307,8 @@ export default function FireCalculatorForm() { } } - // Helper function to format currency - const formatCurrency = (value: number | null) => { + // Helper function to format currency without specific symbols + const formatNumber = (value: number | null) => { if (value === null) return "N/A"; return new Intl.NumberFormat("en", { maximumFractionDigits: 0, @@ -386,49 +462,177 @@ export default function FireCalculatorForm() { {result && ( - - - Results - - - {result.error ? ( -

{result.error}

- ) : ( -
-
- -

- {formatCurrency(result.fireNumber)} -

-
-
- -

- {result.retirementAge ?? "N/A"} -

-
- {result.inflationAdjustedAllowance && ( + <> + + + Results + + + {result.error ? ( +

{result.error}

+ ) : ( +
- +

- {formatCurrency(result.inflationAdjustedAllowance)} + {formatNumber(result.fireNumber)}

- )} - {result.retirementYears && (
- +

- {result.retirementYears} + {result.retirementAge ?? "N/A"}

- )} -
- )} -
-
+ {result.inflationAdjustedAllowance && ( +
+ +

+ {formatNumber(result.inflationAdjustedAllowance)} +

+
+ )} + {result.retirementYears && ( +
+ +

+ {result.retirementYears} +

+
+ )} +
+ )} +
+
+ + {result && result.yearlyData && result.yearlyData.length > 0 && ( + + + Financial Projection + + Projected balance growth and FIRE number threshold + + + + + + + + { + if (value >= 1000000) { + return `${(value / 1000000).toFixed(1)}M`; + } else if (value >= 1000) { + return `${(value / 1000).toFixed(0)}K`; + } + return `${value}`; + }} + width={80} + /> + { + if (active && payload?.[0]?.payload) { + const data = payload[0] + .payload as (typeof result.yearlyData)[0]; + return ( +
+

{`Year: ${data.year} (Age: ${data.age})`}

+

{`Balance: ${formatNumber(data.balance)}`}

+ {result.fireNumber && ( +

{`FIRE Number: ${formatNumber(result.fireNumber)}`}

+ )} +

{`Phase: ${data.phase === "accumulation" ? "Accumulation" : "Retirement"}`}

+
+ ); + } + return null; + }} + /> + + + + + + + + {result.fireNumber && ( + + )} + {result.retirementAge && ( + + )} +
+
+
+
+ )} + )} );