Skip to main content

Data Display Components

OneLibro's data display components present financial information in clear, accessible, and visually appealing formats. These components handle formatting, empty states, loading states, and responsive design.


Overview

Data display components in OneLibro provide:

  • Consistent Formatting - Currency, dates, percentages
  • Visual Hierarchy - Clear information architecture
  • Empty States - Helpful messages when no data
  • Loading States - Skeleton loaders during data fetch
  • Responsive Design - Works on all screen sizes
  • Accessibility - Screen reader friendly, semantic HTML

DashboardCard

Reusable card component for displaying key metrics and statistics on the dashboard.

Props

interface DashboardCardProps {
title: string; // Card title (e.g., "Total Balance")
value: string | number; // Main value to display
subtitle?: string; // Optional subtitle/description
icon?: React.ReactNode; // Icon component (from lucide-react)
trend?: 'up' | 'down' | 'neutral'; // Trend indicator
trendValue?: string; // Trend percentage (e.g., "+5.2%")
onClick?: () => void; // Optional click handler
loading?: boolean; // Show skeleton loader
className?: string; // Additional CSS classes
}

Usage

Basic Card:

import { DashboardCard } from '@/components/finance/DashboardCard';
import { DollarSign } from 'lucide-react';

<DashboardCard
title="Total Balance"
value="$12,543.67"
subtitle="Across all accounts"
icon={<DollarSign className="w-6 h-6" />}
/>

Card with Trend:

<DashboardCard
title="Monthly Spending"
value="$2,456.18"
subtitle="Last 30 days"
icon={<TrendingDown className="w-6 h-6" />}
trend="down"
trendValue="+12.3%"
/>

Clickable Card:

<DashboardCard
title="Connected Accounts"
value="5"
subtitle="2 banks linked"
icon={<Building className="w-6 h-6" />}
onClick={() => router.push('/finance/accounts')}
className="cursor-pointer hover:border-[#10b981]"
/>

Loading State:

<DashboardCard
title="Total Balance"
value="..."
loading={true}
/>

Visual Structure

┌─────────────────────────────────┐
│ TITLE [ICON] │
│ │
│ VALUE TREND ↑ │
│ Subtitle +5.2% │
└─────────────────────────────────┘

Example:

┌─────────────────────────────────┐
│ TOTAL BALANCE [$] │
│ │
│ $12,543.67 ↑ +5.2% │
│ Across all accounts │
└─────────────────────────────────┘

Features

