Compare commits

..

29 Commits

Author SHA1 Message Date
5f90724cf6 update SEO texts 2025-05-01 20:14:21 +02:00
ad4b86ef74 add footer 2025-05-01 20:13:08 +02:00
f96648e162 fix allowance y-axis and chart styling 2025-05-01 20:12:55 +02:00
541c443efd auto update on value change 2025-05-01 17:24:45 +02:00
0d12ab9a47 break out functions from export 2025-05-01 15:56:01 +02:00
09e9485f2f redesigned algorith, use user specified retirement age 2025-05-01 15:25:22 +02:00
383625aede shadcn slider 2025-05-01 13:57:54 +02:00
4d7a936721 new strategy human algo 2025-04-30 23:17:48 +02:00
886afab1ef styling, graph sizing and number precision 2025-04-30 20:05:38 +02:00
0f6cd57f3d result style 2025-04-30 19:40:53 +02:00
ffb8e8d506 prettier 2025-04-30 19:20:25 +02:00
c3867ccbd4 fix faq chevron 2025-04-30 18:12:23 +02:00
6bc7be6336 add logo 2025-04-30 18:06:14 +02:00
26d2ec68b8 fix lint errors 2025-04-29 22:49:23 +02:00
5e0ff2891a attempt new formula 2025-04-29 20:29:56 +02:00
1a0428a8e0 lets not be so strict 2025-04-29 20:25:49 +02:00
9267018d06 Select 2025-04-29 20:15:54 +02:00
acec849428 tracking + web vitals 2025-04-29 20:09:37 +02:00
1e9f2cbc2d env 2025-04-29 20:09:07 +02:00
9c460bab22 SEO 2025-04-29 19:55:15 +02:00
4e7705ce53 SEO 2025-04-29 19:32:09 +02:00
d5962bbf9e fixes 2025-04-29 19:22:01 +02:00
64669e5f58 FIRE chart 2025-04-29 19:11:09 +02:00
f05f3fe37c new algorithm 2025-04-29 18:45:58 +02:00
896b0bf063 fix and add charts 2025-04-29 18:45:41 +02:00
716bcc6fef SEO 2025-04-29 18:33:19 +02:00
31415c10a2 FIRE calculator 2025-04-29 18:32:26 +02:00
fe03807739 shadcn 2025-04-29 17:46:38 +02:00
30d27a212e initial files 2025-04-29 17:09:04 +02:00
16 changed files with 7400 additions and 5648 deletions

View File

@@ -1,31 +0,0 @@
name: Lint
on:
pull_request:
push:
branches:
- "**" # matches every branch
jobs:
lint_and_typecheck:
name: Lint and Typecheck
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Run check
run: pnpm run check

109
README.md
View File

