first build commit

This commit is contained in:
2026-04-19 00:44:43 -04:00
parent bc271b7ce1
commit 55debd082b
82 changed files with 6217 additions and 97 deletions

View File

@@ -0,0 +1,79 @@
'use client'
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell,
} from 'recharts'
import { formatCents, formatCentsAbbrev } from '@/lib/utils/currency'
interface BudgetBar {
name: string
spendCents: number
limitCents: number
color: string | null
}
function ChartTooltip({ active, payload, label }: {
active?: boolean
payload?: Array<{ name: string; value: number }>
label?: string
}) {
if (!active || !payload?.length) return null
return (
<div className="rounded-md border bg-card p-3 shadow-md text-sm">
<p className="font-medium mb-1">{label}</p>
{payload.map((p) => (
<p key={p.name} className="text-muted-foreground">
{p.name}: {formatCents(p.value)}
</p>
))}
</div>
)
}
export function BudgetChart({ data }: { data: BudgetBar[] }) {
if (data.length === 0) {
return (
<div className="flex h-[300px] items-center justify-center text-sm text-muted-foreground">
No active budgets.
</div>
)
}
const chartHeight = Math.max(200, data.length * 60 + 60)
return (
<ResponsiveContainer width="100%" height={chartHeight}>
<BarChart
layout="vertical"
data={data}
margin={{ top: 4, right: 24, left: 8, bottom: 0 }}
barGap={4}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" horizontal={false} />
<XAxis
type="number"
tickFormatter={formatCentsAbbrev}
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={false}
/>
<YAxis
type="category"
dataKey="name"
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={false}
width={90}
/>
<Tooltip content={<ChartTooltip />} />
<Legend wrapperStyle={{ fontSize: 12 }} />
<Bar dataKey="spendCents" name="Spent" radius={[0, 4, 4, 0]} maxBarSize={20}>
{data.map((d, i) => (
<Cell key={i} fill={d.color ?? '#6366f1'} />
))}
</Bar>
<Bar dataKey="limitCents" name="Limit" fill="#e2e8f0" radius={[0, 4, 4, 0]} maxBarSize={20} />
</BarChart>
</ResponsiveContainer>
)
}

View File

@@ -0,0 +1,60 @@
'use client'
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
} from 'recharts'
import { formatCents, formatCentsAbbrev } from '@/lib/utils/currency'
interface DataPoint {
label: string
creditsCents: number
debitsCents: number
}
function ChartTooltip({ active, payload, label }: {
active?: boolean
payload?: Array<{ name: string; value: number; color: string }>
label?: string
}) {
if (!active || !payload?.length) return null
return (
<div className="rounded-md border bg-card p-3 shadow-md text-sm">
<p className="font-medium mb-1">{label}</p>
{payload.map((p) => (
<p key={p.name} style={{ color: p.color }}>
{p.name}: {formatCents(p.value)}
</p>
))}
</div>
)
}
export function CashFlowChart({ data }: { data: DataPoint[] }) {
if (data.length === 0) {
return (
<div className="flex h-[300px] items-center justify-center text-sm text-muted-foreground">
No bank transaction data yet.
</div>
)
}
return (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={data} margin={{ top: 4, right: 16, left: 16, bottom: 0 }} barGap={4}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" vertical={false} />
<XAxis dataKey="label" tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
<YAxis
tickFormatter={formatCentsAbbrev}
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={false}
width={64}
/>
<Tooltip content={<ChartTooltip />} />
<Legend wrapperStyle={{ fontSize: 12 }} />
<Bar dataKey="creditsCents" name="Income" fill="#22c55e" radius={[4, 4, 0, 0]} maxBarSize={40} />
<Bar dataKey="debitsCents" name="Spending" fill="#ef4444" radius={[4, 4, 0, 0]} maxBarSize={40} />
</BarChart>
</ResponsiveContainer>
)
}

View File