Icon Support:

  • Accepts any React component as icon
  • Typically uses lucide-react icons
  • Icon displayed in primary color (#10b981)
  • Positioned top-right of card

Trend Indicators:

  • Up (green): Positive trend, upward arrow
  • Down (red): Negative trend, downward arrow
  • Neutral (gray): No change, horizontal line

Loading State:

  • Skeleton animation when loading={true}
  • Preserves card dimensions
  • Smooth transition to actual data

Responsive Sizing:

  • Desktop: 3-column grid
  • Tablet: 2-column grid
  • Mobile: Single column stack

AccountCard

Displays bank account information including balance, institution, and action buttons.

Props

interface AccountCardProps {
account: {
id: string;
account_name: string;
official_name?: string;
account_type: string;
account_subtype: string;
current_balance: number; // In cents
available_balance?: number; // In cents
institution_name: string;
last_synced?: string; // ISO timestamp
};
onSync?: (accountId: string) => Promise<void>;
onUnlink?: (accountId: string) => void;
loading?: boolean;
}

Usage

import { AccountCard } from '@/components/finance/AccountCard';

<AccountCard
account={{
id: 'acc_123',
account_name: 'Checking ...1234',
official_name: 'Chase Total Checking',
account_type: 'depository',
account_subtype: 'checking',
current_balance: 543210, // $5,432.10
available_balance: 543210,
institution_name: 'Chase',
last_synced: '2025-01-24T10:30:00Z',
}}
onSync={async (id) => {
await syncAccountTransactions(id);
toast.success('Account synced!');
}}
onUnlink={(id) => {
if (confirm('Unlink this account?')) {
unlinkAccount(id);
}
}}
/>

Visual Structure

┌───────────────────────────────────┐
│ [🏦] Account Name │
│ Institution Name │
│ [Type Badge] │
│ │
│ Current Balance: $5,432.10 │
│ Available Balance: $5,432.10 │
│ │
│ Last synced: 2 hours ago │
│ │
│ [Sync Button] [Unlink Button] │
└───────────────────────────────────┘

Account Type Badges

Different account types display color-coded badges:

Checking:

<span className="bg-green-100 text-green-700 px-2 py-1 rounded text-xs">
Checking
</span>

Savings:

<span className="bg-blue-100 text-blue-700 px-2 py-1 rounded text-xs">
Savings
</span>

Credit:

<span className="bg-purple-100 text-purple-700 px-2 py-1 rounded text-xs">
Credit Card
</span>

Investment:

<span className="bg-indigo-100 text-indigo-700 px-2 py-1 rounded text-xs">
Investment
</span>

Balance Display

Current Balance:

  • Actual account balance right now
  • Includes pending transactions
  • Main displayed value

Available Balance:

  • Amount available to spend/withdraw
  • May differ from current due to holds or credit limits
  • Shown if different from current

Credit Cards:

  • Current Balance: Amount owed (debt)
  • Available Balance: Credit limit - current balance

Example:

Current Balance:  -$450.00 (you owe)
Available Balance: $4,550.00 (credit available)

Action Buttons

Sync Button:

  • Manually refresh transactions
  • Shows loading spinner during sync
  • Displays "Syncing..." text
  • Disabled during sync operation

Unlink Button:

  • Remove account from OneLibro
  • Shows confirmation dialog
  • Deletes from database (cascade deletes transactions)
  • Warning: Cannot be undone

Last Synced Timestamp

Displays relative time since last transaction sync:

import { formatRelativeTime } from '@/lib/supabase';

const lastSynced = formatRelativeTime(account.last_synced);
// "2 hours ago"
// "Just now"
// "3 days ago"

RecentTransactions

List component for displaying recent transactions with formatting and actions.

Props

interface RecentTransactionsProps {
transactions: Transaction[]; // Array of transactions to display
limit?: number; // Max transactions to show (default: 10)
showViewAll?: boolean; // Show "View All" link
onEdit?: (transaction: Transaction) => void;
onDelete?: (transactionId: string) => void;
loading?: boolean; // Show skeleton loader
emptyMessage?: string; // Custom empty state message
}

Usage

Dashboard Widget:

import { RecentTransactions } from '@/components/finance/RecentTransactions';

<RecentTransactions
transactions={recentTransactions}
limit={5}
showViewAll={true}
loading={isLoading}
/>

Transactions Page (with actions):

<RecentTransactions
transactions={allTransactions}
onEdit={(tx) => router.push(`/finance/transactions/${tx.id}/edit`)}
onDelete={async (id) => {
if (confirm('Delete this transaction?')) {
await deleteTransaction(id);
toast.success('Transaction deleted');
}
}}
/>

Transaction Row Structure

Desktop View (Table):

┌────────────┬───────────────┬─────────────┬────────────┬──────────┬─────────┐
│ Date │ Merchant │ Category │ Amount │ Status │ Actions │
├────────────┼───────────────┼─────────────┼────────────┼──────────┼─────────┤
│ Jan 24 │ Starbucks │ [Food 🍔] │ -$5.47 │ Posted │ [✏️ 🗑️]│
│ Jan 23 │ Amazon │ [Shopping] │ -$32.15 │ Pending │ [✏️ 🗑️]│
└────────────┴───────────────┴─────────────┴────────────┴──────────┴─────────┘

Mobile View (Cards):

┌─────────────────────────────────┐
│ Starbucks -$5.47 │
│ Food & Drink • Jan 24 │
│ Posted [✏️ 🗑️]│
└─────────────────────────────────┘

Color Coding

Amount Colors:

  • Red (expenses): Money spent
<span className="text-red-500">-$50.00</span>
  • Green (income): Money received
<span className="text-green-500">+$1,500.00</span>

Status Badges:

  • Green (Posted): Transaction completed
<span className="bg-green-100 text-green-700 px-2 py-1 rounded-full text-xs">
Posted
</span>
  • Yellow (Pending): Transaction processing
<span className="bg-yellow-100 text-yellow-700 px-2 py-1 rounded-full text-xs">
Pending
</span>

Category Badges

Each category displays with icon and color:

const categoryIcons = {
'Food and Drink': '🍔',
'Groceries': '🛒',
'Shopping': '🛍️',
'Transportation': '🚗',
'Entertainment': '🎬',
'Bills & Utilities': '💡',
// ... more categories
};

<span className="inline-flex items-center gap-1 px-2 py-1 bg-gray-100 rounded text-xs">
{categoryIcons[category]}
{category}
</span>

Empty State

When transactions.length === 0:

<div className="text-center py-12">
<Receipt className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-300 mb-2">
No transactions yet
</h3>
<p className="text-gray-500">
Connect a bank account to see your transactions
</p>
<PlaidLink>
<button className="mt-4 btn-primary">
Connect Bank Account
</button>
</PlaidLink>
</div>

When showViewAll={true}:

<Link
href="/finance/transactions"
className="flex items-center justify-center gap-2 text-[#10b981] hover:text-[#059669] transition-colors"
>
View All Transactions
<ArrowRight className="w-4 h-4" />
</Link>

Date and Currency Formatting

formatCurrency

Formats cents to dollar display:

import { formatCurrency } from '@/lib/supabase';

formatCurrency(543210); // "$5,432.10"
formatCurrency(-5000); // "-$50.00"
formatCurrency(0); // "$0.00"
formatCurrency(1000000); // "$10,000.00"

Features:

  • Thousand separators (commas)
  • Two decimal places
  • Dollar sign prefix
  • Negative sign for debts

formatDate

Formats ISO date strings for display:

import { formatDate } from '@/lib/supabase';

formatDate('2025-01-24'); // "Jan 24, 2025"
formatDate('2025-01-24T10:30:00Z'); // "Jan 24, 2025"
formatDate('2025-01-24', 'short'); // "Jan 24"
formatDate('2025-01-24', 'full'); // "January 24, 2025"

formatRelativeTime

Formats timestamps as relative time:

import { formatRelativeTime } from '@/lib/supabase';

formatRelativeTime('2025-01-24T10:00:00Z'); // "2 hours ago"
formatRelativeTime('2025-01-23T10:00:00Z'); // "1 day ago"
formatRelativeTime('2025-01-24T12:58:00Z'); // "Just now"
formatRelativeTime('2025-01-17T10:00:00Z'); // "1 week ago"

Responsive Breakpoints

All data display components adapt to screen sizes:

Mobile (< 768px)

  • Stack vertically
  • Full-width cards
  • Simplified transaction cards
  • Hide less important columns
  • Touch-friendly buttons (44x44px minimum)

Tablet (768px - 1024px)

  • 2-column grid for cards
  • Hybrid table/card view for transactions
  • Moderate spacing

Desktop (> 1024px)

  • 3-column grid for cards
  • Full table view for transactions
  • Generous spacing
  • Hover states on all interactive elements

Loading States

Skeleton Loaders

DashboardCard Skeleton:

<div className="bg-[#1a1a1a] rounded-lg border border-[#2a2a2a] p-6 animate-pulse">
<div className="h-4 bg-[#2a2a2a] rounded w-1/3 mb-4"></div>
<div className="h-8 bg-[#2a2a2a] rounded w-2/3"></div>
</div>

Transaction List Skeleton:

{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center justify-between p-4 animate-pulse">
<div className="flex-1 space-y-2">
<div className="h-4 bg-[#2a2a2a] rounded w-1/4"></div>
<div className="h-3 bg-[#2a2a2a] rounded w-1/3"></div>
</div>
<div className="h-6 bg-[#2a2a2a] rounded w-20"></div>
</div>
))}

