Compare commits

...

5 Commits

Author SHA1 Message Date
288a9b4992 calculator fix
All checks were successful
Lint / Lint and Typecheck (push) Successful in 36s
2025-12-06 14:47:38 +01:00
37d8511da7 chart style and descripitons 2025-12-06 14:23:06 +01:00
cd2179f7a0 formatting 2025-12-06 14:15:18 +01:00
21a8c95a2b style and visual fixes 2025-12-06 14:05:39 +01:00
1711c2d16b lint fix 2025-12-06 13:40:38 +01:00
15 changed files with 85 additions and 52 deletions

View File

@@ -1,22 +1,22 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent } from "@/components/ui/card";
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Card, CardContent } from '@/components/ui/card';
export function AuthorBio() {
return (
<Card className="mt-12 bg-muted/50">
<Card className="bg-muted/50 mt-12">
<CardContent className="flex items-center gap-4 p-6">
<Avatar className="h-16 w-16 border-2 border-background">
<Avatar className="border-background h-16 w-16 border-2">
<AvatarImage src="/images/author-profile.jpg" alt="Author" />
<AvatarFallback>IF</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-semibold">Written by The InvestingFIRE Team</p>
<p className="text-sm text-muted-foreground">
We are a group of financial data enthusiasts and early retirees dedicated to building the most accurate FIRE tools on the web. Our goal is to replace guesswork with math.
<p className="text-muted-foreground text-sm">
We are a group of financial data enthusiasts and early retirees dedicated to building the
most accurate FIRE tools on the web. Our goal is to replace guesswork with math.
</p>
</div>
</CardContent>
</Card>
);
}

View File

@@ -143,6 +143,8 @@ export default function FireCalculatorForm() {
baristaIncome: 0,
simulationMode: 'deterministic',
volatility: 15,
withdrawalStrategy: 'fixed',
withdrawalPercentage: 4,
},
});

View File

@@ -1,7 +1,14 @@
'use client';
import { Line, LineChart, CartesianGrid, XAxis, YAxis } from 'recharts';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
ChartContainer,
ChartLegend,
@@ -45,6 +52,9 @@ const generateData = () => {
const data = generateData();
const chartConfig = {
age: {
label: 'Age',
},
Standard: {
label: 'Standard Path',
color: 'var(--chart-4)',
@@ -75,16 +85,22 @@ export function CoastFireChart() {
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value: number) => `$${String(value / 1000)}k`}
tickFormatter={(value: number) => {
if (value < 1000) {
return `$${String(value)}`;
}
if (value < 1000000) {
return `$${String(value / 1000)}k`;
}
if (value < 1000000000) {
return `$${String(value / 1000000)}M`;
}
return `$${String(value / 1000000000)}B`;
}}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
labelFormatter={(value) => `Age ${String(value)}`}
indicator="line"
/>
}
content={<ChartTooltipContent indicator="line" labelKey="age" />}
/>
<Line
dataKey="Standard"
@@ -103,6 +119,12 @@ export function CoastFireChart() {
<ChartLegend content={<ChartLegendContent />} />
</LineChart>
</ChartContainer>
<CardFooter>
<p className="text-muted-foreground text-sm">
Simulation assumes 7% returns. Standard: Save $10k/yr (age 25-65). Coast: Save $30k/yr (age
25-35), then $0.
</p>
</CardFooter>
</CardContent>
</Card>
);

View File

@@ -1,7 +1,14 @@
'use client';
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
ChartContainer,
ChartLegend,
@@ -96,6 +103,15 @@ export function FourPercentRuleChart() {
</AreaChart>
</ChartContainer>
</CardContent>
<CardFooter>
<div>
<p className="font-medium">4% balances safety and spending power</p>
<p className="text-muted-foreground leading-none">
A 5% withdrawal rate risks depleting your portfolio within 30 years, while 3% leaves a large
surplus. The 4% rule is widely considered the safe &quot;sweet spot.&quot;
</p>
</div>
</CardFooter>
</Card>
);
}

View File

