feat: Add account balance chart improvements and icons (#5)
## New <img width="1123" height="195" alt="image" src="https://github.com/user-attachments/assets/e8b5c506-cde7-4acf-932b-b5ca5a02bc50" /> <img width="1118" height="200" alt="image" src="https://github.com/user-attachments/assets/482f8af7-4ec0-460c-8fe9-d30f5f5e5365" /> ## Before <img width="1125" height="194" alt="image" src="https://github.com/user-attachments/assets/dd7de6d6-e428-4df4-8f7f-988701c6ed22" />
This commit is contained in:
parent
a0f44a9144
commit
5f149b4bae
|
|
@ -1,8 +1,9 @@
|
|||
import { EncryptedText } from '@/components/encrypted-text';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Account } from '@/types/account';
|
||||
import { ArrowDownIcon, ArrowUpIcon } from 'lucide-react';
|
||||
import { Line, LineChart, ResponsiveContainer } from 'recharts';
|
||||
import { AmountTrendIndicator } from './amount-trend-indicator';
|
||||
import { AccountTypeIcon } from './account-type-icon';
|
||||
import { Line, LineChart, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
|
||||
interface AccountBalanceCardProps {
|
||||
account: Account & {
|
||||
|
|
@ -46,43 +47,55 @@ export function AccountBalanceCard({
|
|||
<EncryptedText
|
||||
encryptedText={account.name}
|
||||
iv={account.name_iv}
|
||||
length={{ min: 5, max: 15 }}
|
||||
/>
|
||||
</CardTitle>
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
{account.type}
|
||||
<AccountTypeIcon type={account.type} className="inline-block mr-1" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-2xl font-bold">
|
||||
<div className="flex gap-6 items-center justify-between">
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className="text-2xl font-medium">
|
||||
{formatter.format(account.currentBalance / 100)}
|
||||
</div>
|
||||
<p className="flex items-center text-xs text-muted-foreground">
|
||||
<span
|
||||
className={
|
||||
isPositive
|
||||
? 'text-green-600'
|
||||
: 'text-red-600'
|
||||
}
|
||||
>
|
||||
{isPositive ? (
|
||||
<ArrowUpIcon className="mr-1 inline size-3" />
|
||||
) : (
|
||||
<ArrowDownIcon className="mr-1 inline size-3" />
|
||||
)}
|
||||
{formatter.format(Math.abs(account.diff) / 100)}
|
||||
</span>
|
||||
<span className="ml-1">vs last month</span>
|
||||
</p>
|
||||
<AmountTrendIndicator
|
||||
isPositive={isPositive}
|
||||
trend={formatter.format(Math.abs(account.diff) / 100)}
|
||||
label="vs last month"
|
||||
className='text-sm'
|
||||
/>
|
||||
</div>
|
||||
<div className="h-[60px] w-[100px]">
|
||||
<div className="h-[70px] max-w-[250px] w-full flex-1">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={account.history}>
|
||||
<Tooltip
|
||||
content={({ active, payload }) => {
|
||||
if (!active || !payload?.length)
|
||||
return null;
|
||||
const data = payload[0].payload as {
|
||||
date: string;
|
||||
value: number;
|
||||
};
|
||||
return (
|
||||
<div className="rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl">
|
||||
<p className="mb-0.5 text-muted-foreground">
|
||||
{data.date}
|
||||
</p>
|
||||
<p className="font-mono font-medium text-foreground tabular-nums">
|
||||
{formatter.format(
|
||||
data.value / 100,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={isPositive ? '#16a34a' : '#dc2626'}
|
||||
stroke={'var(--color-zinc-700)'}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
import { AccountType } from "@/types/account";
|
||||
import { BadgeQuestionMarkIcon, Building2, CreditCard, FolderKanban, Landmark, LucideIcon, PiggyBank } from "lucide-react";
|
||||
|
||||
export function AccountTypeIcon({ type, className }: { type: AccountType, className?: string }) {
|
||||
const typeMap: Record<AccountType, LucideIcon> = {
|
||||
checking: Building2, // 🏦 - bank / institution
|
||||
credit_card: CreditCard, // 💳 - card
|
||||
loan: Landmark, // 🏠 - "institution/loan", or use Home if it's a mortgage
|
||||
savings: PiggyBank, // 💰 - savings
|
||||
others: FolderKanban, // 📁 - miscellaneous/other
|
||||
};
|
||||
|
||||
const Icon = typeMap[type] || BadgeQuestionMarkIcon;
|
||||
|
||||
return <Icon className={cn([
|
||||
"h-5 w-5 text-muted-foreground",
|
||||
className
|
||||
])} />;
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
import { TrendingUp, TrendingDown } from "lucide-react";
|
||||
|
||||
export function AmountTrendIndicator({
|
||||
trend,
|
||||
isPositive,
|
||||
label,
|
||||
className = '',
|
||||
}: {
|
||||
trend: string | null;
|
||||
isPositive: boolean;
|
||||
label: string;
|
||||
className?: string;
|
||||
}) {
|
||||
if (trend === null) return null;
|
||||
|
||||
const Icon = isPositive ? TrendingUp : TrendingDown;
|
||||
const iconColorClass = isPositive
|
||||
? 'text-green-600/70 dark:text-green-400/70'
|
||||
: 'text-red-600/70 dark:text-red-400/70';
|
||||
|
||||
return (
|
||||
<div className={cn([
|
||||
"flex items-center gap-1",
|
||||
className
|
||||
])}>
|
||||
<span
|
||||
className={
|
||||
isPositive ? 'bg-green-100/25 dark:bg-green-900/25' : ''
|
||||
}
|
||||
>
|
||||
{trend}
|
||||
</span>
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<Icon className={`h-4 w-4 ${iconColorClass}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -12,8 +12,8 @@ import {
|
|||
StackedBarChart,
|
||||
} from '@/components/ui/stacked-bar-chart';
|
||||
import { NetWorthEvolutionData } from '@/hooks/use-dashboard-data';
|
||||
import { TrendingDown, TrendingUp } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { PercentageTrendIndicator } from './percentage-trend-indicator';
|
||||
|
||||
interface NetWorthChartProps {
|
||||
data: NetWorthEvolutionData;
|
||||
|
|
@ -71,37 +71,6 @@ function calculateTrend(
|
|||
return ((currentTotal - previousTotal) / Math.abs(previousTotal)) * 100;
|
||||
}
|
||||
|
||||
function TrendIndicator({
|
||||
trend,
|
||||
label,
|
||||
}: {
|
||||
trend: number | null;
|
||||
label: string;
|
||||
}) {
|
||||
if (trend === null) return null;
|
||||
|
||||
const isPositive = trend >= 0;
|
||||
const Icon = isPositive ? TrendingUp : TrendingDown;
|
||||
const iconColorClass = isPositive
|
||||
? 'text-green-600/70 dark:text-green-400/70'
|
||||
: 'text-red-600/70 dark:text-red-400/70';
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<span
|
||||
className={
|
||||
isPositive ? 'bg-green-100/25 dark:bg-green-900/25' : ''
|
||||
}
|
||||
>
|
||||
{isPositive ? '+' : ''}
|
||||
{trend.toFixed(1)}%
|
||||
</span>
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<Icon className={`h-4 w-4 ${iconColorClass}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface EncryptedLabelProps {
|
||||
account: { name: string; name_iv: string };
|
||||
}
|
||||
|
|
@ -261,11 +230,11 @@ export function NetWorthChart({
|
|||
<CardTitle>Net Worth Evolution</CardTitle>
|
||||
</div>
|
||||
<CardDescription className="flex flex-col gap-1 text-sm">
|
||||
<TrendIndicator
|
||||
<PercentageTrendIndicator
|
||||
trend={monthlyTrend}
|
||||
label="this month"
|
||||
/>
|
||||
<TrendIndicator
|
||||
<PercentageTrendIndicator
|
||||
trend={yearlyTrend}
|
||||
label="for the last 12 months"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import { TrendingUp, TrendingDown } from "lucide-react";
|
||||
|
||||
export function PercentageTrendIndicator({
|
||||
trend,
|
||||
label,
|
||||
}: {
|
||||
trend: number | null;
|
||||
label: string;
|
||||
}) {
|
||||
if (trend === null) return null;
|
||||
|
||||
const isPositive = trend >= 0;
|
||||
const Icon = isPositive ? TrendingUp : TrendingDown;
|
||||
const iconColorClass = isPositive
|
||||
? 'text-green-600/70 dark:text-green-400/70'
|
||||
: 'text-red-600/70 dark:text-red-400/70';
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<span
|
||||
className={
|
||||
isPositive ? 'bg-green-100/25 dark:bg-green-900/25' : ''
|
||||
}
|
||||
>
|
||||
{isPositive ? '+' : ''}
|
||||
{trend.toFixed(1)}%
|
||||
</span>
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<Icon className={`h-4 w-4 ${iconColorClass}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue