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>
View All Link
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>
Related Components
- Form Components - Input forms for creating data
- Charts - Visual data representations
- Components Overview - All component documentation
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!