-
Notifications
You must be signed in to change notification settings - Fork 619
[Docs] Add search and pagination to wallet list #7703
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,11 +1,23 @@ | ||||||||||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||
| QueryClient, | ||||||||||||||||||||||||||||||||||
| QueryClientProvider, | ||||||||||||||||||||||||||||||||||
| useQuery, | ||||||||||||||||||||||||||||||||||
| } from "@tanstack/react-query"; | ||||||||||||||||||||||||||||||||||
| import { ChevronLeftIcon, ChevronRightIcon, SearchIcon } from "lucide-react"; | ||||||||||||||||||||||||||||||||||
| import Image from "next/image"; | ||||||||||||||||||||||||||||||||||
| import { useMemo, useState } from "react"; | ||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||
| getAllWalletsList, | ||||||||||||||||||||||||||||||||||
| getWalletInfo, | ||||||||||||||||||||||||||||||||||
| type WalletId, | ||||||||||||||||||||||||||||||||||
| } from "thirdweb/wallets"; | ||||||||||||||||||||||||||||||||||
| import { DocLink, InlineCode } from "../Document"; | ||||||||||||||||||||||||||||||||||
| import { DocLink } from "../Document/DocLink"; | ||||||||||||||||||||||||||||||||||
| import { InlineCode } from "../Document/InlineCode"; | ||||||||||||||||||||||||||||||||||
| import { Table, TBody, Td, Th, Tr } from "../Document/Table"; | ||||||||||||||||||||||||||||||||||
| import { Button } from "../ui/button"; | ||||||||||||||||||||||||||||||||||
| import { Input } from "../ui/input"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const specialWallets: { | ||||||||||||||||||||||||||||||||||
| [key in WalletId]?: boolean; | ||||||||||||||||||||||||||||||||||
|
|
@@ -14,44 +26,215 @@ const specialWallets: { | |||||||||||||||||||||||||||||||||
| smart: true, | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export async function AllSupportedWallets() { | ||||||||||||||||||||||||||||||||||
| const wallets = await getAllWalletsList(); | ||||||||||||||||||||||||||||||||||
| const ITEMS_PER_PAGE = 20; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const queryClient = new QueryClient(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export function AllSupportedWallets() { | ||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||
| <QueryClientProvider client={queryClient}> | ||||||||||||||||||||||||||||||||||
| <AllSupportedWalletsContent /> | ||||||||||||||||||||||||||||||||||
| </QueryClientProvider> | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+33
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add explicit return type to the component function. Per the coding guidelines, TypeScript functions should have explicit return types. -export function AllSupportedWallets() {
+export function AllSupportedWallets(): JSX.Element {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| function AllSupportedWalletsContent() { | ||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add explicit return type to the component function. -function AllSupportedWalletsContent() {
+function AllSupportedWalletsContent(): JSX.Element {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| const [searchQuery, setSearchQuery] = useState(""); | ||||||||||||||||||||||||||||||||||
| const [currentPage, setCurrentPage] = useState(1); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const { data: wallets, isLoading: loading } = useQuery({ | ||||||||||||||||||||||||||||||||||
| queryKey: ["allWalletsList"], | ||||||||||||||||||||||||||||||||||
| queryFn: async () => { | ||||||||||||||||||||||||||||||||||
| const allWallets = await getAllWalletsList(); | ||||||||||||||||||||||||||||||||||
| return allWallets | ||||||||||||||||||||||||||||||||||
| .filter((w) => !(w.id in specialWallets)) | ||||||||||||||||||||||||||||||||||
| .map((w) => ({ | ||||||||||||||||||||||||||||||||||
| id: w.id, | ||||||||||||||||||||||||||||||||||
| name: w.name, | ||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| staleTime: 1000 * 60 * 5, // 5 minutes | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const filteredWallets = useMemo(() => { | ||||||||||||||||||||||||||||||||||
| if (!searchQuery) return wallets || []; | ||||||||||||||||||||||||||||||||||
| if (!wallets) return []; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| setCurrentPage(1); | ||||||||||||||||||||||||||||||||||
| const query = searchQuery.toLowerCase(); | ||||||||||||||||||||||||||||||||||
| return wallets.filter( | ||||||||||||||||||||||||||||||||||
| (wallet) => | ||||||||||||||||||||||||||||||||||
| wallet.name.toLowerCase().includes(query) || | ||||||||||||||||||||||||||||||||||
| wallet.id.toLowerCase().includes(query), | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| }, [wallets, searchQuery]); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
Comment on lines
+59
to
+71
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove side effect from useMemo and fix inefficient re-renders. The Apply this diff to fix the issue: + // Reset page when search query changes
+ useEffect(() => {
+ setCurrentPage(1);
+ }, [searchQuery]);
+
const filteredWallets = useMemo(() => {
if (!searchQuery) return wallets || [];
if (!wallets) return [];
- setCurrentPage(1);
const query = searchQuery.toLowerCase();
return wallets.filter(
(wallet) =>
wallet.name.toLowerCase().includes(query) ||
wallet.id.toLowerCase().includes(query),
);
- }, [wallets, searchQuery]);
+ }, [wallets, searchQuery]);Don't forget to import -import { useMemo, useState } from "react";
+import { useEffect, useMemo, useState } from "react";🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| const totalPages = Math.ceil(filteredWallets.length / ITEMS_PER_PAGE); | ||||||||||||||||||||||||||||||||||
| const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; | ||||||||||||||||||||||||||||||||||
| const endIndex = startIndex + ITEMS_PER_PAGE; | ||||||||||||||||||||||||||||||||||
| const currentWallets = filteredWallets.slice(startIndex, endIndex); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const handlePreviousPage = () => { | ||||||||||||||||||||||||||||||||||
| setCurrentPage((prev) => Math.max(prev - 1, 1)); | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const handleNextPage = () => { | ||||||||||||||||||||||||||||||||||
| setCurrentPage((prev) => Math.min(prev + 1, totalPages)); | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const handlePageClick = (page: number) => { | ||||||||||||||||||||||||||||||||||
| setCurrentPage(page); | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (loading) { | ||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||
| <div className="flex items-center justify-center py-8"> | ||||||||||||||||||||||||||||||||||
| <div className="text-muted-foreground">Loading wallets...</div> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||
| <Table> | ||||||||||||||||||||||||||||||||||
| <TBody> | ||||||||||||||||||||||||||||||||||
| <Tr> | ||||||||||||||||||||||||||||||||||
| <Th> Wallet </Th> | ||||||||||||||||||||||||||||||||||
| <Th> ID </Th> | ||||||||||||||||||||||||||||||||||
| </Tr> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| {wallets | ||||||||||||||||||||||||||||||||||
| .filter((w) => !(w.id in specialWallets)) | ||||||||||||||||||||||||||||||||||
| .map((w) => { | ||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||
| <Tr key={w.id}> | ||||||||||||||||||||||||||||||||||
| <div className="space-y-6"> | ||||||||||||||||||||||||||||||||||
| {/* Search Input */} | ||||||||||||||||||||||||||||||||||
| <div className="relative"> | ||||||||||||||||||||||||||||||||||
| <SearchIcon className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" /> | ||||||||||||||||||||||||||||||||||
| <Input | ||||||||||||||||||||||||||||||||||
| type="text" | ||||||||||||||||||||||||||||||||||
| placeholder="Search wallets by name or ID..." | ||||||||||||||||||||||||||||||||||
| value={searchQuery} | ||||||||||||||||||||||||||||||||||
| onChange={(e) => setSearchQuery(e.target.value)} | ||||||||||||||||||||||||||||||||||
| className="pl-10" | ||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| {/* Results count */} | ||||||||||||||||||||||||||||||||||
| <div className="text-sm text-muted-foreground"> | ||||||||||||||||||||||||||||||||||
| {filteredWallets.length === wallets?.length | ||||||||||||||||||||||||||||||||||
| ? `Showing ${filteredWallets.length} wallets` | ||||||||||||||||||||||||||||||||||
| : `Found ${filteredWallets.length} of ${wallets?.length} wallets`} | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| {/* Table */} | ||||||||||||||||||||||||||||||||||
| <Table> | ||||||||||||||||||||||||||||||||||
| <TBody> | ||||||||||||||||||||||||||||||||||
| <Tr> | ||||||||||||||||||||||||||||||||||
| <Th>Wallet</Th> | ||||||||||||||||||||||||||||||||||
| <Th>ID</Th> | ||||||||||||||||||||||||||||||||||
| </Tr> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| {currentWallets.length === 0 ? ( | ||||||||||||||||||||||||||||||||||
| <Tr> | ||||||||||||||||||||||||||||||||||
| <Td> | ||||||||||||||||||||||||||||||||||
| {searchQuery | ||||||||||||||||||||||||||||||||||
| ? "No wallets found matching your search." | ||||||||||||||||||||||||||||||||||
| : "No wallets available."} | ||||||||||||||||||||||||||||||||||
| </Td> | ||||||||||||||||||||||||||||||||||
| </Tr> | ||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||
| currentWallets.map((wallet) => ( | ||||||||||||||||||||||||||||||||||
| <Tr key={wallet.id}> | ||||||||||||||||||||||||||||||||||
| <Td> | ||||||||||||||||||||||||||||||||||
| <DocLink | ||||||||||||||||||||||||||||||||||
| className="flex flex-nowrap items-center gap-4 whitespace-nowrap" | ||||||||||||||||||||||||||||||||||
| href={`/wallets/external-wallets/${w.id}`} | ||||||||||||||||||||||||||||||||||
| href={`/wallets/external-wallets/${wallet.id}`} | ||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||
| <WalletImage id={w.id} /> | ||||||||||||||||||||||||||||||||||
| {w.name} | ||||||||||||||||||||||||||||||||||
| <WalletImage id={wallet.id as WalletId} /> | ||||||||||||||||||||||||||||||||||
| {wallet.name} | ||||||||||||||||||||||||||||||||||
| </DocLink> | ||||||||||||||||||||||||||||||||||
| </Td> | ||||||||||||||||||||||||||||||||||
| <Td> | ||||||||||||||||||||||||||||||||||
| <InlineCode code={`"${w.id}"`} /> | ||||||||||||||||||||||||||||||||||
| <InlineCode code={`"${wallet.id}"`} /> | ||||||||||||||||||||||||||||||||||
| </Td> | ||||||||||||||||||||||||||||||||||
| </Tr> | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||||||||||
| </TBody> | ||||||||||||||||||||||||||||||||||
| </Table> | ||||||||||||||||||||||||||||||||||
| )) | ||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||
| </TBody> | ||||||||||||||||||||||||||||||||||
| </Table> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| {/* Pagination */} | ||||||||||||||||||||||||||||||||||
| {totalPages > 1 && ( | ||||||||||||||||||||||||||||||||||
| <div className="flex items-center justify-between"> | ||||||||||||||||||||||||||||||||||
| <div className="text-sm text-muted-foreground"> | ||||||||||||||||||||||||||||||||||
| Page {currentPage} of {totalPages} | ||||||||||||||||||||||||||||||||||
| {filteredWallets.length > 0 && ( | ||||||||||||||||||||||||||||||||||
| <span className="ml-2"> | ||||||||||||||||||||||||||||||||||
| (showing {startIndex + 1}- | ||||||||||||||||||||||||||||||||||
| {Math.min(endIndex, filteredWallets.length)} of{" "} | ||||||||||||||||||||||||||||||||||
| {filteredWallets.length}) | ||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| <div className="flex items-center space-x-2"> | ||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||
| variant="outline" | ||||||||||||||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||||||||||||||
| onClick={handlePreviousPage} | ||||||||||||||||||||||||||||||||||
| disabled={currentPage === 1} | ||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||
| <ChevronLeftIcon className="size-4" /> | ||||||||||||||||||||||||||||||||||
| Previous | ||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| {/* Page numbers */} | ||||||||||||||||||||||||||||||||||
| <div className="flex items-center space-x-1"> | ||||||||||||||||||||||||||||||||||
| {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { | ||||||||||||||||||||||||||||||||||
| let pageNumber: number; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (totalPages <= 5) { | ||||||||||||||||||||||||||||||||||
| pageNumber = i + 1; | ||||||||||||||||||||||||||||||||||
| } else if (currentPage <= 3) { | ||||||||||||||||||||||||||||||||||
| pageNumber = i + 1; | ||||||||||||||||||||||||||||||||||
| } else if (currentPage >= totalPages - 2) { | ||||||||||||||||||||||||||||||||||
| pageNumber = totalPages - 4 + i; | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| pageNumber = currentPage - 2 + i; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||
| key={pageNumber} | ||||||||||||||||||||||||||||||||||
| variant={currentPage === pageNumber ? "default" : "outline"} | ||||||||||||||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||||||||||||||
| onClick={() => handlePageClick(pageNumber)} | ||||||||||||||||||||||||||||||||||
| className="min-w-[32px]" | ||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||
| {pageNumber} | ||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||
| variant="outline" | ||||||||||||||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||||||||||||||
| onClick={handleNextPage} | ||||||||||||||||||||||||||||||||||
| disabled={currentPage === totalPages} | ||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||
| Next | ||||||||||||||||||||||||||||||||||
| <ChevronRightIcon className="size-4" /> | ||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| async function WalletImage(props: { id: WalletId }) { | ||||||||||||||||||||||||||||||||||
| const img = await getWalletInfo(props.id, true); | ||||||||||||||||||||||||||||||||||
| function WalletImage(props: { id: WalletId }) { | ||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add explicit return type and consider adding error handling. -function WalletImage(props: { id: WalletId }) {
+function WalletImage(props: { id: WalletId }): JSX.Element {Also consider handling the error state from useQuery: - const { data: img } = useQuery({
+ const { data: img, error } = useQuery({
queryKey: ["wallet-image", props.id],
queryFn: () => getWalletInfo(props.id, true),
staleTime: 1000 * 60 * 60 * 24, // 24 hours
});
- if (!img) {
+ if (!img || error) {
return (
<div className="rounded-lg bg-muted" style={{ width: 44, height: 44 }} />
);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| const { data: img } = useQuery({ | ||||||||||||||||||||||||||||||||||
| queryKey: ["wallet-image", props.id], | ||||||||||||||||||||||||||||||||||
| queryFn: () => getWalletInfo(props.id, true), | ||||||||||||||||||||||||||||||||||
| staleTime: 1000 * 60 * 60 * 24, // 24 hours | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (!img) { | ||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||
| <div className="rounded-lg bg-muted" style={{ width: 44, height: 44 }} /> | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||
| <Image alt="" className="rounded-lg" height={44} src={img} width={44} /> | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move QueryClient instantiation inside the component to avoid SSR issues.
Creating QueryClient at the module level can cause hydration mismatches and memory leaks in SSR environments. Initialize it inside the component or use a factory function.
Apply this diff:
Also add the missing import:
🤖 Prompt for AI Agents