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!