@@ -69,7 +69,7 @@ export default function CoastVsLeanPage() {
</p>
</header>
<div className="prose prose-lg dark:prose-invert max-w-none">
<div className="max-w-none">
<h2>The Quick Summary</h2>
<p>Not sure which one fits you? Here is the high-level breakdown:</p>
@@ -147,7 +147,7 @@ export default function CoastVsLeanPage() {
discipline.
</p>
<Separator className="my-12" />
<Separator className="my-16" />
<h2>Run The Numbers</h2>
<p>The best way to decide is to see the math. Use our calculator to simulate both scenarios:</p>

View File

@@ -25,7 +25,7 @@ export default function LearnHubPage() {
<BlurThing />
<CardHeader>
<div className="mb-2">
<span className="rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900 dark:text-green-300">
<span className="rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800">
Beginner
</span>
</div>
@@ -51,7 +51,7 @@ export default function LearnHubPage() {
<Card className="hover:border-primary/50 h-full cursor-pointer border-2">
<CardHeader>
<div className="mb-2">
<span className="rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-300">
<span className="rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">
Strategy
</span>
</div>
@@ -75,7 +75,7 @@ export default function LearnHubPage() {
<Card className="hover:border-primary/50 h-full cursor-pointer border-2">
<CardHeader>
<div className="mb-2">
<span className="rounded-full bg-purple-100 px-2.5 py-0.5 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-300">
<span className="rounded-full bg-purple-100 px-2.5 py-0.5 text-xs font-medium text-purple-800">
Comparison
</span>
</div>

View File

@@ -64,7 +64,7 @@ export default function SafeWithdrawalPage() {
</p>
</header>
<div className="prose prose-lg dark:prose-invert max-w-none">
<div className="max-w-none">
<h2>What is the 4% Rule?</h2>
<p>
The rule comes from the <strong>Trinity Study</strong> (1998), which looked at historical

View File

@@ -1,6 +1,5 @@
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { FireFlowchart } from '@/app/components/charts/FireFlowchart';
import { AuthorBio } from '@/app/components/AuthorBio';
@@ -68,7 +67,7 @@ export default function WhatIsFirePage() {
</p>
</header>
<div className="prose prose-lg dark:prose-invert max-w-none">
<div className="max-w-none">
<p>
Imagine waking up on a Monday morning without an alarm clock. You don&apos;t have to rush to a
commute, sit in traffic, or answer to a boss. Instead, you have the ultimate luxury:{' '}

View File

@@ -1,12 +1,12 @@
import { BASE_URL } from "@/lib/constants";
import { type MetadataRoute } from "next";
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",
changeFrequency: 'yearly',
priority: 1,
},
];

View File

@@ -5,18 +5,18 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg border border-transparent text-sm font-semibold transition-[transform,colors,shadow] shadow-[0_10px_30px_-18px_rgba(0,0,0,0.45)] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"z-30 inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-semibold transition-[transform,colors,shadow] shadow-[0_10px_30px_-18px_rgba(0,0,0,0.45)] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2 aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
'border-primary/20 bg-gradient-to-r from-primary to-secondary text-primary-foreground shadow-lg shadow-primary/30 hover:from-primary/90 hover:to-secondary/90',
'bg-gradient-to-r from-primary to-secondary text-primary-foreground shadow-lg shadow-primary/30 hover:from-primary/90 hover:to-secondary/90',
destructive:
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20',
outline:
'border border-primary/25 bg-background/80 shadow-sm hover:bg-primary/10 hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
'border border-primary/25 bg-background/80 shadow-sm hover:bg-primary/10 hover:text-foreground',
secondary: 'bg-secondary/90 text-secondary-foreground shadow-md hover:bg-secondary',
ghost: 'text-foreground/80 hover:bg-primary/10 hover:text-foreground dark:hover:bg-accent/50',
ghost: 'text-foreground/80 hover:bg-primary/10 hover:text-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {

View File

@@ -7,7 +7,7 @@ import * as RechartsPrimitive from 'recharts';
import { cn } from '@/lib/utils';
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: '', dark: '.dark' } as const;
const THEMES = { light: '' } as const;
export type ChartConfig = Record<
string,
@@ -209,7 +209,7 @@ function ChartTooltipContent({
<span className="text-muted-foreground">{itemConfig?.label ?? item.name}</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
<span className="text-foreground pl-2 font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}

View File

@@ -59,7 +59,7 @@ function DropdownMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}

View File

@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
type={type}
data-slot="input"
className={cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input bg-background flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input bg-background z-30 flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 aria-invalid:border-destructive',
className,

View File

@@ -31,7 +31,7 @@ function SelectTrigger({
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 aria-invalid:border-destructive bg-background flex w-fit items-center justify-between gap-2 rounded-md border 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",
"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 aria-invalid:border-destructive bg-background z-30 flex w-fit items-center justify-between gap-2 rounded-md border 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}

View File

@@ -1,12 +1,12 @@
@import "tailwindcss";
@import "tw-animate-css";
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
@theme {
--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";
var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}
@theme inline {
@@ -60,9 +60,7 @@
--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 */
--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 */
@@ -80,9 +78,7 @@
--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-border: oklch(0.67 0.0763 198.81 / 20%); /* verdigris with opacity */
--sidebar-ring: oklch(0.67 0.0763 198.81); /* verdigris */
}
@@ -116,9 +112,7 @@
--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-border: oklch(0.97 0.0228 95.96 / 10%); /* cosmic latte with opacity */
--sidebar-ring: oklch(0.67 0.0763 198.81); /* verdigris */
}
@@ -142,7 +136,7 @@
@apply scroll-m-20 text-xl font-semibold tracking-tight;
}
p {
@apply leading-7 [&:not(:first-child)]:mt-6;
@apply mb-2 leading-7 [&:not(:first-child)]:mt-6;
}
blockquote {
@apply mt-6 border-l-2 pl-6 italic;
@@ -151,6 +145,6 @@
@apply my-6 ml-6 list-disc [&>li]:mt-2;
}
code {
@apply bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold
@apply bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold;
}
}