Compare commits
2 Commits
extra-seo-
...
fe03807739
Author | SHA1 | Date | |
---|---|---|---|
fe03807739 | |||
30d27a212e |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,2 +0,0 @@
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.ico filter=lfs diff=lfs merge=lfs -text
|
@@ -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
109
README.md
@@ -1,108 +1,3 @@
|
||||

|
||||
# 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 project’s 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 you’ve already invested
|
||||
- **Monthly Savings** — What you’ll 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, today’s 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
|
@@ -11,11 +11,11 @@
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
"components": "~/components",
|
||||
"utils": "~/lib/utils",
|
||||
"ui": "~/components/ui",
|
||||
"lib": "~/lib",
|
||||
"hooks": "~/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
}
|
@@ -14,7 +14,7 @@ export default tseslint.config(
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
extends: [
|
||||
...tseslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
rules: {
|
||||
|
6207
package-lock.json
generated
Normal file
6207
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
@@ -6,52 +6,48 @@
|
||||
"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",
|
||||
"@radix-ui/react-accordion": "^1.2.8",
|
||||
"@radix-ui/react-label": "^2.1.4",
|
||||
"@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.539.0",
|
||||
"next": "^15.4.1",
|
||||
"next-plausible": "^3.12.4",
|
||||
"lucide-react": "^0.503.0",
|
||||
"next": "^15.2.3",
|
||||
"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.17.1",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"eslint": "9.33.0",
|
||||
"eslint-config-next": "15.4.4",
|
||||
"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.6",
|
||||
"typescript": "5.9.2",
|
||||
"typescript-eslint": "8.39.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.14.0"
|
||||
"packageManager": "npm@11.2.0"
|
||||
}
|
||||
|
5184
pnpm-lock.yaml
generated
5184
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
ignoredBuiltDependencies:
|
||||
- unrs-resolver
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- '@tailwindcss/oxide'
|
||||
- sharp
|
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="1000" height="1000" viewBox="0 0 264.58 264.58"><defs><linearGradient id="b"><stop offset="0" stop-color="#fd8315"/><stop offset="1" stop-color="#fa6b14"/></linearGradient><linearGradient id="a"><stop offset="0" stop-color="#f24b1b"/><stop offset="1" stop-color="#dc2f12"/></linearGradient><linearGradient xlink:href="#a" id="d" x1="172.49" x2="179.1" y1="64.48" y2="197.19" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#b" id="c" x1="118.9" x2="117.99" y1="14.21" y2="194.34" gradientUnits="userSpaceOnUse"/></defs><g stroke-linecap="round" stroke-linejoin="round" stroke-width=".13"><path fill="url(#c)" stroke="#f14a1b" d="m115.13 9.96-.26.01c-.97.11-1.29 1.02-.75 2.38a45.6 45.6 0 0 1 3.02 15.68c-.09 8.46.04 12.87-7.31 23.68s-23.16 21.9-33.96 34.66c-10.8 12.76-12.28 16.6-16.1 26.2A90.42 90.42 0 0 0 53.05 146c0 41.29 27.68 76.09 65.49 86.91 35.3-25.9 55.47-125.62 55.47-125.62s-14.45-10.54-18.57-18.89c-1.26-2.56-1.97-6.15-1.97-9.58.01-2.54 1.3-8.72 1.47-9.41a42.4 42.4 0 0 0 .94-12.14c-.07-.95-.17-1.9-.3-2.84v-.01a59.45 59.45 0 0 0-7.6-19h0a60.34 60.34 0 0 0-10.24-12.13 66.97 66.97 0 0 0-21.7-13.15 2.94 2.94 0 0 0-.92-.18z"/><path fill="url(#d)" stroke="#510a0c" d="M170.01 58.08a66.66 66.66 0 0 0-10.24 15.94 66.66 66.66 0 0 0-6.3 27.82 66.8 66.8 0 0 0 3.66 20.86h-.08l7.08 105.8s37.38-25.1 45.9-61a74.13 74.13 0 0 0 2.04-25.93c-1.34-14.35-4.35-21.67-9.85-30.3-5.5-8.62-10.36-14-17.63-22.78-6.17-7.43-9.44-18.25-10.39-28.87-.28-3.13-2.13-3.91-4.19-1.54z"/></g><path fill="#510a0c" d="M93.45 115.81h77.91c9.81 0 17.71 7.9 17.71 17.7v104.53c0 9.81-7.9 17.71-17.7 17.71H93.44c-9.81 0-17.71-7.9-17.71-17.7V133.51c0-9.81 7.9-17.71 17.7-17.71z"/><path fill="#e83c1b" d="M91.95 163.12h29.23c3.7 0 6.68 2.98 6.68 6.68v24.8c0 3.7-2.98 6.68-6.68 6.68H91.95a6.67 6.67 0 0 1-6.68-6.68v-24.8c0-3.7 2.98-6.68 6.68-6.68zm0 45.92h29.23c3.7 0 6.68 2.98 6.68 6.68v24.8c0 3.7-2.98 6.68-6.68 6.68H91.95a6.67 6.67 0 0 1-6.68-6.69v-24.8c0-3.7 2.98-6.67 6.68-6.67zm51.25-45.92h29.23c3.7 0 6.68 2.98 6.68 6.68v24.8c0 3.7-2.98 6.68-6.68 6.68H143.2a6.67 6.67 0 0 1-6.68-6.68v-24.8c0-3.7 2.98-6.68 6.68-6.68zm0 45.92h29.23c3.7 0 6.68 2.98 6.68 6.68v24.8c0 3.7-2.98 6.68-6.68 6.68H143.2a6.67 6.67 0 0 1-6.68-6.69v-24.8c0-3.7 2.98-6.67 6.68-6.67z"/><g fill="#520a0c"><path d="M148.74 179.98h18.29a2.41 2.41 0 1 1 0 4.84h-18.29a2.41 2.41 0 1 1 0-4.84zm-51.54.04h18.29a2.41 2.41 0 1 1 0 4.83h-18.3a2.41 2.41 0 1 1 0-4.83z"/><path d="M108.76 173.3v18.28a2.41 2.41 0 1 1-4.84 0V173.3a2.41 2.41 0 1 1 4.84 0zm-10.59 59.18 12.93-12.93a2.41 2.41 0 1 1 3.42 3.42l-12.93 12.93a2.41 2.41 0 1 1-3.42-3.42z"/><path d="m101.59 219.55 12.93 12.93a2.41 2.41 0 1 1-3.42 3.42l-12.93-12.93a2.41 2.41 0 1 1 3.42-3.42zm47.15 1.49h18.29a2.41 2.41 0 1 1 0 4.84h-18.29a2.41 2.41 0 1 1 0-4.84zm0 10.73h18.29a2.41 2.41 0 1 1 0 4.84h-18.29a2.41 2.41 0 1 1 0-4.84z"/></g><path fill="#fcf2e4" d="M92.35 125.2h79.67a7.07 7.07 0 0 1 7.09 7.1v14.36a7.07 7.07 0 0 1-7.09 7.1H92.35a7.07 7.07 0 0 1-7.08-7.1V132.3a7.07 7.07 0 0 1 7.08-7.09z"/></svg>
|
Before Width: | Height: | Size: 3.0 KiB |
BIN
public/web-app-manifest-192x192.png
(Stored with Git LFS)
BIN
public/web-app-manifest-192x192.png
(Stored with Git LFS)
Binary file not shown.
BIN
public/web-app-manifest-512x512.png
(Stored with Git LFS)
BIN
public/web-app-manifest-512x512.png
(Stored with Git LFS)
Binary file not shown.
@@ -1 +0,0 @@
|
||||
wgu5fuk8d5j5wp3pjtta9vrw8d9by9qk
|
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
BIN
src/app/apple-icon.png
(Stored with Git LFS)
BIN
src/app/apple-icon.png
(Stored with Git LFS)
Binary file not shown.
@@ -1,223 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
|
||||
export default function FourPercentRuleCalculator() {
|
||||
const [annualExpenses, setAnnualExpenses] = useState(50000);
|
||||
const [portfolioValue, setPortfolioValue] = useState(0);
|
||||
const [withdrawalRate, setWithdrawalRate] = useState(4);
|
||||
|
||||
// Calculate FIRE number based on withdrawal rate
|
||||
const fireNumber = Math.round(annualExpenses / (withdrawalRate / 100));
|
||||
|
||||
// Calculate safe withdrawal amount from portfolio
|
||||
const safeWithdrawal = Math.round(portfolioValue * (withdrawalRate / 100));
|
||||
|
||||
// Calculate years to FIRE if saving
|
||||
const monthlySavings = 2000; // Default for demo
|
||||
const growthRate = 0.07; // 7% annual growth
|
||||
const yearsToFire =
|
||||
portfolioValue < fireNumber
|
||||
? Math.log(
|
||||
(fireNumber + (monthlySavings * 12) / growthRate) /
|
||||
(portfolioValue + (monthlySavings * 12) / growthRate),
|
||||
) / Math.log(1 + growthRate)
|
||||
: 0;
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Input Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Calculate Your FIRE Number</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your annual expenses and current portfolio value to see your
|
||||
path to financial independence
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="annual-expenses">Annual Expenses</Label>
|
||||
<Input
|
||||
id="annual-expenses"
|
||||
type="number"
|
||||
value={annualExpenses}
|
||||
onChange={(e) => setAnnualExpenses(Number(e.target.value))}
|
||||
min={0}
|
||||
step={1000}
|
||||
/>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Your yearly spending in retirement
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="portfolio-value">Current Portfolio Value</Label>
|
||||
<Input
|
||||
id="portfolio-value"
|
||||
type="number"
|
||||
value={portfolioValue}
|
||||
onChange={(e) => setPortfolioValue(Number(e.target.value))}
|
||||
min={0}
|
||||
step={10000}
|
||||
/>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Your current invested assets
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="withdrawal-rate">Withdrawal Rate</Label>
|
||||
<span className="text-sm font-medium">{withdrawalRate}%</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="withdrawal-rate"
|
||||
min={3}
|
||||
max={5}
|
||||
step={0.1}
|
||||
value={[withdrawalRate]}
|
||||
onValueChange={(value) => setWithdrawalRate(value[0] ?? 4)}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
The classic 4% rule suggests 4%, but adjust based on your risk
|
||||
tolerance
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Results Section */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Your FIRE Number</CardTitle>
|
||||
<CardDescription>
|
||||
Portfolio needed for {withdrawalRate}% withdrawal rate
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-primary text-4xl font-bold">
|
||||
{formatCurrency(fireNumber)}
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
This is {Math.round(fireNumber / annualExpenses)}× your annual
|
||||
expenses
|
||||
</p>
|
||||
{portfolioValue > 0 && portfolioValue < fireNumber && (
|
||||
<div className="bg-foreground/5 mt-4 rounded-lg p-3">
|
||||
<p className="text-sm font-medium">Progress to FIRE</p>
|
||||
<div className="mt-2 h-2 rounded-full bg-gray-200">
|
||||
<div
|
||||
className="bg-primary h-2 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min((portfolioValue / fireNumber) * 100, 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
{Math.round((portfolioValue / fireNumber) * 100)}% complete
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Safe Annual Withdrawal</CardTitle>
|
||||
<CardDescription>From your current portfolio</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-primary text-4xl font-bold">
|
||||
{formatCurrency(safeWithdrawal)}
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
Monthly: {formatCurrency(safeWithdrawal / 12)}
|
||||
</p>
|
||||
{safeWithdrawal > 0 && safeWithdrawal < annualExpenses && (
|
||||
<div className="mt-4 rounded-lg bg-orange-100 p-3 dark:bg-orange-900/20">
|
||||
<p className="text-sm text-orange-900 dark:text-orange-100">
|
||||
⚠️ Your safe withdrawal ({formatCurrency(safeWithdrawal)}) is
|
||||
less than your annual expenses (
|
||||
{formatCurrency(annualExpenses)})
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{safeWithdrawal >= annualExpenses && (
|
||||
<div className="mt-4 rounded-lg bg-green-100 p-3 dark:bg-green-900/20">
|
||||
<p className="text-sm text-green-900 dark:text-green-100">
|
||||
✓ Congratulations! You've reached FIRE
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Additional Insights */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Quick Insights</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="bg-foreground/5 rounded-lg p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">Gap to FIRE</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{formatCurrency(Math.max(fireNumber - portfolioValue, 0))}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-foreground/5 rounded-lg p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">Monthly Target</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{formatCurrency(annualExpenses / 12)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-foreground/5 rounded-lg p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">25× Rule Result</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{formatCurrency(annualExpenses * 25)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Educational Note */}
|
||||
<Card className="border-blue-200 bg-blue-50 dark:border-blue-900 dark:bg-blue-950/20">
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-sm">
|
||||
<strong>💡 Pro Tip:</strong> The 4% rule is based on a 30-year
|
||||
retirement. For early retirees planning 40-50+ years, consider using
|
||||
3.5% or even 3% for added safety. Remember to account for taxes,
|
||||
healthcare costs, and inflation in your planning.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,429 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import BackgroundPattern from "@/app/components/BackgroundPattern";
|
||||
import Footer from "@/app/components/footer";
|
||||
import FourPercentRuleCalculator from "./FourPercentRuleCalculator";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "4% Rule Calculator - Safe Withdrawal Rate for FIRE | InvestingFIRE",
|
||||
description:
|
||||
"Calculate your safe withdrawal rate using the 4% rule. Determine how much you need to retire early and how long your retirement savings will last. Free FIRE calculator with real-time results.",
|
||||
keywords:
|
||||
"4 percent rule calculator, safe withdrawal rate calculator, 4% rule retirement, FIRE calculator, retirement withdrawal calculator, Trinity Study calculator",
|
||||
openGraph: {
|
||||
title: "4% Rule Calculator - Safe Withdrawal Rate Calculator",
|
||||
description:
|
||||
"Free 4% rule calculator to determine your safe withdrawal rate and retirement portfolio size. Based on the Trinity Study for FIRE planning.",
|
||||
type: "website",
|
||||
url: "https://investingfire.com/calculators/4-percent-rule",
|
||||
},
|
||||
};
|
||||
|
||||
export default function FourPercentRulePage() {
|
||||
const faqData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: [
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "What is the 4% rule?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "The 4% rule is a retirement planning guideline that suggests you can safely withdraw 4% of your retirement portfolio in the first year, then adjust that amount for inflation each subsequent year, with a high probability of not running out of money over 30 years.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "How accurate is the 4% rule?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "The 4% rule, based on the Trinity Study, showed a 95% success rate for a 30-year retirement with a 50/50 stock/bond portfolio. However, it's based on historical U.S. market data and may need adjustment for longer retirements, different asset allocations, or varying market conditions.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "Is 4% too conservative or too aggressive?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "It depends on your situation. For early retirees with 40-50+ year horizons, 4% might be too aggressive. Some prefer 3-3.5%. Conversely, flexible spenders who can reduce withdrawals in down markets might safely use 4.5-5%. Personal factors like other income sources and spending flexibility matter.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "How do I calculate my FIRE number using the 4% rule?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Simply multiply your annual expenses by 25. For example, if you need $40,000 per year, your FIRE number is $1,000,000 ($40,000 × 25). This gives you a portfolio where 4% equals your annual spending needs.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const breadcrumbData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 1,
|
||||
name: "Home",
|
||||
item: "https://investingfire.com",
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 2,
|
||||
name: "Calculators",
|
||||
item: "https://investingfire.com/calculators",
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 3,
|
||||
name: "4% Rule Calculator",
|
||||
item: "https://investingfire.com/calculators/4-percent-rule",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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 />
|
||||
|
||||
{/* Header */}
|
||||
<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">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-4 transition-opacity hover:opacity-90"
|
||||
>
|
||||
<Image
|
||||
priority
|
||||
unoptimized
|
||||
src="/investingfire_logo_no-bg.svg"
|
||||
alt="InvestingFIRE Logo"
|
||||
width={60}
|
||||
height={60}
|
||||
/>
|
||||
<span className="text-2xl font-bold">InvestingFIRE</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<h1 className="from-primary via-primary-foreground to-primary mt-8 bg-gradient-to-r bg-clip-text text-4xl font-extrabold tracking-tight text-transparent drop-shadow-md sm:text-[4rem]">
|
||||
4% Rule Calculator
|
||||
</h1>
|
||||
<p className="text-primary-foreground/90 max-w-2xl text-xl font-semibold md:text-2xl">
|
||||
Calculate Your Safe Withdrawal Rate & FIRE Number Using the Trinity
|
||||
Study Method
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Breadcrumb Schema */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbData) }}
|
||||
/>
|
||||
|
||||
{/* Calculator */}
|
||||
<div className="z-10 mt-8 w-full max-w-4xl">
|
||||
<FourPercentRuleCalculator />
|
||||
</div>
|
||||
|
||||
{/* SEO Content */}
|
||||
<div className="z-10 mx-auto max-w-4xl px-4 py-12 text-left">
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
Understanding the 4% Rule for Safe Retirement Withdrawals
|
||||
</h2>
|
||||
<p className="mb-4 text-lg leading-relaxed">
|
||||
The <strong>4% rule</strong> is one of the most widely recognized
|
||||
guidelines in retirement planning, particularly within the FIRE
|
||||
(Financial Independence, Retire Early) community. This rule of thumb
|
||||
suggests you can withdraw 4% of your retirement portfolio in the
|
||||
first year of retirement, then adjust that dollar amount for
|
||||
inflation each subsequent year, with a high probability of not
|
||||
depleting your savings over a 30-year retirement period.
|
||||
</p>
|
||||
<p className="mb-4 text-lg leading-relaxed">
|
||||
Originating from the groundbreaking <strong>Trinity Study</strong>{" "}
|
||||
(1998), which analyzed historical market data from 1926-1995, the 4%
|
||||
rule demonstrated a 95% success rate for mixed portfolios of stocks
|
||||
and bonds over 30-year periods. This simple yet powerful concept
|
||||
revolutionized retirement planning by providing a clear target:
|
||||
accumulate 25 times your annual expenses to achieve financial
|
||||
independence.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
How the 4% Rule Calculator Works
|
||||
</h2>
|
||||
<p className="mb-4 text-lg leading-relaxed">
|
||||
Our 4% rule calculator helps you determine two critical numbers for
|
||||
your retirement planning:
|
||||
</p>
|
||||
<ol className="mb-6 ml-6 list-decimal space-y-3 text-lg">
|
||||
<li>
|
||||
<strong>Your FIRE Number</strong> - The total portfolio size
|
||||
needed to support your desired lifestyle using the 4% withdrawal
|
||||
rate
|
||||
</li>
|
||||
<li>
|
||||
<strong>Safe Annual Withdrawal</strong> - How much you can
|
||||
withdraw from a given portfolio size while maintaining the 4% rule
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div className="bg-foreground/10 mb-6 rounded-lg p-6">
|
||||
<h3 className="mb-3 text-xl font-semibold">
|
||||
Quick 4% Rule Formula:
|
||||
</h3>
|
||||
<p className="mb-2 text-lg">
|
||||
<strong>FIRE Number = Annual Expenses × 25</strong>
|
||||
</p>
|
||||
<p className="text-lg">
|
||||
<strong>Safe Annual Withdrawal = Portfolio Value × 0.04</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="mb-4 text-lg leading-relaxed">
|
||||
For example, if you need $50,000 per year to cover your expenses,
|
||||
your FIRE number would be $1,250,000 ($50,000 × 25). Conversely, if
|
||||
you have a $2,000,000 portfolio, you could safely withdraw $80,000
|
||||
in the first year (2,000,000 × 0.04).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
Adjusting the 4% Rule for Your Situation
|
||||
</h2>
|
||||
<p className="mb-4 text-lg leading-relaxed">
|
||||
While the 4% rule provides an excellent starting point, many
|
||||
financial experts suggest adjustments based on individual
|
||||
circumstances:
|
||||
</p>
|
||||
|
||||
<div className="mb-6 grid gap-6 md:grid-cols-2">
|
||||
<div className="bg-foreground/10 rounded-lg p-6">
|
||||
<h3 className="mb-3 text-xl font-semibold">
|
||||
More Conservative Approaches
|
||||
</h3>
|
||||
<ul className="ml-6 list-disc space-y-2 text-lg">
|
||||
<li>
|
||||
<strong>3.5% Rule</strong> - For early retirees with 40-50+
|
||||
year horizons
|
||||
</li>
|
||||
<li>
|
||||
<strong>3% Rule</strong> - Ultra-conservative for maximum
|
||||
safety
|
||||
</li>
|
||||
<li>
|
||||
<strong>Dynamic Withdrawals</strong> - Adjust based on market
|
||||
performance
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground/10 rounded-lg p-6">
|
||||
<h3 className="mb-3 text-xl font-semibold">
|
||||
Factors That May Allow Higher Withdrawals
|
||||
</h3>
|
||||
<ul className="ml-6 list-disc space-y-2 text-lg">
|
||||
<li>
|
||||
<strong>Flexible Spending</strong> - Ability to reduce
|
||||
expenses in down markets
|
||||
</li>
|
||||
<li>
|
||||
<strong>Part-time Income</strong> - Earning some money in
|
||||
retirement
|
||||
</li>
|
||||
<li>
|
||||
<strong>Social Security/Pensions</strong> - Additional income
|
||||
sources later
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
4% Rule vs. Other FIRE Strategies
|
||||
</h2>
|
||||
<p className="mb-4 text-lg leading-relaxed">
|
||||
The 4% rule is just one approach to achieving financial
|
||||
independence. Here's how it compares to other popular FIRE
|
||||
strategies:
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-foreground/10 rounded-lg p-4">
|
||||
<h3 className="mb-2 text-lg font-semibold">
|
||||
Traditional FIRE (4% Rule)
|
||||
</h3>
|
||||
<p>
|
||||
Target: 25× annual expenses | Best for: Balanced lifestyle with
|
||||
moderate spending
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground/10 rounded-lg p-4">
|
||||
<h3 className="mb-2 text-lg font-semibold">
|
||||
Lean FIRE (3-3.5% Rule)
|
||||
</h3>
|
||||
<p>
|
||||
Target: 28-33× annual expenses | Best for: Minimalist lifestyle,
|
||||
maximum safety
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground/10 rounded-lg p-4">
|
||||
<h3 className="mb-2 text-lg font-semibold">
|
||||
Fat FIRE (4-5% Rule)
|
||||
</h3>
|
||||
<p>
|
||||
Target: 20-25× annual expenses | Best for: Luxurious retirement
|
||||
with higher spending
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground/10 rounded-lg p-4">
|
||||
<h3 className="mb-2 text-lg font-semibold">Coast FIRE</h3>
|
||||
<p>
|
||||
Let investments grow while covering expenses with part-time work
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-lg">
|
||||
Want to explore these strategies in detail? Try our{" "}
|
||||
<Link
|
||||
href="/"
|
||||
className="text-primary font-semibold hover:underline"
|
||||
>
|
||||
comprehensive FIRE calculator
|
||||
</Link>{" "}
|
||||
for a full retirement simulation.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section className="mb-12">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqData) }}
|
||||
/>
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
4% Rule Frequently Asked Questions
|
||||
</h2>
|
||||
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
What is the 4% rule?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
The 4% rule is a retirement planning guideline that suggests you
|
||||
can safely withdraw 4% of your retirement portfolio in the first
|
||||
year, then adjust that amount for inflation each subsequent
|
||||
year, with a high probability of not running out of money over
|
||||
30 years.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
How accurate is the 4% rule?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
The 4% rule, based on the Trinity Study, showed a 95% success
|
||||
rate for a 30-year retirement with a 50/50 stock/bond portfolio.
|
||||
However, it's based on historical U.S. market data and may need
|
||||
adjustment for longer retirements, different asset allocations,
|
||||
or varying market conditions.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
Is 4% too conservative or too aggressive?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
It depends on your situation. For early retirees with 40-50+
|
||||
year horizons, 4% might be too aggressive. Some prefer 3-3.5%.
|
||||
Conversely, flexible spenders who can reduce withdrawals in down
|
||||
markets might safely use 4.5-5%. Personal factors like other
|
||||
income sources and spending flexibility matter.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-4">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
How do I calculate my FIRE number using the 4% rule?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
Simply multiply your annual expenses by 25. For example, if you
|
||||
need $40,000 per year, your FIRE number is $1,000,000 ($40,000 ×
|
||||
25). This gives you a portfolio where 4% equals your annual
|
||||
spending needs.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-5">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
Does the 4% rule account for taxes?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
No, the 4% rule calculates gross withdrawals. You'll need to
|
||||
account for taxes separately based on your account types
|
||||
(traditional vs. Roth IRA, taxable accounts) and tax bracket.
|
||||
Many FIRE planners target a portfolio 25-30% larger than the
|
||||
basic calculation to cover taxes.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-6">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
What asset allocation works best with the 4% rule?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
The original Trinity Study found success with portfolios ranging
|
||||
from 50/50 to 75/25 stocks/bonds. Higher stock allocations
|
||||
generally provided better long-term results but with more
|
||||
volatility. Most FIRE practitioners use 60-80% stocks for the
|
||||
growth needed to sustain long retirements.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</section>
|
||||
|
||||
{/* Call to Action */}
|
||||
<section className="bg-foreground/10 mb-12 rounded-lg p-8 text-center">
|
||||
<h2 className="mb-4 text-2xl font-bold">
|
||||
Ready for a More Detailed FIRE Analysis?
|
||||
</h2>
|
||||
<p className="mb-6 text-lg">
|
||||
While the 4% rule provides a great starting point, our comprehensive
|
||||
FIRE calculator offers year-by-year projections, inflation
|
||||
adjustments, and personalized scenarios for your unique situation.
|
||||
</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="bg-primary text-primary-foreground inline-block rounded-lg px-6 py-3 font-semibold transition-opacity hover:opacity-90"
|
||||
>
|
||||
Try Our Full FIRE Calculator →
|
||||
</Link>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
@@ -1,345 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
|
||||
export default function CoastFireCalculator() {
|
||||
const [currentAge, setCurrentAge] = useState(30);
|
||||
const [retirementAge, setRetirementAge] = useState(65);
|
||||
const [currentPortfolio, setCurrentPortfolio] = useState(50000);
|
||||
const [annualExpenses, setAnnualExpenses] = useState(50000);
|
||||
const [expectedReturn, setExpectedReturn] = useState(7);
|
||||
|
||||
// Calculate years until retirement
|
||||
const yearsUntilRetirement = retirementAge - currentAge;
|
||||
|
||||
// Calculate target FIRE number (25x annual expenses)
|
||||
const targetFireNumber = annualExpenses * 25;
|
||||
|
||||
// Calculate Coast FIRE number (present value of future FIRE number)
|
||||
const coastFireNumber =
|
||||
yearsUntilRetirement > 0
|
||||
? targetFireNumber /
|
||||
Math.pow(1 + expectedReturn / 100, yearsUntilRetirement)
|
||||
: targetFireNumber;
|
||||
|
||||
// Check if already at Coast FIRE
|
||||
const isCoastFire = currentPortfolio >= coastFireNumber;
|
||||
|
||||
// Calculate what portfolio will grow to by retirement
|
||||
const projectedPortfolioAtRetirement =
|
||||
currentPortfolio * Math.pow(1 + expectedReturn / 100, yearsUntilRetirement);
|
||||
|
||||
// Calculate gap to Coast FIRE
|
||||
const gapToCoastFire = Math.max(coastFireNumber - currentPortfolio, 0);
|
||||
|
||||
// Calculate monthly savings needed to reach Coast FIRE in different timeframes
|
||||
const calculateMonthlySavings = (years: number) => {
|
||||
if (years <= 0 || isCoastFire) return 0;
|
||||
const monthlyReturn = expectedReturn / 100 / 12;
|
||||
const months = years * 12;
|
||||
return (
|
||||
(gapToCoastFire * monthlyReturn) /
|
||||
(Math.pow(1 + monthlyReturn, months) - 1)
|
||||
);
|
||||
};
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Input Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Your Coast FIRE Inputs</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your details to calculate when you can stop saving for
|
||||
retirement
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="current-age">Current Age</Label>
|
||||
<span className="text-sm font-medium">{currentAge}</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="current-age"
|
||||
min={20}
|
||||
max={60}
|
||||
step={1}
|
||||
value={[currentAge]}
|
||||
onValueChange={(value) => setCurrentAge(value[0] ?? 30)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="retirement-age">Target Retirement Age</Label>
|
||||
<span className="text-sm font-medium">{retirementAge}</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="retirement-age"
|
||||
min={40}
|
||||
max={80}
|
||||
step={1}
|
||||
value={[retirementAge]}
|
||||
onValueChange={(value) => setRetirementAge(value[0] ?? 65)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="current-portfolio">Current Portfolio Value</Label>
|
||||
<Input
|
||||
id="current-portfolio"
|
||||
type="number"
|
||||
value={currentPortfolio}
|
||||
onChange={(e) => setCurrentPortfolio(Number(e.target.value))}
|
||||
min={0}
|
||||
step={1000}
|
||||
/>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Your current retirement savings
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="annual-expenses">
|
||||
Annual Expenses in Retirement
|
||||
</Label>
|
||||
<Input
|
||||
id="annual-expenses"
|
||||
type="number"
|
||||
value={annualExpenses}
|
||||
onChange={(e) => setAnnualExpenses(Number(e.target.value))}
|
||||
min={0}
|
||||
step={1000}
|
||||
/>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Yearly spending (in today's dollars)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="expected-return">Expected Annual Return</Label>
|
||||
<span className="text-sm font-medium">{expectedReturn}%</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="expected-return"
|
||||
min={4}
|
||||
max={10}
|
||||
step={0.5}
|
||||
value={[expectedReturn]}
|
||||
onValueChange={(value) => setExpectedReturn(value[0] ?? 7)}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Average annual investment return before inflation
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Results Section */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Your Coast FIRE Number</CardTitle>
|
||||
<CardDescription>
|
||||
Amount needed today to coast to retirement
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-primary text-4xl font-bold">
|
||||
{formatCurrency(coastFireNumber)}
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
Grows to {formatCurrency(targetFireNumber)} in{" "}
|
||||
{yearsUntilRetirement} years
|
||||
</p>
|
||||
{isCoastFire ? (
|
||||
<div className="mt-4 rounded-lg bg-green-100 p-3 dark:bg-green-900/20">
|
||||
<p className="text-sm text-green-900 dark:text-green-100">
|
||||
🎉 Congratulations! You've reached Coast FIRE!
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-foreground/5 mt-4 rounded-lg p-3">
|
||||
<p className="text-sm font-medium">Progress to Coast FIRE</p>
|
||||
<div className="mt-2 h-2 rounded-full bg-gray-200">
|
||||
<div
|
||||
className="bg-primary h-2 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min((currentPortfolio / coastFireNumber) * 100, 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
{Math.round((currentPortfolio / coastFireNumber) * 100)}%
|
||||
complete
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Portfolio at Retirement</CardTitle>
|
||||
<CardDescription>
|
||||
What your current savings will grow to
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-primary text-4xl font-bold">
|
||||
{formatCurrency(projectedPortfolioAtRetirement)}
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
{projectedPortfolioAtRetirement >= targetFireNumber
|
||||
? `${Math.round((projectedPortfolioAtRetirement / targetFireNumber) * 100)}% of target`
|
||||
: `${formatCurrency(targetFireNumber - projectedPortfolioAtRetirement)} short`}
|
||||
</p>
|
||||
{projectedPortfolioAtRetirement >= targetFireNumber && (
|
||||
<div className="mt-4 rounded-lg bg-blue-100 p-3 dark:bg-blue-900/20">
|
||||
<p className="text-sm text-blue-900 dark:text-blue-100">
|
||||
💡 You'll exceed your FIRE target by{" "}
|
||||
{formatCurrency(
|
||||
projectedPortfolioAtRetirement - targetFireNumber,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Savings Scenarios */}
|
||||
{!isCoastFire && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Monthly Savings to Reach Coast FIRE</CardTitle>
|
||||
<CardDescription>
|
||||
How much to save monthly to hit Coast FIRE in different timeframes
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="bg-foreground/5 rounded-lg p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">In 5 Years</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{formatCurrency(calculateMonthlySavings(5))}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">per month</p>
|
||||
</div>
|
||||
<div className="bg-foreground/5 rounded-lg p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">In 10 Years</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{formatCurrency(calculateMonthlySavings(10))}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">per month</p>
|
||||
</div>
|
||||
<div className="bg-foreground/5 rounded-lg p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">In 15 Years</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{formatCurrency(calculateMonthlySavings(15))}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">per month</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Key Metrics */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Coast FIRE Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="bg-foreground/5 rounded-lg p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Years to Retirement
|
||||
</p>
|
||||
<p className="text-2xl font-bold">{yearsUntilRetirement}</p>
|
||||
</div>
|
||||
<div className="bg-foreground/5 rounded-lg p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Target FIRE Number
|
||||
</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{formatCurrency(targetFireNumber)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-foreground/5 rounded-lg p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">Gap to Coast FIRE</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{formatCurrency(gapToCoastFire)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-foreground/5 rounded-lg p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">Growth Multiple</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{Math.pow(
|
||||
1 + expectedReturn / 100,
|
||||
yearsUntilRetirement,
|
||||
).toFixed(1)}
|
||||
×
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Educational Notes */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card className="border-blue-200 bg-blue-50 dark:border-blue-900 dark:bg-blue-950/20">
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-sm">
|
||||
<strong>🎯 Coast FIRE Strategy:</strong> Once you hit your Coast
|
||||
FIRE number, you can stop saving for retirement entirely. Work
|
||||
only to cover current expenses while your investments grow to your
|
||||
full FIRE target.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-purple-200 bg-purple-50 dark:border-purple-900 dark:bg-purple-950/20">
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-sm">
|
||||
<strong>⚡ Power of Time:</strong> Starting early is crucial. A
|
||||
25-year-old needs only{" "}
|
||||
{formatCurrency(targetFireNumber / Math.pow(1.07, 40))}
|
||||
to coast to a {formatCurrency(targetFireNumber)} retirement at 65
|
||||
(assuming 7% returns).
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,481 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import BackgroundPattern from "@/app/components/BackgroundPattern";
|
||||
import Footer from "@/app/components/footer";
|
||||
import CoastFireCalculator from "./CoastFireCalculator";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Coast FIRE Calculator - When Can You Stop Saving? | InvestingFIRE",
|
||||
description:
|
||||
"Calculate your Coast FIRE number and find out when you can stop saving for retirement. Free Coast FI calculator shows how compound interest can work for you.",
|
||||
keywords:
|
||||
"coast fire calculator, coast fi calculator, coast fire number, barista fire calculator, coast financial independence, stop saving calculator",
|
||||
openGraph: {
|
||||
title: "Coast FIRE Calculator - Stop Saving & Let Compound Interest Work",
|
||||
description:
|
||||
"Discover when you can stop saving for retirement and coast to financial independence. Free calculator with real-time projections.",
|
||||
type: "website",
|
||||
url: "https://investingfire.com/calculators/coast-fire",
|
||||
},
|
||||
};
|
||||
|
||||
export default function CoastFirePage() {
|
||||
const faqData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: [
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "What is Coast FIRE?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Coast FIRE (Financial Independence, Retire Early) is when you've saved enough that you can stop contributing to retirement accounts and still reach your FIRE number by your target retirement age through compound growth alone. You still need to cover current expenses but no longer need to save for retirement.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "How is Coast FIRE different from regular FIRE?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Regular FIRE means you have enough saved to retire immediately and live off withdrawals. Coast FIRE means you have enough that will grow to your FIRE number by retirement age without additional contributions. You still work to cover current expenses but can spend everything you earn.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "What's the Coast FIRE formula?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Coast FIRE Number = FIRE Number ÷ (1 + growth rate)^years until retirement. For example, if you need $1 million at 65 and you're 35 with 30 years to go, assuming 7% growth: $1,000,000 ÷ (1.07)^30 = $131,367.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "Is Coast FIRE realistic?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Yes, Coast FIRE is very achievable, especially for those who start saving aggressively in their 20s or 30s. The key is front-loading retirement savings early in your career when compound interest has the most time to work. Many achieve Coast FIRE in 10-15 years of focused saving.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const breadcrumbData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 1,
|
||||
name: "Home",
|
||||
item: "https://investingfire.com",
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 2,
|
||||
name: "Calculators",
|
||||
item: "https://investingfire.com/calculators",
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 3,
|
||||
name: "Coast FIRE Calculator",
|
||||
item: "https://investingfire.com/calculators/coast-fire",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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 />
|
||||
|
||||
{/* Header */}
|
||||
<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">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-4 transition-opacity hover:opacity-90"
|
||||
>
|
||||
<Image
|
||||
priority
|
||||
unoptimized
|
||||
src="/investingfire_logo_no-bg.svg"
|
||||
alt="InvestingFIRE Logo"
|
||||
width={60}
|
||||
height={60}
|
||||
/>
|
||||
<span className="text-2xl font-bold">InvestingFIRE</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<h1 className="from-primary via-primary-foreground to-primary mt-8 bg-gradient-to-r bg-clip-text text-4xl font-extrabold tracking-tight text-transparent drop-shadow-md sm:text-[4rem]">
|
||||
Coast FIRE Calculator
|
||||
</h1>
|
||||
<p className="text-primary-foreground/90 max-w-2xl text-xl font-semibold md:text-2xl">
|
||||
Find Out When You Can Stop Saving and Let Compound Interest Do the
|
||||
Work
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Breadcrumb Schema */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbData) }}
|
||||
/>
|
||||
|
||||
{/* Calculator */}
|
||||
<div className="z-10 mt-8 w-full max-w-4xl">
|
||||
<CoastFireCalculator />
|
||||
</div>
|
||||
|
||||
{/* SEO Content */}
|
||||
<div className="z-10 mx-auto max-w-4xl px-4 py-12 text-left">
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
What Is Coast FIRE? Your Path to Stress-Free Saving
|
||||
</h2>
|
||||
<p className="mb-4 text-lg leading-relaxed">
|
||||
<strong>Coast FIRE</strong> represents a powerful milestone in your
|
||||
financial independence journey. It's the point where you've
|
||||
accumulated enough investments that you can completely stop saving
|
||||
for retirement and still reach your FIRE number by your target
|
||||
retirement age through compound growth alone.
|
||||
</p>
|
||||
<p className="mb-4 text-lg leading-relaxed">
|
||||
Unlike traditional FIRE where you need 25× your annual expenses
|
||||
saved before retiring, Coast FIRE allows you to "coast" to
|
||||
retirement. You still work to cover your current living expenses,
|
||||
but you can spend 100% of your income knowing your future retirement
|
||||
is already secured through the magic of compound interest.
|
||||
</p>
|
||||
|
||||
<div className="bg-foreground/10 my-6 rounded-lg p-6">
|
||||
<h3 className="mb-3 text-xl font-semibold">Coast FIRE Benefits:</h3>
|
||||
<ul className="ml-6 list-disc space-y-2 text-lg">
|
||||
<li>
|
||||
<strong>Reduced financial stress</strong> - No more pressure to
|
||||
save aggressively
|
||||
</li>
|
||||
<li>
|
||||
<strong>Career flexibility</strong> - Take lower-paying but more
|
||||
fulfilling work
|
||||
</li>
|
||||
<li>
|
||||
<strong>Lifestyle upgrade</strong> - Spend your entire paycheck
|
||||
guilt-free
|
||||
</li>
|
||||
<li>
|
||||
<strong>Early achievement</strong> - Often reachable in your 30s
|
||||
or 40s
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
How the Coast FIRE Calculator Works
|
||||
</h2>
|
||||
<p className="mb-4 text-lg leading-relaxed">
|
||||
Our Coast FIRE calculator uses the time value of money principle to
|
||||
determine exactly how much you need invested today to reach your
|
||||
retirement goal without any additional contributions. Here's the
|
||||
math behind it:
|
||||
</p>
|
||||
|
||||
<div className="bg-foreground/10 mb-6 rounded-lg p-6">
|
||||
<h3 className="mb-3 text-xl font-semibold">
|
||||
The Coast FIRE Formula:
|
||||
</h3>
|
||||
<p className="mb-4 font-mono text-lg">
|
||||
Coast FIRE Number = Target FIRE Number ÷ (1 + growth rate)^years
|
||||
</p>
|
||||
<p className="text-lg">Where:</p>
|
||||
<ul className="ml-6 list-disc space-y-1 text-lg">
|
||||
<li>Target FIRE Number = 25× your annual retirement expenses</li>
|
||||
<li>
|
||||
Growth rate = Expected annual investment return (e.g., 7%)
|
||||
</li>
|
||||
<li>Years = Time until your target retirement age</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p className="mb-4 text-lg leading-relaxed">
|
||||
For example, if you need $1,000,000 to retire in 30 years and expect
|
||||
7% annual returns, you need just $131,367 invested today to coast to
|
||||
retirement. That's the power of compound interest working over
|
||||
decades!
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
Coast FIRE vs Other FIRE Variations
|
||||
</h2>
|
||||
<p className="mb-4 text-lg leading-relaxed">
|
||||
Understanding how Coast FIRE fits into the broader FIRE movement
|
||||
helps you choose the right strategy for your situation:
|
||||
</p>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="bg-foreground/10 rounded-lg p-6">
|
||||
<h3 className="mb-3 text-xl font-semibold">Coast FIRE</h3>
|
||||
<p className="mb-2">Stop saving, work for expenses only</p>
|
||||
<ul className="ml-4 list-disc space-y-1 text-sm">
|
||||
<li>Achievable in 10-15 years</li>
|
||||
<li>Reduces financial stress immediately</li>
|
||||
<li>Perfect for career pivots</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground/10 rounded-lg p-6">
|
||||
<h3 className="mb-3 text-xl font-semibold">Barista FIRE</h3>
|
||||
<p className="mb-2">Similar to Coast but work part-time</p>
|
||||
<ul className="ml-4 list-disc space-y-1 text-sm">
|
||||
<li>Often includes health benefits</li>
|
||||
<li>More lifestyle flexibility</li>
|
||||
<li>Bridge to full retirement</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground/10 rounded-lg p-6">
|
||||
<h3 className="mb-3 text-xl font-semibold">Traditional FIRE</h3>
|
||||
<p className="mb-2">Full retirement with 4% rule</p>
|
||||
<ul className="ml-4 list-disc space-y-1 text-sm">
|
||||
<li>Need 25× annual expenses</li>
|
||||
<li>Complete work optional</li>
|
||||
<li>Takes longer to achieve</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground/10 rounded-lg p-6">
|
||||
<h3 className="mb-3 text-xl font-semibold">Lean/Fat FIRE</h3>
|
||||
<p className="mb-2">Variations based on spending</p>
|
||||
<ul className="ml-4 list-disc space-y-1 text-sm">
|
||||
<li>Lean: Minimal expenses</li>
|
||||
<li>Fat: Luxury lifestyle</li>
|
||||
<li>Different savings targets</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
Strategies to Reach Coast FIRE Faster
|
||||
</h2>
|
||||
<p className="mb-4 text-lg leading-relaxed">
|
||||
Achieving Coast FIRE is all about front-loading your retirement
|
||||
savings while you're young. Here are proven strategies to accelerate
|
||||
your journey:
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-foreground/10 rounded-lg p-4">
|
||||
<h3 className="mb-2 text-lg font-semibold">
|
||||
1. Maximize Tax-Advantaged Accounts Early
|
||||
</h3>
|
||||
<p>
|
||||
Prioritize 401(k), IRA, and HSA contributions in your 20s and
|
||||
30s. The tax savings compound alongside your investments.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground/10 rounded-lg p-4">
|
||||
<h3 className="mb-2 text-lg font-semibold">
|
||||
2. Live on One Income (If Partnered)
|
||||
</h3>
|
||||
<p>
|
||||
Save 100% of one partner's income while living on the other.
|
||||
This can cut your Coast FIRE timeline in half.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground/10 rounded-lg p-4">
|
||||
<h3 className="mb-2 text-lg font-semibold">
|
||||
3. House Hack or Geographic Arbitrage
|
||||
</h3>
|
||||
<p>
|
||||
Minimize housing costs through rental income or moving to lower
|
||||
cost areas while maintaining income.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground/10 rounded-lg p-4">
|
||||
<h3 className="mb-2 text-lg font-semibold">
|
||||
4. Invest Windfalls Immediately
|
||||
</h3>
|
||||
<p>
|
||||
Bonuses, tax refunds, and gifts go straight to investments.
|
||||
These lump sums have maximum time to compound.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground/10 rounded-lg p-4">
|
||||
<h3 className="mb-2 text-lg font-semibold">
|
||||
5. Increase Savings Rate Annually
|
||||
</h3>
|
||||
<p>
|
||||
Bump up your savings by 1-2% each year. You won't feel the pinch
|
||||
but will dramatically accelerate your timeline.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section className="mb-12">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqData) }}
|
||||
/>
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
Coast FIRE Frequently Asked Questions
|
||||
</h2>
|
||||
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
What is Coast FIRE?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
Coast FIRE (Financial Independence, Retire Early) is when you've
|
||||
saved enough that you can stop contributing to retirement
|
||||
accounts and still reach your FIRE number by your target
|
||||
retirement age through compound growth alone. You still need to
|
||||
cover current expenses but no longer need to save for
|
||||
retirement.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
How is Coast FIRE different from regular FIRE?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
Regular FIRE means you have enough saved to retire immediately
|
||||
and live off withdrawals. Coast FIRE means you have enough that
|
||||
will grow to your FIRE number by retirement age without
|
||||
additional contributions. You still work to cover current
|
||||
expenses but can spend everything you earn.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
What's the Coast FIRE formula?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
Coast FIRE Number = FIRE Number ÷ (1 + growth rate)^years until
|
||||
retirement. For example, if you need $1 million at 65 and you're
|
||||
35 with 30 years to go, assuming 7% growth: $1,000,000 ÷
|
||||
(1.07)^30 = $131,367.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-4">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
Is Coast FIRE realistic?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
Yes, Coast FIRE is very achievable, especially for those who
|
||||
start saving aggressively in their 20s or 30s. The key is
|
||||
front-loading retirement savings early in your career when
|
||||
compound interest has the most time to work. Many achieve Coast
|
||||
FIRE in 10-15 years of focused saving.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-5">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
What if I already have some retirement savings?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
Great! You're already on your way. Enter your current portfolio
|
||||
value in the calculator to see how close you are to Coast FIRE.
|
||||
You might be surprised to find you're closer than you think,
|
||||
especially if you have many years until retirement.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-6">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
Should I actually stop saving once I hit Coast FIRE?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
That's a personal choice! Many people continue saving to reach
|
||||
full FIRE faster, build a buffer for market downturns, or
|
||||
upgrade their retirement lifestyle. Others use Coast FIRE as
|
||||
permission to pursue lower-paying passion projects or reduce
|
||||
work hours. The beauty is having options.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</section>
|
||||
|
||||
{/* Success Stories */}
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
Real Coast FIRE Success Stories
|
||||
</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="bg-foreground/10 rounded-lg p-6">
|
||||
<h3 className="mb-2 text-xl font-semibold">The Teacher's Tale</h3>
|
||||
<p className="text-sm">
|
||||
"I hit Coast FIRE at 32 after 10 years of saving 60% as an
|
||||
engineer. Now I teach high school physics—half the pay but 10x
|
||||
the satisfaction. My retirement is secured, so every paycheck
|
||||
goes to enjoying life now."
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground/10 rounded-lg p-6">
|
||||
<h3 className="mb-2 text-xl font-semibold">
|
||||
The Entrepreneur's Freedom
|
||||
</h3>
|
||||
<p className="text-sm">
|
||||
"Reaching Coast FIRE at 38 gave me the courage to start my
|
||||
business. Without needing to save for retirement, I could
|
||||
reinvest everything back into growth. Best decision ever."
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Call to Action */}
|
||||
<section className="bg-foreground/10 mb-12 rounded-lg p-8 text-center">
|
||||
<h2 className="mb-4 text-2xl font-bold">
|
||||
Want a Complete FIRE Plan?
|
||||
</h2>
|
||||
<p className="mb-6 text-lg">
|
||||
Coast FIRE is just one strategy. Our comprehensive FIRE calculator
|
||||
models your entire journey with multiple scenarios, withdrawal
|
||||
strategies, and detailed projections.
|
||||
</p>
|
||||
<div className="flex flex-col justify-center gap-4 sm:flex-row">
|
||||
<Link
|
||||
href="/"
|
||||
className="bg-primary text-primary-foreground inline-block rounded-lg px-6 py-3 font-semibold transition-opacity hover:opacity-90"
|
||||
>
|
||||
Try Full FIRE Calculator →
|
||||
</Link>
|
||||
<Link
|
||||
href="/calculators/4-percent-rule"
|
||||
className="bg-secondary text-secondary-foreground inline-block rounded-lg px-6 py-3 font-semibold transition-opacity hover:opacity-90"
|
||||
>
|
||||
Explore 4% Rule →
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
@@ -1,741 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
CartesianGrid,
|
||||
XAxis,
|
||||
YAxis,
|
||||
ReferenceLine,
|
||||
type TooltipProps,
|
||||
} from "recharts";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import assert from "assert";
|
||||
import type {
|
||||
NameType,
|
||||
ValueType,
|
||||
} from "recharts/types/component/DefaultTooltipContent";
|
||||
|
||||
// Schema for form validation
|
||||
const formSchema = z.object({
|
||||
startingCapital: z.coerce.number(),
|
||||
monthlySavings: z.coerce
|
||||
.number()
|
||||
.min(0, "Monthly savings must be a non-negative number"),
|
||||
currentAge: z.coerce
|
||||
.number()
|
||||
.min(1, "Age must be at least 1")
|
||||
.max(100, "No point in starting this late"),
|
||||
cagr: z.coerce.number().min(0, "Growth rate must be a non-negative number"),
|
||||
desiredMonthlyAllowance: z.coerce
|
||||
.number()
|
||||
.min(0, "Monthly allowance must be a non-negative number"),
|
||||
inflationRate: z.coerce
|
||||
.number()
|
||||
.min(0, "Inflation rate must be a non-negative number"),
|
||||
lifeExpectancy: z.coerce
|
||||
.number()
|
||||
.min(40, "Be a bit more optimistic buddy :(")
|
||||
.max(100, "You should be more realistic..."),
|
||||
retirementAge: z.coerce
|
||||
.number()
|
||||
.min(18, "Retirement age must be at least 18")
|
||||
.max(100, "Retirement age must be at most 100"),
|
||||
});
|
||||
|
||||
// Type for form values
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
interface YearlyData {
|
||||
age: number;
|
||||
year: number;
|
||||
balance: number;
|
||||
untouchedBalance: number;
|
||||
phase: "accumulation" | "retirement";
|
||||
monthlyAllowance: number;
|
||||
untouchedMonthlyAllowance: number;
|
||||
}
|
||||
|
||||
interface CalculationResult {
|
||||
fireNumber: number | null;
|
||||
fireNumber4percent: number | null;
|
||||
retirementAge4percent: number | null;
|
||||
yearlyData: YearlyData[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Helper function to format currency without specific symbols
|
||||
const formatNumber = (value: number | null) => {
|
||||
if (!value) return "N/A";
|
||||
return new Intl.NumberFormat("en", {
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
// Helper function to render tooltip for chart
|
||||
const tooltipRenderer = ({
|
||||
active,
|
||||
payload,
|
||||
}: TooltipProps<ValueType, NameType>) => {
|
||||
if (active && payload?.[0]?.payload) {
|
||||
const data = payload[0].payload as YearlyData;
|
||||
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>{`Phase: ${data.phase === "accumulation" ? "Accumulation" : "Retirement"}`}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
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>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
startingCapital: 50000,
|
||||
monthlySavings: 1500,
|
||||
currentAge: 25,
|
||||
cagr: 7,
|
||||
desiredMonthlyAllowance: 3000,
|
||||
inflationRate: 2.3,
|
||||
lifeExpectancy: 84,
|
||||
retirementAge: 55,
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: FormValues) {
|
||||
setResult(null); // Reset previous results
|
||||
|
||||
const startingCapital = values.startingCapital;
|
||||
const monthlySavings = values.monthlySavings;
|
||||
const age = values.currentAge;
|
||||
const annualGrowthRate = 1 + values.cagr / 100;
|
||||
const initialMonthlyAllowance = values.desiredMonthlyAllowance;
|
||||
const annualInflation = 1 + values.inflationRate / 100;
|
||||
const ageOfDeath = values.lifeExpectancy;
|
||||
const retirementAge = values.retirementAge;
|
||||
|
||||
// Array to store yearly data for the chart
|
||||
const yearlyData: YearlyData[] = [];
|
||||
|
||||
// Initial year data
|
||||
yearlyData.push({
|
||||
age: age,
|
||||
year: irlYear,
|
||||
balance: startingCapital,
|
||||
untouchedBalance: startingCapital,
|
||||
phase: "accumulation",
|
||||
monthlyAllowance: 0,
|
||||
untouchedMonthlyAllowance: initialMonthlyAllowance,
|
||||
});
|
||||
|
||||
// Calculate accumulation phase (before retirement)
|
||||
for (let year = irlYear + 1; year <= irlYear + (ageOfDeath - age); year++) {
|
||||
const currentAge = age + (year - irlYear);
|
||||
const previousYearData = yearlyData[yearlyData.length - 1];
|
||||
const inflatedAllowance =
|
||||
initialMonthlyAllowance * Math.pow(annualInflation, year - irlYear);
|
||||
|
||||
const isRetirementYear = currentAge >= retirementAge;
|
||||
const phase = isRetirementYear ? "retirement" : "accumulation";
|
||||
|
||||
assert(!!previousYearData);
|
||||
// Calculate balance based on phase
|
||||
let newBalance;
|
||||
if (phase === "accumulation") {
|
||||
// During accumulation: grow previous balance + add savings
|
||||
newBalance =
|
||||
previousYearData.balance * annualGrowthRate + monthlySavings * 12;
|
||||
} else {
|
||||
// During retirement: grow previous balance - withdraw allowance
|
||||
newBalance =
|
||||
previousYearData.balance * annualGrowthRate - inflatedAllowance * 12;
|
||||
}
|
||||
const untouchedBalance =
|
||||
previousYearData.untouchedBalance * annualGrowthRate +
|
||||
monthlySavings * 12;
|
||||
const allowance = phase === "retirement" ? inflatedAllowance : 0;
|
||||
yearlyData.push({
|
||||
age: currentAge,
|
||||
year: year,
|
||||
balance: newBalance,
|
||||
untouchedBalance: untouchedBalance,
|
||||
phase: phase,
|
||||
monthlyAllowance: allowance,
|
||||
untouchedMonthlyAllowance: inflatedAllowance,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate FIRE number at retirement
|
||||
const retirementYear = irlYear + (retirementAge - age);
|
||||
const retirementIndex = yearlyData.findIndex(
|
||||
(data) => data.year === retirementYear,
|
||||
);
|
||||
const retirementData = yearlyData[retirementIndex];
|
||||
|
||||
const [fireNumber4percent, retirementAge4percent] = (() => {
|
||||
for (const yearData of yearlyData) {
|
||||
if (
|
||||
yearData.untouchedBalance >
|
||||
(yearData.untouchedMonthlyAllowance * 12) / 0.04
|
||||
) {
|
||||
return [yearData.untouchedBalance, yearData.age];
|
||||
}
|
||||
}
|
||||
return [0, 0];
|
||||
})();
|
||||
|
||||
if (retirementIndex === -1 || !retirementData) {
|
||||
setResult({
|
||||
fireNumber: null,
|
||||
fireNumber4percent: null,
|
||||
retirementAge4percent: null,
|
||||
error: "Could not calculate retirement data",
|
||||
yearlyData: yearlyData,
|
||||
});
|
||||
} else {
|
||||
// Set the result
|
||||
setResult({
|
||||
fireNumber: retirementData.balance,
|
||||
fireNumber4percent: fireNumber4percent,
|
||||
retirementAge4percent: retirementAge4percent,
|
||||
yearlyData: yearlyData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">FIRE Calculator</CardTitle>
|
||||
<CardDescription>
|
||||
Calculate your path to financial independence and retirement
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="startingCapital"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Starting Capital</FormLabel>
|
||||
<FormControl>
|
||||
<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),
|
||||
);
|
||||
void form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="monthlySavings"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Monthly Savings</FormLabel>
|
||||
<FormControl>
|
||||
<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),
|
||||
);
|
||||
void form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="currentAge"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Current Age</FormLabel>
|
||||
<FormControl>
|
||||
<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),
|
||||
);
|
||||
void form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lifeExpectancy"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Life Expectancy (Age)</FormLabel>
|
||||
<FormControl>
|
||||
<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),
|
||||
);
|
||||
void form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cagr"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Expected Annual Growth Rate (%)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
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),
|
||||
);
|
||||
void form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="inflationRate"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Annual Inflation Rate (%)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
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),
|
||||
);
|
||||
void form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="desiredMonthlyAllowance"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Desired Monthly Allowance (Today'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),
|
||||
);
|
||||
void form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Retirement Age Slider */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="retirementAge"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Retirement Age: {field.value as number}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Slider
|
||||
name="retirementAge"
|
||||
value={[field.value as number]}
|
||||
min={25}
|
||||
max={75}
|
||||
step={1}
|
||||
onValueChange={(value: number[]) => {
|
||||
field.onChange(value[0]);
|
||||
void form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
className="py-4"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!result && (
|
||||
<Button type="submit" className="w-full">
|
||||
Calculate
|
||||
</Button>
|
||||
)}
|
||||
{result?.yearlyData && (
|
||||
<Card className="rounded-md shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Financial Projection</CardTitle>
|
||||
<CardDescription>
|
||||
Projected balance growth with your selected retirement age
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2">
|
||||
<ChartContainer
|
||||
className="aspect-auto h-80 w-full"
|
||||
config={{}}
|
||||
>
|
||||
<AreaChart
|
||||
data={result.yearlyData}
|
||||
margin={{ top: 10, right: 20, left: 20, bottom: 10 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="year"
|
||||
label={{
|
||||
value: "Year",
|
||||
position: "insideBottom",
|
||||
offset: -10,
|
||||
}}
|
||||
/>
|
||||
{/* Right Y axis */}
|
||||
<YAxis
|
||||
yAxisId={"right"}
|
||||
orientation="right"
|
||||
tickFormatter={(value: number) => {
|
||||
if (value >= 1000000) {
|
||||
return `${(value / 1000000).toPrecision(3)}M`;
|
||||
} else if (value >= 1000) {
|
||||
return `${(value / 1000).toPrecision(3)}K`;
|
||||
} else if (value <= -1000000) {
|
||||
return `${(value / 1000000).toPrecision(3)}M`;
|
||||
} else if (value <= -1000) {
|
||||
return `${(value / 1000).toPrecision(3)}K`;
|
||||
}
|
||||
return value.toString();
|
||||
}}
|
||||
width={30}
|
||||
stroke="var(--color-orange-500)"
|
||||
tick={{}}
|
||||
/>
|
||||
{/* Left Y axis */}
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
orientation="left"
|
||||
tickFormatter={(value: number) => {
|
||||
if (value >= 1000000) {
|
||||
return `${(value / 1000000).toPrecision(3)}M`;
|
||||
} else if (value >= 1000) {
|
||||
return `${(value / 1000).toPrecision(3)}K`;
|
||||
}
|
||||
return value.toString();
|
||||
}}
|
||||
width={30}
|
||||
stroke="var(--color-red-600)"
|
||||
/>
|
||||
<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>
|
||||
</defs>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="balance"
|
||||
name="balance"
|
||||
stroke="var(--color-orange-500)"
|
||||
fill="url(#fillBalance)"
|
||||
fillOpacity={0.9}
|
||||
activeDot={{ r: 6 }}
|
||||
yAxisId={"right"}
|
||||
stackId={"a"}
|
||||
/>
|
||||
<Area
|
||||
type="step"
|
||||
dataKey="monthlyAllowance"
|
||||
name="allowance"
|
||||
stroke="var(--color-red-600)"
|
||||
fill="none"
|
||||
activeDot={{ r: 6 }}
|
||||
yAxisId="left"
|
||||
/>
|
||||
{result.fireNumber && (
|
||||
<ReferenceLine
|
||||
y={result.fireNumber}
|
||||
stroke="var(--primary)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="2 1"
|
||||
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"}
|
||||
/>
|
||||
)}
|
||||
<ReferenceLine
|
||||
x={
|
||||
irlYear +
|
||||
(Number(form.getValues("retirementAge")) -
|
||||
Number(form.getValues("currentAge")))
|
||||
}
|
||||
stroke="var(--primary)"
|
||||
strokeWidth={2}
|
||||
label={{
|
||||
value: "Retirement",
|
||||
position: "insideTopRight",
|
||||
}}
|
||||
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>
|
||||
</Card>
|
||||
|
||||
{result && (
|
||||
<div className="mb-4 grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
{result.error ? (
|
||||
<Card className="col-span-full">
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-destructive">{result.error}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>FIRE Number</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Capital at retirement
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-3xl font-bold">
|
||||
{formatNumber(result.fireNumber)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Retirement Duration</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Years to enjoy your financial independence
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-3xl font-bold">
|
||||
{Number(form.getValues("lifeExpectancy")) -
|
||||
Number(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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="w-full py-8 text-center text-xs">
|
||||
<p className="text-xs">
|
||||
© {new Date().getFullYear()} InvestingFIRE. All rights reserved.{" "}
|
||||
<a
|
||||
href="https://schulze.network"
|
||||
target="_blank"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Hosting by Schulze.network
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
);
|
||||
}
|
@@ -1,77 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { usePlausible } from "next-plausible";
|
||||
import { useReportWebVitals } from "next/web-vitals";
|
||||
interface Metric {
|
||||
/**
|
||||
* The name of the metric (in acronym form).
|
||||
*/
|
||||
name: "CLS" | "FCP" | "FID" | "INP" | "LCP" | "TTFB";
|
||||
|
||||
/**
|
||||
* The current value of the metric.
|
||||
*/
|
||||
value: number;
|
||||
|
||||
/**
|
||||
* The rating as to whether the metric value is within the "good",
|
||||
* "needs improvement", or "poor" thresholds of the metric.
|
||||
*/
|
||||
rating: "good" | "needs-improvement" | "poor";
|
||||
|
||||
/**
|
||||
* The delta between the current value and the last-reported value.
|
||||
* On the first report, `delta` and `value` will always be the same.
|
||||
*/
|
||||
delta: number;
|
||||
|
||||
/**
|
||||
* A unique ID representing this particular metric instance. This ID can
|
||||
* be used by an analytics tool to dedupe multiple values sent for the same
|
||||
* metric instance, or to group multiple deltas together and calculate a
|
||||
* total. It can also be used to differentiate multiple different metric
|
||||
* instances sent from the same page, which can happen if the page is
|
||||
* restored from the back/forward cache (in that case new metrics object
|
||||
* get created).
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Any performance entries relevant to the metric value calculation.
|
||||
* The array may also be empty if the metric value was not based on any
|
||||
* entries (e.g. a CLS value of 0 given no layout shifts).
|
||||
*/
|
||||
entries: PerformanceEntry[];
|
||||
|
||||
/**
|
||||
* The type of navigation.
|
||||
*
|
||||
* This will be the value returned by the Navigation Timing API (or
|
||||
* `undefined` if the browser doesn't support that API), with the following
|
||||
* exceptions:
|
||||
* - 'back-forward-cache': for pages that are restored from the bfcache.
|
||||
* - 'back_forward' is renamed to 'back-forward' for consistency.
|
||||
* - 'prerender': for pages that were prerendered.
|
||||
* - 'restore': for pages that were discarded by the browser and then
|
||||
* restored by the user.
|
||||
*/
|
||||
navigationType:
|
||||
| "navigate"
|
||||
| "reload"
|
||||
| "back-forward"
|
||||
| "back-forward-cache"
|
||||
| "prerender"
|
||||
| "restore";
|
||||
}
|
||||
|
||||
export function WebVitals() {
|
||||
const plausible = usePlausible();
|
||||
useReportWebVitals((metric: Metric) => {
|
||||
plausible("web-vitals", {
|
||||
props: {
|
||||
[metric.name]: metric.rating,
|
||||
},
|
||||
});
|
||||
});
|
||||
return <></>;
|
||||
}
|
BIN
src/app/favicon.ico
(Stored with Git LFS)
BIN
src/app/favicon.ico
(Stored with Git LFS)
Binary file not shown.
@@ -1,404 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
export default function FireByAgeCalculator() {
|
||||
const [currentAge, setCurrentAge] = useState(25);
|
||||
const [targetRetirementAge, setTargetRetirementAge] = useState(40);
|
||||
const [currentSavings, setCurrentSavings] = useState(10000);
|
||||
const [annualExpenses, setAnnualExpenses] = useState(50000);
|
||||
const [expectedReturn, setExpectedReturn] = useState(7);
|
||||
const [currentIncome, setCurrentIncome] = useState(75000);
|
||||
|
||||
// Calculate years to retirement
|
||||
const yearsToRetirement = targetRetirementAge - currentAge;
|
||||
|
||||
// Determine withdrawal rate based on retirement age
|
||||
const getWithdrawalRate = (age: number) => {
|
||||
if (age <= 35) return 3;
|
||||
if (age <= 40) return 3.5;
|
||||
if (age <= 45) return 3.75;
|
||||
if (age <= 50) return 4;
|
||||
return 4.25;
|
||||
};
|
||||
|
||||
const withdrawalRate = getWithdrawalRate(targetRetirementAge);
|
||||
const fireMultiplier = 100 / withdrawalRate;
|
||||
|
||||
// Calculate FIRE number
|
||||
const fireNumber = annualExpenses * fireMultiplier;
|
||||
|
||||
// Calculate future value of current savings
|
||||
const futureValueOfCurrentSavings =
|
||||
currentSavings * Math.pow(1 + expectedReturn / 100, yearsToRetirement);
|
||||
|
||||
// Calculate gap
|
||||
const gap = fireNumber - futureValueOfCurrentSavings;
|
||||
|
||||
// Calculate required monthly savings
|
||||
const calculateMonthlySavings = () => {
|
||||
if (yearsToRetirement <= 0 || gap <= 0) return 0;
|
||||
const monthlyReturn = expectedReturn / 100 / 12;
|
||||
const months = yearsToRetirement * 12;
|
||||
return (gap * monthlyReturn) / (Math.pow(1 + monthlyReturn, months) - 1);
|
||||
};
|
||||
|
||||
const requiredMonthlySavings = calculateMonthlySavings();
|
||||
const requiredAnnualSavings = requiredMonthlySavings * 12;
|
||||
const savingsRate = (requiredAnnualSavings / currentIncome) * 100;
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
// Age-specific recommendations
|
||||
const getRecommendations = () => {
|
||||
if (targetRetirementAge <= 35) {
|
||||
return {
|
||||
difficulty: "Extremely Challenging",
|
||||
color: "text-red-600",
|
||||
tips: [
|
||||
"Requires 60-80% savings rate",
|
||||
"Focus on maximizing income",
|
||||
"Consider geographic arbitrage",
|
||||
"Live extremely frugally",
|
||||
],
|
||||
};
|
||||
} else if (targetRetirementAge <= 40) {
|
||||
return {
|
||||
difficulty: "Very Challenging",
|
||||
color: "text-orange-600",
|
||||
tips: [
|
||||
"Requires 50-60% savings rate",
|
||||
"Maximize career growth",
|
||||
"House hack or minimize housing",
|
||||
"Avoid lifestyle inflation",
|
||||
],
|
||||
};
|
||||
} else if (targetRetirementAge <= 45) {
|
||||
return {
|
||||
difficulty: "Challenging",
|
||||
color: "text-yellow-600",
|
||||
tips: [
|
||||
"Requires 40-50% savings rate",
|
||||
"Build multiple income streams",
|
||||
"Consider Coast FIRE strategy",
|
||||
"Plan for healthcare costs",
|
||||
],
|
||||
};
|
||||
} else if (targetRetirementAge <= 50) {
|
||||
return {
|
||||
difficulty: "Moderate",
|
||||
color: "text-blue-600",
|
||||
tips: [
|
||||
"Requires 30-40% savings rate",
|
||||
"Use tax-advantaged accounts",
|
||||
"Consider part-time work",
|
||||
"Plan Roth conversions",
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
difficulty: "Achievable",
|
||||
color: "text-green-600",
|
||||
tips: [
|
||||
"Requires 25-35% savings rate",
|
||||
"Maximize 401(k) contributions",
|
||||
"Use Rule of 55 if applicable",
|
||||
"Bridge to Social Security",
|
||||
],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const recommendations = getRecommendations();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Input Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Your FIRE by Age Inputs</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your details to see what it takes to retire at your target age
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="current-age">Current Age</Label>
|
||||
<span className="text-sm font-medium">{currentAge}</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="current-age"
|
||||
min={20}
|
||||
max={55}
|
||||
step={1}
|
||||
value={[currentAge]}
|
||||
onValueChange={(value) => setCurrentAge(value[0] ?? 25)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="target-age">Target Retirement Age</Label>
|
||||
<Select
|
||||
value={targetRetirementAge.toString()}
|
||||
onValueChange={(value) => setTargetRetirementAge(Number(value))}
|
||||
>
|
||||
<SelectTrigger id="target-age">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="30">Retire at 30</SelectItem>
|
||||
<SelectItem value="35">Retire at 35</SelectItem>
|
||||
<SelectItem value="40">Retire at 40</SelectItem>
|
||||
<SelectItem value="45">Retire at 45</SelectItem>
|
||||
<SelectItem value="50">Retire at 50</SelectItem>
|
||||
<SelectItem value="55">Retire at 55</SelectItem>
|
||||
<SelectItem value="60">Retire at 60</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="current-savings">
|
||||
Current Retirement Savings
|
||||
</Label>
|
||||
<Input
|
||||
id="current-savings"
|
||||
type="number"
|
||||
value={currentSavings}
|
||||
onChange={(e) => setCurrentSavings(Number(e.target.value))}
|
||||
min={0}
|
||||
step={1000}
|
||||
/>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Total invested for retirement
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="current-income">Current Annual Income</Label>
|
||||
<Input
|
||||
id="current-income"
|
||||
type="number"
|
||||
value={currentIncome}
|
||||
onChange={(e) => setCurrentIncome(Number(e.target.value))}
|
||||
min={0}
|
||||
step={1000}
|
||||
/>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Pre-tax annual income
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="annual-expenses">
|
||||
Annual Expenses in Retirement
|
||||
</Label>
|
||||
<Input
|
||||
id="annual-expenses"
|
||||
type="number"
|
||||
value={annualExpenses}
|
||||
onChange={(e) => setAnnualExpenses(Number(e.target.value))}
|
||||
min={0}
|
||||
step={1000}
|
||||
/>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Yearly spending needs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="expected-return">Expected Annual Return</Label>
|
||||
<span className="text-sm font-medium">{expectedReturn}%</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="expected-return"
|
||||
min={4}
|
||||
max={10}
|
||||
step={0.5}
|
||||
value={[expectedReturn]}
|
||||
onValueChange={(value) => setExpectedReturn(value[0] ?? 7)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Results Section */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Your FIRE Number</CardTitle>
|
||||
<CardDescription>
|
||||
Target for retiring at {targetRetirementAge}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-primary text-4xl font-bold">
|
||||
{formatCurrency(fireNumber)}
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
{fireMultiplier.toFixed(1)}× annual expenses ({withdrawalRate}%
|
||||
withdrawal rate)
|
||||
</p>
|
||||
<div className="bg-foreground/5 mt-4 rounded-lg p-3">
|
||||
<p className="text-sm font-medium">Years to Retirement</p>
|
||||
<p className="text-2xl font-bold">{yearsToRetirement}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Required Monthly Savings</CardTitle>
|
||||
<CardDescription>To reach your FIRE goal</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-primary text-4xl font-bold">
|
||||
{formatCurrency(requiredMonthlySavings)}
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
{formatCurrency(requiredAnnualSavings)}/year
|
||||
</p>
|
||||
<div className="bg-foreground/5 mt-4 rounded-lg p-3">
|
||||
<p className="text-sm font-medium">Savings Rate Required</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{Math.min(savingsRate, 100).toFixed(0)}%
|
||||
</p>
|
||||
{savingsRate > 100 && (
|
||||
<p className="mt-1 text-xs text-red-600">
|
||||
⚠️ Exceeds current income
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Difficulty Assessment */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Difficulty Assessment: Retiring at {targetRetirementAge}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-4">
|
||||
<p className="text-lg font-medium">Difficulty Level:</p>
|
||||
<p className={`text-3xl font-bold ${recommendations.color}`}>
|
||||
{recommendations.difficulty}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-3 text-lg font-medium">Key Success Factors:</p>
|
||||
<ul className="ml-6 list-disc space-y-2">
|
||||
{recommendations.tips.map((tip, idx) => (
|
||||
<li key={idx} className="text-lg">
|
||||
{tip}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Breakdown */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Your Path to FIRE at {targetRetirementAge}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="bg-foreground/5 rounded-lg p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Current Savings Growth
|
||||
</p>
|
||||
<p className="text-xl font-bold">
|
||||
{formatCurrency(futureValueOfCurrentSavings)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-foreground/5 rounded-lg p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">Additional Needed</p>
|
||||
<p className="text-xl font-bold">
|
||||
{formatCurrency(Math.max(gap, 0))}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-foreground/5 rounded-lg p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Total Contributions
|
||||
</p>
|
||||
<p className="text-xl font-bold">
|
||||
{formatCurrency(requiredAnnualSavings * yearsToRetirement)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-foreground/5 rounded-lg p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">Investment Growth</p>
|
||||
<p className="text-xl font-bold">
|
||||
{formatCurrency(
|
||||
fireNumber -
|
||||
currentSavings -
|
||||
requiredAnnualSavings * yearsToRetirement,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Tips Card */}
|
||||
{savingsRate > 50 && (
|
||||
<Card className="border-orange-200 bg-orange-50 dark:border-orange-900 dark:bg-orange-950/20">
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-sm">
|
||||
<strong>⚡ High Savings Rate Required:</strong> Achieving a{" "}
|
||||
{savingsRate.toFixed(0)}% savings rate is challenging. Consider
|
||||
increasing income through side hustles, reducing major expenses
|
||||
like housing/transportation, or adjusting your target retirement
|
||||
age to {targetRetirementAge + 5} for a more manageable{" "}
|
||||
{(
|
||||
((requiredAnnualSavings * yearsToRetirement) /
|
||||
((targetRetirementAge + 5 - currentAge) * currentIncome)) *
|
||||
100
|
||||
).toFixed(0)}
|
||||
% savings rate.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,625 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import BackgroundPattern from "@/app/components/BackgroundPattern";
|
||||
import Footer from "@/app/components/footer";
|
||||
import FireByAgeCalculator from "./FireByAgeCalculator";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "FIRE by Age Guide - Retire at 30, 35, 40, 45, 50, 55 | InvestingFIRE",
|
||||
description:
|
||||
"Complete guide to achieving FIRE at any age. Learn how much you need to retire at 30, 35, 40, 45, 50, or 55. Free calculator with age-specific strategies and savings targets.",
|
||||
keywords:
|
||||
"retire at 40, retire at 45, retire at 50, retire at 35, retire at 30, early retirement by age, FIRE age calculator, how much to retire at 40",
|
||||
openGraph: {
|
||||
title: "FIRE by Age Guide - When Can You Retire?",
|
||||
description:
|
||||
"Discover exactly how much you need to retire at 30, 35, 40, 45, 50, or 55. Complete guide with calculator and age-specific strategies.",
|
||||
type: "website",
|
||||
url: "https://investingfire.com/guides/fire-by-age",
|
||||
},
|
||||
};
|
||||
|
||||
export default function FireByAgePage() {
|
||||
const faqData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: [
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "How much do I need to retire at 40?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "To retire at 40, you typically need 25-30x your annual expenses saved. For $50,000/year in expenses, that's $1.25-1.5 million. The higher multiplier accounts for a longer retirement period. Starting at 25, you'd need to save about $3,000-4,000/month assuming 7% returns.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "Can I retire at 50 with $1 million?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Yes, you can retire at 50 with $1 million if your annual expenses are $40,000 or less (using the 4% rule). For a more conservative 3.5% withdrawal rate, you'd need expenses under $35,000/year. Consider that you'll have 15 years before Medicare eligibility, so factor in health insurance costs.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "What's the best age to retire early?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "The 'best' age depends on your personal circumstances, but many FIRE achievers target 40-50. This balances having enough working years to accumulate wealth with plenty of healthy retirement years. Earlier retirement requires more aggressive saving and potentially lower withdrawal rates.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "How does retirement age affect withdrawal rates?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Younger retirees should use lower withdrawal rates. While the 4% rule works for 30-year retirements, consider: Age 30-35: 3-3.25% withdrawal rate. Age 40-45: 3.5% withdrawal rate. Age 50-55: 3.75-4% withdrawal rate. Age 60+: 4-4.5% withdrawal rate.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const breadcrumbData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 1,
|
||||
name: "Home",
|
||||
item: "https://investingfire.com",
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 2,
|
||||
name: "Guides",
|
||||
item: "https://investingfire.com/guides",
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 3,
|
||||
name: "FIRE by Age",
|
||||
item: "https://investingfire.com/guides/fire-by-age",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ageTargets = [
|
||||
{
|
||||
age: 30,
|
||||
multiplier: 33,
|
||||
withdrawalRate: 3,
|
||||
savingsYears: "5-10",
|
||||
challenges: [
|
||||
"Extremely aggressive saving required",
|
||||
"Limited career earnings time",
|
||||
"60+ year retirement horizon",
|
||||
],
|
||||
strategies: [
|
||||
"Save 70-80% of income",
|
||||
"High-income career essential",
|
||||
"Consider geographic arbitrage",
|
||||
],
|
||||
},
|
||||
{
|
||||
age: 35,
|
||||
multiplier: 30,
|
||||
withdrawalRate: 3.25,
|
||||
savingsYears: "10-15",
|
||||
challenges: [
|
||||
"Very high savings rate needed",
|
||||
"Family formation years",
|
||||
"55+ year retirement",
|
||||
],
|
||||
strategies: [
|
||||
"Save 60-70% of income",
|
||||
"Maximize career growth",
|
||||
"House hack or minimize housing",
|
||||
],
|
||||
},
|
||||
{
|
||||
age: 40,
|
||||
multiplier: 28,
|
||||
withdrawalRate: 3.5,
|
||||
savingsYears: "15-20",
|
||||
challenges: [
|
||||
"Peak earning years cut short",
|
||||
"Children's education costs",
|
||||
"Healthcare before Medicare",
|
||||
],
|
||||
strategies: [
|
||||
"Save 50-60% of income",
|
||||
"Build multiple income streams",
|
||||
"Plan for health insurance",
|
||||
],
|
||||
},
|
||||
{
|
||||
age: 45,
|
||||
multiplier: 27,
|
||||
withdrawalRate: 3.75,
|
||||
savingsYears: "20-25",
|
||||
challenges: [
|
||||
"Mid-career transition",
|
||||
"Aging parents care",
|
||||
"20 years to Medicare",
|
||||
],
|
||||
strategies: [
|
||||
"Save 40-50% of income",
|
||||
"Consider Coast FIRE first",
|
||||
"Build health insurance fund",
|
||||
],
|
||||
},
|
||||
{
|
||||
age: 50,
|
||||
multiplier: 25,
|
||||
withdrawalRate: 4,
|
||||
savingsYears: "25-30",
|
||||
challenges: [
|
||||
"Early retirement penalties",
|
||||
"15 years to Medicare",
|
||||
"Sequence of returns risk",
|
||||
],
|
||||
strategies: [
|
||||
"Save 30-40% of income",
|
||||
"Ladder conversions",
|
||||
"Part-time work option",
|
||||
],
|
||||
},
|
||||
{
|
||||
age: 55,
|
||||
multiplier: 25,
|
||||
withdrawalRate: 4.25,
|
||||
savingsYears: "30-35",
|
||||
challenges: [
|
||||
"10 years to Medicare",
|
||||
"Social Security timing",
|
||||
"Market volatility impact",
|
||||
],
|
||||
strategies: [
|
||||
"Save 25-35% of income",
|
||||
"Rule of 55 withdrawals",
|
||||
"Bridge account planning",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
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 />
|
||||
|
||||
{/* Header */}
|
||||
<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">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-4 transition-opacity hover:opacity-90"
|
||||
>
|
||||
<Image
|
||||
priority
|
||||
unoptimized
|
||||
src="/investingfire_logo_no-bg.svg"
|
||||
alt="InvestingFIRE Logo"
|
||||
width={60}
|
||||
height={60}
|
||||
/>
|
||||
<span className="text-2xl font-bold">InvestingFIRE</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<h1 className="from-primary via-primary-foreground to-primary mt-8 bg-gradient-to-r bg-clip-text text-4xl font-extrabold tracking-tight text-transparent drop-shadow-md sm:text-[4rem]">
|
||||
FIRE by Age Guide
|
||||
</h1>
|
||||
<p className="text-primary-foreground/90 max-w-2xl text-xl font-semibold md:text-2xl">
|
||||
Complete Guide to Retiring at 30, 35, 40, 45, 50, or 55
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Breadcrumb Schema */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbData) }}
|
||||
/>
|
||||
|
||||
{/* Calculator */}
|
||||
<div className="z-10 mt-8 w-full max-w-4xl">
|
||||
<FireByAgeCalculator />
|
||||
</div>
|
||||
|
||||
{/* SEO Content */}
|
||||
<div className="z-10 mx-auto max-w-4xl px-4 py-12 text-left">
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
Your Complete Guide to FIRE at Any Age
|
||||
</h2>
|
||||
<p className="mb-4 text-lg leading-relaxed">
|
||||
Achieving{" "}
|
||||
<strong>Financial Independence, Retire Early (FIRE)</strong> is
|
||||
possible at virtually any age, but the strategies, savings rates,
|
||||
and challenges vary dramatically depending on when you want to
|
||||
retire. This comprehensive guide breaks down exactly what it takes
|
||||
to retire at 30, 35, 40, 45, 50, or 55.
|
||||
</p>
|
||||
<p className="mb-4 text-lg leading-relaxed">
|
||||
The younger your target retirement age, the more aggressive your
|
||||
approach needs to be. While retiring at 55 might require saving
|
||||
25-35% of your income, retiring at 35 could demand 60-70% savings
|
||||
rates and significant lifestyle adjustments. Let's explore what's
|
||||
realistic for each age milestone.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Age-Specific Sections */}
|
||||
{ageTargets.map((target) => (
|
||||
<section
|
||||
key={target.age}
|
||||
className="mb-12 scroll-mt-20"
|
||||
id={`retire-at-${target.age}`}
|
||||
>
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
How to Retire at {target.age}: Complete Strategy
|
||||
</h2>
|
||||
|
||||
<div className="bg-foreground/10 mb-6 rounded-lg p-6">
|
||||
<h3 className="mb-4 text-xl font-semibold">
|
||||
Quick Facts: Retiring at {target.age}
|
||||
</h3>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<p className="font-medium">Target Multiple:</p>
|
||||
<p className="text-primary text-2xl font-bold">
|
||||
{target.multiplier}× annual expenses
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Safe Withdrawal Rate:</p>
|
||||
<p className="text-primary text-2xl font-bold">
|
||||
{target.withdrawalRate}%
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Typical Saving Period:</p>
|
||||
<p className="text-primary text-2xl font-bold">
|
||||
{target.savingsYears} years
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">For $50k/year expenses:</p>
|
||||
<p className="text-primary text-2xl font-bold">
|
||||
${(target.multiplier * 50).toLocaleString()}k needed
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="mb-3 text-xl font-semibold">Key Challenges</h3>
|
||||
<ul className="ml-6 list-disc space-y-2 text-lg">
|
||||
{target.challenges.map((challenge, idx) => (
|
||||
<li key={idx}>{challenge}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-3 text-xl font-semibold">
|
||||
Winning Strategies
|
||||
</h3>
|
||||
<ul className="ml-6 list-disc space-y-2 text-lg">
|
||||
{target.strategies.map((strategy, idx) => (
|
||||
<li key={idx}>{strategy}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-lg leading-relaxed">
|
||||
{target.age <= 35 &&
|
||||
"This ultra-early retirement requires exceptional discipline and often a very high income. Most successful retirees at this age work in tech, finance, or have entrepreneurial success. Geographic arbitrage is almost essential."}
|
||||
{target.age > 35 &&
|
||||
target.age <= 45 &&
|
||||
"This is the sweet spot for many FIRE achievers - enough time to build wealth while still having decades of healthy retirement. Focus on maximizing your peak earning years and maintaining a high savings rate."}
|
||||
{target.age > 45 &&
|
||||
"This more traditional early retirement timeline allows for a balanced approach. You'll have more time to benefit from compound growth and can use strategies like the Rule of 55 for penalty-free 401(k) access."}
|
||||
</p>
|
||||
</section>
|
||||
))}
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
Age-Specific FIRE Strategies Comparison
|
||||
</h2>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="bg-foreground/5 w-full border-collapse rounded-lg">
|
||||
<thead>
|
||||
<tr className="border-foreground/20 border-b">
|
||||
<th className="p-4 text-left">Retirement Age</th>
|
||||
<th className="p-4 text-center">Savings Rate</th>
|
||||
<th className="p-4 text-center">Years to Save</th>
|
||||
<th className="p-4 text-center">Withdrawal Rate</th>
|
||||
<th className="p-4 text-center">Risk Level</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-foreground/10 border-b">
|
||||
<td className="p-4 font-medium">Retire at 30</td>
|
||||
<td className="p-4 text-center">70-80%</td>
|
||||
<td className="p-4 text-center">5-10</td>
|
||||
<td className="p-4 text-center">3%</td>
|
||||
<td className="p-4 text-center text-red-600">Very High</td>
|
||||
</tr>
|
||||
<tr className="border-foreground/10 border-b">
|
||||
<td className="p-4 font-medium">Retire at 35</td>
|
||||
<td className="p-4 text-center">60-70%</td>
|
||||
<td className="p-4 text-center">10-15</td>
|
||||
<td className="p-4 text-center">3.25%</td>
|
||||
<td className="p-4 text-center text-orange-600">High</td>
|
||||
</tr>
|
||||
<tr className="border-foreground/10 border-b">
|
||||
<td className="p-4 font-medium">Retire at 40</td>
|
||||
<td className="p-4 text-center">50-60%</td>
|
||||
<td className="p-4 text-center">15-20</td>
|
||||
<td className="p-4 text-center">3.5%</td>
|
||||
<td className="p-4 text-center text-yellow-600">
|
||||
Moderate-High
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="border-foreground/10 border-b">
|
||||
<td className="p-4 font-medium">Retire at 45</td>
|
||||
<td className="p-4 text-center">40-50%</td>
|
||||
<td className="p-4 text-center">20-25</td>
|
||||
<td className="p-4 text-center">3.75%</td>
|
||||
<td className="p-4 text-center text-blue-600">Moderate</td>
|
||||
</tr>
|
||||
<tr className="border-foreground/10 border-b">
|
||||
<td className="p-4 font-medium">Retire at 50</td>
|
||||
<td className="p-4 text-center">30-40%</td>
|
||||
<td className="p-4 text-center">25-30</td>
|
||||
<td className="p-4 text-center">4%</td>
|
||||
<td className="p-4 text-center text-green-600">
|
||||
Low-Moderate
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-4 font-medium">Retire at 55</td>
|
||||
<td className="p-4 text-center">25-35%</td>
|
||||
<td className="p-4 text-center">30-35</td>
|
||||
<td className="p-4 text-center">4.25%</td>
|
||||
<td className="p-4 text-center text-green-600">Low</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
Critical Considerations by Retirement Age
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-foreground/10 rounded-lg p-6">
|
||||
<h3 className="mb-3 text-xl font-semibold">
|
||||
Healthcare Coverage Gap
|
||||
</h3>
|
||||
<ul className="ml-6 list-disc space-y-2 text-lg">
|
||||
<li>
|
||||
<strong>Retire at 30-40:</strong> 25-35 years until Medicare -
|
||||
Budget $15-25k/year for health insurance
|
||||
</li>
|
||||
<li>
|
||||
<strong>Retire at 45-50:</strong> 15-20 years gap - Consider
|
||||
ACA subsidies and HSA maximization
|
||||
</li>
|
||||
<li>
|
||||
<strong>Retire at 55:</strong> 10-year gap - Explore COBRA,
|
||||
spouse's plan, or private insurance
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground/10 rounded-lg p-6">
|
||||
<h3 className="mb-3 text-xl font-semibold">
|
||||
Social Security Strategy
|
||||
</h3>
|
||||
<ul className="ml-6 list-disc space-y-2 text-lg">
|
||||
<li>
|
||||
<strong>Retire before 35:</strong> Minimal SS benefits - Plan
|
||||
without relying on it
|
||||
</li>
|
||||
<li>
|
||||
<strong>Retire at 40-45:</strong> Reduced benefits - Factor in
|
||||
25-50% of normal benefit
|
||||
</li>
|
||||
<li>
|
||||
<strong>Retire at 50-55:</strong> Near-full benefits - Can be
|
||||
significant income supplement
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground/10 rounded-lg p-6">
|
||||
<h3 className="mb-3 text-xl font-semibold">
|
||||
Investment Allocation
|
||||
</h3>
|
||||
<ul className="ml-6 list-disc space-y-2 text-lg">
|
||||
<li>
|
||||
<strong>60+ year retirement:</strong> 80-90% stocks for
|
||||
growth, rebalance gradually
|
||||
</li>
|
||||
<li>
|
||||
<strong>40-50 year retirement:</strong> 70-80% stocks,
|
||||
consider bond ladder for first decade
|
||||
</li>
|
||||
<li>
|
||||
<strong>30-40 year retirement:</strong> 60-70% stocks,
|
||||
traditional balanced approach
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section className="mb-12">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqData) }}
|
||||
/>
|
||||
<h2 className="mb-4 text-3xl font-bold">FIRE by Age FAQ</h2>
|
||||
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
How much do I need to retire at 40?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
To retire at 40, you typically need 25-30x your annual expenses
|
||||
saved. For $50,000/year in expenses, that's $1.25-1.5 million.
|
||||
The higher multiplier accounts for a longer retirement period.
|
||||
Starting at 25, you'd need to save about $3,000-4,000/month
|
||||
assuming 7% returns.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
Can I retire at 50 with $1 million?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
Yes, you can retire at 50 with $1 million if your annual
|
||||
expenses are $40,000 or less (using the 4% rule). For a more
|
||||
conservative 3.5% withdrawal rate, you'd need expenses under
|
||||
$35,000/year. Consider that you'll have 15 years before Medicare
|
||||
eligibility, so factor in health insurance costs.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
What's the best age to retire early?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
The "best" age depends on your personal circumstances, but many
|
||||
FIRE achievers target 40-50. This balances having enough working
|
||||
years to accumulate wealth with plenty of healthy retirement
|
||||
years. Earlier retirement requires more aggressive saving and
|
||||
potentially lower withdrawal rates.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-4">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
How does retirement age affect withdrawal rates?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
Younger retirees should use lower withdrawal rates. While the 4%
|
||||
rule works for 30-year retirements, consider:
|
||||
<ul className="mt-2 ml-6 list-disc">
|
||||
<li>Age 30-35: 3-3.25% withdrawal rate</li>
|
||||
<li>Age 40-45: 3.5% withdrawal rate</li>
|
||||
<li>Age 50-55: 3.75-4% withdrawal rate</li>
|
||||
<li>Age 60+: 4-4.5% withdrawal rate</li>
|
||||
</ul>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-5">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
Is retiring at 35 realistic?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
Retiring at 35 is challenging but achievable with the right
|
||||
circumstances: high income ($100k+), low expenses, 60-70%
|
||||
savings rate, and disciplined investing. Most who achieve this
|
||||
work in high-paying fields, live frugally, and often have no
|
||||
children or delay having them. Geographic arbitrage to low-cost
|
||||
areas helps significantly.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-6">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
What about retiring with kids?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
Retiring early with children adds complexity but is doable. Key
|
||||
considerations: Budget $10-15k per child annually, plan for
|
||||
college (529 plans), factor in larger housing needs, and
|
||||
consider part-time work for stability. Many FIRE families find
|
||||
that having more time with kids is worth the extra financial
|
||||
planning required.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</section>
|
||||
|
||||
{/* Quick Reference */}
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
Quick Reference: FIRE Numbers by Age
|
||||
</h2>
|
||||
<div className="bg-foreground/10 rounded-lg p-6">
|
||||
<p className="mb-4 text-lg font-medium">
|
||||
Based on $50,000 annual expenses:
|
||||
</p>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{ageTargets.map((target) => (
|
||||
<div
|
||||
key={target.age}
|
||||
className="bg-background/50 flex justify-between rounded p-3"
|
||||
>
|
||||
<span className="font-medium">Retire at {target.age}:</span>
|
||||
<span className="text-primary font-bold">
|
||||
${(target.multiplier * 50).toLocaleString()},000
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Call to Action */}
|
||||
<section className="bg-foreground/10 mb-12 rounded-lg p-8 text-center">
|
||||
<h2 className="mb-4 text-2xl font-bold">
|
||||
Ready to Plan Your Early Retirement?
|
||||
</h2>
|
||||
<p className="mb-6 text-lg">
|
||||
Use our comprehensive calculators to create your personalized FIRE
|
||||
plan, whether you're targeting retirement at 35 or 55.
|
||||
</p>
|
||||
<div className="flex flex-col justify-center gap-4 sm:flex-row">
|
||||
<Link
|
||||
href="/"
|
||||
className="bg-primary text-primary-foreground inline-block rounded-lg px-6 py-3 font-semibold transition-opacity hover:opacity-90"
|
||||
>
|
||||
FIRE Calculator →
|
||||
</Link>
|
||||
<Link
|
||||
href="/calculators/coast-fire"
|
||||
className="bg-secondary text-secondary-foreground inline-block rounded-lg px-6 py-3 font-semibold transition-opacity hover:opacity-90"
|
||||
>
|
||||
Coast FIRE Calculator →
|
||||
</Link>
|
||||
<Link
|
||||
href="/calculators/4-percent-rule"
|
||||
className="bg-secondary text-secondary-foreground inline-block rounded-lg px-6 py-3 font-semibold transition-opacity hover:opacity-90"
|
||||
>
|
||||
4% Rule Calculator →
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="1000" height="1000" viewBox="0 0 264.58 264.58"><defs><linearGradient id="b"><stop offset="0" stop-color="#fd8315"/><stop offset="1" stop-color="#fa6b14"/></linearGradient><linearGradient id="a"><stop offset="0" stop-color="#f24b1b"/><stop offset="1" stop-color="#dc2f12"/></linearGradient><linearGradient xlink:href="#a" id="d" x1="172.49" x2="179.1" y1="64.48" y2="197.19" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#b" id="c" x1="118.9" x2="117.99" y1="14.21" y2="194.34" gradientUnits="userSpaceOnUse"/></defs><rect width="264.58" height="264.58" fill="#fdf2e4" ry="42.08" style="-inkscape-stroke:none"/><g stroke-linecap="round" stroke-linejoin="round" stroke-width=".13"><path fill="url(#c)" stroke="#f14a1b" d="m115.13 9.96-.26.01c-.97.11-1.29 1.02-.75 2.38a45.6 45.6 0 0 1 3.02 15.68c-.09 8.46.04 12.87-7.31 23.68s-23.16 21.9-33.96 34.66c-10.8 12.76-12.28 16.6-16.1 26.2A90.42 90.42 0 0 0 53.05 146c0 41.29 27.68 76.09 65.49 86.91 35.3-25.9 55.47-125.62 55.47-125.62s-14.45-10.54-18.57-18.89c-1.26-2.56-1.97-6.15-1.97-9.58.01-2.54 1.3-8.72 1.47-9.41a42.4 42.4 0 0 0 .94-12.14c-.07-.95-.17-1.9-.3-2.84v-.01a59.45 59.45 0 0 0-7.6-19h0a60.34 60.34 0 0 0-10.24-12.13 66.97 66.97 0 0 0-21.7-13.15 2.94 2.94 0 0 0-.92-.18z"/><path fill="url(#d)" stroke="#510a0c" d="M170.01 58.08a66.66 66.66 0 0 0-10.24 15.94 66.66 66.66 0 0 0-6.3 27.82 66.8 66.8 0 0 0 3.66 20.86h-.08l7.08 105.8s37.38-25.1 45.9-61a74.13 74.13 0 0 0 2.04-25.93c-1.34-14.35-4.35-21.67-9.85-30.3-5.5-8.62-10.36-14-17.63-22.78-6.17-7.43-9.44-18.25-10.39-28.87-.28-3.13-2.13-3.91-4.19-1.54z"/></g><path fill="#510a0c" d="M93.45 115.81h77.91c9.81 0 17.71 7.9 17.71 17.7v104.53c0 9.81-7.9 17.71-17.7 17.71H93.44c-9.81 0-17.71-7.9-17.71-17.7V133.51c0-9.81 7.9-17.71 17.7-17.71z"/><path fill="#e83c1b" d="M91.95 163.12h29.23c3.7 0 6.68 2.98 6.68 6.68v24.8c0 3.7-2.98 6.68-6.68 6.68H91.95a6.67 6.67 0 0 1-6.68-6.68v-24.8c0-3.7 2.98-6.68 6.68-6.68zm0 45.92h29.23c3.7 0 6.68 2.98 6.68 6.68v24.8c0 3.7-2.98 6.68-6.68 6.68H91.95a6.67 6.67 0 0 1-6.68-6.69v-24.8c0-3.7 2.98-6.67 6.68-6.67zm51.25-45.92h29.23c3.7 0 6.68 2.98 6.68 6.68v24.8c0 3.7-2.98 6.68-6.68 6.68H143.2a6.67 6.67 0 0 1-6.68-6.68v-24.8c0-3.7 2.98-6.68 6.68-6.68zm0 45.92h29.23c3.7 0 6.68 2.98 6.68 6.68v24.8c0 3.7-2.98 6.68-6.68 6.68H143.2a6.67 6.67 0 0 1-6.68-6.69v-24.8c0-3.7 2.98-6.67 6.68-6.67z"/><g fill="#520a0c"><path d="M148.74 179.98h18.29a2.41 2.41 0 1 1 0 4.84h-18.29a2.41 2.41 0 1 1 0-4.84zm-51.54.04h18.29a2.41 2.41 0 1 1 0 4.83h-18.3a2.41 2.41 0 1 1 0-4.83z"/><path d="M108.76 173.3v18.28a2.41 2.41 0 1 1-4.84 0V173.3a2.41 2.41 0 1 1 4.84 0zm-10.59 59.18 12.93-12.93a2.41 2.41 0 1 1 3.42 3.42l-12.93 12.93a2.41 2.41 0 1 1-3.42-3.42z"/><path d="m101.59 219.55 12.93 12.93a2.41 2.41 0 1 1-3.42 3.42l-12.93-12.93a2.41 2.41 0 1 1 3.42-3.42zm47.15 1.49h18.29a2.41 2.41 0 1 1 0 4.84h-18.29a2.41 2.41 0 1 1 0-4.84zm0 10.73h18.29a2.41 2.41 0 1 1 0 4.84h-18.29a2.41 2.41 0 1 1 0-4.84z"/></g><path fill="#fcf2e4" d="M92.35 125.2h79.67a7.07 7.07 0 0 1 7.09 7.1v14.36a7.07 7.07 0 0 1-7.09 7.1H92.35a7.07 7.07 0 0 1-7.08-7.1V132.3a7.07 7.07 0 0 1 7.08-7.09z"/></svg>
|
Before Width: | Height: | Size: 3.1 KiB |
BIN
src/app/icon1.png
(Stored with Git LFS)
BIN
src/app/icon1.png
(Stored with Git LFS)
Binary file not shown.
@@ -1,41 +0,0 @@
|
||||
import "@/styles/globals.css";
|
||||
import PlausibleProvider from "next-plausible";
|
||||
import { type Metadata, type Viewport } 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",
|
||||
description:
|
||||
"Achieve Financial Independence & Early Retirement! Plan your FIRE journey with the InvestingFIRE calculator and get personalized projections in buttersmooth graphs.",
|
||||
};
|
||||
|
||||
const geist = Geist({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-geist-sans",
|
||||
});
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="en" className={geist.variable}>
|
||||
<head>
|
||||
<meta name="apple-mobile-web-app-title" content="FIRE" />
|
||||
<PlausibleProvider
|
||||
domain="investingfire.com"
|
||||
customDomain="https://analytics.schulze.network"
|
||||
selfHosted={true}
|
||||
enabled={true}
|
||||
trackOutboundLinks={true}
|
||||
/>
|
||||
</head>
|
||||
<WebVitals />
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "InvestingFIRE",
|
||||
"short_name": "FIRE",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#fdf2e4",
|
||||
"background_color": "#fdf2e4",
|
||||
"display": "standalone"
|
||||
}
|
456
src/app/page.tsx
456
src/app/page.tsx
@@ -1,456 +0,0 @@
|
||||
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";
|
||||
|
||||
export default function HomePage() {
|
||||
const faqData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: [
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "What methodology does this calculator use?",
|
||||
acceptedAnswer: {
|
||||
"@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",
|
||||
name: "Why isn't this just the 4% rule?",
|
||||
acceptedAnswer: {
|
||||
"@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?",
|
||||
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": "Question",
|
||||
name: "How does inflation factor into my FIRE Number?",
|
||||
acceptedAnswer: {
|
||||
"@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?",
|
||||
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": "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.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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="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}
|
||||
height={100}
|
||||
/>
|
||||
<h1 className="from-primary via-primary-foreground to-primary bg-gradient-to-r bg-clip-text text-5xl font-extrabold tracking-tight text-transparent drop-shadow-md sm:text-[5rem]">
|
||||
InvestingFIRE
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-primary-foreground/90 text-xl font-semibold md:text-2xl">
|
||||
The #1 FIRE Calculator
|
||||
</p>
|
||||
<div className="mt-8 w-full max-w-2xl">
|
||||
<FireCalculatorForm />
|
||||
</div>
|
||||
</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
|
||||
</h2>
|
||||
<p className="mb-4 text-lg leading-relaxed">
|
||||
FIRE stands for{" "}
|
||||
<strong>Financial Independence, Retire Early</strong>. It'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
|
||||
income—so 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.
|
||||
</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 spending—you 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>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-4 text-3xl font-bold">
|
||||
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:
|
||||
</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
|
||||
</li>
|
||||
<li>
|
||||
<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
|
||||
</li>
|
||||
<li>
|
||||
<strong>Desired Monthly Allowance</strong>—today's-value
|
||||
spending goal
|
||||
</li>
|
||||
<li>
|
||||
<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.
|
||||
</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.
|
||||
</li>
|
||||
<li>
|
||||
<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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqData) }}
|
||||
/>
|
||||
<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">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
What methodology does this calculator use?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
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.
|
||||
</li>
|
||||
<li>
|
||||
<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'll have
|
||||
at retirement (your “FIRE Number”) and how long it will last
|
||||
until your chosen life expectancy.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
Why isn'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'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.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
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”
|
||||
scenarios—5% on the conservative side, 10% on the aggressive
|
||||
side—to see how they affect your timeline.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-4">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
How does inflation factor into my FIRE Number?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-lg leading-relaxed">
|
||||
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.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-5">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
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 inputs—so you can build
|
||||
confidence in your plan and make informed trade-offs between
|
||||
lifestyle, risk, and timeline.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-6">
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
How should I use this calculator effectively?
|
||||
</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>
|
||||
Review the chart—especially the reference lines—to 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.
|
||||
</li>
|
||||
</ul>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</section>
|
||||
|
||||
{/* 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>
|
||||
<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.
|
||||
</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>Identify areas to boost savings or reduce expenses.</li>
|
||||
<li>
|
||||
Study index-fund strategies and low-cost investing advice.
|
||||
</li>
|
||||
<li>
|
||||
Join{" "}
|
||||
<a
|
||||
href="https://www.reddit.com/r/Fire/"
|
||||
target="_blank"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
supportive communities like r/Fire
|
||||
</a>{" "}
|
||||
to learn from real journeys.
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="mb-3 text-xl font-semibold">Blogs & Websites</h3>
|
||||
<ul className="ml-6 list-disc space-y-2 text-lg">
|
||||
<li>
|
||||
<a
|
||||
href="https://www.mrmoneymustache.com/"
|
||||
target="_blank"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Mr. Money Mustache
|
||||
</a>{" "}
|
||||
- Hardcore frugality & early retirement success stories.
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.playingwithfire.co/"
|
||||
target="_blank"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Playing With FIRE
|
||||
</a>{" "}
|
||||
- Community resources & real-life case studies.
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.reddit.com/r/Fire/"
|
||||
target="_blank"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
r/Fire
|
||||
</a>{" "}
|
||||
- Active forum for questions, tips, and support.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-3 text-xl font-semibold">Books & Podcasts</h3>
|
||||
<ul className="ml-6 list-disc space-y-2 text-lg">
|
||||
<li>
|
||||
<a
|
||||
href="https://www.amazon.com/Your-Money-Life-Transforming-Relationship/dp/0143115766"
|
||||
target="_blank"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Your Money or Your Life
|
||||
</a>{" "}
|
||||
- The classic guide to aligning money with values.
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://podcasts.apple.com/us/podcast/biggerpockets-money-podcast/id1330225136"
|
||||
target="_blank"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
BiggerPockets Money Podcast
|
||||
</a>{" "}
|
||||
- Interviews on FIRE strategies and wealth building.
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://podcasts.apple.com/us/podcast/can-you-retire-now-this-fire-calculator-will-tell-you/id1330225136?i=1000683436292"
|
||||
target="_blank"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
InvestingFIRE Calculator Demo
|
||||
</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>
|
||||
<ul className="ml-6 list-disc space-y-2 text-lg">
|
||||
<li>
|
||||
<a
|
||||
href="https://ghostfol.io"
|
||||
target="_blank"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Ghostfolio
|
||||
</a>{" "}
|
||||
- Wealth management application for individuals.
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://walletburst.com/tools/coast-fire-calculator/"
|
||||
target="_blank"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Coast FIRE Calculator
|
||||
</a>{" "}
|
||||
- When you “max out” early contributions but let compounding
|
||||
do the rest.
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.investor.gov/financial-tools-calculators/calculators/compound-interest-calculator"
|
||||
target="_blank"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Compound Interest Calculator
|
||||
</a>{" "}
|
||||
- Explore the power of growth rates in isolation.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
@@ -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",
|
||||
};
|
||||
}
|
@@ -1,31 +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,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/calculators/4-percent-rule`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/calculators/coast-fire`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/guides/fire-by-age`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.9,
|
||||
},
|
||||
];
|
||||
}
|
@@ -1,69 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn(
|
||||
"border-primary-foreground/20 border-b last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-primary-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
);
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
@@ -1,357 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const;
|
||||
|
||||
export type ChartConfig = Record<
|
||||
string,
|
||||
{
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
)
|
||||
>;
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function ChartContainer({
|
||||
id,
|
||||
className,
|
||||
children,
|
||||
config,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"];
|
||||
}) {
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`;
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-slot="chart"
|
||||
data-chart={chartId}
|
||||
className={cn(
|
||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme ?? config.color,
|
||||
);
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ??
|
||||
itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`,
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [item] = payload;
|
||||
const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? (config[label]?.label ?? label)
|
||||
: itemConfig?.label;
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
]);
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor: string | undefined =
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
color ?? item.payload.fill ?? item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||
indicator === "dot" && "items-center",
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
},
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center",
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label ?? item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
hideIcon = false,
|
||||
payload,
|
||||
verticalAlign = "bottom",
|
||||
nameKey,
|
||||
}: React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
|
||||
if (!payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
const key = `${nameKey ?? item.dataKey ?? "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string,
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config ? config[configLabelKey] : config[key];
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
};
|
@@ -1,185 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default";
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
@@ -1,63 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Slider({
|
||||
className,
|
||||
defaultValue,
|
||||
value,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||
const _values = React.useMemo(
|
||||
() =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [min, max],
|
||||
[value, defaultValue, min, max],
|
||||
);
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
data-slot="slider"
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
data-slot="slider-track"
|
||||
className={cn(
|
||||
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
data-slot="slider-range"
|
||||
className={cn(
|
||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
||||
)}
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
{Array.from({ length: _values.length }, (_, index) => (
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
key={index}
|
||||
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Slider };
|
40
src/env.js
40
src/env.js
@@ -1,40 +0,0 @@
|
||||
import { createEnv } from "@t3-oss/env-nextjs";
|
||||
import { z } from "zod";
|
||||
|
||||
export const env = createEnv({
|
||||
/**
|
||||
* Specify your server-side environment variables schema here. This way you can ensure the app
|
||||
* isn't built with invalid env vars.
|
||||
*/
|
||||
server: {
|
||||
NODE_ENV: z.enum(["development", "test", "production"]),
|
||||
},
|
||||
|
||||
/**
|
||||
* Specify your client-side environment variables schema here. This way you can ensure the app
|
||||
* isn't built with invalid env vars. To expose them to the client, prefix them with
|
||||
* `NEXT_PUBLIC_`.
|
||||
*/
|
||||
client: {
|
||||
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
||||
},
|
||||
|
||||
/**
|
||||
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
|
||||
* middlewares) or client-side so we need to destruct manually.
|
||||
*/
|
||||
runtimeEnv: {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
||||
},
|
||||
/**
|
||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
||||
* useful for Docker builds.
|
||||
*/
|
||||
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
||||
/**
|
||||
* Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
|
||||
* `SOME_VAR=''` will throw an error.
|
||||
*/
|
||||
emptyStringAsUndefined: true,
|
||||
});
|
@@ -1 +0,0 @@
|
||||
export const BASE_URL = "https://investingfire.com/";
|
@@ -1,6 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
@@ -4,8 +4,7 @@
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
--font-sans:
|
||||
var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
|
||||
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
@@ -49,77 +48,71 @@
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--foreground: oklch(0.39 0.0215 96.47); /* black olive */
|
||||
--card: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--card-foreground: oklch(0.39 0.0215 96.47); /* black olive */
|
||||
--popover: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--popover-foreground: oklch(0.39 0.0215 96.47); /* black olive */
|
||||
--primary: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||
--primary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--secondary: oklch(0.49 0.1326 259.29); /* denim */
|
||||
--secondary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--muted: oklch(0.67 0.0763 198.81 / 20%); /* verdigris with opacity */
|
||||
--muted-foreground: oklch(
|
||||
0.39 0.0215 96.47 / 80%
|
||||
); /* black olive with opacity */
|
||||
--accent: oklch(0.49 0.1326 259.29); /* denim */
|
||||
--accent-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--destructive: oklch(0.33 0.1316 336.24); /* palatinate */
|
||||
--border: oklch(0.67 0.0763 198.81 / 30%); /* verdigris with opacity */
|
||||
--input: oklch(0.67 0.0763 198.81 / 30%); /* verdigris with opacity */
|
||||
--ring: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||
--chart-1: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||
--chart-2: oklch(0.49 0.1326 259.29); /* denim */
|
||||
--chart-3: oklch(0.33 0.1316 336.24); /* palatinate */
|
||||
--chart-4: oklch(0.39 0.0215 96.47); /* black olive */
|
||||
--chart-5: oklch(0.67 0.0763 198.81 / 70%); /* verdigris with opacity */
|
||||
--sidebar: oklch(0.49 0.1326 259.29 / 10%); /* denim with opacity */
|
||||
--sidebar-foreground: oklch(0.39 0.0215 96.47); /* black olive */
|
||||
--sidebar-primary: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||
--sidebar-primary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--sidebar-accent: oklch(0.49 0.1326 259.29); /* denim */
|
||||
--sidebar-accent-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--sidebar-border: oklch(
|
||||
0.67 0.0763 198.81 / 20%
|
||||
); /* verdigris with opacity */
|
||||
--sidebar-ring: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.39 0.0215 96.47); /* black olive */
|
||||
--foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--card: oklch(0.39 0.0215 96.47 / 80%); /* black olive with opacity */
|
||||
--card-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--popover: oklch(0.39 0.0215 96.47); /* black olive */
|
||||
--popover-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--primary: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||
--primary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--secondary: oklch(0.49 0.1326 259.29); /* denim */
|
||||
--secondary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--muted: oklch(0.39 0.0215 96.47 / 70%); /* black olive with opacity */
|
||||
--muted-foreground: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||
--accent: oklch(0.49 0.1326 259.29); /* denim */
|
||||
--accent-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--destructive: oklch(0.33 0.1316 336.24); /* palatinate */
|
||||
--border: oklch(0.97 0.0228 95.96 / 20%); /* cosmic latte with opacity */
|
||||
--input: oklch(0.97 0.0228 95.96 / 20%); /* cosmic latte with opacity */
|
||||
--ring: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||
--chart-1: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||
--chart-2: oklch(0.49 0.1326 259.29); /* denim */
|
||||
--chart-3: oklch(0.33 0.1316 336.24); /* palatinate */
|
||||
--chart-4: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--chart-5: oklch(0.67 0.0763 198.81 / 70%); /* verdigris with opacity */
|
||||
--sidebar: oklch(0.39 0.0215 96.47 / 90%); /* black olive with opacity */
|
||||
--sidebar-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--sidebar-primary: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||
--sidebar-primary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--sidebar-accent: oklch(0.49 0.1326 259.29); /* denim */
|
||||
--sidebar-accent-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||
--sidebar-border: oklch(
|
||||
0.97 0.0228 95.96 / 10%
|
||||
); /* cosmic latte with opacity */
|
||||
--sidebar-ring: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
Reference in New Issue
Block a user