learn pages

This commit is contained in:
2025-12-06 00:53:27 +01:00
parent 67af131500
commit 7b24da6f35
13 changed files with 1374 additions and 473 deletions

View File

@@ -0,0 +1,22 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent } from "@/components/ui/card";
export function AuthorBio() {
return (
<Card className="mt-12 bg-muted/50">
<CardContent className="flex items-center gap-4 p-6">
<Avatar className="h-16 w-16 border-2 border-background">
<AvatarImage src="/images/author-profile.jpg" alt="Author" />
<AvatarFallback>IF</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-semibold">Written by The InvestingFIRE Team</p>
<p className="text-sm text-muted-foreground">
We are a group of financial data enthusiasts and early retirees dedicated to building the most accurate FIRE tools on the web. Our goal is to replace guesswork with math.
</p>
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import type React from "react";
'use client';
import { useState, useEffect } from 'react';
import type React from 'react';
import {
type LucideIcon,
HandCoins,
@@ -39,7 +39,7 @@ import {
Hourglass,
Sprout,
Target,
} from "lucide-react";
} from 'lucide-react';
export default function MultiIconPattern({ opacity = 0.2, spacing = 160 }) {
const [width, setWidth] = useState(0);
@@ -58,10 +58,10 @@ export default function MultiIconPattern({ opacity = 0.2, spacing = 160 }) {
};
updateDimensions();
window.addEventListener("resize", updateDimensions);
window.addEventListener('resize', updateDimensions);
return () => {
window.removeEventListener("resize", updateDimensions);
window.removeEventListener('resize', updateDimensions);
};
}, [height, width, spacing]);
@@ -153,9 +153,5 @@ export default function MultiIconPattern({ opacity = 0.2, spacing = 160 }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rows, columns, spacing, opacity]);
return (
<div className="absolute h-full w-full">
{width > 0 && icons}
</div>
);
return <div className="absolute z-0 h-full w-full">{width > 0 && icons}</div>;
}

View File

