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:
Víctor Falcón 2025-12-03 09:31:32 +01:00 committed by GitHub
parent a0f44a9144
commit 5f149b4bae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 131 additions and 59 deletions

View File

@ -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}
/>

View File

@ -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
])} />;
}

View File

@ -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>
);
}

View File

@ -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"
/>

View File

@ -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>
);
}