Best Practices

Performance

1. Virtualization for Long Lists:

// For 100+ transactions, use react-window
import { FixedSizeList } from 'react-window';

<FixedSizeList
height={600}
itemCount={transactions.length}
itemSize={80}
>
{({ index, style }) => (
<TransactionRow transaction={transactions[index]} style={style} />
)}
</FixedSizeList>

2. Memoization:

const MemoizedAccountCard = React.memo(AccountCard);
const MemoizedDashboardCard = React.memo(DashboardCard);

3. Lazy Loading:

// Load transactions as user scrolls
const { data, fetchNextPage } = useInfiniteQuery({
queryKey: ['transactions'],
queryFn: ({ pageParam = 0 }) => fetchTransactions(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
});

Accessibility

1. Semantic HTML:

<table>
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Merchant</th>
{/* ... */}
</tr>
</thead>
<tbody>
{/* Transaction rows */}
</tbody>
</table>

2. ARIA Labels:

<button
onClick={() => onSync(account.id)}
aria-label={`Sync transactions for ${account.account_name}`}
>
<RefreshCw className="w-4 h-4" />
</button>

3. Screen Reader Text:

<span className="sr-only">
Transaction on {formatDate(tx.date)} at {tx.merchant} for {formatCurrency(tx.amount)}
</span>


Summary

Data display components in OneLibro provide:

  • ✅ Consistent formatting (currency, dates, percentages)
  • ✅ Visual hierarchy with cards and tables
  • ✅ Responsive design (mobile, tablet, desktop)
  • ✅ Empty states with helpful messages
  • ✅ Loading states with skeleton loaders
  • ✅ Color coding for quick comprehension
  • ✅ Accessibility features (ARIA, semantic HTML)
  • ✅ Performance optimizations (memoization, lazy loading)

Use these components to display financial data consistently across OneLibro!