"use client"; import * as React from "react"; import { useState } from "react"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import * as z from "zod"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { ChartContainer, ChartTooltip } from "@/components/ui/chart"; import { Area, AreaChart, CartesianGrid, XAxis, YAxis, ReferenceLine, } from "recharts"; import { Slider } from "@/components/ui/slider"; import assert from "assert"; // Schema for form validation const formSchema = z.object({ startingCapital: z.coerce.number(), monthlySavings: z.coerce .number() .min(0, "Monthly savings must be a non-negative number"), currentAge: z.coerce .number() .min(1, "Age must be at least 1") .max(100, "No point in starting this late"), cagr: z.coerce.number().min(0, "Growth rate must be a non-negative number"), desiredMonthlyAllowance: z.coerce .number() .min(0, "Monthly allowance must be a non-negative number"), inflationRate: z.coerce .number() .min(0, "Inflation rate must be a non-negative number"), lifeExpectancy: z.coerce .number() .min(40, "Be a bit more optimistic buddy :(") .max(100, "You should be more realistic..."), retirementAge: z.coerce .number() .min(18, "Retirement age must be at least 18") .max(100, "Retirement age must be at most 100"), }); // Type for form values type FormValues = z.infer; interface YearlyData { age: number; year: number; balance: number; phase: "accumulation" | "retirement"; monthlyAllowance: number; } interface CalculationResult { fireNumber: number | null; yearlyData: YearlyData[]; error?: string; } export default function FireCalculatorForm() { const [result, setResult] = useState(null); const irlYear = new Date().getFullYear(); // Initialize form with default values const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { startingCapital: 50000, monthlySavings: 1500, currentAge: 25, cagr: 7, desiredMonthlyAllowance: 3000, inflationRate: 2, lifeExpectancy: 84, retirementAge: 55, }, }); function onSubmit(values: FormValues) { setResult(null); // Reset previous results const startingCapital = values.startingCapital; const monthlySavings = values.monthlySavings; const age = values.currentAge; const annualGrowthRate = 1 + values.cagr / 100; const initialMonthlyAllowance = values.desiredMonthlyAllowance; const annualInflation = 1 + values.inflationRate / 100; const ageOfDeath = values.lifeExpectancy; const retirementAge = values.retirementAge; // Array to store yearly data for the chart const yearlyData: YearlyData[] = []; // Initial year data yearlyData.push({ age: age, year: irlYear, balance: startingCapital, phase: "accumulation", monthlyAllowance: initialMonthlyAllowance, }); // Calculate accumulation phase (before retirement) for (let year = irlYear + 1; year <= irlYear + (ageOfDeath - age); year++) { const currentAge = age + (year - irlYear); const previousYearData = yearlyData[yearlyData.length - 1]; const inflatedAllowance = initialMonthlyAllowance * Math.pow(annualInflation, year - irlYear); const isRetirementYear = currentAge >= retirementAge; const phase = isRetirementYear ? "retirement" : "accumulation"; assert(!!previousYearData); // Calculate balance based on phase let newBalance; if (phase === "accumulation") { // During accumulation: grow previous balance + add savings newBalance = previousYearData.balance * annualGrowthRate + monthlySavings * 12; } else { // During retirement: grow previous balance - withdraw allowance newBalance = previousYearData.balance * annualGrowthRate - inflatedAllowance * 12; } yearlyData.push({ age: currentAge, year: year, balance: newBalance, phase: phase, monthlyAllowance: inflatedAllowance, }); } // Calculate FIRE number at retirement const retirementYear = irlYear + (retirementAge - age); const retirementIndex = yearlyData.findIndex( (data) => data.year === retirementYear, ); const retirementData = yearlyData[retirementIndex]; if (retirementIndex === -1 || !retirementData) { setResult({ fireNumber: null, error: "Could not calculate retirement data", yearlyData: yearlyData, }); } else { // Set the result setResult({ fireNumber: retirementData.balance, yearlyData: yearlyData, }); } } // 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, }).format(value); }; return ( <> FIRE Calculator Calculate your path to financial independence and retirement
( Starting Capital )} /> ( Monthly Savings )} /> ( Current Age )} /> ( Life Expectancy (Age) )} /> ( Expected Annual Growth Rate (%) )} /> ( Annual Inflation Rate (%) )} /> ( Desired Monthly Allowance (Today's Value) )} /> {/* Retirement Age Slider */} ( Retirement Age: {field.value} )} />
{result?.yearlyData && ( Financial Projection Projected balance growth with your selected retirement age { if (value >= 1000000) { return `${(value / 1000000).toPrecision(3)}M`; } else if (value >= 1000) { return `${(value / 1000).toPrecision(3)}K`; } else if (value <= -1000000) { return `${(value / 1000000).toPrecision(3)}M`; } else if (value <= -1000) { return `${(value / 1000).toPrecision(3)}K`; } return value.toString(); }} width={25} /> { if (active && payload?.[0]?.payload) { const data = payload[0] .payload as (typeof result.yearlyData)[0]; return (

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

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

{`Monthly allowance: ${formatNumber(data.monthlyAllowance)}`}

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

); } return null; }} /> {result.fireNumber && ( )}
)}
{result && (
{result.error ? (

{result.error}

) : ( <> FIRE Number Capital at retirement

{formatNumber(result.fireNumber)}

Retirement Duration Years to enjoy your financial independence

{form.getValues().lifeExpectancy - form.getValues().retirementAge}

)}
)} ); }