Compare commits
1 Commits
renovate/r
...
17b822493b
Author | SHA1 | Date | |
---|---|---|---|
17b822493b |
@@ -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
|
||||
|
@@ -29,6 +29,7 @@ The project’s 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
7659
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -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.536.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": "^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/node": "22.15.34",
|
||||
"@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",
|
||||
"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.38.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"
|
||||
}
|
||||
|
5065
pnpm-lock.yaml
generated
5065
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
|
@@ -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>
|
||||
|
@@ -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">
|
||||
|
Reference in New Issue
Block a user