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" ;
1
9
import Image from "next/image" ;
10
+ import { useMemo , useState } from "react" ;
2
11
import {
3
12
getAllWalletsList ,
4
13
getWalletInfo ,
5
14
type WalletId ,
6
15
} from "thirdweb/wallets" ;
7
- import { DocLink , InlineCode } from "../Document" ;
16
+ import { DocLink } from "../Document/DocLink" ;
17
+ import { InlineCode } from "../Document/InlineCode" ;
8
18
import { Table , TBody , Td , Th , Tr } from "../Document/Table" ;
19
+ import { Button } from "../ui/button" ;
20
+ import { Input } from "../ui/input" ;
9
21
10
22
const specialWallets : {
11
23
[ key in WalletId ] ?: boolean ;
@@ -14,44 +26,215 @@ const specialWallets: {
14
26
smart : true ,
15
27
} ;
16
28
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
+ }
19
96
20
97
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 } >
33
137
< Td >
34
138
< DocLink
35
139
className = "flex flex-nowrap items-center gap-4 whitespace-nowrap"
36
- href = { `/wallets/external-wallets/${ w . id } ` }
140
+ href = { `/wallets/external-wallets/${ wallet . id } ` }
37
141
>
38
- < WalletImage id = { w . id } />
39
- { w . name }
142
+ < WalletImage id = { wallet . id as WalletId } />
143
+ { wallet . name }
40
144
</ DocLink >
41
145
</ Td >
42
146
< Td >
43
- < InlineCode code = { `"${ w . id } "` } />
147
+ < InlineCode code = { `"${ wallet . id } "` } />
44
148
</ Td >
45
149
</ 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 >
50
222
) ;
51
223
}
52
224
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
+
55
238
return (
56
239
< Image alt = "" className = "rounded-lg" height = { 44 } src = { img } width = { 44 } />
57
240
) ;
0 commit comments