Skip to content

Commit c80eed3

Browse files
[Docs] Add search and pagination to wallet list (#7703)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 0c7051b commit c80eed3

File tree

1 file changed

+208
-25
lines changed

1 file changed

+208
-25
lines changed

apps/portal/src/components/others/AllSupportedWallets.tsx

Lines changed: 208 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1+
"use client";
2+
3+
import {
4+
QueryClient,
5+
QueryClientProvider,
6+
useQuery,
7+
} from "@tanstack/react-query";
8+
import { ChevronLeftIcon, ChevronRightIcon, SearchIcon } from "lucide-react";
19
import Image from "next/image";
10+
import { useMemo, useState } from "react";
211
import {
312
getAllWalletsList,
413
getWalletInfo,
514
type WalletId,
615
} from "thirdweb/wallets";
7-
import { DocLink, InlineCode } from "../Document";
16+
import { DocLink } from "../Document/DocLink";
17+
import { InlineCode } from "../Document/InlineCode";
818
import { Table, TBody, Td, Th, Tr } from "../Document/Table";
19+
import { Button } from "../ui/button";
20+
import { Input } from "../ui/input";
921

1022
const specialWallets: {
1123
[key in WalletId]?: boolean;
@@ -14,44 +26,215 @@ const specialWallets: {
1426
smart: true,
1527
};
1628

17-
export async function AllSupportedWallets() {
18-
const wallets = await getAllWalletsList();
29+
const ITEMS_PER_PAGE = 20;
30+
31+
const queryClient = new QueryClient();
32+
33+
export function AllSupportedWallets() {
34+
return (
35+
<QueryClientProvider client={queryClient}>
36+
<AllSupportedWalletsContent />
37+
</QueryClientProvider>
38+
);
39+
}
40+
41+
function AllSupportedWalletsContent() {
42+
const [searchQuery, setSearchQuery] = useState("");
43+
const [currentPage, setCurrentPage] = useState(1);
44+
45+
const { data: wallets, isLoading: loading } = useQuery({
46+
queryKey: ["allWalletsList"],
47+
queryFn: async () => {
48+
const allWallets = await getAllWalletsList();
49+
return allWallets
50+
.filter((w) => !(w.id in specialWallets))
51+
.map((w) => ({
52+
id: w.id,
53+
name: w.name,
54+
}));
55+
},
56+
staleTime: 1000 * 60 * 5, // 5 minutes
57+
});
58+
59+
const filteredWallets = useMemo(() => {
60+
if (!searchQuery) return wallets || [];
61+
if (!wallets) return [];
62+
63+
setCurrentPage(1);
64+
const query = searchQuery.toLowerCase();
65+
return wallets.filter(
66+
(wallet) =>
67+
wallet.name.toLowerCase().includes(query) ||
68+
wallet.id.toLowerCase().includes(query),
69+
);
70+
}, [wallets, searchQuery]);
71+
72+
const totalPages = Math.ceil(filteredWallets.length / ITEMS_PER_PAGE);
73+
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
74+
const endIndex = startIndex + ITEMS_PER_PAGE;
75+
const currentWallets = filteredWallets.slice(startIndex, endIndex);
76+
77+
const handlePreviousPage = () => {
78+
setCurrentPage((prev) => Math.max(prev - 1, 1));
79+
};
80+
81+
const handleNextPage = () => {
82+
setCurrentPage((prev) => Math.min(prev + 1, totalPages));
83+
};
84+
85+
const handlePageClick = (page: number) => {
86+
setCurrentPage(page);
87+
};
88+
89+
if (loading) {
90+
return (
91+
<div className="flex items-center justify-center py-8">
92+
<div className="text-muted-foreground">Loading wallets...</div>
93+
</div>
94+
);
95+
}
1996

2097
return (
21-
<Table>
22-
<TBody>
23-
<Tr>
24-
<Th> Wallet </Th>
25-
<Th> ID </Th>
26-
</Tr>
27-
28-
{wallets
29-
.filter((w) => !(w.id in specialWallets))
30-
.map((w) => {
31-
return (
32-
<Tr key={w.id}>
98+
<div className="space-y-6">
99+
{/* Search Input */}
100+
<div className="relative">
101+
<SearchIcon className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
102+
<Input
103+
type="text"
104+
placeholder="Search wallets by name or ID..."
105+
value={searchQuery}
106+
onChange={(e) => setSearchQuery(e.target.value)}
107+
className="pl-10"
108+
/>
109+
</div>
110+
111+
{/* Results count */}
112+
<div className="text-sm text-muted-foreground">
113+
{filteredWallets.length === wallets?.length
114+
? `Showing ${filteredWallets.length} wallets`
115+
: `Found ${filteredWallets.length} of ${wallets?.length} wallets`}
116+
</div>
117+
118+
{/* Table */}
119+
<Table>
120+
<TBody>
121+
<Tr>
122+
<Th>Wallet</Th>
123+
<Th>ID</Th>
124+
</Tr>
125+
126+
{currentWallets.length === 0 ? (
127+
<Tr>
128+
<Td>
129+
{searchQuery
130+
? "No wallets found matching your search."
131+
: "No wallets available."}
132+
</Td>
133+
</Tr>
134+
) : (
135+
currentWallets.map((wallet) => (
136+
<Tr key={wallet.id}>
33137
<Td>
34138
<DocLink
35139
className="flex flex-nowrap items-center gap-4 whitespace-nowrap"
36-
href={`/wallets/external-wallets/${w.id}`}
140+
href={`/wallets/external-wallets/${wallet.id}`}
37141
>
38-
<WalletImage id={w.id} />
39-
{w.name}
142+
<WalletImage id={wallet.id as WalletId} />
143+
{wallet.name}
40144
</DocLink>
41145
</Td>
42146
<Td>
43-
<InlineCode code={`"${w.id}"`} />
147+
<InlineCode code={`"${wallet.id}"`} />
44148
</Td>
45149
</Tr>
46-
);
47-
})}
48-
</TBody>
49-
</Table>
150+
))
151+
)}
152+
</TBody>
153+
</Table>
154+
155+
{/* Pagination */}
156+
{totalPages > 1 && (
157+
<div className="flex items-center justify-between">
158+
<div className="text-sm text-muted-foreground">
159+
Page {currentPage} of {totalPages}
160+
{filteredWallets.length > 0 && (
161+
<span className="ml-2">
162+
(showing {startIndex + 1}-
163+
{Math.min(endIndex, filteredWallets.length)} of{" "}
164+
{filteredWallets.length})
165+
</span>
166+
)}
167+
</div>
168+
169+
<div className="flex items-center space-x-2">
170+
<Button
171+
variant="outline"
172+
size="sm"
173+
onClick={handlePreviousPage}
174+
disabled={currentPage === 1}
175+
>
176+
<ChevronLeftIcon className="size-4" />
177+
Previous
178+
</Button>
179+
180+
{/* Page numbers */}
181+
<div className="flex items-center space-x-1">
182+
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
183+
let pageNumber: number;
184+
185+
if (totalPages <= 5) {
186+
pageNumber = i + 1;
187+
} else if (currentPage <= 3) {
188+
pageNumber = i + 1;
189+
} else if (currentPage >= totalPages - 2) {
190+
pageNumber = totalPages - 4 + i;
191+
} else {
192+
pageNumber = currentPage - 2 + i;
193+
}
194+
195+
return (
196+
<Button
197+
key={pageNumber}
198+
variant={currentPage === pageNumber ? "default" : "outline"}
199+
size="sm"
200+
onClick={() => handlePageClick(pageNumber)}
201+
className="min-w-[32px]"
202+
>
203+
{pageNumber}
204+
</Button>
205+
);
206+
})}
207+
</div>
208+
209+
<Button
210+
variant="outline"
211+
size="sm"
212+
onClick={handleNextPage}
213+
disabled={currentPage === totalPages}
214+
>
215+
Next
216+
<ChevronRightIcon className="size-4" />
217+
</Button>
218+
</div>
219+
</div>
220+
)}
221+
</div>
50222
);
51223
}
52224

53-
async function WalletImage(props: { id: WalletId }) {
54-
const img = await getWalletInfo(props.id, true);
225+
function WalletImage(props: { id: WalletId }) {
226+
const { data: img } = useQuery({
227+
queryKey: ["wallet-image", props.id],
228+
queryFn: () => getWalletInfo(props.id, true),
229+
staleTime: 1000 * 60 * 60 * 24, // 24 hours
230+
});
231+
232+
if (!img) {
233+
return (
234+
<div className="rounded-lg bg-muted" style={{ width: 44, height: 44 }} />
235+
);
236+
}
237+
55238
return (
56239
<Image alt="" className="rounded-lg" height={44} src={img} width={44} />
57240
);

0 commit comments

Comments
 (0)