Migrate all components and pages to shadcn/ui with DataTable
All checks were successful
Deploy to VPS / deploy (push) Successful in 28s

Replace custom markup across all pages and components with shadcn/ui
primitives (Dialog, Sheet, Select, Card, Tabs, etc.). Add reusable
DataTable component powered by @tanstack/react-table with sortable
column headers and client-side pagination. Introduce TransactionList
with responsive mobile cards and desktop DataTable, dashboard section
customization (DashboardSection, SectionConfigDialog), and settings
API types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-03-22 14:45:44 -06:00
parent 46f2d8679c
commit 2cd0d3b2e1
17 changed files with 1626 additions and 1109 deletions

View File

@@ -1,17 +1,9 @@
import { useEffect, useState, useCallback } from 'react';
import { Plus, Search, Pencil, Trash2, ArrowLeftRight } from 'lucide-react';
import { ArrowLeftRight } from 'lucide-react';
import api, { type Transaction } from '../api';
import TransactionModal from '../components/TransactionModal';
import ConfirmDialog from '../components/ConfirmDialog';
function formatAmount(amount: number, currency: string) {
const abs = Math.abs(amount);
if (currency === 'USD') {
return `$${abs.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
return `${abs.toLocaleString('es-CR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
import TransactionList from '../components/TransactionList';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
type SourceTab = 'CASH' | 'TRANSFER';
@@ -20,10 +12,6 @@ export default function Transfers() {
const [search, setSearch] = useState('');
const [sourceTab, setSourceTab] = useState<SourceTab>('CASH');
const [loading, setLoading] = useState(true);
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<Transaction | null>(null);
const [deleteId, setDeleteId] = useState<number | null>(null);
const [deleting, setDeleting] = useState(false);
const fetchTransactions = useCallback(async () => {
setLoading(true);
@@ -42,142 +30,36 @@ export default function Transfers() {
return () => clearTimeout(timer);
}, [fetchTransactions]);
const handleDelete = async () => {
if (deleteId === null) return;
setDeleting(true);
try {
await api.delete(`/transactions/${deleteId}`);
setDeleteId(null);
fetchTransactions();
} finally {
setDeleting(false);
}
};
return (
<div className="space-y-5">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold">Cash & Transfers</h1>
<p className="text-sm text-text-muted mt-1">
Track non-credit-card expenses
</p>
</div>
<button
onClick={() => {
setEditing(null);
setModalOpen(true);
}}
className="flex items-center gap-2 bg-[#606C38] hover:bg-[#7a8a4a] text-white dark:text-[#FEFAE0] font-semibold px-4 py-2.5 rounded-lg text-sm transition-colors"
>
<Plus className="w-4 h-4" />
Add {sourceTab === 'CASH' ? 'Cash Expense' : 'Transfer'}
</button>
<div>
<h1 className="text-2xl font-bold font-heading">Cash & Transfers</h1>
<p className="text-sm text-muted-foreground mt-1">
Track non-credit-card expenses
</p>
</div>
{/* Source tabs */}
<div className="flex gap-1 bg-surface-card border border-border rounded-lg p-1 w-fit">
{(['CASH', 'TRANSFER'] as const).map((tab) => (
<button
key={tab}
onClick={() => setSourceTab(tab)}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
sourceTab === tab
? 'bg-[#606C38]/10 text-[#606C38] dark:text-[#7a8a4a]'
: 'text-text-muted hover:text-text-primary'
}`}
>
{tab === 'CASH' ? 'Cash' : 'Transfers'}
</button>
))}
</div>
<Tabs value={sourceTab} onValueChange={(v) => setSourceTab(v as SourceTab)}>
<TabsList>
<TabsTrigger value="CASH">Cash</TabsTrigger>
<TabsTrigger value="TRANSFER">Transfers</TabsTrigger>
</TabsList>
{/* Search */}
<div className="relative max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full bg-input-bg border border-border rounded-lg pl-10 pr-4 py-2.5 text-sm text-text-primary placeholder-text-faint focus:outline-none focus:border-[#606C38]/50 transition-colors"
placeholder="Search..."
/>
</div>
{/* List */}
<div className="bg-surface-card border border-border rounded-xl divide-y divide-border-subtle">
{transactions.length === 0 && !loading ? (
<div className="px-5 py-16 text-center text-text-faint text-sm">
<ArrowLeftRight className="w-8 h-8 mx-auto mb-3 text-text-faint" />
No {sourceTab.toLowerCase()} transactions yet
</div>
) : (
<>
{transactions.map((tx) => (
<div
key={tx.id}
className="flex items-center justify-between px-5 py-4 hover:bg-surface-hover transition-colors group"
>
<div className="min-w-0">
<p className="text-sm font-medium truncate">{tx.merchant}</p>
<p className="text-xs text-text-muted mt-0.5">
{new Date(tx.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
{tx.category && (
<span className="ml-2 bg-surface-hover text-text-secondary px-2 py-0.5 rounded">
{tx.category.name}
</span>
)}
</p>
</div>
<div className="flex items-center gap-3 flex-shrink-0 ml-4">
<span className="font-mono text-sm font-medium">
{formatAmount(tx.amount, tx.currency)}
</span>
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => {
setEditing(tx);
setModalOpen(true);
}}
className="p-1.5 rounded hover:bg-surface-hover text-text-muted hover:text-text-primary transition-colors"
>
<Pencil className="w-3.5 h-3.5" />
</button>
<button
onClick={() => setDeleteId(tx.id)}
className="p-1.5 rounded hover:bg-red-500/10 text-text-muted hover:text-red-500 dark:hover:text-red-400 transition-colors"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
))}
</>
)}
</div>
{modalOpen && (
<TransactionModal
transaction={editing}
source={sourceTab}
onClose={() => setModalOpen(false)}
onSaved={fetchTransactions}
/>
)}
{deleteId !== null && (
<ConfirmDialog
title="Delete Transaction"
message="This transaction will be permanently deleted. This action cannot be undone."
onConfirm={handleDelete}
onCancel={() => setDeleteId(null)}
loading={deleting}
/>
)}
<TabsContent value={sourceTab} className="mt-5 space-y-5">
<TransactionList
transactions={transactions}
loading={loading}
source={sourceTab}
search={search}
onSearchChange={setSearch}
onRefresh={fetchTransactions}
showCategory={false}
addLabel={sourceTab === 'CASH' ? 'Add Cash Expense' : 'Add Transfer'}
emptyIcon={<ArrowLeftRight className="w-8 h-8 mx-auto mb-3 text-muted-foreground/50" />}
emptyMessage={`No ${sourceTab.toLowerCase()} transactions yet`}
/>
</TabsContent>
</Tabs>
</div>
);
}