@@ -1,108 +1,3 @@
![InvestingFIRE logo](/src/app/apple-icon.png)
# fire
# InvestingFIRE 🔥 — The #1 Interactive FIRE Calculator
**InvestingFIRE** is a responsive web application for calculating your path to Financial Independence and Early Retirement (FIRE). It features a year-by-year projection engine that simulates both accumulation (savings and investment growth) and retirement (withdrawals) phases, allowing users to:
- Input starting capital, monthly savings, expected annual growth rate, inflation rate, current age, desired retirement age, life expectancy, and desired monthly retirement allowance.
- View a dynamic chart displaying projected portfolio balance and monthly allowance over time.
- Instantly see their estimated “FIRE number” (required capital at retirement), how long their capital will last, and compare results to the “4% rule.”
- Adjust assumptions live, with all calculations and visualizations updating automatically.
- Access explanatory content about FIRE methodology, key variables, and additional community resources, all on a single, consolidated page.
The projects code is structured using React/Next.js with TypeScript, focusing on user experience, modern UI components, and clarity of financial assumptions.
---
## 🚀 Features at a Glance
- **⚡️ Real-Time Projections:** Every field updates the chart _as you type_. Experiment with savings, growth rates, inflation, or retirement age and see your future instantly.
- **📈 Interactive Chart:** Dual-area plots for portfolio value and future monthly spending, plus reference lines for FIRE milestones and “4% rule” legends.
- **🧠 Education Baked In:** Contextual tooltips, deep-dive sections on how FIRE works, FAQs, and must-read resources included.
- **🔎 Detailed Methodology:** Not just a 25x rule — runs a full, year-by-year simulation with inflation-adjusted withdrawals and optional 4%-rule overlays.
- **👌 Modern UX:** Typing, sliding, or clicking feels _good_. Responsive on all devices.
---
## 🧰 How It Works
The calculator models your FIRE journey in two phases:
1. **Accumulation:**
- Your starting capital is grown by your expected CAGR (~7% by default).
- Monthly savings are added for each year until retirement.
- Every variable can be adjusted live (capital, savings, age, growth, inflation, spending, target retirement).
2. **Retirement:**
- Your balance continues to grow by CAGR.
- Each year, an inflation-adjusted monthly allowance is withdrawn.
- The simulation runs until your selected life expectancy, showing the possibility of portfolio depletion.
**Key Outputs:**
- 🔥 “FIRE Number”: Portfolio value at your defined retirement age
- 📊 Interactive projection chart: See how your nest egg and withdrawals evolve over time
- 4⃣ “4% Rule” overlays: Compare dynamic results to classic rule-of-thumb
---
## 🌟 Try It For Yourself
To run locally:
1. **Clone the repo**
```bash
git clone https://git.schulze.network/schulze/fire.git
cd fire
```
2. **Install dependencies**
```bash
pnpm install
```
3. **Run the app**
```bash
pnpm run dev
```
4. Visit [http://localhost:3000](http://localhost:3000) and unleash the fire.
Deployed version: [https://investingfire.com](https://investingfire.com)
---
## ✏️ Inputs & Variables
- **Starting Capital** — How much youve already invested
- **Monthly Savings** — What youll add each month
- **Current Age & Retirement Age** — Your FI timeline
- **Life Expectancy** — How long do you want income to last?
- **Expected Growth Rate (CAGR)** — Portfolio annual % return, before inflation
- **Inflation Rate** — Cost of living increases
- **Desired Monthly Allowance** — Your lifestyle, todays dollars
As you adjust these, all projections update instantly _without needing to hit “Calculate.”_
Try many “what ifs” fast.
---
## 👩‍💻 Contributing
Pull requests are welcome! Open issues for bugs, new features, or debate about safe withdrawal rates and tax assumptions.
---
## 📄 License
[GPL-3.0](./LICENSE)
---
## 🥇 Why Use InvestingFIRE?
- You want the truth — not just a 4% fantasy.
- You want to learn, not just punch in numbers.
- You want clarity, speed, and modern UI.
- You want to show your friends the best FIRE tool on the web.
Enjoy the _rocket ride_ to financial independence.
**InvestingFIRE — Know your number. Change your future.**
FIRE calculator

7300
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -6,12 +6,14 @@
"scripts": {
"build": "next build",
"check": "next lint && tsc --noEmit",
"dev": "next dev --turbopack",
"dev": "next dev --turbo",
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
"lint": "next lint",
"lint:fix": "next lint --fix",
"preview": "next build && next start",
"start": "next start"
"start": "next start",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
@@ -20,38 +22,37 @@
"@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-slider": "^1.3.2",
"@radix-ui/react-slot": "^1.2.0",
"@t3-oss/env-nextjs": "^0.13.0",
"@t3-oss/env-nextjs": "^0.12.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.525.0",
"next": "^15.4.1",
"lucide-react": "^0.503.0",
"next": "^15.2.3",
"next-plausible": "^3.12.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.56.1",
"recharts": "^2.15.3",
"tailwind-merge": "^3.2.0",
"zod": "^4.0.0"
"zod": "^3.24.3"
},
"devDependencies": {
"@eslint/eslintrc": "3.3.1",
"@tailwindcss/postcss": "4.1.11",
"@types/node": "22.16.3",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"eslint": "9.31.0",
"eslint-config-next": "15.4.1",
"eslint-plugin-react-hooks": "5.2.0",
"postcss": "8.5.6",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.6.14",
"tailwindcss": "4.1.11",
"tw-animate-css": "1.3.5",
"typescript": "5.8.3",
"typescript-eslint": "8.36.0"
"@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.0.15",
"@types/node": "^20.14.10",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"eslint": "^9.23.0",
"eslint-config-next": "^15.2.3",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^4.0.15",
"tw-animate-css": "^1.2.8",
"typescript": "^5.8.2",
"typescript-eslint": "^8.27.0"
},
"ct3aMetadata": {
"initVersion": "7.39.3"
},
"packageManager": "pnpm@10.13.1"
"packageManager": "npm@11.2.0"
}

5055
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
ignoredBuiltDependencies:
- unrs-resolver
onlyBuiltDependencies:
- '@tailwindcss/oxide'
- sharp

View File

@@ -1 +0,0 @@
wgu5fuk8d5j5wp3pjtta9vrw8d9by9qk

View File

@@ -1,11 +0,0 @@
{
"extends": ["config:best-practices", ":semanticCommits"],
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
"automerge": true,
"automergeType": "branch"
}
]
}

