Skip to content

Dashboard: Migrate contract/accounts page from chakra to tailwind #7739

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,51 +1,47 @@
"use client";

import { ButtonGroup, Flex } from "@chakra-ui/react";
import { LinkButton } from "chakra/button";
import { Heading } from "chakra/heading";
import type { ThirdwebContract } from "thirdweb";
import { UnderlineLink } from "@/components/ui/UnderlineLink";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { AccountsCount } from "./components/accounts-count";
import { AccountsTable } from "./components/accounts-table";
import { CreateAccountButton } from "./components/create-account-button";

interface AccountsPageProps {
export function AccountsPage(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
projectMeta: ProjectMeta | undefined;
}

export const AccountsPage: React.FC<AccountsPageProps> = ({
contract,
isLoggedIn,
projectMeta,
}) => {
}) {
return (
<Flex direction="column" gap={6}>
<Flex
align={{ base: "left", md: "center" }}
direction={{ base: "column", md: "row" }}
gap={4}
justify="space-between"
>
<Heading size="title.sm">Accounts</Heading>
<ButtonGroup
flexDirection={{ base: "column", md: "row" }}
gap={2}
w="inherit"
>
<LinkButton
href="https://portal.thirdweb.com/wallets/smart-wallet/get-started#3-connect-smart-wallets-in-your-application"
isExternal
variant="solid"
>
View Documentation
</LinkButton>
<CreateAccountButton contract={contract} isLoggedIn={isLoggedIn} />
</ButtonGroup>
</Flex>
<AccountsCount contract={contract} />
<AccountsTable contract={contract} projectMeta={projectMeta} />
</Flex>
<div>
<div className="flex flex-col md:justify-between gap-4 md:flex-row md:items-center">
<div>
<h1 className="text-2xl font-semibold tracking-tight mb-1">
Accounts
</h1>
<p className="text-sm text-muted-foreground">
View list of smart accounts that have been created for this
contract.{" "}
<UnderlineLink
href="https://portal.thirdweb.com/transactions/sponsor"
target="_blank"
rel="noopener noreferrer"
>
Learn more about gas sponsorship
</UnderlineLink>
</p>
</div>
<CreateAccountButton
contract={props.contract}
isLoggedIn={props.isLoggedIn}
/>
</div>

<div className="h-5" />

<AccountsTable
contract={props.contract}
projectMeta={props.projectMeta}
/>
</div>
);
};
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,58 +1,38 @@
"use client";

import { Flex, IconButton, Select, Skeleton } from "@chakra-ui/react";
import { createColumnHelper } from "@tanstack/react-table";
import { Legacy_CopyButton } from "chakra/button";
import { Text } from "chakra/text";
import {
ChevronFirstIcon,
ChevronLastIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react";
import { ArrowUpRightIcon } from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";
import type { ThirdwebContract } from "thirdweb";
import { getAccounts, totalAccounts } from "thirdweb/extensions/erc4337";
import { useReadContract } from "thirdweb/react";
import { TWTable } from "@/components/blocks/TWTable";
import { PaginationButtons } from "@/components/blocks/pagination-buttons";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useChainSlug } from "@/hooks/chains/chainSlug";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../../_utils/contract-page-path";

const columnHelper = createColumnHelper<{ account: string }>();

const columns = [
columnHelper.accessor("account", {
cell: (info) => (
<Flex align="center" gap={2}>
<Text fontFamily="mono">{info.getValue()}</Text>
<Legacy_CopyButton
aria-label="Copy account address"
colorScheme="primary"
value={info.getValue()}
/>
</Flex>
),
header: "Account",
}),
];
const pageSize = 10;

type AccountsTableProps = {
contract: ThirdwebContract;
projectMeta: ProjectMeta | undefined;
};

export const AccountsTable: React.FC<AccountsTableProps> = ({
contract,
projectMeta,
}) => {
const router = useDashboardRouter();
export function AccountsTable({ contract, projectMeta }: AccountsTableProps) {
const chainSlug = useChainSlug(contract.chain.id);

const [currentPage, setCurrentPage] = useState(0);
// default page size of 25
const [pageSize, setPageSize] = useState(25);

const totalAccountsQuery = useReadContract(totalAccounts, { contract });

Expand All @@ -72,7 +52,7 @@ export const AccountsTable: React.FC<AccountsTableProps> = ({
);
}
return currentPage * pageSize + pageSize;
}, [currentPage, pageSize, totalAccountsQuery.data]);
}, [currentPage, totalAccountsQuery.data]);

