content, blurthing

This commit is contained in:
2025-12-06 02:20:40 +01:00
parent 91dadaedaa
commit 19709f531d
9 changed files with 173 additions and 145 deletions

View File

@@ -23,6 +23,7 @@ import { Slider } from '@/components/ui/slider';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent';
import { Calculator, Percent } from 'lucide-react';
import BlurThing from './blur-thing';
// Schema for form validation
const formSchema = z.object({
@@ -334,6 +335,7 @@ export default function FireCalculatorForm() {
return (
<>
<Card className="border-primary/15 bg-background/90 shadow-primary/10 mb-6 border shadow-lg backdrop-blur">
<BlurThing />
<CardHeader>
<CardTitle className="text-2xl">FIRE Calculator</CardTitle>
<CardDescription className="text-muted-foreground text-sm">

View File

@@ -0,0 +1,11 @@
export default function BlurThing() {
return (
<>
{/* Decorative background elements */}
<div className="pointer-events-none absolute inset-0 overflow-hidden rounded-xl">
<div className="from-primary/25 to-primary/15 absolute -top-24 -right-24 h-64 w-64 rounded-full bg-gradient-to-br blur-3xl" />
<div className="absolute -bottom-24 -left-24 h-64 w-64 rounded-full bg-gradient-to-br from-orange-500/25 to-red-500/15 blur-3xl" />
</div>
</>
);
}

View File

@@ -10,6 +10,7 @@ import {
ChartTooltipContent,
type ChartConfig,
} from '@/components/ui/chart';
import BlurThing from '../blur-thing';
// Simulation
// Standard: Start 25, Retire 65. Save $10k/yr.
@@ -57,6 +58,8 @@ const chartConfig = {
export function CoastFireChart() {
return (
<Card className="w-full">
{/* Decorative background elements */}
<BlurThing />
<CardHeader>
<CardTitle>Coast FIRE vs. Standard Path</CardTitle>
<CardDescription>

View File

@@ -1,71 +1,113 @@
import { ArrowRight, Banknote, Coins, Landmark, TrendingUp, Wallet } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Banknote, Coins, Flame, Landmark, TrendingUp, Wallet } from 'lucide-react';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import BlurThing from '../blur-thing';
const steps = [
{
icon: Banknote,
title: 'Income',
description: 'Maximize earnings & side hustles',
color: 'from-emerald-400 to-teal-500',
glow: 'shadow-emerald-500/30',
},
{
icon: Wallet,
title: 'Low Expenses',
description: 'Frugality & mindful spending',
color: 'from-rose-400 to-pink-500',
glow: 'shadow-rose-500/30',
},
{
icon: Coins,
title: 'Savings Gap',
description: 'The difference is your fuel',
color: 'from-sky-400 to-blue-500',
glow: 'shadow-sky-500/30',
},
{
icon: TrendingUp,
title: 'Investments',
description: 'Index funds & compounding',
color: 'from-violet-400 to-purple-500',
glow: 'shadow-violet-500/30',
},
{
icon: Landmark,
title: 'Freedom',
description: 'Work becomes optional',
color: 'from-amber-400 to-orange-500',
glow: 'shadow-amber-500/30',
},
];
export function FireFlowchart() {
return (
<Card className="w-full overflow-hidden">
<CardHeader className="pb-2 text-center">
<CardTitle>The FIRE Engine</CardTitle>
<Card className="relative w-full overflow-hidden">
<BlurThing />
<CardHeader className="relative pb-0 text-center">
<CardTitle className="flex items-center justify-center gap-3 text-2xl">
<Flame className="h-7 w-7 text-orange-500" />
The FIRE Engine
</CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="flex flex-col items-center justify-center gap-4 md:flex-row md:items-start">
{/* Step 1: Income */}
<div className="flex flex-col items-center gap-2">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-green-100 text-green-600 dark:bg-green-900 dark:text-green-300">
<Banknote className="h-8 w-8" />
<CardContent className="relative">
{/* Connecting line - visible on md+ */}
<div className="absolute top-10 right-12 left-12 hidden h-0.5 bg-gradient-to-r from-emerald-400 via-purple-400 to-orange-400 opacity-30 md:block" />
{/* Steps */}
<div className="relative grid grid-cols-1 gap-6 md:grid-cols-5 md:gap-4">
{steps.map((step, index) => (
<div key={step.title} className="group relative flex flex-col items-center">
{/* Step number badge */}
<div className="absolute -top-2 -left-2 z-10 flex h-6 w-6 items-center justify-center rounded-full bg-zinc-900 text-xs font-bold text-white md:-top-1 md:-left-1">
{index + 1}
</div>
{/* Icon container */}
<div
className={`relative flex h-20 w-20 items-center justify-center rounded-2xl bg-gradient-to-br ${step.color} shadow-lg ${step.glow} transition-all duration-300 group-hover:scale-105 group-hover:shadow-xl`}
>
<step.icon className="h-9 w-9 text-white" strokeWidth={1.5} />
</div>
{/* Content */}
<div className="mt-4 text-center">
<h4 className="font-semibold tracking-tight">{step.title}</h4>
<p className="text-muted-foreground mt-1 max-w-[140px] text-xs leading-relaxed">
{step.description}
</p>
</div>
{/* Arrow connector for mobile */}
{index < steps.length - 1 && (
<div className="my-2 flex items-center justify-center md:hidden">
<svg
className="text-muted-foreground h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 14l-7 7m0 0l-7-7m7 7V3"
/>
</svg>
</div>
)}
</div>
<span className="font-bold">Income</span>
<p className="w-32 text-center text-xs text-muted-foreground">Maximize earnings & side hustles</p>
</div>
<ArrowRight className="hidden h-8 w-8 text-muted-foreground md:block rotate-90 md:rotate-0" />
{/* Step 2: Expenses */}
<div className="flex flex-col items-center gap-2">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-red-100 text-red-600 dark:bg-red-900 dark:text-red-300">
<Wallet className="h-8 w-8" />
</div>
<span className="font-bold">Low Expenses</span>
<p className="w-32 text-center text-xs text-muted-foreground">Frugality & mindful spending</p>
</div>
<ArrowRight className="hidden h-8 w-8 text-muted-foreground md:block rotate-90 md:rotate-0" />
{/* Step 3: Savings Gap */}
<div className="flex flex-col items-center gap-2">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-300">
<Coins className="h-8 w-8" />
</div>
<span className="font-bold">Savings Gap</span>
<p className="w-32 text-center text-xs text-muted-foreground">The difference is your fuel</p>
</div>
<ArrowRight className="hidden h-8 w-8 text-muted-foreground md:block rotate-90 md:rotate-0" />
{/* Step 4: Investments */}
<div className="flex flex-col items-center gap-2">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-purple-100 text-purple-600 dark:bg-purple-900 dark:text-purple-300">
<TrendingUp className="h-8 w-8" />
</div>
<span className="font-bold">Investments</span>
<p className="w-32 text-center text-xs text-muted-foreground">Index funds & compounding</p>
</div>
<ArrowRight className="hidden h-8 w-8 text-muted-foreground md:block rotate-90 md:rotate-0" />
{/* Step 5: Freedom */}
<div className="flex flex-col items-center gap-2">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-orange-100 text-orange-600 dark:bg-orange-900 dark:text-orange-300">
<Landmark className="h-8 w-8" />
</div>
<span className="font-bold">Freedom</span>
<p className="w-32 text-center text-xs text-muted-foreground">Work becomes optional</p>
</div>
))}
</div>
{/* Bottom tagline */}
<CardFooter>
<p className="text-muted-foreground mx-auto mt-8 text-center text-sm">
Build the gap. Invest the gap. Let time do the rest.
</p>
</CardFooter>
</CardContent>
</Card>
);
}

View File

@@ -10,6 +10,7 @@ import {
ChartTooltipContent,
type ChartConfig,
} from '@/components/ui/chart';
import BlurThing from '../blur-thing';
// Simulation data for 4% rule
const storyData = [
@@ -47,6 +48,7 @@ const chartConfig = {
export function FourPercentRuleChart() {
return (
<Card className="w-full">
<BlurThing />
<CardHeader>
<CardTitle>Portfolio Survival Scenarios</CardTitle>
<CardDescription>

View File

@@ -1,5 +1,6 @@
import Link from 'next/link';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import BlurThing from '../components/blur-thing';
export const metadata = {
title: 'Learn FIRE | Financial Independence Guides & Resources',
@@ -20,7 +21,8 @@ export default function LearnHubPage() {
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{/* Article 1 */}
<Link href="/learn/what-is-fire" className="transition-transform hover:scale-[1.02]">
<Card className="hover:border-primary/50 h-full cursor-pointer border-2">
<Card className="hover:border-primary/50 h-full cursor-pointer border-2 transition-all">
<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">

View File

@@ -1,26 +1,28 @@
import * as React from "react";
import * as React from 'react';
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils';
function Card({ className, ...props }: React.ComponentProps<"div">) {
function Card({ className, children, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
'bg-card text-card-foreground relative flex flex-col gap-6 overflow-hidden rounded-xl border py-6 shadow-sm',
className,
)}
{...props}
/>
>
{children}
</div>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className,
)}
{...props}
@@ -28,65 +30,44 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
<div data-slot="card-title" className={cn('leading-none font-semibold', className)} {...props} />
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
);
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="card-content" className={cn('px-6', className)} {...props} />;
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };

View File

@@ -1,16 +1,16 @@
import * as React from "react";
import * as React from 'react';
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils';
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent 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 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
'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',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 aria-invalid:border-destructive',
className,
)}
{...props}

View File

@@ -1,43 +1,37 @@
"use client";
'use client';
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
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";
import { cn } from '@/lib/utils';
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = "default",
size = 'default',
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
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",
"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",
className,
)}
{...props}
@@ -53,7 +47,7 @@ function SelectTrigger({
function SelectContent({
className,
children,
position = "popper",
position = 'popper',
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
@@ -61,9 +55,9 @@ function SelectContent({
<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-32 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",
'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-32 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}
@@ -72,9 +66,9 @@ function SelectContent({
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1",
'p-1',
position === 'popper' &&
'h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1',
)}
>
{children}
@@ -85,14 +79,11 @@ function SelectContent({
);
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
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)}
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
{...props}
/>
);
@@ -129,7 +120,7 @@ function SelectSeparator({
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
{...props}
/>
);
@@ -142,10 +133,7 @@ function SelectScrollUpButton({
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronUpIcon className="size-4" />
@@ -160,10 +148,7 @@ function SelectScrollDownButton({
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronDownIcon className="size-4" />