View File

@@ -1,157 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import {
type LucideIcon,
HandCoins,
Bitcoin,
Coins,
DollarSign,
Euro,
IndianRupee,
JapaneseYen,
PiggyBank,
PoundSterling,
Wallet,
Banknote,
ChartCandlestick,
CirclePercent,
CreditCard,
Gem,
Receipt,
ShoppingBasket,
Rocket,
RockingChair,
Sparkles,
ChartPie,
ChartBar,
BarChart3,
ChartLine,
TrendingDown,
TrendingUp,
Vault,
Landmark,
Briefcase,
Handshake,
Shield,
Lock,
CalendarRange,
Hourglass,
Sprout,
Target,
} from "lucide-react";
export default function MultiIconPattern({ opacity = 0.2, spacing = 160 }) {
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
const [rows, setRows] = useState(0);
const [columns, setColumns] = useState(0);
useEffect(() => {
const updateDimensions = () => {
if (window.innerWidth > width + spacing * 2) {
setWidth(window.innerWidth);
}
if (window.innerHeight > height + spacing * 2) {
setHeight(window.innerHeight);
}
};
updateDimensions();
window.addEventListener("resize", updateDimensions);
return () => {
window.removeEventListener("resize", updateDimensions);
};
}, [height, width, spacing]);
useEffect(() => {
setColumns(Math.ceil(width / spacing) + 3);
}, [width, spacing]);
useEffect(() => {
setRows(Math.ceil(height / spacing) + 3);
}, [height, spacing]);
// Explicitly type the array as LucideIcon[]
const iconComponents: LucideIcon[] = [
HandCoins,
Bitcoin,
Coins,
DollarSign,
Euro,
IndianRupee,
JapaneseYen,
PiggyBank,
PoundSterling,
Wallet,
Banknote,
ChartCandlestick,
CirclePercent,
CreditCard,
Gem,
Receipt,
ShoppingBasket,
Rocket,
RockingChair,
Sparkles,
ChartPie,
ChartBar,
BarChart3,
ChartLine,
TrendingDown,
TrendingUp,
Vault,
Landmark,
Briefcase,
Handshake,
Shield,
Lock,
CalendarRange,
Hourglass,
Sprout,
Target,
];
const renderIcons = ({
rows,
columns,
}: {
rows: number;
columns: number;
}) => {
const icons = [];
for (let y = 0; y < rows; y++) {
for (let x = 0; x < columns; x++) {
// Pick a random icon component from the array
const randomIndex = Math.floor(Math.random() * iconComponents.length);
const IconComponent = iconComponents[randomIndex]!;
// Slightly randomize size and position for more organic feel
const size = 28 + Math.floor(Math.random() * 8);
const xOffset = Math.floor(Math.random() * (spacing / 1.618));
const yOffset = Math.floor(Math.random() * (spacing / 1.618));
icons.push(
<IconComponent
key={`icon-${x}-${y}`}
size={size}
className="text-primary fixed"
style={{
left: `${x * spacing + xOffset}px`,
top: `${y * spacing + yOffset}px`,
opacity: opacity,
transform: `rotate(${Math.round((Math.random() - 0.5) * 30)}deg)`,
}}
/>,
);
}
}
return icons;
};
return (
<div className="absolute h-full w-full">
{width > 0 && renderIcons({ rows, columns })}
</div>
);
}

