Skip to content

Commit 385e5fe

Browse files
authored
Merge pull request #286 from dijangh904/server-side-pagination
Implement server-side pagination for the analytics data table
2 parents adc82f7 + 8be8035 commit 385e5fe

8 files changed

Lines changed: 454 additions & 0 deletions

File tree

package-lock.json

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@sentry/nextjs": "^10.49.0",
1919
"@stellar/freighter-api": "^1.7.1",
2020
"@tailwindcss/postcss": "^4.0.0",
21+
"@tanstack/react-query": "^5.99.2",
2122
"axios": "^1.15.0",
2223
"class-variance-authority": "^0.7.1",
2324
"clsx": "^2.1.1",

src/app/api/invoices/route.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
3+
// Mock data generator for demonstration
4+
const generateMockInvoices = (count: number) => {
5+
return Array.from({ length: count }, (_, index) => ({
6+
id: `INV-${String(index + 1).padStart(4, '0')}`,
7+
riskScore: Math.floor(Math.random() * 40) + 60, // 60-100
8+
status: Math.random() > 0.3 ? 'Approved' : 'Pending',
9+
amount: Math.floor(Math.random() * 50000) + 1000, // $1,000 - $51,000
10+
}));
11+
};
12+
13+
// Generate 10,000 mock invoices
14+
const ALL_INVOICES = generateMockInvoices(10000);
15+
16+
export async function GET(request: NextRequest) {
17+
const { searchParams } = new URL(request.url);
18+
19+
// Parse pagination parameters
20+
const page = parseInt(searchParams.get('page') || '1');
21+
const limit = parseInt(searchParams.get('limit') || '20');
22+
23+
// Validate parameters
24+
const validPage = Math.max(1, page);
25+
const validLimit = Math.min(100, Math.max(1, limit)); // Max 100 items per page
26+
27+
// Calculate pagination
28+
const offset = (validPage - 1) * validLimit;
29+
const totalItems = ALL_INVOICES.length;
30+
const totalPages = Math.ceil(totalItems / validLimit);
31+
32+
// Get paginated data
33+
const invoices = ALL_INVOICES.slice(offset, offset + validLimit);
34+
35+
// Return paginated response
36+
return NextResponse.json({
37+
data: invoices,
38+
pagination: {
39+
currentPage: validPage,
40+
totalPages,
41+
totalItems,
42+
itemsPerPage: validLimit,
43+
hasNextPage: validPage < totalPages,
44+
hasPreviousPage: validPage > 1,
45+
},
46+
});
47+
}

src/app/layout.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ import React from "react";
33
import { Inter } from "next/font/google";
44
import { Toaster } from "sonner";
55
import ToasterProvider from "../components/general/ToasterProvider";
6+
import { SlippageProvider } from "../contexts/SlippageContext";
7+
import { NetworkCongestionProvider } from "../contexts/NetworkCongestionContext";
8+
import Footer from "../components/layout/Footer";
9+
import NetworkCongestionBanner from "../components/NetworkCongestionBanner";
10+
import ErrorBoundary from "../components/ErrorBoundary";
11+
import PageTransition from "../components/PageTransition";
12+
import QueryProvider from "../providers/QueryClientProvider";
613
import { SettingsProvider } from "../lib/context/SettingsContext";
714
import NetworkGuard from "../components/general/NetworkGuard";
815