@@ -1,28 +1,15 @@
"use client";
'use client';
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
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 { 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,
@@ -31,59 +18,40 @@ import {
YAxis,
ReferenceLine,
type TooltipProps,
Line,
} from "recharts";
import { Slider } from "@/components/ui/slider";
import assert from "assert";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type {
NameType,
ValueType,
} from "recharts/types/component/DefaultTooltipContent";
} from 'recharts';
import { Slider } from '@/components/ui/slider';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent';
import { Calculator, Percent } from 'lucide-react';
// 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"),
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"),
.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..."),
.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"),
.min(18, 'Retirement age must be at least 18')
.max(100, 'Retirement age must be at most 100'),
coastFireAge: z.coerce
.number()
.min(18, "Coast FIRE age must be at least 18")
.max(100, "Coast FIRE age must be at most 100")
.min(18, 'Coast FIRE age must be at least 18')
.max(100, 'Coast FIRE age must be at most 100')
.optional(),
baristaIncome: z.coerce
.number()
.min(0, "Barista income must be a non-negative number")
.optional(),
simulationMode: z.enum(["deterministic", "monte-carlo"]).default("deterministic"),
baristaIncome: z.coerce.number().min(0, 'Barista income must be a non-negative number').optional(),
simulationMode: z.enum(['deterministic', 'monte-carlo']).default('deterministic'),
volatility: z.coerce.number().min(0).default(15),
withdrawalStrategy: z.enum(["fixed", "percentage"]).default("fixed"),
withdrawalStrategy: z.enum(['fixed', 'percentage']).default('fixed'),
withdrawalPercentage: z.coerce.number().min(0).max(100).default(4),
});
@@ -95,7 +63,7 @@ interface YearlyData {
year: number;
balance: number;
untouchedBalance: number;
phase: "accumulation" | "retirement";
phase: 'accumulation' | 'retirement';
monthlyAllowance: number;
untouchedMonthlyAllowance: number;
// Monte Carlo percentiles
@@ -123,17 +91,14 @@ function randomNormal(mean: number, stdDev: number): number {
// Helper function to format currency without specific symbols
const formatNumber = (value: number | null) => {
if (!value) return "N/A";
return new Intl.NumberFormat("en", {
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<ValueType, NameType>) => {
const tooltipRenderer = ({ active, payload }: TooltipProps<ValueType, NameType>) => {
if (active && payload?.[0]?.payload) {
const data = payload[0].payload as YearlyData;
return (
@@ -142,14 +107,14 @@ const tooltipRenderer = ({
{data.balanceP50 !== undefined ? (
<>
<p className="text-orange-500">{`Median Balance: ${formatNumber(data.balanceP50)}`}</p>
<p className="text-orange-300 text-xs">{`10th %: ${formatNumber(data.balanceP10 ?? 0)}`}</p>
<p className="text-orange-300 text-xs">{`90th %: ${formatNumber(data.balanceP90 ?? 0)}`}</p>
<p className="text-xs text-orange-300">{`10th %: ${formatNumber(data.balanceP10 ?? 0)}`}</p>
<p className="text-xs text-orange-300">{`90th %: ${formatNumber(data.balanceP90 ?? 0)}`}</p>
</>
) : (
<p className="text-orange-500">{`Balance: ${formatNumber(data.balance)}`}</p>
)}
<p className="text-red-600">{`Monthly allowance: ${formatNumber(data.monthlyAllowance)}`}</p>
<p>{`Phase: ${data.phase === "accumulation" ? "Accumulation" : "Retirement"}`}</p>
<p>{`Phase: ${data.phase === 'accumulation' ? 'Accumulation' : 'Retirement'}`}</p>
</div>
);
}
@@ -175,7 +140,7 @@ export default function FireCalculatorForm() {
retirementAge: 55,
coastFireAge: undefined,
baristaIncome: 0,
simulationMode: "deterministic",
simulationMode: 'deterministic',
volatility: 15,
},
});
@@ -196,7 +161,7 @@ export default function FireCalculatorForm() {
const simulationMode = values.simulationMode;
const volatility = values.volatility;
const numSimulations = simulationMode === "monte-carlo" ? 500 : 1;
const numSimulations = simulationMode === 'monte-carlo' ? 500 : 1;
const simulationResults: number[][] = []; // [yearIndex][simulationIndex] -> balance
// Prepare simulation runs
@@ -204,17 +169,12 @@ export default function FireCalculatorForm() {
let currentBalance = startingCapital;
const runBalances: number[] = [];
for (
let year = irlYear + 1;
year <= irlYear + (ageOfDeath - age);
year++
) {
for (let year = irlYear + 1; year <= irlYear + (ageOfDeath - age); year++) {
const currentAge = age + (year - irlYear);
const yearIndex = year - (irlYear + 1);
// Determine growth rate for this year
let annualGrowthRate: number;
if (simulationMode === "monte-carlo") {
if (simulationMode === 'monte-carlo') {
// Random walk
const randomReturn = randomNormal(cagr, volatility) / 100;
annualGrowthRate = 1 + randomReturn;
@@ -223,23 +183,18 @@ export default function FireCalculatorForm() {
annualGrowthRate = 1 + cagr / 100;
}
const inflatedAllowance =
initialMonthlyAllowance * Math.pow(annualInflation, year - irlYear);
const inflatedBaristaIncome =
initialBaristaIncome * Math.pow(annualInflation, year - irlYear);
const inflatedAllowance = initialMonthlyAllowance * Math.pow(annualInflation, year - irlYear);
const inflatedBaristaIncome = initialBaristaIncome * Math.pow(annualInflation, year - irlYear);
const isRetirementYear = currentAge >= retirementAge;
const phase = isRetirementYear ? "retirement" : "accumulation";
const phase = isRetirementYear ? 'retirement' : 'accumulation';
const isContributing = currentAge < coastFireAge;
let newBalance;
if (phase === "accumulation") {
newBalance =
currentBalance * annualGrowthRate +
(isContributing ? monthlySavings * 12 : 0);
if (phase === 'accumulation') {
newBalance = currentBalance * annualGrowthRate + (isContributing ? monthlySavings * 12 : 0);
} else {
const netAnnualWithdrawal =
(inflatedAllowance - inflatedBaristaIncome) * 12;
const netAnnualWithdrawal = (inflatedAllowance - inflatedBaristaIncome) * 12;
newBalance = currentBalance * annualGrowthRate - netAnnualWithdrawal;
}
// Prevent negative balance from recovering (once you're broke, you're broke)
@@ -265,8 +220,8 @@ export default function FireCalculatorForm() {
year: irlYear,
balance: startingCapital,
untouchedBalance: startingCapital,
phase: "accumulation",
monthlyAllowance: 0,
phase: 'accumulation',
monthlyAllowance: 0,
untouchedMonthlyAllowance: initialMonthlyAllowance,
balanceP10: startingCapital,
balanceP50: startingCapital,
@@ -277,13 +232,13 @@ export default function FireCalculatorForm() {
for (let i = 0; i < numYears; i++) {
const year = irlYear + 1 + i;
const currentAge = age + 1 + i;
// Collect all balances for this year across simulations
const balancesForYear = simulationResults.map((run) => run[i]);
// Sort to find percentiles
balancesForYear.sort((a, b) => a - b);
const p10 = balancesForYear[Math.floor(numSimulations * 0.1)];
const p50 = balancesForYear[Math.floor(numSimulations * 0.5)];
const p90 = balancesForYear[Math.floor(numSimulations * 0.9)];
@@ -291,31 +246,30 @@ export default function FireCalculatorForm() {
// Calculate other metrics (using deterministic logic for "untouched" etc for simplicity, or p50)
// We need to reconstruct the "standard" fields for compatibility with the chart
// Let's use p50 (Median) as the "main" line
const inflatedAllowance =
initialMonthlyAllowance * Math.pow(annualInflation, year - irlYear);
const inflatedAllowance = initialMonthlyAllowance * Math.pow(annualInflation, year - irlYear);
const isRetirementYear = currentAge >= retirementAge;
const phase = isRetirementYear ? "retirement" : "accumulation";
const phase = isRetirementYear ? 'retirement' : 'accumulation';
// Reconstruct untouched balance for deterministic mode (for 4% rule)
let untouchedBalance = 0;
if (simulationMode === "deterministic") {
// We can just use the single run we have
// In deterministic mode, there's only 1 simulation, so balancesForYear[0] is it.
// But wait, `simulationResults` stores the *actual* balance (with withdrawals).
// We need a separate tracker for "untouched" (never withdrawing) if we want accurate 4% rule.
// Let's just re-calculate it simply here since it's deterministic.
const prevUntouched = yearlyData[yearlyData.length - 1].untouchedBalance;
const growth = 1 + cagr / 100;
untouchedBalance = prevUntouched * growth + monthlySavings * 12;
if (simulationMode === 'deterministic') {
// We can just use the single run we have
// In deterministic mode, there's only 1 simulation, so balancesForYear[0] is it.
// But wait, `simulationResults` stores the *actual* balance (with withdrawals).
// We need a separate tracker for "untouched" (never withdrawing) if we want accurate 4% rule.
// Let's just re-calculate it simply here since it's deterministic.
const prevUntouched = yearlyData[yearlyData.length - 1].untouchedBalance;
const growth = 1 + cagr / 100;
untouchedBalance = prevUntouched * growth + monthlySavings * 12;
}
yearlyData.push({
age: currentAge,
year: year,
balance: p50, // Use Median for the main line
untouchedBalance: untouchedBalance,
untouchedBalance: untouchedBalance,
phase: phase,
monthlyAllowance: phase === "retirement" ? inflatedAllowance : 0,
monthlyAllowance: phase === 'retirement' ? inflatedAllowance : 0,
untouchedMonthlyAllowance: inflatedAllowance,
balanceP10: p10,
balanceP50: p50,
@@ -324,37 +278,36 @@ export default function FireCalculatorForm() {
}
// Calculate Success Rate (only for Monte Carlo)
if (simulationMode === "monte-carlo") {
const finalBalances = simulationResults.map(run => run[run.length - 1]);
successCount = finalBalances.filter(b => b > 0).length;
if (simulationMode === 'monte-carlo') {
const finalBalances = simulationResults.map((run) => run[run.length - 1]);
successCount = finalBalances.filter((b) => b > 0).length;
}
// Calculate FIRE number (using Median/Deterministic run)
const retirementYear = irlYear + (retirementAge - age);
const retirementIndex = yearlyData.findIndex(
(data) => data.year === retirementYear,
);
const retirementIndex = yearlyData.findIndex((data) => data.year === retirementYear);
const retirementData = yearlyData[retirementIndex];
const [fireNumber4percent, retirementAge4percent] = (() => {
// Re-enable 4% rule for deterministic mode or use p50 for MC
// For MC, "untouchedBalance" isn't tracked per run in aggregate, but we can use balanceP50 roughly
// or just disable it as it's a different philosophy.
// For now, let's calculate it based on the main "balance" field (which is p50 in MC)
for (const yearData of yearlyData) {
// Estimate untouched roughly if not tracking exact
const balanceToCheck = yearData.balance;
// Note: This is imperfect for MC because 'balance' includes withdrawals in retirement
// whereas 4% rule check usually looks at "if I retired now with this balance".
// The original code had `untouchedBalance` which grew without withdrawals.
// Since we removed `untouchedBalance` calculation in the aggregate loop, let's skip 4% for MC for now.
if (simulationMode === "deterministic" && yearData.untouchedBalance &&
yearData.untouchedBalance > (yearData.untouchedMonthlyAllowance * 12) / 0.04) {
return [yearData.untouchedBalance, yearData.age];
}
// Re-enable 4% rule for deterministic mode or use p50 for MC
// For MC, "untouchedBalance" isn't tracked per run in aggregate, but we can use balanceP50 roughly
// or just disable it as it's a different philosophy.
// For now, let's calculate it based on the main "balance" field (which is p50 in MC)
for (const yearData of yearlyData) {
// Note: This is imperfect for MC because 'balance' includes withdrawals in retirement
// whereas 4% rule check usually looks at "if I retired now with this balance".
// The original code had `untouchedBalance` which grew without withdrawals.
// Since we removed `untouchedBalance` calculation in the aggregate loop, let's skip 4% for MC for now.
if (
simulationMode === 'deterministic' &&
yearData.untouchedBalance &&
yearData.untouchedBalance > (yearData.untouchedMonthlyAllowance * 12) / 0.04
) {
return [yearData.untouchedBalance, yearData.age];
}
return [null, null];
}
return [null, null];
})();
if (retirementIndex === -1) {
@@ -362,28 +315,29 @@ export default function FireCalculatorForm() {
fireNumber: null,
fireNumber4percent: null,
retirementAge4percent: null,
error: "Could not calculate retirement data",
error: 'Could not calculate retirement data',
yearlyData: yearlyData,
});
} else {
// Set the result
setResult({
fireNumber: retirementData.balance,
fireNumber4percent: null,
retirementAge4percent: null,
fireNumber4percent: fireNumber4percent,
retirementAge4percent: retirementAge4percent,
yearlyData: yearlyData,
successRate: simulationMode === "monte-carlo" ? (successCount / numSimulations) * 100 : undefined,
successRate:
simulationMode === 'monte-carlo' ? (successCount / numSimulations) * 100 : undefined,
});
}
}
return (
<>
<Card className="mb-4">
<Card className="border-primary/15 bg-background/90 shadow-primary/10 mb-6 border shadow-lg backdrop-blur">
<CardHeader>
<CardTitle className="text-2xl">FIRE Calculator</CardTitle>
<CardDescription>
Calculate your path to financial independence and retirement
<CardDescription className="text-muted-foreground text-sm">
Calculate your path to financial independence and retirement.
</CardDescription>
</CardHeader>
<CardContent>
@@ -408,11 +362,7 @@ export default function FireCalculatorForm() {
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
field.onChange(e.target.value === '' ? undefined : Number(e.target.value));
// eslint-disable-next-line @typescript-eslint/no-floating-promises
form.handleSubmit(onSubmit)();
}}
@@ -437,11 +387,7 @@ export default function FireCalculatorForm() {
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
field.onChange(e.target.value === '' ? undefined : Number(e.target.value));
// eslint-disable-next-line @typescript-eslint/no-floating-promises
form.handleSubmit(onSubmit)();
}}
@@ -466,11 +412,7 @@ export default function FireCalculatorForm() {
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
field.onChange(e.target.value === '' ? undefined : Number(e.target.value));
// eslint-disable-next-line @typescript-eslint/no-floating-promises
form.handleSubmit(onSubmit)();
}}
@@ -495,11 +437,7 @@ export default function FireCalculatorForm() {
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
field.onChange(e.target.value === '' ? undefined : Number(e.target.value));
// eslint-disable-next-line @typescript-eslint/no-floating-promises
form.handleSubmit(onSubmit)();
}}
@@ -525,11 +463,7 @@ export default function FireCalculatorForm() {
step="0.1"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
field.onChange(e.target.value === '' ? undefined : Number(e.target.value));
// eslint-disable-next-line @typescript-eslint/no-floating-promises
form.handleSubmit(onSubmit)();
}}
@@ -555,11 +489,7 @@ export default function FireCalculatorForm() {
step="0.1"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
field.onChange(e.target.value === '' ? undefined : Number(e.target.value));
// eslint-disable-next-line @typescript-eslint/no-floating-promises
form.handleSubmit(onSubmit)();
}}
@@ -577,20 +507,14 @@ export default function FireCalculatorForm() {
name="desiredMonthlyAllowance"
render={({ field }) => (
<FormItem>
<FormLabel>
Desired Monthly Allowance (Today&apos;s Value)
</FormLabel>
<FormLabel>Desired Monthly Allowance (Today&apos;s Value)</FormLabel>
<FormControl>
<Input
placeholder="e.g., 2000"
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
field.onChange(e.target.value === '' ? undefined : Number(e.target.value));
// eslint-disable-next-line @typescript-eslint/no-floating-promises
form.handleSubmit(onSubmit)();
}}
@@ -610,9 +534,7 @@ export default function FireCalculatorForm() {
name="retirementAge"
render={({ field }) => (
<FormItem>
<FormLabel>
Retirement Age: {field.value as number}
</FormLabel>
<FormLabel>Retirement Age: {field.value as number}</FormLabel>
<FormControl>
<Slider
name="retirementAge"
@@ -637,20 +559,14 @@ export default function FireCalculatorForm() {
name="coastFireAge"
render={({ field }) => (
<FormItem>
<FormLabel>
Coast FIRE Age (Optional) - Stop contributing at age:
</FormLabel>
<FormLabel>Coast FIRE Age (Optional) - Stop contributing at age:</FormLabel>
<FormControl>
<Input
placeholder="e.g., 45 (defaults to Retirement Age)"
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
field.onChange(e.target.value === '' ? undefined : Number(e.target.value));
// eslint-disable-next-line @typescript-eslint/no-floating-promises
form.handleSubmit(onSubmit)();
}}
@@ -668,20 +584,14 @@ export default function FireCalculatorForm() {
name="baristaIncome"
render={({ field }) => (
<FormItem>
<FormLabel>
Barista FIRE Income (Monthly during Retirement)
</FormLabel>
<FormLabel>Barista FIRE Income (Monthly during Retirement)</FormLabel>
<FormControl>
<Input
placeholder="e.g., 1000"
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
field.onChange(e.target.value === '' ? undefined : Number(e.target.value));
// eslint-disable-next-line @typescript-eslint/no-floating-promises
form.handleSubmit(onSubmit)();
}}
@@ -722,7 +632,7 @@ export default function FireCalculatorForm() {
</FormItem>
)}
/>
{form.watch("simulationMode") === "monte-carlo" && (
{form.watch('simulationMode') === 'monte-carlo' && (
<FormField
control={form.control}
name="volatility"
@@ -735,11 +645,7 @@ export default function FireCalculatorForm() {
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
field.onChange(e.target.value === '' ? undefined : Number(e.target.value));
// eslint-disable-next-line @typescript-eslint/no-floating-promises
form.handleSubmit(onSubmit)();
}}
@@ -781,7 +687,7 @@ export default function FireCalculatorForm() {
</FormItem>
)}
/>
{form.watch("withdrawalStrategy") === "percentage" && (
{form.watch('withdrawalStrategy') === 'percentage' && (
<FormField
control={form.control}
name="withdrawalPercentage"
@@ -795,11 +701,7 @@ export default function FireCalculatorForm() {
step="0.1"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
field.onChange(e.target.value === '' ? undefined : Number(e.target.value));
// eslint-disable-next-line @typescript-eslint/no-floating-promises
form.handleSubmit(onSubmit)();
}}
@@ -816,7 +718,8 @@ export default function FireCalculatorForm() {
</div>
{!result && (
<Button type="submit" className="w-full">
<Button type="submit" className="mx-auto w-full max-w-md justify-center" size="lg">
<Calculator className="h-4 w-4" />
Calculate
</Button>
)}
@@ -829,10 +732,7 @@ export default function FireCalculatorForm() {
</CardDescription>
</CardHeader>
<CardContent className="px-2">
<ChartContainer
className="aspect-auto h-80 w-full"
config={{}}
>
<ChartContainer className="aspect-auto h-80 w-full" config={{}}>
<AreaChart
data={result.yearlyData}
margin={{ top: 10, right: 20, left: 20, bottom: 10 }}
@@ -841,14 +741,14 @@ export default function FireCalculatorForm() {
<XAxis
dataKey="year"
label={{
value: "Year",
position: "insideBottom",
value: 'Year',
position: 'insideBottom',
offset: -10,
}}
/>
{/* Right Y axis */}
<YAxis
yAxisId={"right"}
yAxisId={'right'}
orientation="right"
tickFormatter={(value: number) => {
if (value >= 1000000) {
@@ -883,23 +783,9 @@ export default function FireCalculatorForm() {
/>
<ChartTooltip content={tooltipRenderer} />
<defs>
<linearGradient
id="fillBalance"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="5%"
stopColor="var(--color-orange-500)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--color-orange-500)"
stopOpacity={0.1}
/>
<linearGradient id="fillBalance" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-orange-500)" stopOpacity={0.8} />
<stop offset="95%" stopColor="var(--color-orange-500)" stopOpacity={0.1} />
</linearGradient>
</defs>
<Area
@@ -910,10 +796,10 @@ export default function FireCalculatorForm() {
fill="url(#fillBalance)"
fillOpacity={0.9}
activeDot={{ r: 6 }}
yAxisId={"right"}
stackId={"a"}
yAxisId={'right'}
stackId={'a'}
/>
{form.getValues("simulationMode") === "monte-carlo" && (
{form.getValues('simulationMode') === 'monte-carlo' && (
<>
<Area
type="monotone"
@@ -921,7 +807,7 @@ export default function FireCalculatorForm() {
stroke="none"
fill="var(--color-orange-500)"
fillOpacity={0.1}
yAxisId={"right"}
yAxisId={'right'}
connectNulls
/>
<Area
@@ -930,7 +816,7 @@ export default function FireCalculatorForm() {
stroke="none"
fill="var(--color-orange-500)"
fillOpacity={0.1}
yAxisId={"right"}
yAxisId={'right'}
connectNulls
/>
</>
@@ -951,10 +837,10 @@ export default function FireCalculatorForm() {
strokeWidth={2}
strokeDasharray="2 1"
label={{
value: "FIRE Number",
position: "insideBottomRight",
value: 'FIRE Number',
position: 'insideBottomRight',
}}
yAxisId={"right"}
yAxisId={'right'}
/>
)}
{result.fireNumber4percent && showing4percent && (
@@ -964,40 +850,39 @@ export default function FireCalculatorForm() {
strokeWidth={1}
strokeDasharray="1 1"
label={{
value: "4%-Rule FIRE Number",
position: "insideBottomLeft",
value: '4%-Rule FIRE Number',
position: 'insideBottomLeft',
}}
yAxisId={"right"}
yAxisId={'right'}
/>
)}
<ReferenceLine
x={
irlYear +
(Number(form.getValues("retirementAge")) -
Number(form.getValues("currentAge")))
(Number(form.getValues('retirementAge')) -
Number(form.getValues('currentAge')))
}
stroke="var(--primary)"
strokeWidth={2}
label={{
value: "Retirement",
position: "insideTopRight",
value: 'Retirement',
position: 'insideTopRight',
}}
yAxisId={"left"}
yAxisId={'left'}
/>
{result.retirementAge4percent && showing4percent && (
<ReferenceLine
x={
irlYear +
(result.retirementAge4percent -
Number(form.getValues("currentAge")))
(result.retirementAge4percent - Number(form.getValues('currentAge')))
}
stroke="var(--secondary)"
strokeWidth={1}
label={{
value: "4%-Rule Retirement",
position: "insideBottomLeft",
value: '4%-Rule Retirement',
position: 'insideBottomLeft',
}}
yAxisId={"left"}
yAxisId={'left'}
/>
)}
</AreaChart>
@@ -1007,11 +892,15 @@ export default function FireCalculatorForm() {
)}
{result && (
<Button
onClick={() => { setShowing4percent(!showing4percent); }}
variant={showing4percent ? "secondary" : "default"}
size={"sm"}
onClick={() => {
setShowing4percent(!showing4percent);
}}
variant={showing4percent ? 'secondary' : 'default'}
size={'sm'}
className="mt-2 gap-2 self-start"
>
{showing4percent ? "Hide" : "Show"} 4%-Rule
<Percent className="h-4 w-4" />
{showing4percent ? 'Hide' : 'Show'} 4%-Rule
</Button>
)}
</form>
@@ -1032,14 +921,10 @@ export default function FireCalculatorForm() {
<Card>
<CardHeader>
<CardTitle>FIRE Number</CardTitle>
<CardDescription className="text-xs">
Capital at retirement
</CardDescription>
<CardDescription className="text-xs">Capital at retirement</CardDescription>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">
{formatNumber(result.fireNumber)}
</p>
<p className="text-3xl font-bold">{formatNumber(result.fireNumber)}</p>
</CardContent>
</Card>
@@ -1052,8 +937,7 @@ export default function FireCalculatorForm() {
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">
{Number(form.getValues("lifeExpectancy")) -
Number(form.getValues("retirementAge"))}
{Number(form.getValues('lifeExpectancy')) - Number(form.getValues('retirementAge'))}
</p>
</CardContent>
</Card>
@@ -1063,14 +947,11 @@ export default function FireCalculatorForm() {
<CardHeader>
<CardTitle>4%-Rule FIRE Number</CardTitle>
<CardDescription className="text-xs">
Capital needed for 4% of it to be greater than your
yearly allowance
Capital needed for 4% of it to be greater than your yearly allowance
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">
{formatNumber(result.fireNumber4percent)}
</p>
<p className="text-3xl font-bold">{formatNumber(result.fireNumber4percent)}</p>
</CardContent>
</Card>
@@ -1078,14 +959,12 @@ export default function FireCalculatorForm() {
<CardHeader>
<CardTitle>4%-Rule Retirement Duration</CardTitle>
<CardDescription className="text-xs">
Years to enjoy your financial independence if you follow
the 4% rule
Years to enjoy your financial independence if you follow the 4% rule
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">
{Number(form.getValues("lifeExpectancy")) -
(result.retirementAge4percent ?? 0)}
{Number(form.getValues('lifeExpectancy')) - (result.retirementAge4percent ?? 0)}
</p>
</CardContent>
</Card>

View File

@@ -0,0 +1,44 @@
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Quote } from "lucide-react";
export function Testimonials() {
return (
<section className="my-16 grid gap-6 md:grid-cols-3">
<Card className="bg-card border-none shadow-md">
<CardHeader className="pb-2">
<Quote className="h-8 w-8 text-primary/20" />
</CardHeader>
<CardContent>
<p className="mb-4 text-lg italic text-muted-foreground">
&quot;I always struggled with the math behind early retirement. This calculator made it click instantly. Seeing the graph change in real-time is a game changer.&quot;
</p>
<p className="font-semibold">- Sarah J., Software Engineer</p>
</CardContent>
</Card>
<Card className="bg-card border-none shadow-md">
<CardHeader className="pb-2">
<Quote className="h-8 w-8 text-primary/20" />
</CardHeader>
<CardContent>
<p className="mb-4 text-lg italic text-muted-foreground">
&quot;Most FIRE calculators are too simple. I love that I can toggle Monte Carlo simulations to see if my plan survives a market crash. Highly recommended.&quot;
</p>
<p className="font-semibold">- Mike T., Financial Analyst</p>
</CardContent>
</Card>
<Card className="bg-card border-none shadow-md">
<CardHeader className="pb-2">
<Quote className="h-8 w-8 text-primary/20" />
</CardHeader>
<CardContent>
<p className="mb-4 text-lg italic text-muted-foreground">
&quot;The inflation adjustment feature is crucial. It showed me I needed to save a bit more to be truly safe, but now I sleep better knowing the real numbers.&quot;
</p>
<p className="font-semibold">- Emily R., Teacher (Coast FIRE)</p>
</CardContent>
</Card>
</section>
);
}

View File

@@ -1,21 +1,35 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import FireCalculatorForm from "../FireCalculatorForm";
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi, beforeAll } from "vitest";
// Mocking ResizeObserver because it's not available in jsdom and Recharts uses it
// Mocking ResizeObserver
class ResizeObserver {
observe() {
// Mock implementation
}
unobserve() {
// Mock implementation
}
disconnect() {
// Mock implementation
}
observe() { /* noop */ }
unobserve() { /* noop */ }
disconnect() { /* noop */ }
}
global.ResizeObserver = ResizeObserver;
// Fix for Radix UI pointer capture error in JSDOM
beforeAll(() => {
window.HTMLElement.prototype.hasPointerCapture = vi.fn();
window.HTMLElement.prototype.setPointerCapture = vi.fn();
window.HTMLElement.prototype.releasePointerCapture = vi.fn();
window.HTMLElement.prototype.scrollIntoView = vi.fn();
});
// Mock Recharts ResponsiveContainer
vi.mock("recharts", async () => {
const originalModule = await vi.importActual("recharts");
return {
...originalModule,
ResponsiveContainer: ({ children }: { children: React.ReactNode }) => (
<div style={{ width: "500px", height: "300px" }}>{children}</div>
),
};
});
describe("FireCalculatorForm", () => {
it("renders the form with default values", () => {
render(<FireCalculatorForm />);
@@ -23,13 +37,15 @@ describe("FireCalculatorForm", () => {
expect(screen.getByText("FIRE Calculator")).toBeInTheDocument();
expect(screen.getByLabelText(/Starting Capital/i)).toHaveValue(50000);
expect(screen.getByLabelText(/Monthly Savings/i)).toHaveValue(1500);
expect(screen.getByLabelText(/Current Age/i)).toHaveValue(25);
});
it("calculates and displays results when submitted", async () => {
const user = userEvent.setup();
render(<FireCalculatorForm />);
const calculateButton = screen.getByRole("button", { name: /Calculate/i });
fireEvent.click(calculateButton);
await user.click(calculateButton);
await waitFor(() => {
expect(screen.getByText("Financial Projection")).toBeInTheDocument();
@@ -38,25 +54,81 @@ describe("FireCalculatorForm", () => {
});
it("allows changing inputs", () => {
// using fireEvent for reliability with number inputs in jsdom
render(<FireCalculatorForm />);
const savingsInput = screen.getByLabelText(/Monthly Savings/i);
fireEvent.change(savingsInput, { target: { value: "2000" } });
expect(savingsInput).toHaveValue(2000);
});
it("validates inputs", async () => {
const user = userEvent.setup();
render(<FireCalculatorForm />);
const ageInput = screen.getByLabelText(/Current Age/i);
fireEvent.change(ageInput, { target: { value: "-5" } });
fireEvent.blur(ageInput); // Trigger validation
// Find validation error (might need to adjust selector based on how FormMessage renders)
// Expecting some error message about min value
// Since validation happens on submit mostly in this form unless touched, lets try submit
const calculateButton = screen.getByRole("button", { name: /Calculate/i });
fireEvent.click(calculateButton);
// You might need to adjust what text to look for based on zod schema messages
const ageInput = screen.getByLabelText(/Current Age/i);
// Use fireEvent to set invalid value directly
fireEvent.change(ageInput, { target: { value: "-5" } });
const calculateButton = screen.getByRole("button", { name: /Calculate/i });
await user.click(calculateButton);
// Look for error message text
expect(await screen.findByText(/Age must be at least 1/i)).toBeInTheDocument();
});
it("toggles Monte Carlo simulation mode", async () => {
const user = userEvent.setup();
render(<FireCalculatorForm />);
// Select Trigger
const modeTrigger = screen.getByRole("combobox", { name: /Simulation Mode/i });
await user.click(modeTrigger);
// Select Monte Carlo from dropdown
const monteCarloOption = await screen.findByRole("option", { name: /Monte Carlo/i });
await user.click(monteCarloOption);
// Verify Volatility input appears
expect(await screen.findByLabelText(/Market Volatility/i)).toBeInTheDocument();
});
it("toggles 4% Rule overlay", async () => {
const user = userEvent.setup();
render(<FireCalculatorForm />);
// Calculate first to show results
const calculateButton = screen.getByRole("button", { name: /Calculate/i });
await user.click(calculateButton);
// Wait for results
await waitFor(() => {
expect(screen.getByText("Financial Projection")).toBeInTheDocument();
});
// Find the Show 4%-Rule button
const showButton = screen.getByRole("button", { name: /Show 4%-Rule/i });
await user.click(showButton);
// Should now see 4%-Rule stats
expect(await screen.findByText("4%-Rule FIRE Number")).toBeInTheDocument();
// Button text should change
expect(screen.getByRole("button", { name: /Hide 4%-Rule/i })).toBeInTheDocument();
});
it("handles withdrawal strategy selection", async () => {
const user = userEvent.setup();
render(<FireCalculatorForm />);
const strategyTrigger = screen.getByRole("combobox", { name: /Withdrawal Strategy/i });
await user.click(strategyTrigger);
const percentageOption = await screen.findByRole("option", { name: /Percentage of Portfolio/i });
await user.click(percentageOption);
expect(await screen.findByLabelText(/Withdrawal Percentage/i)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,106 @@
'use client';
import { Line, LineChart, CartesianGrid, XAxis, YAxis } from 'recharts';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from '@/components/ui/chart';
// Simulation
// Standard: Start 25, Retire 65. Save $10k/yr.
// Coast: Start 25, Save $30k/yr until 35. Then $0.
// Return: 7%
const generateData = () => {
const data = [];
let standardBal = 0;
let coastBal = 0;
const rate = 1.07;
for (let age = 25; age <= 65; age++) {
data.push({
age,
Standard: Math.round(standardBal),
Coast: Math.round(coastBal),
});
// Standard: consistent
standardBal = (standardBal + 10000) * rate;
// Coast: heavy early, then stop
if (age < 35) {
coastBal = (coastBal + 30000) * rate;
} else {
coastBal = coastBal * rate;
}
}
return data;
};
const data = generateData();
const chartConfig = {
Standard: {
label: 'Standard Path',
color: 'var(--chart-4)',
},
Coast: {
label: 'Coast FIRE',
color: 'var(--chart-1)',
},
} satisfies ChartConfig;
export function CoastFireChart() {
return (
<Card className="w-full">
<CardHeader>
<CardTitle>Coast FIRE vs. Standard Path</CardTitle>
<CardDescription>
Comparing heavy early savings (Coast) vs. consistent saving (Standard)
</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="aspect-auto h-[300px] w-full">
<LineChart data={data}>
<CartesianGrid vertical={false} />
<XAxis dataKey="age" tickLine={false} axisLine={false} tickMargin={8} />
<YAxis
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value: number) => `$${String(value / 1000)}k`}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
labelFormatter={(value) => `Age ${String(value)}`}
indicator="line"
/>
}
/>
<Line
dataKey="Standard"
type="natural"
stroke="var(--color-Standard)"
strokeWidth={2}
dot={false}
/>
<Line
dataKey="Coast"
type="natural"
stroke="var(--color-Coast)"
strokeWidth={2}
dot={false}
/>
<ChartLegend content={<ChartLegendContent />} />
</LineChart>
</ChartContainer>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,71 @@
import { ArrowRight, Banknote, Coins, Landmark, TrendingUp, Wallet } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export function FireFlowchart() {
return (
<Card className="w-full overflow-hidden">
<CardHeader className="pb-2 text-center">
<CardTitle>The FIRE Engine</CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="flex flex-col items-center justify-center gap-4 md:flex-row md:items-start">
{/* Step 1: Income */}
<div className="flex flex-col items-center gap-2">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-green-100 text-green-600 dark:bg-green-900 dark:text-green-300">
<Banknote className="h-8 w-8" />
</div>
<span className="font-bold">Income</span>
<p className="w-32 text-center text-xs text-muted-foreground">Maximize earnings & side hustles</p>
</div>
<ArrowRight className="hidden h-8 w-8 text-muted-foreground md:block rotate-90 md:rotate-0" />
{/* Step 2: Expenses */}
<div className="flex flex-col items-center gap-2">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-red-100 text-red-600 dark:bg-red-900 dark:text-red-300">
<Wallet className="h-8 w-8" />
</div>
<span className="font-bold">Low Expenses</span>
<p className="w-32 text-center text-xs text-muted-foreground">Frugality & mindful spending</p>
</div>
<ArrowRight className="hidden h-8 w-8 text-muted-foreground md:block rotate-90 md:rotate-0" />
{/* Step 3: Savings Gap */}
<div className="flex flex-col items-center gap-2">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-300">
<Coins className="h-8 w-8" />
</div>
<span className="font-bold">Savings Gap</span>
<p className="w-32 text-center text-xs text-muted-foreground">The difference is your fuel</p>
</div>
<ArrowRight className="hidden h-8 w-8 text-muted-foreground md:block rotate-90 md:rotate-0" />
{/* Step 4: Investments */}
<div className="flex flex-col items-center gap-2">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-purple-100 text-purple-600 dark:bg-purple-900 dark:text-purple-300">
<TrendingUp className="h-8 w-8" />
</div>
<span className="font-bold">Investments</span>
<p className="w-32 text-center text-xs text-muted-foreground">Index funds & compounding</p>
</div>
<ArrowRight className="hidden h-8 w-8 text-muted-foreground md:block rotate-90 md:rotate-0" />
{/* Step 5: Freedom */}
<div className="flex flex-col items-center gap-2">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-orange-100 text-orange-600 dark:bg-orange-900 dark:text-orange-300">
<Landmark className="h-8 w-8" />
</div>
<span className="font-bold">Freedom</span>
<p className="w-32 text-center text-xs text-muted-foreground">Work becomes optional</p>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,99 @@
'use client';
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from '@/components/ui/chart';
// Simulation data for 4% rule
const storyData = [
{ year: 0, w3: 100, w4: 100, w5: 100 },
{ year: 1, w3: 105, w4: 104, w5: 103 },
{ year: 2, w3: 90, w4: 88, w5: 86 }, // Crash
{ year: 3, w3: 95, w4: 92, w5: 89 },
{ year: 4, w3: 102, w4: 98, w5: 94 },
{ year: 5, w3: 110, w4: 105, w5: 100 },
{ year: 10, w3: 150, w4: 130, w5: 110 },
{ year: 15, w3: 200, w4: 160, w5: 100 }, // 5% starts dragging
{ year: 20, w3: 280, w4: 200, w5: 80 },
{ year: 25, w3: 380, w4: 250, w5: 40 },
{ year: 30, w3: 500, w4: 300, w5: 0 },
];
const chartConfig = {
year: {
label: 'Year',
},
w3: {
label: '3% Withdrawal (Safe)',
color: 'var(--chart-1)',
},
w4: {
label: '4% Withdrawal (Standard)',
color: 'var(--chart-2)',
},
w5: {
label: '5% Withdrawal (Risky)',
color: 'var(--chart-3)',
},
} satisfies ChartConfig;
export function FourPercentRuleChart() {
return (
<Card className="w-full">
<CardHeader>
<CardTitle>Portfolio Survival Scenarios</CardTitle>
<CardDescription>
Impact of initial withdrawal rate on portfolio longevity (Start: $1M)
</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="aspect-auto h-[300px] w-full">
<AreaChart data={storyData}>
<defs>
<linearGradient id="fillW3" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-w3)" stopOpacity={0.8} />
<stop offset="95%" stopColor="var(--color-w3)" stopOpacity={0.1} />
</linearGradient>
<linearGradient id="fillW4" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-w4)" stopOpacity={0.8} />
<stop offset="95%" stopColor="var(--color-w4)" stopOpacity={0.1} />
</linearGradient>
<linearGradient id="fillW5" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-w5)" stopOpacity={0.8} />
<stop offset="95%" stopColor="var(--color-w5)" stopOpacity={0.1} />
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis dataKey="year" tickLine={false} axisLine={false} tickMargin={8} />
<YAxis
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => `${String(value)}%`}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
labelFormatter={(value) => `Year ${String(value)}`}
indicator="dot"
/>
}
/>
<Area dataKey="w3" type="natural" fill="url(#fillW3)" stroke="var(--color-w3)" />
<Area dataKey="w4" type="natural" fill="url(#fillW4)" stroke="var(--color-w4)" />
<Area dataKey="w5" type="natural" fill="url(#fillW5)" stroke="var(--color-w5)" />
<ChartLegend content={<ChartLegendContent />} />
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,187 @@
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { CoastFireChart } from '@/app/components/charts/CoastFireChart';
import { AuthorBio } from '@/app/components/AuthorBio';
export const metadata = {
title: `Coast FIRE vs. Lean FIRE: Which Strategy Is Right For You? (${new Date().getFullYear().toString()})`,
description:
'Compare Coast FIRE (front-loading savings) with Lean FIRE (minimalist living). See the math, pros, cons, and find your path to freedom.',
openGraph: {
title: 'Coast FIRE vs. Lean FIRE: The Ultimate Comparison',
description:
"Don't just retire early—retire smarter. We break down the two most popular alternative FIRE strategies.",
type: 'article',
url: 'https://investingfire.com/learn/coast-fire-vs-lean-fire',
},
};
export default function CoastVsLeanPage() {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: 'Coast FIRE vs. Lean FIRE: Which Strategy Is Right For You?',
author: {
'@type': 'Organization',
name: 'InvestingFIRE Team',
},
publisher: {
'@type': 'Organization',
name: 'InvestingFIRE',
logo: {
'@type': 'ImageObject',
url: 'https://investingfire.com/apple-icon.png',
},
},
datePublished: '2025-01-20',
description:
'Compare Coast FIRE vs Lean FIRE strategies to find your best path to financial independence.',
};
return (
<article className="container mx-auto max-w-3xl px-4 py-12">
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
{/* Breadcrumb */}
<nav className="text-muted-foreground mb-6 text-sm">
<Link href="/" className="hover:text-primary">
Home
</Link>
<span className="mx-2">/</span>
<Link href="/learn" className="hover:text-primary">
Learn
</Link>
<span className="mx-2">/</span>
<span className="text-foreground">Coast vs. Lean FIRE</span>
</nav>
<header className="mb-10">
<h1 className="mb-6 text-4xl font-extrabold tracking-tight lg:text-5xl">
Coast FIRE vs. Lean FIRE <br />
<span className="text-primary">Choosing Your Path to Freedom</span>
</h1>
<p className="text-muted-foreground text-xl leading-relaxed">
Traditional FIRE requires a massive nest egg. But what if you could retire sooner by tweaking
the variables? Enter <strong>Coast FIRE</strong> and <strong>Lean FIRE</strong>two powerful
strategies for those who want freedom without the wait.
</p>
</header>
<div className="prose prose-lg dark:prose-invert max-w-none">
<h2>The Quick Summary</h2>
<p>Not sure which one fits you? Here is the high-level breakdown:</p>
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-primary">🏖 Coast FIRE</CardTitle>
</CardHeader>
<CardContent>
<ul className="mt-0 list-disc space-y-2 pl-4">
<li>
<strong>Goal:</strong> Save enough <em>early</em> so compound interest covers your
retirement.
</li>
<li>
<strong>Lifestyle:</strong> Work to cover <em>current</em> expenses only.
</li>
<li>
<strong>Best For:</strong> Young professionals with high savings rates who want to
&quot;downshift&quot; careers later.
</li>
</ul>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-green-600">🌱 Lean FIRE</CardTitle>
</CardHeader>
<CardContent>
<ul className="mt-0 list-disc space-y-2 pl-4">
<li>
<strong>Goal:</strong> Retire completely on a smaller budget (e.g., $30k-$40k/year).
</li>
<li>
<strong>Lifestyle:</strong> Minimalist, frugal, simple living.
</li>
<li>
<strong>Best For:</strong> People who hate their jobs and value time over luxury.
</li>
</ul>
</CardContent>
</Card>
</div>
<h2 className="mt-12">Deep Dive: Coast FIRE</h2>
<p>
<strong>Coast FIRE</strong> is about reaching a &quot;tipping point&quot; where you no longer
need to contribute to your retirement accounts. Your existing investments, left alone to
compound for 10-20 years, will grow into a full retirement fund.
</p>
<p>
Once you hit your Coast number, you only need to earn enough money to pay your monthly bills.
This opens the door to:
</p>
<ul>
<li>Switching to a lower-stress job</li>
<li>Working part-time</li>
<li>Taking sabbaticals</li>
</ul>
<div className="my-8">
<CoastFireChart />
</div>
<h2 className="mt-12">Deep Dive: Lean FIRE</h2>
<p>
<strong>Lean FIRE</strong> attacks the equation from the expense side. By drastically lowering
your cost of living, you lower your required FIRE number.
</p>
<p>
If you can live happily on $35,000 a year, you &quot;only&quot; need $875,000 to retire (based
on the 4% rule). Compare that to a &quot;Fat FIRE&quot; lifestyle spending $100,000, which
requires $2.5 million. Lean FIRE is the fastest path out of the workforce, but it requires
discipline.
</p>
<Separator className="my-12" />
<h2>Run The Numbers</h2>
<p>The best way to decide is to see the math. Use our calculator to simulate both scenarios:</p>
<ol>
<li>
<strong>For Coast FIRE:</strong> Input your current age and a &quot;Coast Age&quot; (e.g.,
35). See if your current balance grows enough by age 60 without adding more.
</li>
<li>
<strong>For Lean FIRE:</strong> Lower your &quot;Desired Monthly Allowance&quot; to a
minimalist level and see how fast you reach freedom.
</li>
</ol>
<div className="my-10 text-center">
<Link href="/">
<Button size="lg" className="text-lg">
Compare Strategies with the Calculator
</Button>
</Link>
</div>
<h2>Which Should You Choose?</h2>
<p>
You don&apos;t have to pick one today. Many people start with a <strong>Lean FIRE</strong>{' '}
mindset to save aggressively, then transition to <strong>Coast FIRE</strong> once they have a
safety net, allowing them to enjoy their 30s and 40s more.
</p>
<p>
The most important step is to just <strong>start</strong>.
</p>
<AuthorBio />
</div>
</article>
);
}

107
src/app/learn/page.tsx Normal file
View File

@@ -0,0 +1,107 @@
import Link from 'next/link';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
export const metadata = {
title: 'Learn FIRE | Financial Independence Guides & Resources',
description:
'Master the art of Financial Independence and Early Retirement. Deep dives into safe withdrawal rates, asset allocation, and FIRE strategies.',
};
export default function LearnHubPage() {
return (
<div className="container mx-auto max-w-4xl px-4 py-12">
<div className="mb-12 text-center">
<h1 className="mb-4 text-4xl font-extrabold tracking-tight lg:text-5xl">FIRE Knowledge Base</h1>
<p className="text-muted-foreground text-xl">
Everything you need to know to leave the rat race behind.
</p>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{/* Article 1 */}
<Link href="/learn/what-is-fire" className="transition-transform hover:scale-[1.02]">
<Card className="hover:border-primary/50 h-full cursor-pointer border-2">
<CardHeader>
<div className="mb-2">
<span className="rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900 dark:text-green-300">
Beginner
</span>
</div>
<CardTitle className="text-2xl">What is FIRE?</CardTitle>
<CardDescription>
The comprehensive guide to Financial Independence, Retire Early.
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-sm">
Understand the core philosophy, the math behind the movement, and how to start your
journey today.
</p>
</CardContent>
</Card>
</Link>
{/* Article 2 */}
<Link
href="/learn/safe-withdrawal-rate-4-percent-rule"
className="transition-transform hover:scale-[1.02]"
>
<Card className="hover:border-primary/50 h-full cursor-pointer border-2">
<CardHeader>
<div className="mb-2">
<span className="rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-300">
Strategy
</span>
</div>
<CardTitle className="text-2xl">The 4% Rule Explained</CardTitle>
<CardDescription>
Is it still safe in {new Date().getFullYear().toString()}? A data-driven look at
withdrawal rates.
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-sm">
Dive into the Trinity Study, sequence of returns risk, and how to bulletproof your
retirement income.
</p>
</CardContent>
</Card>
</Link>
{/* Article 3 */}
<Link href="/learn/coast-fire-vs-lean-fire" className="transition-transform hover:scale-[1.02]">
<Card className="hover:border-primary/50 h-full cursor-pointer border-2">
<CardHeader>
<div className="mb-2">
<span className="rounded-full bg-purple-100 px-2.5 py-0.5 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-300">
Comparison
</span>
</div>
<CardTitle className="text-2xl">Coast FIRE vs. Lean FIRE</CardTitle>
<CardDescription>Which strategy fits your lifestyle?</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-sm">
Comparing different flavors of financial independence to find your perfect fit.
Front-load your savings or minimize your expenses?
</p>
</CardContent>
</Card>
</Link>
</div>
<div className="bg-muted mt-16 rounded-xl p-8 text-center">
<h2 className="mb-4 text-2xl font-bold">Ready to see the numbers?</h2>
<p className="text-muted-foreground mb-6">
Put theory into practice with our interactive projection tool.
</p>
<Link
href="/"
className="bg-primary text-primary-foreground ring-offset-background hover:bg-primary/90 focus-visible:ring-ring inline-flex h-10 items-center justify-center rounded-md px-8 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50"
>
Launch Calculator
</Link>
</div>
</div>
);
}

View File

@@ -0,0 +1,165 @@
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Info } from 'lucide-react';
import { FourPercentRuleChart } from '@/app/components/charts/FourPercentRuleChart';
import { AuthorBio } from '@/app/components/AuthorBio';
export const metadata = {
title: 'Safe Withdrawal Rates & The 4% Rule Explained (2025 Update)',
description: `Is the 4% rule safe in ${new Date().getFullYear().toString()}? We analyze the Trinity Study, sequence of returns risk, and variable withdrawal strategies for a bulletproof retirement.`,
openGraph: {
title: 'Safe Withdrawal Rates & The 4% Rule Explained',
description: "Don't run out of money. Understanding the math behind safe retirement withdrawals.",
type: 'article',
url: 'https://investingfire.com/learn/safe-withdrawal-rate-4-percent-rule',
},
};
export default function SafeWithdrawalPage() {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: `Safe Withdrawal Rates & The 4% Rule Explained (${new Date().getFullYear().toString()} Update)`,
author: {
'@type': 'Organization',
name: 'InvestingFIRE Team',
},
publisher: {
'@type': 'Organization',
name: 'InvestingFIRE',
logo: {
'@type': 'ImageObject',
url: 'https://investingfire.com/apple-icon.png',
},
},
datePublished: '2025-01-15',
description: `Is the 4% rule safe in ${new Date().getFullYear().toString()}? Analysis of the Trinity Study and modern withdrawal strategies.`,
};
return (
<article className="container mx-auto max-w-3xl px-4 py-12">
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<nav className="text-muted-foreground mb-6 text-sm">
<Link href="/" className="hover:text-primary">
Home
</Link>
<span className="mx-2">/</span>
<Link href="/learn" className="hover:text-primary">
Learn
</Link>
<span className="mx-2">/</span>
<span className="text-foreground">Safe Withdrawal Rates</span>
</nav>
<header className="mb-10">
<h1 className="mb-6 text-4xl font-extrabold tracking-tight lg:text-5xl">
The 4% Rule Explained: <br />
<span className="text-primary">Is It Safe in {new Date().getFullYear().toString()}?</span>
</h1>
<p className="text-muted-foreground text-xl leading-relaxed">
The &quot;4% Rule&quot; is the bedrock of the FIRE movement. But originally published in 1994,
does it hold up against modern inflation and market valuations? Let&apos;s look at the data.
</p>
</header>
<div className="prose prose-lg dark:prose-invert max-w-none">
<h2>What is the 4% Rule?</h2>
<p>
The rule comes from the <strong>Trinity Study</strong> (1998), which looked at historical
stock/bond portfolios to see how often they would last for 30 years given various withdrawal
rates.
</p>
<p>
The Conclusion: A portfolio of 50% stocks and 50% bonds survived{' '}
<strong>95% of the time</strong> over 30-year periods when the retiree withdrew 4% of the
initial balance, adjusted annually for inflation.
</p>
<Alert className="my-6">
<Info className="h-4 w-4" />
<AlertTitle>Key Distinction</AlertTitle>
<AlertDescription>
<span>
The 4% is based on your <span className="italic">initial</span> portfolio value. If you
start with $1M, you withdraw $40k. In year 2, if inflation was 3%, you withdraw
$41,200regardless of whether the market is up or down.
</span>
</AlertDescription>
</Alert>
<div className="my-8">
<FourPercentRuleChart />
</div>
<h2>The Problem with 4% in {new Date().getFullYear().toString()}</h2>
<p>
While 4% worked historically, many experts argue it might be too aggressive for early retirees
today. Why?
</p>
<ul className="list-disc pl-5">
<li>
<strong>Longer Horizons:</strong> The Trinity Study looked at 30 years. If you retire at 35,
you might need your money to last 50 or 60 years.
</li>
<li>
<strong>Valuations:</strong> When stock market valuations (CAPE ratios) are high, future
returns tend to be lower.
</li>
<li>
<strong>Sequence of Returns Risk:</strong> If the market crashes right after you retire (like
in 2000 or 2008), depleting your portfolio early can make it impossible to recover, even if
the market rebounds later.
</li>
</ul>
<h2>Better Alternatives: Variable Withdrawal Rates</h2>
<p>
Instead of a rigid &quot;blind&quot; withdrawal, modern FIRE strategies suggest being dynamic.
</p>
<h3>1. The &quot;Guardrails&quot; Approach</h3>
<p>
If the market drops significantly, you cut your spending (e.g., skip the vacation, eat out
less). If the market booms, you give yourself a raise. This flexibility massively increases
your portfolio&apos;s success rate.
</p>
<h3>2. Lower the Initial Rate</h3>
<p>
Many cautious early retirees target a <strong>3.25% to 3.5%</strong> withdrawal rate. This
virtually guarantees capital preservation across almost all historical scenarios, even extended
bear markets.
</p>
<h2>Simulate Your Safe Rate</h2>
<p>
Reading about it is one thing; seeing it is another. We&apos;ve built these scenarios directly
into our calculator.
</p>
<p>
Go to the calculator, expand the advanced options (or check the &quot;Simulation Mode&quot; if
available), and switch between &quot;Deterministic&quot; (Fixed return) and &quot;Monte
Carlo&quot; (Randomized) to see how volatility impacts your success chance.
</p>
<div className="my-8 text-center">
<Link href="/?simulationMode=monte-carlo">
<Button size="lg" variant="secondary" className="text-lg">
Run Monte Carlo Simulation
</Button>
</Link>
</div>
<h2>Conclusion</h2>
<p>
The 4% rule is a fantastic rule of thumb for planning, but a dangerous rule of law for
execution. Use it to set your savings target, but remain flexible once you actually pull the
trigger on retirement.
</p>
<AuthorBio />
</div>
</article>
);
}

View File

@@ -0,0 +1,191 @@
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { FireFlowchart } from '@/app/components/charts/FireFlowchart';
import { AuthorBio } from '@/app/components/AuthorBio';
export const metadata = {
title: `What is FIRE? The Ultimate Guide to Financial Independence (${new Date().getFullYear().toString()})`,
description:
'Discover the FIRE movement (Financial Independence, Retire Early). Learn how to calculate your FIRE number, savings rate, and retire decades ahead of schedule.',
openGraph: {
title: 'What is FIRE? The Ultimate Guide to Financial Independence',
description: 'Stop trading time for money. The comprehensive guide to regaining your freedom.',
type: 'article',
url: 'https://investingfire.com/learn/what-is-fire',
},
};
export default function WhatIsFirePage() {
// JSON-LD for SEO
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: 'What is FIRE? The Ultimate Guide to Financial Independence',
author: {
'@type': 'Organization',
name: 'InvestingFIRE Team',
},
publisher: {
'@type': 'Organization',
name: 'InvestingFIRE',
logo: {
'@type': 'ImageObject',
url: 'https://investingfire.com/apple-icon.png',
},
},
datePublished: '2025-01-15',
description:
'Discover the FIRE movement. Learn how to calculate your FIRE number, savings rate, and retire decades ahead of schedule.',
};
return (
<article className="container mx-auto max-w-3xl px-4 py-12">
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
{/* Breadcrumb */}
<nav className="text-muted-foreground mb-6 text-sm">
<Link href="/" className="hover:text-primary">
Home
</Link>
<span className="mx-2">/</span>
<Link href="/learn" className="hover:text-primary">
Learn
</Link>
<span className="mx-2">/</span>
<span className="text-foreground">What is FIRE?</span>
</nav>
<header className="mb-10">
<h1 className="mb-6 text-4xl font-extrabold tracking-tight lg:text-5xl">
What Is FIRE? <br />
<span className="text-primary">The Modern Guide to Financial Freedom</span>
</h1>
<p className="text-muted-foreground text-xl leading-relaxed">
FIRE stands for <strong>Financial Independence, Retire Early</strong>. Its not just about
quitting your jobits about reaching a point where work is optional, and your assets generate
enough income to cover your lifestyle forever.
</p>
</header>
<div className="prose prose-lg dark:prose-invert max-w-none">
<p>
Imagine waking up on a Monday morning without an alarm clock. You don&apos;t have to rush to a
commute, sit in traffic, or answer to a boss. Instead, you have the ultimate luxury:{' '}
<strong>ownership of your time</strong>.
</p>
<div className="bg-card my-8 rounded-lg border p-6 shadow-sm">
<h3 className="mt-0 text-xl font-semibold">💡 The Core Equation</h3>
<p className="mb-4">
FIRE isn&apos;t magic; it&apos;s math. The speed at which you can retire depends on one
primary variable: <strong>Your Savings Rate</strong>.
</p>
<p className="mb-0">
<span className="text-primary font-mono">
High Income - Low Expenses = High Savings = Freedom
</span>
</p>
</div>
<div className="my-8">
<FireFlowchart />
</div>
<h2>The 3 Pillars of FIRE</h2>
<p>To achieve financial independence, you need to optimize three levers:</p>
<ol>
<li>
<strong>Spend Less (Frugality):</strong> Cutting unnecessary costs is the most powerful lever
because it has a double effect: it increases your savings <em>and</em> lowers the amount you
need to save forever.
</li>
<li>
<strong>Earn More (Income):</strong> There is a floor to how much you can cut, but no ceiling
to how much you can earn. Side hustles, career growth, and upskilling are key.
</li>
<li>
<strong>Invest Wisely (Growth):</strong> Your money must work for you. Low-cost index funds
(like VTSAX) are the vehicle of choice for the FIRE community due to their diversification
and low fees.
</li>
</ol>
<h2>What is &quot;The Number&quot;?</h2>
<p>
Your <strong>FIRE Number</strong> is the net worth you need to retire. The most common rule of
thumb is the <strong>Rule of 25</strong>:
</p>
<blockquote>
<p className="text-foreground text-xl font-medium not-italic">
Annual Expenses × 25 = FIRE Number
</p>
</blockquote>
<p>
For example, if you spend <strong>$40,000</strong> per year, you need{' '}
<strong>$1,000,000</strong> invested. This is based on the <em>4% Rule</em>, which suggests you
can withdraw 4% of your portfolio in the first year of retirement (adjusted for inflation
thereafter) with a high probability of not running out of money.
</p>
<div className="my-8 text-center">
<Link href="/">
<Button size="lg" className="text-lg">
Calculate Your FIRE Number Now
</Button>
</Link>
</div>
<h2>Types of FIRE</h2>
<p>FIRE isn&apos;t one-size-fits-all. Over the years, several variations have emerged:</p>
<ul>
<li>
<strong>Lean FIRE:</strong> Retiring on a budget (e.g., less than $40k/year). Great for
minimalists.
</li>
<li>
<strong>Fat FIRE:</strong> Retiring with abundance (e.g., $100k+/year). Requires a larger
nest egg but offers a luxurious lifestyle.
</li>
<li>
<strong>Barista FIRE:</strong> Reaching a portfolio size where you still work part-time
(perhaps as a barista) for benefits or extra cash, reducing withdrawal pressure.
</li>
<li>
<strong>Coast FIRE:</strong> Saving enough early on so that compound interest alone will hit
your retirement target by age 65, allowing you to stop saving and just cover expenses.
</li>
</ul>
<Separator className="my-8" />
<h2>Why {new Date().getFullYear().toString()} Changes Things</h2>
<p>
In {new Date().getFullYear().toString()}, we face unique challenges: higher inflation than the
previous decade and potentially lower future stock market returns. This makes{' '}
<strong>flexibility</strong> essential.
</p>
<p>
Static calculators often fail to capture this nuance. That&apos;s why{' '}
<Link href="/" className="text-primary hover:underline">
InvestingFIRE.com
</Link>{' '}
allows you to adjust inflation assumptions and growth rates dynamically, helping you
stress-test your plan against modern economic reality.
</p>
<h2>Conclusion</h2>
<p>
FIRE is more than a financial goal; it&apos;s a lifestyle design choice. It asks the question:{' '}
<em>&quot;What would you do if money were no object?&quot;</em>
</p>
<p>
Start by tracking your expenses, calculating your savings rate, and running your numbers. The
best time to plant a tree was 20 years ago. The second best time is today.
</p>
<AuthorBio />
</div>
</article>
);
}

View File

@@ -1,75 +1,71 @@
import Image from "next/image";
import FireCalculatorForm from "./components/FireCalculatorForm";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import Footer from "./components/footer";
import BackgroundPattern from "./components/BackgroundPattern";
import Image from 'next/image';
import FireCalculatorForm from './components/FireCalculatorForm';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
import BackgroundPattern from './components/BackgroundPattern';
import { Testimonials } from './components/Testimonials';
export default function HomePage() {
const faqData = {
"@context": "https://schema.org",
"@type": "FAQPage",
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: [
{
"@type": "Question",
name: "What methodology does this calculator use?",
'@type': 'Question',
name: 'What methodology does this calculator use?',
acceptedAnswer: {
"@type": "Answer",
'@type': 'Answer',
text: "We run a multi-year projection in two phases: 1. Accumulation: Your balance grows by CAGR and you add monthly savings. 2. Retirement: The balance continues compounding, but you withdraw an inflation-adjusted monthly allowance. The result: a precise estimate of the capital you'll have at retirement (your “FIRE Number”) and how long it will last until your chosen life expectancy.",
},
},
{
"@type": "Question",
'@type': 'Question',
name: "Why isn't this just the 4% rule?",
acceptedAnswer: {
"@type": "Answer",
'@type': 'Answer',
text: "The 4% rule is a useful starting point (25× annual spending), but it assumes a fixed withdrawal rate with inflation adjustments and doesn't model ongoing savings or dynamic market returns. Our calculator simulates each year's growth, contributions, and inflation-indexed withdrawals to give you a tailored picture.",
},
},
{
"@type": "Question",
name: "How do I choose a realistic growth rate?",
'@type': 'Question',
name: 'How do I choose a realistic growth rate?',
acceptedAnswer: {
"@type": "Answer",
text: "Historically, a diversified portfolio of equities and bonds has returned around 7-10% per year before inflation. We recommend starting around 6-8% (net of fees), then running “what-if” scenarios—5% on the conservative side, 10% on the aggressive side—to see how they affect your timeline.",
'@type': 'Answer',
text: 'Historically, a diversified portfolio of equities and bonds has returned around 7-10% per year before inflation. We recommend starting around 6-8% (net of fees), then running “what-if” scenarios—5% on the conservative side, 10% on the aggressive side—to see how they affect your timeline.',
},
},
{
"@type": "Question",
name: "How does inflation factor into my FIRE Number?",
'@type': 'Question',
name: 'How does inflation factor into my FIRE Number?',
acceptedAnswer: {
"@type": "Answer",
'@type': 'Answer',
text: "Cost of living rises. To maintain today's lifestyle, your monthly allowance must grow each year by your inflation rate. This calculator automatically inflates your desired monthly spending and subtracts it from your portfolio during retirement, ensuring your FIRE Number keeps pace with rising expenses.",
},
},
{
"@type": "Question",
name: "Can I really retire early with FIRE?",
'@type': 'Question',
name: 'Can I really retire early with FIRE?',
acceptedAnswer: {
"@type": "Answer",
text: "Early retirement is achievable with disciplined saving, smart investing, and realistic assumptions. This tool helps you set targets, visualize outcomes, and adjust inputs—so you can build confidence in your plan and make informed trade-offs between lifestyle, risk, and timeline.",
'@type': 'Answer',
text: 'Early retirement is achievable with disciplined saving, smart investing, and realistic assumptions. This tool helps you set targets, visualize outcomes, and adjust inputs—so you can build confidence in your plan and make informed trade-offs between lifestyle, risk, and timeline.',
},
},
{
"@type": "Question",
name: "How should I use this calculator effectively?",
'@type': 'Question',
name: 'How should I use this calculator effectively?',
acceptedAnswer: {
"@type": "Answer",
text: "Start with your actual numbers (capital, savings, age). Set conservative - mid - aggressive growth rates to bound possibilities. Slide your retirement age to explore “early” vs. “traditional” scenarios. Review the chart—especially the reference lines—to see when you hit FI and how withdrawals impact your balance. Experiment with higher savings rates or lower target spending to accelerate your path.",
'@type': 'Answer',
text: 'Start with your actual numbers (capital, savings, age). Set conservative - mid - aggressive growth rates to bound possibilities. Slide your retirement age to explore “early” vs. “traditional” scenarios. Review the chart—especially the reference lines—to see when you hit FI and how withdrawals impact your balance. Experiment with higher savings rates or lower target spending to accelerate your path.',
},
},
],
};
return (
<main className="text-primary-foreground to-destructive from-secondary flex min-h-screen flex-col items-center bg-linear-to-b p-2">
<div className="from-background via-primary/10 to-secondary/10 text-foreground relative flex min-h-screen w-full flex-col items-center overflow-hidden bg-gradient-to-b px-4 pt-6 pb-16">
<BackgroundPattern />
<div className="z-10 mx-auto flex flex-col items-center justify-center gap-4 text-center">
<div className="mt-8 flex flex-row flex-wrap items-center justify-center gap-4 align-middle">
<div className="flex flex-row flex-wrap items-center justify-center gap-4 align-middle">
<Image
priority
unoptimized
@@ -78,48 +74,55 @@ export default function HomePage() {
width={100}
height={100}
/>
<h1 className="from-primary via-primary-foreground to-primary bg-linear-to-r bg-clip-text text-5xl font-extrabold tracking-tight text-transparent drop-shadow-md sm:text-[5rem]">
<h1 className="from-primary via-accent to-primary bg-linear-to-r bg-clip-text text-5xl font-extrabold tracking-tight text-transparent drop-shadow-md sm:text-[5rem]">
InvestingFIRE
</h1>
</div>
<span className="bg-primary/15 text-primary rounded-full px-4 py-2 text-xs font-semibold tracking-wide uppercase shadow-sm">
100% free built for educational use
</span>
<p className="text-primary-foreground/90 text-xl font-semibold md:text-2xl">
The #1 FIRE Calculator
</p>
<p className="text-foreground/80 max-w-2xl text-base text-balance md:text-lg">
Plan your path to financial independence with transparent mathad-free and built to teach you
how FIRE works.
</p>
<div className="mt-8 w-full max-w-2xl">
<FireCalculatorForm />
</div>
</div>
<div className="z-10 mx-auto max-w-4xl px-4">
<Testimonials />
</div>
{/* Added SEO Content Sections */}
<div className="z-10 mx-auto max-w-2xl py-12 text-left">
<section className="mb-12">
<h2 className="mb-4 text-3xl font-bold">
What Is FIRE? Understanding Financial Independence and Early
Retirement
What Is FIRE? Understanding Financial Independence and Early Retirement
</h2>
<p className="mb-4 text-lg leading-relaxed">
FIRE stands for{" "}
<strong>Financial Independence, Retire Early</strong>. It&apos;s a
lifestyle movement built around two core ideas:
FIRE stands for <strong>Financial Independence, Retire Early</strong>. It&apos;s a lifestyle
movement built around two core ideas:
</p>
<ul className="mb-4 ml-6 list-disc space-y-2 text-lg">
<li>
<strong>Aggressive saving & investing</strong>often 50%+ of
incomeso your capital grows rapidly.
<strong>Aggressive saving & investing</strong>often 50%+ of incomeso your capital grows
rapidly.
</li>
<li>
<strong>Passive-income coverage</strong>when your investment
returns exceed your living expenses, you gain freedom from a
traditional 9-5.
<strong>Passive-income coverage</strong>when your investment returns exceed your living
expenses, you gain freedom from a traditional 9-5.
</li>
</ul>
<p className="text-lg leading-relaxed">
By reaching your personal <em>FIRE Number</em>the nest egg needed
to cover your inflation-adjusted spendingyou unlock the option to
step away from a daily paycheck and pursue passion projects, travel,
family, or anything else. This calculator helps you simulate your
journey, estimate how much you need, and visualize both your
accumulation phase and your retirement withdrawals over time.
By reaching your personal <em>FIRE Number</em>the nest egg needed to cover your
inflation-adjusted spendingyou unlock the option to step away from a daily paycheck and
pursue passion projects, travel, family, or anything else. This calculator helps you simulate
your journey, estimate how much you need, and visualize both your accumulation phase and your
retirement withdrawals over time.
</p>
</section>
@@ -128,58 +131,50 @@ export default function HomePage() {
How This FIRE Calculator Provides Investing Insights
</h2>
<p className="mb-4 text-lg leading-relaxed">
Our interactive tool goes beyond a simple 25x annual spending
rule. It runs a <strong>year-by-year simulation</strong> of your
portfolio, combining:
Our interactive tool goes beyond a simple 25x annual spending rule. It runs a{' '}
<strong>year-by-year simulation</strong> of your portfolio, combining:
</p>
<ul className="mb-4 ml-6 list-disc space-y-2 text-lg">
<li>
<strong>Starting Capital</strong>your current invested balance
</li>
<li>
<strong>Monthly Savings</strong>ongoing contributions to your
portfolio
<strong>Monthly Savings</strong>ongoing contributions to your portfolio
</li>
<li>
<strong>Expected Annual Growth Rate (CAGR)</strong>compounding
returns before inflation
<strong>Expected Annual Growth Rate (CAGR)</strong>compounding returns before inflation
</li>
<li>
<strong>Annual Inflation Rate</strong>to inflate your target
withdrawal each year
<strong>Annual Inflation Rate</strong>to inflate your target withdrawal each year
</li>
<li>
<strong>Desired Monthly Allowance</strong>today&apos;s-value
spending goal
<strong>Desired Monthly Allowance</strong>today&apos;s-value spending goal
</li>
<li>
<strong>Retirement Age & Life Expectancy</strong>defines your
accumulation horizon and payout period
<strong>Retirement Age & Life Expectancy</strong>defines your accumulation horizon and
payout period
</li>
</ul>
<p className="text-lg leading-relaxed">Key features:</p>
<ul className="mb-4 ml-6 list-disc space-y-2 text-lg">
<li>
<strong>Real-time calculation</strong>as you tweak any input,
your FIRE Number and chart update instantly.
<strong>Real-time calculation</strong>as you tweak any input, your FIRE Number and chart
update instantly.
</li>
<li>
<strong>Interactive chart</strong> with area plots for both{" "}
<em>portfolio balance</em> and{" "}
<em>inflation-adjusted allowance</em>, plus reference lines
showing your retirement date and required FIRE Number.
<strong>Interactive chart</strong> with area plots for both <em>portfolio balance</em> and{' '}
<em>inflation-adjusted allowance</em>, plus reference lines showing your retirement date
and required FIRE Number.
</li>
<li>
<strong>Custom simulation</strong>switches from accumulation
(adding savings) to retirement (withdrawing allowance),
compounding each year based on your growth rate.
<strong>Custom simulation</strong>switches from accumulation (adding savings) to
retirement (withdrawing allowance), compounding each year based on your growth rate.
</li>
</ul>
<p className="text-lg leading-relaxed">
With this level of granularity, you can confidently experiment with
savings rate, target retirement age, and investment assumptions to
discover how small tweaks speed up or delay your path to financial
independence.
With this level of granularity, you can confidently experiment with savings rate, target
retirement age, and investment assumptions to discover how small tweaks speed up or delay
your path to financial independence.
</p>
</section>
@@ -188,9 +183,7 @@ export default function HomePage() {
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqData) }}
/>
<h2 className="mb-4 text-3xl font-bold">
FIRE & Investing Frequently Asked Questions (FAQ)
</h2>
<h2 className="mb-4 text-3xl font-bold">FIRE & Investing Frequently Asked Questions (FAQ)</h2>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1">
@@ -201,18 +194,16 @@ export default function HomePage() {
We run a multi-year projection in two phases:
<ol className="ml-6 list-decimal space-y-1">
<li>
<strong>Accumulation:</strong> Your balance grows by CAGR
and you add monthly savings.
<strong>Accumulation:</strong> Your balance grows by CAGR and you add monthly
savings.
</li>
<li>
<strong>Retirement:</strong> The balance continues
compounding, but you withdraw an inflation-adjusted monthly
allowance.
<strong>Retirement:</strong> The balance continues compounding, but you withdraw an
inflation-adjusted monthly allowance.
</li>
</ol>
The result: a precise estimate of the capital you&apos;ll have
at retirement (your FIRE Number) and how long it will last
until your chosen life expectancy.
The result: a precise estimate of the capital you&apos;ll have at retirement (your FIRE
Number) and how long it will last until your chosen life expectancy.
</AccordionContent>
</AccordionItem>
@@ -221,12 +212,10 @@ export default function HomePage() {
Why isn&apos;t this just the 4% rule?
</AccordionTrigger>
<AccordionContent className="text-lg leading-relaxed">
The 4% rule is a useful starting point (25× annual spending),
but it assumes a fixed withdrawal rate with inflation
adjustments and doesn&apos;t model ongoing savings or dynamic
market returns. Our calculator simulates each year&apos;s
growth, contributions, and inflation-indexed withdrawals to give
you a tailored picture.
The 4% rule is a useful starting point (25× annual spending), but it assumes a fixed
withdrawal rate with inflation adjustments and doesn&apos;t model ongoing savings or
dynamic market returns. Our calculator simulates each year&apos;s growth, contributions,
and inflation-indexed withdrawals to give you a tailored picture.
</AccordionContent>
</AccordionItem>
@@ -235,11 +224,10 @@ export default function HomePage() {
How do I choose a realistic growth rate?
</AccordionTrigger>
<AccordionContent className="text-lg leading-relaxed">
Historically, a diversified portfolio of equities and bonds has
returned around 7-10% per year before inflation. We recommend
starting around 6-8% (net of fees), then running what-if
scenarios5% on the conservative side, 10% on the aggressive
sideto see how they affect your timeline.
Historically, a diversified portfolio of equities and bonds has returned around 7-10% per
year before inflation. We recommend starting around 6-8% (net of fees), then running
what-if scenarios5% on the conservative side, 10% on the aggressive sideto see how
they affect your timeline.
</AccordionContent>
</AccordionItem>
@@ -248,11 +236,10 @@ export default function HomePage() {
How does inflation factor into my FIRE Number?
</AccordionTrigger>
<AccordionContent className="text-lg leading-relaxed">
Cost of living rises. To maintain today&apos;s lifestyle, your
monthly allowance must grow each year by your inflation rate.
This calculator automatically inflates your desired monthly
spending and subtracts it from your portfolio during retirement,
ensuring your FIRE Number keeps pace with rising expenses.
Cost of living rises. To maintain today&apos;s lifestyle, your monthly allowance must
grow each year by your inflation rate. This calculator automatically inflates your
desired monthly spending and subtracts it from your portfolio during retirement, ensuring
your FIRE Number keeps pace with rising expenses.
</AccordionContent>
</AccordionItem>
@@ -261,11 +248,10 @@ export default function HomePage() {
Can I really retire early with FIRE?
</AccordionTrigger>
<AccordionContent className="text-lg leading-relaxed">
Early retirement is achievable with disciplined saving, smart
investing, and realistic assumptions. This tool helps you set
targets, visualize outcomes, and adjust inputsso you can build
confidence in your plan and make informed trade-offs between
lifestyle, risk, and timeline.
Early retirement is achievable with disciplined saving, smart investing, and realistic
assumptions. This tool helps you set targets, visualize outcomes, and adjust inputsso
you can build confidence in your plan and make informed trade-offs between lifestyle,
risk, and timeline.
</AccordionContent>
</AccordionItem>
@@ -275,24 +261,16 @@ export default function HomePage() {
</AccordionTrigger>
<AccordionContent className="text-lg leading-relaxed">
<ul className="ml-6 list-disc space-y-1">
<li>Start with your actual numbers (capital, savings, age).</li>
<li>Set conservative - mid - aggressive growth rates to bound possibilities.</li>
<li>Slide your retirement age to explore early vs. traditional scenarios.</li>
<li>
Start with your actual numbers (capital, savings, age).
Review the chartespecially the reference linesto see when you hit FI and how
withdrawals impact your balance.
</li>
<li>
Set conservative - mid - aggressive growth rates to bound
possibilities.
</li>
<li>
Slide your retirement age to explore early vs.
traditional scenarios.
</li>
<li>
Review the chartespecially the reference linesto see when
you hit FI and how withdrawals impact your balance.
</li>
<li>
Experiment with higher savings rates or lower target
spending to accelerate your path.
Experiment with higher savings rates or lower target spending to accelerate your
path.
</li>
</ul>
</AccordionContent>
@@ -302,34 +280,27 @@ export default function HomePage() {
{/* Optional: Add a section for relevant resources/links here */}
<section className="mb-12">
<h2 className="mb-4 text-3xl font-bold">
FIRE Journey & Investing Resources
</h2>
<h2 className="mb-4 text-3xl font-bold">FIRE Journey & Investing Resources</h2>
<p className="mb-6 text-lg leading-relaxed">
Ready to deepen your knowledge and build a bullet-proof plan? Below
are some of our favorite blogs, books, tools, and communities for
financial independence and smart investing.
Ready to deepen your knowledge and build a bullet-proof plan? Below are some of our favorite
blogs, books, tools, and communities for financial independence and smart investing.
</p>
<div className="bg-foreground my-8 rounded-md p-4 text-lg">
<p className="font-semibold">Getting Started with FIRE:</p>
<ol className="ml-6 list-decimal space-y-1">
<li>
Run your first projection above to find your target FIRE Number.
</li>
<li>Run your first projection above to find your target FIRE Number.</li>
<li>Identify areas to boost savings or reduce expenses.</li>
<li>Study index-fund strategies and low-cost investing advice.</li>
<li>
Study index-fund strategies and low-cost investing advice.
</li>
<li>
Join{" "}
Join{' '}
<a
href="https://www.reddit.com/r/Fire/"
target="_blank"
className="text-primary hover:underline"
>
supportive communities like r/Fire
</a>{" "}
</a>{' '}
to learn from real journeys.
</li>
</ol>
@@ -346,7 +317,7 @@ export default function HomePage() {
className="text-primary hover:underline"
>
Mr. Money Mustache
</a>{" "}
</a>{' '}
- Hardcore frugality & early retirement success stories.
</li>
<li>
@@ -356,7 +327,7 @@ export default function HomePage() {
className="text-primary hover:underline"
>
Playing With FIRE
</a>{" "}
</a>{' '}
- Community resources & real-life case studies.
</li>
<li>
@@ -366,7 +337,7 @@ export default function HomePage() {
className="text-primary hover:underline"
>
r/Fire
</a>{" "}
</a>{' '}
- Active forum for questions, tips, and support.
</li>
</ul>
@@ -382,7 +353,7 @@ export default function HomePage() {
className="text-primary hover:underline"
>
Your Money or Your Life
</a>{" "}
</a>{' '}
- The classic guide to aligning money with values.
</li>
<li>
@@ -392,7 +363,7 @@ export default function HomePage() {
className="text-primary hover:underline"
>
BiggerPockets Money Podcast
</a>{" "}
</a>{' '}
- Interviews on FIRE strategies and wealth building.
</li>
<li>
@@ -402,26 +373,19 @@ export default function HomePage() {
className="text-primary hover:underline"
>
InvestingFIRE Calculator Demo
</a>{" "}
- Deep dive on how interactive projections can guide your
plan.
</a>{' '}
- Deep dive on how interactive projections can guide your plan.
</li>
</ul>
</div>
<div>
<h3 className="mb-3 text-xl font-semibold">
Additional Calculators & Tools
</h3>
<h3 className="mb-3 text-xl font-semibold">Additional Calculators & Tools</h3>
<ul className="ml-6 list-disc space-y-2 text-lg">
<li>
<a
href="https://ghostfol.io"
target="_blank"
className="text-primary hover:underline"
>
<a href="https://ghostfol.io" target="_blank" className="text-primary hover:underline">
Ghostfolio
</a>{" "}
</a>{' '}
- Wealth management application for individuals.
</li>
<li>
@@ -431,9 +395,8 @@ export default function HomePage() {
className="text-primary hover:underline"
>
Coast FIRE Calculator
</a>{" "}
- When you max out early contributions but let compounding
do the rest.
</a>{' '}
- When you max out early contributions but let compounding do the rest.
</li>
<li>
<a
@@ -442,7 +405,7 @@ export default function HomePage() {
className="text-primary hover:underline"
>
Compound Interest Calculator
</a>{" "}
</a>{' '}
- Explore the power of growth rates in isolation.
</li>
</ul>
@@ -450,7 +413,6 @@ export default function HomePage() {
</div>
</section>
</div>
<Footer />
</main>
</div>
);
}