Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 84 additions & 8 deletions frontend/src/components/TransactionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ const TransactionList: React.FC = () => {
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [filterType, setFilterType] = useState<string>('all');
const [currentPage, setCurrentPage] = useState<number>(1);
const pageSize = 10;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about making this configurable?


useEffect(() => {
const fetchTransactions = async () => {
try {
setLoading(true);
const backendUrl = import.meta.env.VITE_BACKEND_URL;
const response = await fetch(`${backendUrl}/api/v1/transactions/`);
const response = await fetch('http://localhost:8000/api/v1/transactions/?limit=1000');
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Hardcoded localhost:8000 can break in different environments. We should use an environment variable or config.
  • I think limit=1000 fetches too many records upfront. As it is a client-side pagination, this will be slow and can waste bandwidth. I think we should consider server-side pagination or a more reasonable limit.

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: Transaction[] = await response.json();
setTransactions(data);
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'An error occurred');
} catch (e: any) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid any. We can use unknown and narrow the error as original.

setError(e.message);
} finally {
setLoading(false);
}
Expand All @@ -39,6 +41,33 @@ const TransactionList: React.FC = () => {
fetchTransactions();
}, []);

const filteredTransactions = transactions.filter(transaction => {
if (filterType === 'all') return true;
return transaction.type === filterType;
});
Comment on lines +44 to +47
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs every render. We can use useMemo here.


const totalPages = Math.ceil(filteredTransactions.length / pageSize);
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedTransactions = filteredTransactions.slice(startIndex, endIndex);
Comment on lines +49 to +52
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use useMemo here as well.


const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFilterType(e.target.value);
setCurrentPage(1); // Reset to first page when filter changes
};

const handleNextPage = () => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
};

const handlePreviousPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};

if (loading) {
return <div className="text-gray-700">Loading transactions...</div>;
}
Expand All @@ -49,8 +78,23 @@ const TransactionList: React.FC = () => {

return (
<div className="bg-white shadow overflow-hidden sm:rounded-lg mt-8">
<div className="px-4 py-5 sm:px-6">
<div className="px-4 py-5 sm:px-6 flex justify-between items-center">
<h3 className="text-lg leading-6 font-medium text-gray-900">Recent Transactions</h3>
<div className="flex items-center gap-2">
<label htmlFor="type-filter" className="text-sm font-medium text-gray-700">
Filter by type:
</label>
<select
id="type-filter"
value={filterType}
onChange={handleFilterChange}
className="border border-gray-300 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Comment on lines +87 to +92
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add aria-label for screen readers.

<option value="all">All</option>
<option value="credit">Credit</option>
<option value="debit">Debit</option>
</select>
</div>
</div>
<div className="border-t border-gray-200">
<table className="min-w-full divide-y divide-gray-200">
Expand All @@ -64,7 +108,7 @@ const TransactionList: React.FC = () => {
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{transactions.map((transaction, index) => (
{paginatedTransactions.map((transaction, index) => (
<tr key={transaction.id} className={`${index % 2 === 0 ? 'bg-gray-50' : 'bg-white'}`}>
<td className="px-6 py-4 whitespace-nowrap text-center text-sm text-gray-500">
{new Date(transaction.date).toLocaleDateString()}
Expand All @@ -87,16 +131,48 @@ const TransactionList: React.FC = () => {
</td>
</tr>
))}
{transactions.length === 0 && (
{paginatedTransactions.length === 0 && (
<tr>
<td colSpan={5} className="px-6 py-4 text-center text-gray-500">No transactions found.</td>
</tr>
)}
</tbody>
</table>
</div>

{totalPages > 1 && (
<div className="px-4 py-3 bg-gray-50 border-t border-gray-200 sm:px-6 flex items-center justify-between">
<div className="text-sm text-gray-700">
Page {currentPage} of {totalPages} ({filteredTransactions.length} transactions)
</div>
<div className="flex gap-2">
<button
onClick={handlePreviousPage}
disabled={currentPage === 1}
className={`px-3 py-1 rounded text-sm font-medium ${
currentPage === 1
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
: 'bg-blue-500 text-white hover:bg-blue-600'
}`}
>
Previous
</button>
<button
onClick={handleNextPage}
disabled={currentPage === totalPages}
className={`px-3 py-1 rounded text-sm font-medium ${
currentPage === totalPages
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
: 'bg-blue-500 text-white hover:bg-blue-600'
}`}
>
Next
</button>
</div>
</div>
)}
</div>
);
};

export default TransactionList;
export default TransactionList;