const accountsQuery = useReadContract(getAccounts, {
contract,
Expand All @@ -82,105 +62,91 @@ export const AccountsTable: React.FC<AccountsTableProps> = ({
});

const totalPages = Math.ceil(totalAccountsNum / pageSize);

const canNextPage = currentPage < totalPages - 1;
const canPreviousPage = currentPage > 0;
const showPagination = totalPages > 1;

const data = accountsQuery.data || [];

return (
<Flex direction="column" gap={4}>
<div className="border rounded-lg overflow-hidden">
{/* TODO add a skeleton when loading*/}
<TWTable
columns={columns}
data={data.map((account) => ({ account }))}
isFetched={accountsQuery.isFetched}
isPending={accountsQuery.isPending}
onRowClick={(row) => {
const accountContractPagePath = buildContractPagePath({
chainIdOrSlug: chainSlug.toString(),
contractAddress: row.account,
projectMeta,
});

router.push(accountContractPagePath);
}}
title="account"
/>
{/* pagination */}
<div className="flex w-full items-center justify-center">
<Flex align="center" direction="row" gap={2}>
<IconButton
aria-label="first page"
icon={<ChevronFirstIcon className="size-4" />}
isDisabled={totalAccountsQuery.isPending}
onClick={() => setCurrentPage(0)}
/>
<IconButton
aria-label="previous page"
icon={<ChevronLeftIcon className="size-4" />}
isDisabled={totalAccountsQuery.isPending || !canPreviousPage}
onClick={() => {
setCurrentPage((curr) => {
if (curr > 0) {
return curr - 1;
}
return curr;
});
}}
/>
<Text whiteSpace="nowrap">
Page <strong>{currentPage + 1}</strong> of{" "}
<Skeleton
as="span"
display="inline"
isLoaded={totalAccountsQuery.isSuccess}
>
<strong>{totalPages}</strong>
</Skeleton>
</Text>
<IconButton
aria-label="next page"
icon={<ChevronRightIcon className="size-4" />}
isDisabled={totalAccountsQuery.isPending || !canNextPage}
onClick={() =>
setCurrentPage((curr) => {
if (curr < totalPages - 1) {
return curr + 1;
}
return curr;
})
}
/>
<IconButton
aria-label="last page"
icon={<ChevronLastIcon className="size-4" />}
isDisabled={totalAccountsQuery.isPending || !canNextPage}
onClick={() => setCurrentPage(totalPages - 1)}
<TableContainer className="border-0 rounded-none">
<Table>
<TableHeader>
<TableRow>
<TableHead>Account</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{accountsQuery.isPending &&
new Array(pageSize).fill(0).map((_, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: ok
<SkeletonRow key={index} />
))}

{!accountsQuery.isPending &&
data.map((account) => {
const accountContractPagePath = buildContractPagePath({
chainIdOrSlug: chainSlug.toString(),
contractAddress: account,
projectMeta,
});

return (
<TableRow linkBox key={account} className="hover:bg-muted/50">
<TableCell className="py-6">
<Link
href={accountContractPagePath}
target="_blank"
rel="noopener noreferrer"
className="before:absolute before:inset-0"
aria-label="View account"
/>
Comment on lines +95 to +103
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Review accessibility and UX of the linkBox pattern.

The current implementation uses a before:absolute before:inset-0 pseudo-element to make the entire row clickable, but this can create accessibility issues. The linkBox className and pseudo-element approach may interfere with screen readers and keyboard navigation.

Consider using a more accessible approach:

- <TableRow linkBox key={account} className="hover:bg-muted/50">
-   <TableCell className="py-6">
-     <Link
-       href={accountContractPagePath}
-       target="_blank"
-       rel="noopener noreferrer"
-       className="before:absolute before:inset-0"
-       aria-label="View account"
-     />
+ <TableRow key={account} className="hover:bg-muted/50">
+   <TableCell className="py-6">
+     <div className="flex items-center justify-between gap-2">
+       <Link
+         href={accountContractPagePath}
+         target="_blank"
+         rel="noopener noreferrer"
+         aria-label={`View account ${account}`}
+         className="flex-1 min-w-0"
+       >
+         <CopyTextButton
+           textToShow={account}
+           textToCopy={account}
+           variant="ghost"
+           tooltip="Copy account address"
+           copyIconPosition="right"
+         />
+       </Link>
+       <ArrowUpRightIcon className="size-4 text-muted-foreground flex-shrink-0" />
+     </div>

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/accounts/components/accounts-table.tsx
around lines 95 to 103, the current use of the linkBox pattern with a
before:absolute before:inset-0 pseudo-element to make the entire TableRow
clickable can cause accessibility issues for screen readers and keyboard users.
To fix this, remove the pseudo-element approach and instead wrap the entire
content of the TableRow in a semantic interactive element like a Link or button,
ensuring proper focus management and keyboard navigation support. Also, avoid
using empty Link components and ensure all interactive elements have accessible
labels and roles.


<div className="flex items-center justify-between gap-2">
<CopyTextButton
textToShow={account}
textToCopy={account}
variant="ghost"
tooltip="Copy account address"
copyIconPosition="right"
className="z-10 relative"
/>

<ArrowUpRightIcon className="size-4 text-muted-foreground" />
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>

{!accountsQuery.isPending && data.length === 0 && (
<div className="px-4 text-center py-20 flex items-center justify-center text-muted-foreground">
No accounts
</div>
)}
</TableContainer>

{showPagination && (
<div className="py-4 border-t bg-card">
<PaginationButtons
activePage={currentPage + 1}
totalPages={totalPages}
onPageClick={(page) => setCurrentPage(page - 1)}
/>
</div>
)}
</div>
);
}

<Select
isDisabled={totalAccountsQuery.isPending}
onChange={(e) => {
const newPageSize = Number.parseInt(e.target.value as string, 10);
// compute the new page number based on the new page size
const newPage = Math.floor(
(currentPage * pageSize) / newPageSize,
);
setCurrentPage(newPage);
setPageSize(newPageSize);
}}
value={pageSize}
>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="250">250</option>
<option value="500">500</option>
</Select>
</Flex>
</div>
</Flex>
function SkeletonRow() {
return (
<TableRow>
<TableCell className="py-6">
<Skeleton className="h-6 w-[364px]" />
</TableCell>
</TableRow>
);
};
//
}
Loading
Loading