"use client"; 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, type TooltipProps, } from "recharts"; import { Slider } from "@/components/ui/slider"; import assert from "assert"; import type { NameType, ValueType, } from "recharts/types/component/DefaultTooltipContent"; // 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; untouchedBalance: number; phase: "accumulation" | "retirement"; monthlyAllowance: number; untouchedMonthlyAllowance: number; } interface CalculationResult { fireNumber: number | null; fireNumber4percent: number | null; retirementAge4percent: number | null; yearlyData: YearlyData[]; error?: string; } // Helper function to format currency without specific symbols const formatNumber = (value: number | null) => { if (!value) return "N/A"; return new Intl.NumberFormat("en", { maximumFractionDigits: 0, }).format(value); }; // Helper function to render tooltip for chart const tooltipRenderer = ({ active, payload, }: TooltipProps) => { if (active && payload?.[0]?.payload) { const data = payload[0].payload as YearlyData; 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; }; export default function FireCalculatorForm() { const [result, setResult] = useState(null); const irlYear = new Date().getFullYear(); const [showing4percent, setShowing4percent] = useState(false); // Initialize form with default values const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { startingCapital: 50000, monthlySavings: 1500, currentAge: 25, cagr: 7, desiredMonthlyAllowance: 3000, inflationRate: 2.3, 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, untouchedBalance: startingCapital, phase: "accumulation", monthlyAllowance: 0, untouchedMonthlyAllowance: 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; } const untouchedBalance = previousYearData.untouchedBalance * annualGrowthRate + monthlySavings * 12; const allowance = phase === "retirement" ? inflatedAllowance : 0; yearlyData.push({ age: currentAge, year: year, balance: newBalance, untouchedBalance: untouchedBalance, phase: phase, monthlyAllowance: allowance, untouchedMonthlyAllowance: inflatedAllowance, }); } // Calculate FIRE number at retirement const retirementYear = irlYear + (retirementAge - age); const retirementIndex = yearlyData.findIndex( (data) => data.year === retirementYear, ); const retirementData = yearlyData[retirementIndex]; const [fireNumber4percent, retirementAge4percent] = (() => { for (const yearData of yearlyData) { if ( yearData.untouchedBalance > (yearData.untouchedMonthlyAllowance * 12) / 0.04 ) { return [yearData.untouchedBalance, yearData.age]; } } return [0, 0]; })(); if (retirementIndex === -1 || !retirementData) { setResult({ fireNumber: null, fireNumber4percent: null, retirementAge4percent: null, error: "Could not calculate retirement data", yearlyData: yearlyData, }); } else { // Set the result setResult({ fireNumber: retirementData.balance, fireNumber4percent: fireNumber4percent, retirementAge4percent: retirementAge4percent, yearlyData: yearlyData, }); } } return ( <> FIRE Calculator Calculate your path to financial independence and retirement
( Starting Capital { field.onChange(value); void form.handleSubmit(onSubmit)(); }} /> )} /> ( Monthly Savings { field.onChange(value); void form.handleSubmit(onSubmit)(); }} /> )} /> ( Current Age { field.onChange(value); void form.handleSubmit(onSubmit)(); }} /> )} /> ( Life Expectancy (Age) { field.onChange(value); void form.handleSubmit(onSubmit)(); }} /> )} /> ( Expected Annual Growth Rate (%) { field.onChange(value); void form.handleSubmit(onSubmit)(); }} /> )} /> ( Annual Inflation Rate (%) { field.onChange(value); void form.handleSubmit(onSubmit)(); }} /> )} /> ( Desired Monthly Allowance (Today's Value) { field.onChange(value); void form.handleSubmit(onSubmit)(); }} /> )} /> {/* Retirement Age Slider */} ( Retirement Age: {field.value} { field.onChange(value[0]); void form.handleSubmit(onSubmit)(); }} className="py-4" /> )} />
{!result && ( )} {result?.yearlyData && ( Financial Projection Projected balance growth with your selected retirement age {/* Right Y axis */} { 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={30} stroke="var(--color-orange-500)" tick={{}} /> {/* Left Y axis */} { if (value >= 1000000) { return `${(value / 1000000).toPrecision(3)}M`; } else if (value >= 1000) { return `${(value / 1000).toPrecision(3)}K`; } return value.toString(); }} width={30} stroke="var(--color-red-600)" /> {result.fireNumber && ( )} {result.fireNumber4percent && showing4percent && ( )} {result.retirementAge4percent && showing4percent && ( )} )} {result && ( )}
{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")}

{showing4percent && ( <> 4%-Rule FIRE Number Capital needed for 4% of it to be greater than your yearly allowance

{formatNumber(result.fireNumber4percent)}

4%-Rule Retirement Duration Years to enjoy your financial independence if you follow the 4% rule

{form.getValues("lifeExpectancy") - (result.retirementAge4percent ?? 0)}

)} )}
)} ); }