@@ -18,6 +25,26 @@ export const metadata = {
1825

1926
export default function RootLayout({ children }: { children: React.ReactNode }) {
2027
return (
28+
<html lang="en">
29+
<body className={`${inter.variable} font-sans antialiased`}>
30+
<ErrorBoundary>
31+
<NetworkCongestionProvider>
32+
<SlippageProvider>
33+
<ToasterProvider />
34+
{/* <Toaster position="top-right" richColors closeButton /> */}
35+
<NetworkCongestionBanner />
36+
<QueryProvider>
37+
<PageTransition>
38+
{children}
39+
</PageTransition>
40+
</QueryProvider>
41+
<PageTransition>
42+
{children}
43+
</PageTransition>
44+
<Footer />
45+
</SlippageProvider>
46+
</NetworkCongestionProvider>
47+
</ErrorBoundary>
2148
<html lang="en" className={inter.variable}>
2249
<body className="font-sans">
2350
<SettingsProvider>

src/app/page.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import StickyHeader from "../components/StickyHeader";
1010
import Card from "../components/Card";
1111
import WalletModal from "../components/WalletModal";
1212
import InvoiceMintForm from "../components/InvoiceMintForm";
13+
import InvoiceTable from "../components/InvoiceTable";
1314
import NewsBanner from "../components/NewsBanner";
1415
import useTransactionToast from "../lib/useTransactionToast";
1516
import AddTrustlineButton from "../components/AddTrustlineButton";
@@ -237,6 +238,7 @@ export default function Page() {
237238
</div>
238239

239240
{/* Invoice Table */}
241+
<InvoiceTable />
240242
<div className="bg-tradeflow-secondary rounded-2xl border border-tradeflow-muted overflow-hidden mb-12">
241243
<div className="p-6 border-b border-slate-700">
242244
<h2 className="text-xl font-semibold">Verified Asset Pipeline</h2>
@@ -295,6 +297,56 @@ export default function Page() {
295297
</div>
296298
<div className="p-6 bg-tradeflow-dark/50">
297299
<LoanTable />
300+
{/* Invoice Table */}
301+
<div className="bg-tradeflow-secondary rounded-2xl border border-tradeflow-muted overflow-hidden mb-12">
302+
<div className="p-6 border-b border-slate-700">
303+
<h2 className="text-xl font-semibold">Verified Asset Pipeline</h2>
304+
</div>
305+
<table className="w-full text-left">
306+
<thead className="bg-tradeflow-dark/50 text-tradeflow-muted text-sm uppercase">
307+
<tr>
308+
<th className="p-4">Invoice ID</th>
309+
<th className="p-4">Risk Score</th>
310+
<th className="p-4">Status</th>
311+
<th className="p-4">Amount</th>
312+
</tr>
313+
</thead>
314+
<tbody>
315+
{loading ? (
316+
// Show 5 skeleton rows while loading
317+
Array.from({ length: 5 }).map((_, index) => (
318+
<SkeletonRow key={`skeleton-${index}`} />
319+
))
320+
) : (
321+
invoices.map((inv: { id: string; riskScore: number; status: string; amount: number | string }) => (
322+
<tr
323+
key={inv.id}
324+
className="border-b border-tradeflow-muted/50 hover:bg-tradeflow-muted/20 transition"
325+
>
326+
<td className="p-4 font-mono text-sm text-blue-300">
327+
#{inv.id.slice(-6)}
328+
</td>
329+
<td className="p-4">
330+
<div className="w-full bg-tradeflow-muted h-2 rounded-full max-w-[100px]">
331+
<div
332+
className="bg-blue-500 h-2 rounded-full"
333+
style={{ width: `${inv.riskScore}%` }}
334+
></div>
335+
</div>
336+
</td>
337+
<td className="p-4 text-sm font-medium">
338+
<span
339+
className={`px-3 py-1 rounded-full ${inv.status === "Approved" ? "bg-tradeflow-success/20 text-tradeflow-success" : "bg-tradeflow-warning/20 text-tradeflow-warning"}`}
340+
>
341+
{inv.status}
342+
</span>
343+
</td>
344+
<td className="p-4 font-bold text-lg">${inv.amount}</td>
345+
</tr>
346+
))
347+
)}
348+
</tbody>
349+
</table>
298350
</div>
299351

300352
{/* Active Loans Table (Issue #6) */}

src/components/InvoiceTable.tsx

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"use client";
2+
3+
import React, { useState } from 'react';
4+
import { useQuery } from '@tanstack/react-query';
5+
import PaginationControls from './PaginationControls';
6+
import SkeletonRow from './SkeletonRow';
7+
8+
interface Invoice {
9+
id: string;
10+
riskScore: number;
11+
status: string;
12+
amount: number;
13+
}
14+
15+
interface PaginationInfo {
16+
currentPage: number;
17+
totalPages: number;
18+
totalItems: number;
19+
itemsPerPage: number;
20+
hasNextPage: boolean;
21+
hasPreviousPage: boolean;
22+
}
23+
24+
interface InvoicesResponse {
25+
data: Invoice[];
26+
pagination: PaginationInfo;
27+
}
28+
29+
const InvoiceTable: React.FC = () => {
30+
const [currentPage, setCurrentPage] = useState(1);
31+
const itemsPerPage = 20;
32+
33+
const {
34+
data: invoicesData,
35+
isLoading,
36+
isFetching,
37+
error,
38+
} = useQuery<InvoicesResponse>({
39+
queryKey: ['invoices', currentPage, itemsPerPage],
40+
queryFn: async () => {
41+
const response = await fetch(
42+
`/api/invoices?page=${currentPage}&limit=${itemsPerPage}`
43+
);
44+
if (!response.ok) {
45+
throw new Error('Failed to fetch invoices');
46+
}
47+
return response.json();
48+
},
49+
keepPreviousData: true, // Prevents UI from flashing empty while fetching next page
50+
staleTime: 1000 * 60 * 5, // 5 minutes
51+
});
52+
53+
const handlePageChange = (page: number) => {
54+
setCurrentPage(page);
55+
};
56+
57+
if (error) {
58+
return (
59+
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4 text-red-400">
60+
<p className="font-medium">Error loading invoices</p>
61+
<p className="text-sm opacity-80">{error.message}</p>
62+
</div>
63+
);
64+
}
65+
66+
return (
67+
<div className="bg-tradeflow-secondary rounded-2xl border border-tradeflow-muted overflow-hidden">
68+
<div className="p-6 border-b border-slate-700">
69+
<h2 className="text-xl font-semibold">Verified Asset Pipeline</h2>
70+
</div>
71+
72+
<div className="relative">
73+
<table className="w-full text-left">
74+
<thead className="bg-tradeflow-dark/50 text-tradeflow-muted text-sm uppercase sticky top-0 z-10">
75+
<tr>
76+
<th className="p-4">Invoice ID</th>
77+
<th className="p-4">Risk Score</th>
78+
<th className="p-4">Status</th>
79+
<th className="p-4">Amount</th>
80+
</tr>
81+
</thead>
82+
<tbody className="relative">
83+
{isLoading && !invoicesData ? (
84+
// Show skeleton rows on initial load
85+
Array.from({ length: itemsPerPage }).map((_, index) => (
86+
<SkeletonRow key={`skeleton-${index}`} />
87+
))
88+
) : (
89+
invoicesData?.data.map((invoice) => (
90+
<tr
91+
key={invoice.id}
92+
className={`border-b border-tradeflow-muted/50 hover:bg-tradeflow-muted/20 transition ${isFetching ? 'opacity-60' : ''
93+
}`}
94+
>
95+
<td className="p-4 font-mono text-sm text-blue-300">
96+
#{invoice.id.slice(-6)}
97+
</td>
98+
<td className="p-4">
99+
<div className="w-full bg-tradeflow-muted h-2 rounded-full max-w-25">
100+
<div
101+
className="bg-blue-500 h-2 rounded-full transition-all duration-300"
102+
style={{ width: `${invoice.riskScore}%` }}
103+
></div>
104+
</div>
105+
</td>
106+
<td className="p-4 text-sm font-medium">
107+
<span
108+
className={`px-3 py-1 rounded-full ${invoice.status === "Approved"
109+
? "bg-tradeflow-success/20 text-tradeflow-success"
110+
: "bg-tradeflow-warning/20 text-tradeflow-warning"
111+
}`}
112+
>
113+
{invoice.status}
114+
</span>
115+
</td>
116+
<td className="p-4 font-bold text-lg">${invoice.amount.toLocaleString()}</td>
117+
</tr>
118+
))
119+
)}
120+
</tbody>
121+
</table>
122+
123+
{/* Loading overlay for page changes */}
124+
{isFetching && !isLoading && (
125+
<div className="absolute inset-0 bg-slate-900/20 flex items-center justify-center z-20">
126+
<div className="text-blue-400 text-sm font-medium">Loading...</div>
127+
</div>
128+
)}
129+
</div>
130+
131+
{/* Pagination Controls */}
132+
{invoicesData && (
133+
<PaginationControls
134+
pagination={invoicesData.pagination}
135+
onPageChange={handlePageChange}
136+
isLoading={isFetching}
137+
/>
138+
)}
139+
</div>
140+
);
141+
};
142+
143+
export default InvoiceTable;

0 commit comments

Comments
 (0)