Compare commits
1 Commits
main
...
92a8e4f84e
Author | SHA1 | Date | |
---|---|---|---|
92a8e4f84e |
@@ -4,7 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**" # matches every branch
|
- '**' # matches every branch
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint_and_typecheck:
|
lint_and_typecheck:
|
||||||
@@ -15,17 +15,14 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: "pnpm"
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Run check
|
- 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:
|
The calculator models your FIRE journey in two phases:
|
||||||
|
|
||||||
1. **Accumulation:**
|
1. **Accumulation:**
|
||||||
|
|
||||||
- Your starting capital is grown by your expected CAGR (~7% by default).
|
- Your starting capital is grown by your expected CAGR (~7% by default).
|
||||||
- Monthly savings are added for each year until retirement.
|
- Monthly savings are added for each year until retirement.
|
||||||
- Every variable can be adjusted live (capital, savings, age, growth, inflation, spending, target 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**
|
2. **Install dependencies**
|
||||||
```bash
|
```bash
|
||||||
pnpm install
|
npm install
|
||||||
```
|
```
|
||||||
3. **Run the app**
|
3. **Run the app**
|
||||||
```bash
|
```bash
|
||||||
pnpm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
4. Visit [http://localhost:3000](http://localhost:3000) and unleash the fire.
|
4. Visit [http://localhost:3000](http://localhost:3000) and unleash the fire.
|
||||||
|
|
||||||
|
7628
package-lock.json
generated
Normal file
7628
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"check": "next lint && tsc --noEmit",
|
"check": "next lint && tsc --noEmit",
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbo",
|
||||||
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
||||||
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
||||||
"lint:fix": "next lint --fix",
|
"lint:fix": "next lint --fix",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"next": "^15.4.1",
|
"next": "^15.2.3",
|
||||||
"next-plausible": "^3.12.4",
|
"next-plausible": "^3.12.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@@ -36,22 +36,21 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "3.3.1",
|
"@eslint/eslintrc": "3.3.1",
|
||||||
"@tailwindcss/postcss": "4.1.11",
|
"@tailwindcss/postcss": "4.1.11",
|
||||||
"@types/node": "22.16.3",
|
"@types/node": "22.16.0",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"@types/react-dom": "19.1.6",
|
"@types/react-dom": "19.1.6",
|
||||||
"eslint": "9.31.0",
|
"eslint": "9.30.1",
|
||||||
"eslint-config-next": "15.4.1",
|
"eslint-config-next": "15.3.5",
|
||||||
"eslint-plugin-react-hooks": "5.2.0",
|
|
||||||
"postcss": "8.5.6",
|
"postcss": "8.5.6",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"prettier-plugin-tailwindcss": "0.6.14",
|
"prettier-plugin-tailwindcss": "0.6.13",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"tw-animate-css": "1.3.5",
|
"tw-animate-css": "1.3.5",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"typescript-eslint": "8.36.0"
|
"typescript-eslint": "8.35.1"
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.39.3"
|
"initVersion": "7.39.3"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.13.1"
|
"packageManager": "npm@11.4.2"
|
||||||
}
|
}
|
||||||
|
5055
pnpm-lock.yaml
generated
5055
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);
|
const [showing4percent, setShowing4percent] = useState(false);
|
||||||
|
|
||||||
// Initialize form with default values
|
// Initialize form with default values
|
||||||
const form = useForm<z.input<typeof formSchema>, undefined, FormValues>({
|
const form = useForm<FormValues>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
startingCapital: 50000,
|
startingCapital: 50000,
|
||||||
@@ -258,18 +258,11 @@ export default function FireCalculatorForm() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="e.g., 10000"
|
placeholder="e.g., 10000"
|
||||||
type="number"
|
type="number"
|
||||||
value={field.value as number | string | undefined}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(value) => {
|
||||||
field.onChange(
|
field.onChange(value);
|
||||||
e.target.value === ""
|
|
||||||
? undefined
|
|
||||||
: Number(e.target.value),
|
|
||||||
);
|
|
||||||
void form.handleSubmit(onSubmit)();
|
void form.handleSubmit(onSubmit)();
|
||||||
}}
|
}}
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
ref={field.ref}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -286,18 +279,11 @@ export default function FireCalculatorForm() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="e.g., 500"
|
placeholder="e.g., 500"
|
||||||
type="number"
|
type="number"
|
||||||
value={field.value as number | string | undefined}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(value) => {
|
||||||
field.onChange(
|
field.onChange(value);
|
||||||
e.target.value === ""
|
|
||||||
? undefined
|
|
||||||
: Number(e.target.value),
|
|
||||||
);
|
|
||||||
void form.handleSubmit(onSubmit)();
|
void form.handleSubmit(onSubmit)();
|
||||||
}}
|
}}
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
ref={field.ref}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -314,18 +300,11 @@ export default function FireCalculatorForm() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="e.g., 30"
|
placeholder="e.g., 30"
|
||||||
type="number"
|
type="number"
|
||||||
value={field.value as number | string | undefined}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(value) => {
|
||||||
field.onChange(
|
field.onChange(value);
|
||||||
e.target.value === ""
|
|
||||||
? undefined
|
|
||||||
: Number(e.target.value),
|
|
||||||
);
|
|
||||||
void form.handleSubmit(onSubmit)();
|
void form.handleSubmit(onSubmit)();
|
||||||
}}
|
}}
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
ref={field.ref}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -342,18 +321,11 @@ export default function FireCalculatorForm() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="e.g., 90"
|
placeholder="e.g., 90"
|
||||||
type="number"
|
type="number"
|
||||||
value={field.value as number | string | undefined}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(value) => {
|
||||||
field.onChange(
|
field.onChange(value);
|
||||||
e.target.value === ""
|
|
||||||
? undefined
|
|
||||||
: Number(e.target.value),
|
|
||||||
);
|
|
||||||
void form.handleSubmit(onSubmit)();
|
void form.handleSubmit(onSubmit)();
|
||||||
}}
|
}}
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
ref={field.ref}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -371,18 +343,11 @@ export default function FireCalculatorForm() {
|
|||||||
placeholder="e.g., 7"
|
placeholder="e.g., 7"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
value={field.value as number | string | undefined}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(value) => {
|
||||||
field.onChange(
|
field.onChange(value);
|
||||||
e.target.value === ""
|
|
||||||
? undefined
|
|
||||||
: Number(e.target.value),
|
|
||||||
);
|
|
||||||
void form.handleSubmit(onSubmit)();
|
void form.handleSubmit(onSubmit)();
|
||||||
}}
|
}}
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
ref={field.ref}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -400,18 +365,11 @@ export default function FireCalculatorForm() {
|
|||||||
placeholder="e.g., 2"
|
placeholder="e.g., 2"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
value={field.value as number | string | undefined}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(value) => {
|
||||||
field.onChange(
|
field.onChange(value);
|
||||||
e.target.value === ""
|
|
||||||
? undefined
|
|
||||||
: Number(e.target.value),
|
|
||||||
);
|
|
||||||
void form.handleSubmit(onSubmit)();
|
void form.handleSubmit(onSubmit)();
|
||||||
}}
|
}}
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
ref={field.ref}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -430,18 +388,11 @@ export default function FireCalculatorForm() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="e.g., 2000"
|
placeholder="e.g., 2000"
|
||||||
type="number"
|
type="number"
|
||||||
value={field.value as number | string | undefined}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(value) => {
|
||||||
field.onChange(
|
field.onChange(value);
|
||||||
e.target.value === ""
|
|
||||||
? undefined
|
|
||||||
: Number(e.target.value),
|
|
||||||
);
|
|
||||||
void form.handleSubmit(onSubmit)();
|
void form.handleSubmit(onSubmit)();
|
||||||
}}
|
}}
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
ref={field.ref}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -455,13 +406,11 @@ export default function FireCalculatorForm() {
|
|||||||
name="retirementAge"
|
name="retirementAge"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>Retirement Age: {field.value}</FormLabel>
|
||||||
Retirement Age: {field.value as number}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Slider
|
<Slider
|
||||||
name="retirementAge"
|
name="retirementAge"
|
||||||
value={[field.value as number]}
|
value={[field.value]}
|
||||||
min={25}
|
min={25}
|
||||||
max={75}
|
max={75}
|
||||||
step={1}
|
step={1}
|
||||||
@@ -614,8 +563,8 @@ export default function FireCalculatorForm() {
|
|||||||
<ReferenceLine
|
<ReferenceLine
|
||||||
x={
|
x={
|
||||||
irlYear +
|
irlYear +
|
||||||
(Number(form.getValues("retirementAge")) -
|
(form.getValues("retirementAge") -
|
||||||
Number(form.getValues("currentAge")))
|
form.getValues("currentAge"))
|
||||||
}
|
}
|
||||||
stroke="var(--primary)"
|
stroke="var(--primary)"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
@@ -630,7 +579,7 @@ export default function FireCalculatorForm() {
|
|||||||
x={
|
x={
|
||||||
irlYear +
|
irlYear +
|
||||||
(result.retirementAge4percent -
|
(result.retirementAge4percent -
|
||||||
Number(form.getValues("currentAge")))
|
form.getValues("currentAge"))
|
||||||
}
|
}
|
||||||
stroke="var(--secondary)"
|
stroke="var(--secondary)"
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
@@ -693,8 +642,8 @@ export default function FireCalculatorForm() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-3xl font-bold">
|
<p className="text-3xl font-bold">
|
||||||
{Number(form.getValues("lifeExpectancy")) -
|
{form.getValues("lifeExpectancy") -
|
||||||
Number(form.getValues("retirementAge"))}
|
form.getValues("retirementAge")}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -725,7 +674,7 @@ export default function FireCalculatorForm() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-3xl font-bold">
|
<p className="text-3xl font-bold">
|
||||||
{Number(form.getValues("lifeExpectancy")) -
|
{form.getValues("lifeExpectancy") -
|
||||||
(result.retirementAge4percent ?? 0)}
|
(result.retirementAge4percent ?? 0)}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
Reference in New Issue
Block a user