1 Commits

Author SHA1 Message Date
17b822493b fix(deps): update dependency recharts to v3
Some checks failed
Lint / Lint and Typecheck (push) Failing after 39s
Lint / Lint and Typecheck (pull_request) Failing after 38s
2025-07-02 02:01:54 +00:00
8 changed files with 7711 additions and 5356 deletions

View File

@@ -4,7 +4,7 @@ on:
pull_request:
push:
branches:
- "**" # matches every branch
- '**' # matches every branch
jobs:
lint_and_typecheck:
@@ -15,17 +15,14 @@ jobs:
- 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"
cache: 'npm'
- name: Install dependencies
run: pnpm install
run: npm ci
- name: Run check
run: pnpm run check
run: npm run check

View File

@@ -29,6 +29,7 @@ The projects code is structured using React/Next.js with TypeScript, focusing
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).
@@ -57,11 +58,11 @@ To run locally:
```
2. **Install dependencies**
```bash
pnpm install
npm install
```
3. **Run the app**
```bash
pnpm run dev
npm run dev
```
4. Visit [http://localhost:3000](http://localhost:3000) and unleash the fire.

7659
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
"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:fix": "next lint --fix",
@@ -23,35 +23,34 @@
"@t3-oss/env-nextjs": "^0.13.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.539.0",
"next": "^15.4.1",
"lucide-react": "^0.525.0",
"next": "^15.2.3",
"next-plausible": "^3.12.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.56.1",
"recharts": "^2.15.3",
"recharts": "^3.0.0",
"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.10",
"@types/react-dom": "19.1.7",
"eslint": "9.33.0",
"eslint-config-next": "15.4.4",
"eslint-plugin-react-hooks": "5.2.0",
"@types/node": "22.15.34",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"eslint": "9.30.0",
"eslint-config-next": "15.3.4",
"postcss": "8.5.6",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.6.14",
"prettier-plugin-tailwindcss": "0.6.13",
"tailwindcss": "4.1.11",
"tw-animate-css": "1.3.6",
"typescript": "5.9.2",
"typescript-eslint": "8.39.0"
"tw-animate-css": "1.3.4",
"typescript": "5.8.3",
"typescript-eslint": "8.35.0"
},
"ct3aMetadata": {
"initVersion": "7.39.3"
},
"packageManager": "pnpm@10.14.0"
"packageManager": "npm@11.4.2"
}

5184
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -120,7 +120,7 @@ export default function FireCalculatorForm() {
const [showing4percent, setShowing4percent] = useState(false);
// Initialize form with default values
const form = useForm<z.input<typeof formSchema>, undefined, FormValues>({
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
startingCapital: 50000,
@@ -258,18 +258,11 @@ export default function FireCalculatorForm() {
<Input
placeholder="e.g., 10000"
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
{...field}
onChange={(value) => {
field.onChange(value);
void form.handleSubmit(onSubmit)();
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormControl>
<FormMessage />
@@ -286,18 +279,11 @@ export default function FireCalculatorForm() {
<Input
placeholder="e.g., 500"
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
{...field}
onChange={(value) => {
field.onChange(value);
void form.handleSubmit(onSubmit)();
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormControl>
<FormMessage />
@@ -314,18 +300,11 @@ export default function FireCalculatorForm() {
<Input
placeholder="e.g., 30"
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
{...field}
onChange={(value) => {
field.onChange(value);
void form.handleSubmit(onSubmit)();
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormControl>
<FormMessage />
@@ -342,18 +321,11 @@ export default function FireCalculatorForm() {
<Input
placeholder="e.g., 90"
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
{...field}
onChange={(value) => {
field.onChange(value);
void form.handleSubmit(onSubmit)();
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormControl>
<FormMessage />
@@ -371,18 +343,11 @@ export default function FireCalculatorForm() {
placeholder="e.g., 7"
type="number"
step="0.1"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
{...field}
onChange={(value) => {
field.onChange(value);
void form.handleSubmit(onSubmit)();
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormControl>
<FormMessage />
@@ -400,18 +365,11 @@ export default function FireCalculatorForm() {
placeholder="e.g., 2"
type="number"
step="0.1"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
{...field}
onChange={(value) => {
field.onChange(value);
void form.handleSubmit(onSubmit)();
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormControl>
<FormMessage />
@@ -430,18 +388,11 @@ export default function FireCalculatorForm() {
<Input
placeholder="e.g., 2000"
type="number"
value={field.value as number | string | undefined}
onChange={(e) => {
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value),
);
{...field}
onChange={(value) => {
field.onChange(value);
void form.handleSubmit(onSubmit)();
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormControl>
<FormMessage />
@@ -455,13 +406,11 @@ export default function FireCalculatorForm() {
name="retirementAge"
render={({ field }) => (
<FormItem>
<FormLabel>
Retirement Age: {field.value as number}
</FormLabel>
<FormLabel>Retirement Age: {field.value}</FormLabel>
<FormControl>
<Slider
name="retirementAge"
value={[field.value as number]}
value={[field.value]}
min={25}
max={75}
step={1}
@@ -614,8 +563,8 @@ export default function FireCalculatorForm() {
<ReferenceLine
x={
irlYear +
(Number(form.getValues("retirementAge")) -
Number(form.getValues("currentAge")))
(form.getValues("retirementAge") -
form.getValues("currentAge"))
}
stroke="var(--primary)"
strokeWidth={2}
@@ -630,7 +579,7 @@ export default function FireCalculatorForm() {
x={
irlYear +
(result.retirementAge4percent -
Number(form.getValues("currentAge")))
form.getValues("currentAge"))
}
stroke="var(--secondary)"
strokeWidth={1}
@@ -693,8 +642,8 @@ export default function FireCalculatorForm() {
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">
{Number(form.getValues("lifeExpectancy")) -
Number(form.getValues("retirementAge"))}
{form.getValues("lifeExpectancy") -
form.getValues("retirementAge")}
</p>
</CardContent>
</Card>
@@ -725,7 +674,7 @@ export default function FireCalculatorForm() {
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">
{Number(form.getValues("lifeExpectancy")) -
{form.getValues("lifeExpectancy") -
(result.retirementAge4percent ?? 0)}
</p>
</CardContent>

View File

@@ -10,61 +10,6 @@ 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 />
@@ -184,14 +129,9 @@ export default function HomePage() {
</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">