- Added new dependencies: @fontsource-variable/geist, @radix-ui/react-separator, @radix-ui/react-tooltip, date-fns, react-router-dom, and recharts. - Updated index.html to set the HTML class to "dark" for dark mode support. - Refactored App component to implement routing with React Router, replacing the previous UI structure with a layout and multiple pages (NetWorth, Debts, Invoices, Clients). - Enhanced CSS for dark mode and added depth utilities for improved UI aesthetics. - Expanded Redux store to include net worth, debts, and invoices slices for comprehensive state management.
181 lines
6.1 KiB
TypeScript
181 lines
6.1 KiB
TypeScript
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card';
|
|
import {Button} from '@/components/ui/button';
|
|
import {useAppSelector} from '@/store';
|
|
import {
|
|
AreaChart,
|
|
Area,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
} from 'recharts';
|
|
|
|
// Demo data for the chart
|
|
const demoData = [
|
|
{month: 'Jan', netWorth: 45000},
|
|
{month: 'Feb', netWorth: 47500},
|
|
{month: 'Mar', netWorth: 46200},
|
|
{month: 'Apr', netWorth: 52000},
|
|
{month: 'May', netWorth: 55800},
|
|
{month: 'Jun', netWorth: 58200},
|
|
];
|
|
|
|
export default function NetWorthPage() {
|
|
const {assets, liabilities} = useAppSelector(state => state.netWorth);
|
|
|
|
const totalAssets = assets.reduce((sum, a) => sum + a.value, 0);
|
|
const totalLiabilities = liabilities.reduce((sum, l) => sum + l.balance, 0);
|
|
const netWorth = totalAssets - totalLiabilities;
|
|
|
|
const formatCurrency = (value: number) =>
|
|
new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'}).format(value);
|
|
|
|
return (
|
|
<div className="p-8">
|
|
<div className="mb-8">
|
|
<h1 className="text-2xl font-medium tracking-tight">Net Worth</h1>
|
|
<p className="text-muted-foreground">Track your wealth over time</p>
|
|
</div>
|
|
|
|
{/* Summary Cards */}
|
|
<div className="mb-8 grid gap-4 md:grid-cols-3">
|
|
<Card className="card-elevated">
|
|
<CardHeader className="pb-2">
|
|
<CardDescription>Total Assets</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-2xl font-semibold">{formatCurrency(totalAssets)}</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="card-elevated">
|
|
<CardHeader className="pb-2">
|
|
<CardDescription>Total Liabilities</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-2xl font-semibold">{formatCurrency(totalLiabilities)}</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="card-elevated">
|
|
<CardHeader className="pb-2">
|
|
<CardDescription>Net Worth</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-2xl font-semibold">{formatCurrency(netWorth)}</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Chart */}
|
|
<Card className="card-elevated mb-8">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg font-medium">Net Worth Over Time</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-[300px]">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<AreaChart data={demoData}>
|
|
<defs>
|
|
<linearGradient id="netWorthGradient" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor="oklch(0.7 0 0)" stopOpacity={0.3} />
|
|
<stop offset="100%" stopColor="oklch(0.7 0 0)" stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="oklch(0.26 0 0)" />
|
|
<XAxis
|
|
dataKey="month"
|
|
stroke="oklch(0.55 0 0)"
|
|
tick={{fill: 'oklch(0.55 0 0)'}}
|
|
/>
|
|
<YAxis
|
|
stroke="oklch(0.55 0 0)"
|
|
tick={{fill: 'oklch(0.55 0 0)'}}
|
|
tickFormatter={value => `$${value / 1000}k`}
|
|
/>
|
|
<Tooltip
|
|
contentStyle={{
|
|
background: 'oklch(0.18 0 0)',
|
|
border: '1px solid oklch(0.26 0 0)',
|
|
borderRadius: '8px',
|
|
}}
|
|
labelStyle={{color: 'oklch(0.92 0 0)'}}
|
|
formatter={(value: number) => [formatCurrency(value), 'Net Worth']}
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="netWorth"
|
|
stroke="oklch(0.85 0 0)"
|
|
strokeWidth={2}
|
|
fill="url(#netWorthGradient)"
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Assets & Liabilities */}
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<Card className="card-elevated">
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<div>
|
|
<CardTitle className="text-lg font-medium">Assets</CardTitle>
|
|
<CardDescription>What you own</CardDescription>
|
|
</div>
|
|
<Button variant="secondary" size="sm">
|
|
Add Asset
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{assets.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">No assets added yet</p>
|
|
) : (
|
|
<ul className="space-y-2">
|
|
{assets.map(asset => (
|
|
<li key={asset.id} className="flex justify-between">
|
|
<span>{asset.name}</span>
|
|
<span>{formatCurrency(asset.value)}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="card-elevated">
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<div>
|
|
<CardTitle className="text-lg font-medium">Liabilities</CardTitle>
|
|
<CardDescription>What you owe</CardDescription>
|
|
</div>
|
|
<Button variant="secondary" size="sm">
|
|
Add Liability
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{liabilities.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">No liabilities added yet</p>
|
|
) : (
|
|
<ul className="space-y-2">
|
|
{liabilities.map(liability => (
|
|
<li key={liability.id} className="flex justify-between">
|
|
<span>{liability.name}</span>
|
|
<span>{formatCurrency(liability.balance)}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|