View File

@@ -73,16 +73,12 @@ 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;
}
@@ -105,8 +101,8 @@ const tooltipRenderer = ({
return (
<div className="bg-background border p-2 shadow-sm">
<p className="font-medium">{`Year: ${data.year.toString()} (Age: ${data.age.toString()})`}</p>
<p className="text-orange-500">{`Balance: ${formatNumber(data.balance)}`}</p>
<p className="text-red-600">{`Monthly allowance: ${formatNumber(data.monthlyAllowance)}`}</p>
<p className="text-chart-1">{`Balance: ${formatNumber(data.balance)}`}</p>
<p className="text-chart-2">{`Monthly allowance: ${formatNumber(data.monthlyAllowance)}`}</p>
<p>{`Phase: ${data.phase === "accumulation" ? "Accumulation" : "Retirement"}`}</p>
</div>
);
@@ -117,10 +113,9 @@ const tooltipRenderer = ({
export default function FireCalculatorForm() {
const [result, setResult] = useState<CalculationResult | null>(null);
const irlYear = new Date().getFullYear();
const [showing4percent, setShowing4percent] = useState(false);
// Initialize form with default values
const form = useForm<z.input<typeof formSchema>, undefined, FormValues>({
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
startingCapital: 50000,
@@ -128,7 +123,7 @@ export default function FireCalculatorForm() {
currentAge: 25,
cagr: 7,
desiredMonthlyAllowance: 3000,
inflationRate: 2.3,
inflationRate: 2,
lifeExpectancy: 84,
retirementAge: 55,
},
@@ -154,10 +149,8 @@ export default function FireCalculatorForm() {
age: age,
year: irlYear,
balance: startingCapital,
untouchedBalance: startingCapital,
phase: "accumulation",
monthlyAllowance: 0,
untouchedMonthlyAllowance: initialMonthlyAllowance,
monthlyAllowance: initialMonthlyAllowance,
});
// Calculate accumulation phase (before retirement)
@@ -182,18 +175,13 @@ export default function FireCalculatorForm() {
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,
monthlyAllowance: inflatedAllowance,
});
}
@@ -204,23 +192,9 @@ export default function FireCalculatorForm() {
);
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,
});
@@ -228,8 +202,6 @@ export default function FireCalculatorForm() {
// Set the result
setResult({
fireNumber: retirementData.balance,
fireNumber4percent: fireNumber4percent,
retirementAge4percent: retirementAge4percent,
yearlyData: yearlyData,
});
}
@@ -258,18 +230,11 @@ export default function FireCalculatorForm() {
<Input
placeholder="e.g., 10000"
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
{...field}
onChange={(value) => {
field.onChange(value);
void form.handleSubmit(onSubmit)();
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormControl>
<FormMessage />
@@ -286,18 +251,11 @@ export default function FireCalculatorForm() {
<Input
placeholder="e.g., 500"
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
{...field}
onChange={(value) => {
field.onChange(value);
void form.handleSubmit(onSubmit)();
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormControl>
<FormMessage />
@@ -314,18 +272,11 @@ export default function FireCalculatorForm() {
<Input
placeholder="e.g., 30"
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
{...field}
onChange={(value) => {
field.onChange(value);
void form.handleSubmit(onSubmit)();
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormControl>
<FormMessage />
@@ -342,18 +293,11 @@ export default function FireCalculatorForm() {
<Input
placeholder="e.g., 90"
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
{...field}
onChange={(value) => {
field.onChange(value);
void form.handleSubmit(onSubmit)();
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormControl>
<FormMessage />
@@ -371,18 +315,11 @@ export default function FireCalculatorForm() {
placeholder="e.g., 7"
type="number"
step="0.1"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
{...field}
onChange={(value) => {
field.onChange(value);
void form.handleSubmit(onSubmit)();
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormControl>
<FormMessage />
@@ -400,18 +337,11 @@ export default function FireCalculatorForm() {
placeholder="e.g., 2"
type="number"
step="0.1"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
{...field}
onChange={(value) => {
field.onChange(value);
void form.handleSubmit(onSubmit)();
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormControl>
<FormMessage />
@@ -430,18 +360,11 @@ export default function FireCalculatorForm() {
<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={(value) => {
field.onChange(value);
void form.handleSubmit(onSubmit)();
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormControl>
<FormMessage />
@@ -455,18 +378,16 @@ export default function FireCalculatorForm() {
name="retirementAge"
render={({ field }) => (
<FormItem>
<FormLabel>
Retirement Age: {field.value as number}
</FormLabel>
<FormLabel>Retirement Age: {field.value}</FormLabel>
<FormControl>
<Slider
name="retirementAge"
value={[field.value as number]}
value={[field.value]}
min={25}
max={75}
step={1}
onValueChange={(value: number[]) => {
field.onChange(value[0]);
onValueChange={(value) => {
field.onChange(...value);
void form.handleSubmit(onSubmit)();
}}
className="py-4"
@@ -509,10 +430,10 @@ export default function FireCalculatorForm() {
offset: -10,
}}
/>
{/* Right Y axis */}
{/* Left Y axis */}
<YAxis
yAxisId={"right"}
orientation="right"
yAxisId={"left"}
orientation="left"
tickFormatter={(value: number) => {
if (value >= 1000000) {
return `${(value / 1000000).toPrecision(3)}M`;
@@ -526,13 +447,11 @@ export default function FireCalculatorForm() {
return value.toString();
}}
width={30}
stroke="var(--color-orange-500)"
tick={{}}
/>
{/* Left Y axis */}
{/* Right Y axis */}
<YAxis
yAxisId="left"
orientation="left"
yAxisId="right"
orientation="right"
tickFormatter={(value: number) => {
if (value >= 1000000) {
return `${(value / 1000000).toPrecision(3)}M`;
@@ -542,7 +461,6 @@ export default function FireCalculatorForm() {
return value.toString();
}}
width={30}
stroke="var(--color-red-600)"
/>
<ChartTooltip content={tooltipRenderer} />
<defs>
@@ -555,12 +473,12 @@ export default function FireCalculatorForm() {
>
<stop
offset="5%"
stopColor="var(--color-orange-500)"
stopColor="var(--chart-1)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--color-orange-500)"
stopColor="var(--chart-1)"
stopOpacity={0.1}
/>
</linearGradient>
@@ -569,55 +487,43 @@ export default function FireCalculatorForm() {
type="monotone"
dataKey="balance"
name="balance"
stroke="var(--color-orange-500)"
stroke="var(--chart-1)"
fill="url(#fillBalance)"
fillOpacity={0.9}
activeDot={{ r: 6 }}
yAxisId={"right"}
yAxisId={"left"}
stackId={"a"}
/>
<Area
type="step"
type="monotone"
dataKey="monthlyAllowance"
name="allowance"
stroke="var(--color-red-600)"
stroke="var(--chart-2)"
fill="none"
activeDot={{ r: 6 }}
yAxisId="left"
yAxisId="right"
stackId={"a"}
/>
{result.fireNumber && (
<ReferenceLine
y={result.fireNumber}
stroke="var(--primary)"
strokeWidth={2}
strokeDasharray="2 1"
stroke="var(--chart-3)"
strokeWidth={1}
strokeDasharray="2 2"
label={{
value: "FIRE Number",
position: "insideBottomRight",
}}
yAxisId={"right"}
/>
)}
{result.fireNumber4percent && showing4percent && (
<ReferenceLine
y={result.fireNumber4percent}
stroke="var(--secondary)"
strokeWidth={1}
strokeDasharray="1 1"
label={{
value: "4%-Rule FIRE Number",
position: "insideBottomLeft",
}}
yAxisId={"right"}
yAxisId={"left"}
/>
)}
<ReferenceLine
x={
irlYear +
(Number(form.getValues("retirementAge")) -
Number(form.getValues("currentAge")))
(form.getValues("retirementAge") -
form.getValues("currentAge"))
}
stroke="var(--primary)"
stroke="var(--chart-2)"
strokeWidth={2}
label={{
value: "Retirement",
@@ -625,36 +531,11 @@ export default function FireCalculatorForm() {
}}
yAxisId={"left"}
/>
{result.retirementAge4percent && showing4percent && (
<ReferenceLine
x={
irlYear +
(result.retirementAge4percent -
Number(form.getValues("currentAge")))
}
stroke="var(--secondary)"
strokeWidth={1}
label={{
value: "4%-Rule Retirement",
position: "insideBottomLeft",
}}
yAxisId={"left"}
/>
)}
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
)}
{result && (
<Button
onClick={() => setShowing4percent(!showing4percent)}
variant={showing4percent ? "secondary" : "default"}
size={"sm"}
>
{showing4percent ? "Hide" : "Show"} 4%-Rule
</Button>
)}
</form>
</Form>
</CardContent>
@@ -693,45 +574,11 @@ export default function FireCalculatorForm() {
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">
{Number(form.getValues("lifeExpectancy")) -
Number(form.getValues("retirementAge"))}
{form.getValues("lifeExpectancy") -
form.getValues("retirementAge")}
</p>
</CardContent>
</Card>
{showing4percent && (
<>
<Card>
<CardHeader>
<CardTitle>4%-Rule FIRE Number</CardTitle>
<CardDescription className="text-xs">
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>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>4%-Rule Retirement Duration</CardTitle>
<CardDescription className="text-xs">
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)}
</p>
</CardContent>
</Card>
</>
)}
</>
)}
</div>

BIN
src/app/favicon.ico (Stored with Git LFS)

Binary file not shown.

View File

@@ -1,17 +1,15 @@
import "@/styles/globals.css";
import PlausibleProvider from "next-plausible";
import { type Metadata, type Viewport } from "next";
import { type Metadata } from "next";
import { Geist } from "next/font/google";
import { WebVitals } from "./components/web-vitals";
export const viewport: Viewport = {
themeColor: [{ color: "oklch(0.97 0.0228 95.96)" }],
};
export const metadata: Metadata = {
title: "InvestingFIRE | Finance and Retirement Calculator",
title:
"InvestingFIRE Calculator | Plan Your Financial Independence & Early Retirement",
description:
"Achieve Financial Independence & Early Retirement! Plan your FIRE journey with the InvestingFIRE calculator and get personalized projections in buttersmooth graphs.",
"Achieve Financial Independence, Retire Early (FIRE) with the InvestingFIRE calculator. Get personalized projections and investing advice to plan your journey.",
icons: [{ rel: "icon", url: "/favicon.ico" }],
};
const geist = Geist({
@@ -26,16 +24,17 @@ export default function RootLayout({
<html lang="en" className={geist.variable}>
<head>
<meta name="apple-mobile-web-app-title" content="FIRE" />
</head>
<PlausibleProvider
domain="investingfire.com"
customDomain="https://analytics.schulze.network"
selfHosted={true}
enabled={true}
trackOutboundLinks={true}
/>
</head>
>
<WebVitals />
<body>{children}</body>
</PlausibleProvider>
</html>
);
}

View File

@@ -7,17 +7,13 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import Footer from "./components/footer";
import BackgroundPattern from "./components/BackgroundPattern";
export default function HomePage() {
return (
<main className="text-primary-foreground to-destructive from-secondary flex min-h-screen flex-col items-center bg-gradient-to-b p-2">
<BackgroundPattern />
<div className="z-10 mx-auto flex flex-col items-center justify-center gap-4 text-center">
<div className="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">
<Image
priority
unoptimized
src="/investingfire_logo_no-bg.svg"
alt="InvestingFIRE Logo"
width={100}
@@ -36,7 +32,7 @@ export default function HomePage() {
</div>
{/* Added SEO Content Sections */}
<div className="z-10 mx-auto max-w-2xl py-12 text-left">
<div className="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
@@ -251,7 +247,7 @@ export default function HomePage() {
financial independence and smart investing.
</p>
<div className="bg-foreground my-8 rounded-md p-4 text-lg">
<div className="bg-secondary/20 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>

View File

@@ -1,11 +0,0 @@
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: "https://investingfire.com/sitemap.xml",
};
}

View File

@@ -1,13 +0,0 @@
import { BASE_URL } from "@/lib/constants";
import { type MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: BASE_URL,
lastModified: new Date(),
changeFrequency: "yearly",
priority: 1,
},
];
}

View File

@@ -1 +0,0 @@
export const BASE_URL = "https://investingfire.com/";