Skip to content

Commit 841feb0

Browse files
feat(website): add pagination feature (#1826)
Co-authored-by: Saturn <saturn@dbeal.dev>
1 parent a735b85 commit 841feb0

File tree

13 files changed

+690
-11
lines changed

13 files changed

+690
-11
lines changed

packages/website/public/_redirects

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
/packages/:name/:tag/:variant/cannonfile /packages/[name]/[tag]/[variant]/cannonfile.html 200
1010
/deploy/txn/:chainId/:safeAddress/:nonce/:sigHash /deploy/txn/[chainId]/[safeAddress]/[nonce]/[sigHash].html 200
1111
/address/:chainId/:address /address/[chainId]/[address].html 200
12-
/tx/:chainId/:txHash /tx/[chainId]/[txHash].html 200
12+
/tx/:chainId/:txHash /tx/[chainId]/[txHash].html 200
13+
/txs /txs.html 200
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const MAX_PAGE_SIZE = 100;

packages/website/src/features/Address/AddressDataTable.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,24 @@ import {
33
Table,
44
TableBody,
55
TableCell,
6+
TableFooter,
67
TableHead,
78
TableHeader,
89
TableRow,
910
} from '@/components/ui/table';
1011
import { flexRender } from '@tanstack/react-table';
11-
import { Inbox } from 'lucide-react';
12+
import { Inbox, MoveRight } from 'lucide-react';
13+
import Link from 'next/link';
1214

1315
type AddressDataTableProps = {
1416
table: any;
17+
url?: string;
1518
};
1619

17-
const AddressDataTable: React.FC<AddressDataTableProps> = ({ table }) => {
20+
const AddressDataTable: React.FC<AddressDataTableProps> = ({
21+
table,
22+
url = '',
23+
}) => {
1824
return (
1925
<>
2026
<Table>
@@ -87,6 +93,23 @@ const AddressDataTable: React.FC<AddressDataTableProps> = ({ table }) => {
8793
</TableRow>
8894
)}
8995
</TableBody>
96+
{url && (
97+
<TableFooter>
98+
<TableRow>
99+
<TableCell colSpan={table.getAllColumns().length}>
100+
<div className="flex items-center justify-center py-3">
101+
<Link
102+
href={url}
103+
className="flex text-sm font-mono border-b border-dotted border-muted-foreground hover:border-solid"
104+
>
105+
<span>VIEW ALL TRANSACTIONS</span>
106+
<MoveRight className="h-4 w-4 ml-3" />
107+
</Link>
108+
</div>
109+
</TableCell>
110+
</TableRow>
111+
</TableFooter>
112+
)}
90113
</Table>
91114
</>
92115
);

packages/website/src/features/Address/AddressPage.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@ const AddressPage = () => {
2323
const { getChainById } = useCannonChains();
2424
const chain = getChainById(Number(chainId));
2525
const displayAddress = Array.isArray(address) ? address[0] : address;
26+
2627
const {
2728
data: transactionData,
2829
isLoading,
2930
isError,
3031
} = useAddressTransactions(
3132
parseInt(chainId as string) || 0,
32-
displayAddress ?? ''
33+
displayAddress ?? '',
34+
'0',
35+
true
3336
);
3437

3538
if (isLoading) {
@@ -54,7 +57,7 @@ const AddressPage = () => {
5457
);
5558
}
5659

57-
const { txs, receipts, oldReceipts } = transactionData;
60+
const { txs, receipts, oldReceipts, isLastPage } = transactionData;
5861

5962
const renderContent = () => {
6063
if (displayAddress) {
@@ -70,6 +73,7 @@ const AddressPage = () => {
7073
chain={chain}
7174
txs={txs}
7275
receipts={receipts}
76+
isLastPage={isLastPage}
7377
/>
7478
);
7579
}

packages/website/src/features/Address/AddressTxLists.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@ type AddressListsProps = {
3333
chain: Chain;
3434
txs: OtterscanTransaction[];
3535
receipts: OtterscanReceipt[];
36+
isLastPage: boolean;
3637
};
3738

3839
const AddressLists: React.FC<AddressListsProps> = ({
3940
address,
4041
chain,
4142
txs,
4243
receipts,
44+
isLastPage,
4345
}) => {
4446
const [isDate, setIsDate] = useState<boolean>(false);
4547
const [isGasPrice, setIsGasPrice] = useState<boolean>(false);
@@ -52,6 +54,7 @@ const AddressLists: React.FC<AddressListsProps> = ({
5254
const columnHelper = createColumnHelper<TransactionRow>();
5355
const [openToolTipIndex, setOpenTooltipIndex] = useState<number | null>();
5456

57+
const url = isLastPage ? '' : `/txs?a=${address}&c=${chain?.id}`;
5558
const columns = [
5659
columnHelper.accessor('detail', {
5760
cell: (info: any) => (
@@ -164,7 +167,7 @@ const AddressLists: React.FC<AddressListsProps> = ({
164167
</CardHeader>
165168
<CardContent>
166169
<div className="w-full rounded-md border border-border overflow-x-auto">
167-
<AddressDataTable table={table} />
170+
<AddressDataTable table={table} url={url} />
168171
</div>
169172
</CardContent>
170173
</Card>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from 'react';
2+
import { useRouter } from 'next/router';
3+
import { useCannonChains } from '@/providers/CannonProvidersProvider';
4+
import TransactionsSection from '@/features/Txs/TransactionsSection';
5+
import Link from 'next/link';
6+
7+
const AddressTransactionPage = () => {
8+
const router = useRouter();
9+
const { a, c, p } = router.query;
10+
const chainId = Array.isArray(c) ? c[0] : c;
11+
const { getChainById, getExplorerUrl } = useCannonChains();
12+
const chain = getChainById(Number(chainId));
13+
const displayAddress = Array.isArray(a) ? a[0] : a;
14+
15+
const pageIndex = Array.isArray(p) ? p[0] : p;
16+
const explorerUrl = getExplorerUrl(chain?.id || 0, displayAddress ?? '');
17+
18+
return (
19+
<div className="w-full max-w-screen-xl mx-auto px-4">
20+
<div className="ml-3">
21+
<h1 className="text-2xl font-bold mt-4">Transactions</h1>
22+
</div>
23+
<div className="ml-3 flex items-baseline space-x-2">
24+
<span>For</span>
25+
<Link
26+
href={explorerUrl}
27+
className="flex text-sm font-mono border-b border-dotted border-muted-foreground hover:border-solid"
28+
>
29+
{displayAddress}
30+
</Link>
31+
</div>
32+
<hr className="opacity-75 mt-3" />
33+
<TransactionsSection
34+
address={displayAddress ?? ''}
35+
chain={chain}
36+
pageIndex={pageIndex}
37+
/>
38+
</div>
39+
);
40+
};
41+
42+
export default AddressTransactionPage;
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import React, { useState } from 'react';
2+
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
3+
import { CircleHelp } from 'lucide-react';
4+
import {
5+
createColumnHelper,
6+
useReactTable,
7+
getCoreRowModel,
8+
} from '@tanstack/react-table';
9+
import AddressAdditionalInfo from '@/features/Address/AddressAdditionalDialog';
10+
import { Chain } from '@/types/Chain';
11+
import { mapToTransactionList } from '@/lib/address';
12+
import {
13+
TransactionRow,
14+
OtterscanTransaction,
15+
OtterscanReceipt,
16+
} from '@/types/AddressList';
17+
import AmountColumn from '@/features/Address/column/AmountColumn';
18+
import FromColumn from '@/features/Address/column/FromColumn';
19+
import ToColumn from '@/features/Address/column/ToColumn';
20+
import HashColumn from '@/features/Address/column/HashColumn';
21+
import MethodColumn from '@/features/Address/column/MethodColumn';
22+
import MethodHeader from '@/features/Address/column/MethodHeader';
23+
import AgeColumn from '@/features/Address/column/AgeColumn';
24+
import AgeHeader from '@/features/Address/column/AgeHeader';
25+
import TxFeeHeader from '@/features/Address/column/TxFeeHeader';
26+
import TxFeeColumn from '@/features/Address/column/TxFeeColumn';
27+
import BlockColumn from '@/features/Address/column/BlockColumn';
28+
import AddressDataTable from '@/features/Address/AddressDataTable';
29+
import DownloadListButton from '@/features/Address/DownloadListButton';
30+
import TransactionsPagination from '@/features/Txs/TransactionsPagination';
31+
import { Alert, AlertDescription } from '@/components/ui/alert';
32+
import { MAX_PAGE_SIZE } from '@/constants/pagination';
33+
34+
type TransactionsPaginatedListProps = {
35+
address: string;
36+
chain: Chain;
37+
txs: OtterscanTransaction[];
38+
receipts: OtterscanReceipt[];
39+
isLastPage: boolean;
40+
isFirstPage: boolean;
41+
currentPageIndex: number; // 1-indexed
42+
totalPages: number | null; // null if unknown
43+
totalTxs: number;
44+
};
45+
46+
const TransactionsPaginatedList: React.FC<TransactionsPaginatedListProps> = ({
47+
address,
48+
chain,
49+
txs,
50+
receipts,
51+
isLastPage,
52+
isFirstPage,
53+
currentPageIndex,
54+
totalPages,
55+
totalTxs,
56+
}) => {
57+
const [isDate, setIsDate] = useState<boolean>(false);
58+
const [isGasPrice, setIsGasPrice] = useState<boolean>(false);
59+
const [hoverId, setHoverId] = useState<string>('');
60+
const chainId = chain?.id ?? 0;
61+
const data = React.useMemo(() => {
62+
return mapToTransactionList(txs, receipts);
63+
}, [txs, receipts]);
64+
65+
const columnHelper = createColumnHelper<TransactionRow>();
66+
const [openToolTipIndex, setOpenTooltipIndex] = useState<number | null>();
67+
68+
const columns = [
69+
columnHelper.accessor('detail', {
70+
cell: (info: any) => (
71+
<AddressAdditionalInfo
72+
rowIndex={info.row.index}
73+
openToolTipIndex={openToolTipIndex!}
74+
setOpenTooltipIndex={setOpenTooltipIndex}
75+
chain={chain}
76+
txHash={info.row.getValue('hash')}
77+
method={info.row.getValue('method')}
78+
/>
79+
),
80+
header: () => <CircleHelp className="h-4 w-4" />,
81+
}),
82+
columnHelper.accessor('hash', {
83+
cell: (info: any) => <HashColumn info={info} chainId={chainId!} />,
84+
header: 'Transaction Hash',
85+
}),
86+
columnHelper.accessor('method', {
87+
cell: (info: any) => <MethodColumn info={info} />,
88+
header: () => <MethodHeader />,
89+
}),
90+
columnHelper.accessor('blockNumber', {
91+
cell: (info: any) => <BlockColumn info={info} />,
92+
header: 'Block',
93+
}),
94+
columnHelper.accessor('age', {
95+
cell: (info: any) => <AgeColumn info={info} isDate={isDate} />,
96+
header: () => <AgeHeader isDate={isDate} setIsDate={setIsDate} />,
97+
}),
98+
columnHelper.accessor('from', {
99+
cell: (info: any) => (
100+
<FromColumn
101+
info={info}
102+
hoverId={hoverId}
103+
setHoverId={setHoverId}
104+
address={address}
105+
chainId={chainId!}
106+
/>
107+
),
108+
header: 'From',
109+
}),
110+
columnHelper.accessor('to', {
111+
cell: (info: any) => (
112+
<ToColumn
113+
info={info}
114+
hoverId={hoverId}
115+
setHoverId={setHoverId}
116+
address={address}
117+
chainId={chainId!}
118+
/>
119+
),
120+
header: 'To',
121+
}),
122+
columnHelper.accessor('amount', {
123+
cell: (info: any) => (
124+
<AmountColumn info={info} symbol={chain?.nativeCurrency.symbol!} />
125+
),
126+
header: 'Amount',
127+
}),
128+
columnHelper.accessor('txnFee', {
129+
cell: (info: any) => <TxFeeColumn info={info} isGasPrice={isGasPrice} />,
130+
header: () => (
131+
<TxFeeHeader isGasPrice={isGasPrice} setIsGasPrice={setIsGasPrice} />
132+
),
133+
}),
134+
columnHelper.accessor('gasPrice', {
135+
enableHiding: true,
136+
cell: () => null,
137+
header: () => null,
138+
}),
139+
columnHelper.accessor('contractAddress', {
140+
enableHiding: true,
141+
cell: () => null,
142+
header: () => null,
143+
}),
144+
];
145+
146+
const table = useReactTable({
147+
columns,
148+
data,
149+
getCoreRowModel: getCoreRowModel(),
150+
});
151+
152+
const isMaxPage = currentPageIndex >= MAX_PAGE_SIZE;
153+
154+
return (
155+
<>
156+
<Card className="rounded-sm w-full">
157+
<CardHeader>
158+
{table.getRowModel().rows.length > 0 && (
159+
<>
160+
<CardTitle>
161+
<div className="flex sm:flex-row flex-col justify-between w-full sm:space-y-0 space-y-2">
162+
<div className="flex flex-wrap items-center space-x-1">
163+
<span className="text-sm">
164+
A total of {totalTxs.toLocaleString()} transactions found
165+
</span>
166+
</div>
167+
<div className="flex items-center gap-2">
168+
<DownloadListButton
169+
txs={txs}
170+
receipts={receipts}
171+
chain={chain}
172+
fileName={`export-${address}.csv`}
173+
/>
174+
<TransactionsPagination
175+
address={address}
176+
chainId={chainId}
177+
currentPageIndex={currentPageIndex}
178+
isLastPage={isLastPage}
179+
isFirstPage={isFirstPage}
180+
totalPages={totalPages}
181+
/>
182+
</div>
183+
</div>
184+
{!isLastPage && isMaxPage && (
185+
<Alert variant="info" className="mt-2">
186+
<AlertDescription>
187+
This is the maximum number of pages currently supported
188+
by this website.
189+
</AlertDescription>
190+
</Alert>
191+
)}
192+
</CardTitle>
193+
</>
194+
)}
195+
</CardHeader>
196+
<CardContent>
197+
<div className="w-full rounded-md border border-border overflow-x-auto">
198+
<AddressDataTable table={table} />
199+
</div>
200+
</CardContent>
201+
</Card>
202+
</>
203+
);
204+
};
205+
206+
export default TransactionsPaginatedList;

0 commit comments

Comments
 (0)