@@ -0,0 +1,68 @@
'use client'
import {
PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer,
} from 'recharts'
import { formatCents } from '@/lib/utils/currency'
interface DataPoint {
category: string
totalCents: number
}
const COLORS = [
'#6366f1', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6',
'#14b8a6', '#f97316', '#ec4899', '#0ea5e9', '#84cc16',
]
function ChartTooltip({ active, payload }: {
active?: boolean
payload?: Array<{ name: string; value: number; payload: DataPoint }>
}) {
if (!active || !payload?.length) return null
const item = payload[0]
return (
<div className="rounded-md border bg-card p-3 shadow-md text-sm">
<p className="font-medium">{item.payload.category}</p>
<p className="text-muted-foreground">{formatCents(item.value)}</p>
</div>
)
}
export function CategoryBreakdownChart({ data }: { data: DataPoint[] }) {
if (data.length === 0) {
return (
<div className="flex h-[320px] items-center justify-center text-sm text-muted-foreground">
No spending this month.
</div>
)
}
return (
<ResponsiveContainer width="100%" height={320}>
<PieChart>
<Pie
data={data}
dataKey="totalCents"
nameKey="category"
cx="50%"
cy="45%"
innerRadius={70}
outerRadius={110}
paddingAngle={2}
>
{data.map((_, i) => (
<Cell key={i} fill={COLORS[i % COLORS.length]} />
))}
</Pie>
<Tooltip content={<ChartTooltip />} />
<Legend
iconType="circle"
iconSize={8}
wrapperStyle={{ fontSize: 12 }}
formatter={(value) => <span className="text-foreground">{value}</span>}
/>
</PieChart>
</ResponsiveContainer>
)
}

View File

@@ -0,0 +1,53 @@
'use client'
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
} from 'recharts'
import { formatCents, formatCentsAbbrev } from '@/lib/utils/currency'
interface DataPoint {
label: string
totalCents: number
}
function ChartTooltip({ active, payload, label }: {
active?: boolean
payload?: Array<{ value: number }>
label?: string
}) {
if (!active || !payload?.length) return null
return (
<div className="rounded-md border bg-card p-3 shadow-md text-sm">
<p className="font-medium mb-1">{label}</p>
<p className="text-foreground">Total: {formatCents(payload[0].value)}</p>
</div>
)
}
export function MonthlySpendingChart({ data }: { data: DataPoint[] }) {
if (data.length === 0) {
return (
<div className="flex h-[300px] items-center justify-center text-sm text-muted-foreground">
No spending data yet.
</div>
)
}
return (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={data} margin={{ top: 4, right: 16, left: 16, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" vertical={false} />
<XAxis dataKey="label" tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
<YAxis
tickFormatter={formatCentsAbbrev}
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={false}
width={64}
/>
<Tooltip content={<ChartTooltip />} />
<Bar dataKey="totalCents" name="Spending" fill="#8b5cf6" radius={[4, 4, 0, 0]} maxBarSize={48} />
</BarChart>
</ResponsiveContainer>
)
}

View File

@@ -0,0 +1,68 @@
'use client'
import {
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
} from 'recharts'
import { formatCents, formatCentsAbbrev } from '@/lib/utils/currency'
interface DataPoint {
label: string
totalCents: number
}
function ChartTooltip({ active, payload, label }: {
active?: boolean
payload?: Array<{ value: number }>
label?: string
}) {
if (!active || !payload?.length) return null
return (
<div className="rounded-md border bg-card p-3 shadow-md text-sm">
<p className="font-medium mb-1">{label}</p>
<p className="text-primary">{formatCents(payload[0].value)}</p>
</div>
)
}
export function NetWorthTrendChart({ data }: { data: DataPoint[] }) {
if (data.length === 0) {
return (
<div className="flex h-[300px] items-center justify-center text-sm text-muted-foreground">
No snapshot data yet upload transactions to populate this chart.
</div>
)
}
return (
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ top: 4, right: 16, left: 16, bottom: 0 }}>
<defs>
<linearGradient id="netWorthGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis dataKey="label" tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
<YAxis
tickFormatter={formatCentsAbbrev}
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={false}
width={64}
/>
<Tooltip content={<ChartTooltip />} />
<Area
type="monotone"
dataKey="totalCents"
name="Net Worth"
stroke="#6366f1"
strokeWidth={2}
fill="url(#netWorthGradient)"
dot={false}
activeDot={{ r: 4 }}
/>
</AreaChart>
</ResponsiveContainer>
)
}