Skip to content

Commit 92fbb18

Browse files
committed
Move Deployed Contracts Page to App Router (#4995)
## Problem solved Short description of the bug fixed or feature added <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the contract deployment and management features in the dashboard application. It introduces new components, improves layouts, and refines data handling for better user experience. ### Detailed summary - Updated layout in `deploy.tsx` and `layout.tsx` to use flexbox for better responsiveness. - Added `getAuthTokenWalletAddress` function in `getAuthToken.ts` for wallet address retrieval. - Introduced `DeployedContractsPage` and `DeployedContractsTable` components for contract management. - Refactored `DeployedContracts` component to accept contract list directly and handle loading state. - Added `GetStartedWithContractsDeploy` component for user onboarding. - Enhanced `DeployedContractsPageHeader` with import and deploy contract buttons. - Improved sorting logic in `getSortedDeployedContracts.tsx`. - Removed unused imports and cleaned up code for better readability. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 6ceb24b commit 92fbb18

File tree

14 files changed

+265
-147
lines changed

14 files changed

+265
-147
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { redirect } from "next/navigation";
2+
import { getAuthTokenWalletAddress } from "../../../../api/lib/getAuthToken";
3+
import { DeployedContractsPage } from "../../../../team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsPage";
4+
5+
export default function Page() {
6+
const accountAddress = getAuthTokenWalletAddress();
7+
if (!accountAddress) {
8+
return redirect(
9+
`/login?next=${encodeURIComponent("/dashboard/contracts/deploy")}`,
10+
);
11+
}
12+
13+
return (
14+
<DeployedContractsPage
15+
address={accountAddress}
16+
className="flex grow flex-col"
17+
/>
18+
);
19+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { BillingAlerts } from "../../../../components/settings/Account/Billing/alerts/Alert";
2+
import { ContractsSidebarLayout } from "../../../../core-ui/sidebar/contracts";
3+
4+
export default function Layout(props: {
5+
children: React.ReactNode;
6+
}) {
7+
return (
8+
<ContractsSidebarLayout>
9+
<BillingAlerts className="pb-6" />
10+
{props.children}
11+
</ContractsSidebarLayout>
12+
);
13+
}

apps/dashboard/src/app/(dashboard)/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default function DashboardLayout(props: { children: React.ReactNode }) {
77
<ErrorProvider>
88
<div className="flex min-h-screen flex-col bg-background">
99
<DashboardHeader />
10-
<main className="grow">{props.children}</main>
10+
<div className="flex grow flex-col">{props.children}</div>
1111
<AppFooter />
1212
</div>
1313
</ErrorProvider>

apps/dashboard/src/app/api/lib/getAuthToken.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,19 @@ export function getAuthToken() {
1010

1111
return token;
1212
}
13+
14+
export function getAuthTokenWalletAddress() {
15+
const cookiesManager = cookies();
16+
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
17+
if (!activeAccount) {
18+
return null;
19+
}
20+
21+
const token = cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value;
22+
23+
if (token) {
24+
return activeAccount;
25+
}
26+
27+
return null;
28+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"use client";
2+
3+
import { DownloadIcon, PlusIcon } from "lucide-react";
4+
import Link from "next/link";
5+
import { useState } from "react";
6+
import { Button } from "../../../../../@/components/ui/button";
7+
import { ImportModal } from "../../../../../components/contract-components/import-contract/modal";
8+
9+
export function DeployedContractsPageHeader() {
10+
const [importModalOpen, setImportModalOpen] = useState(false);
11+
12+
return (
13+
<div>
14+
<ImportModal
15+
isOpen={importModalOpen}
16+
onClose={() => {
17+
setImportModalOpen(false);
18+
}}
19+
/>
20+
<div className="flex flex-col gap-4 md:pb-4 lg:flex-row lg:justify-between">
21+
<div>
22+
<h1 className="mb-1.5 font-semibold text-3xl tracking-tight lg:text-4xl">
23+
Your contracts
24+
</h1>
25+
<p className="text-muted-foreground text-sm">
26+
The list of contract instances that you have deployed or imported
27+
with thirdweb across all networks
28+
</p>
29+
</div>
30+
<div className="flex gap-2 [&>*]:grow">
31+
<Button
32+
className="gap-2"
33+
variant="outline"
34+
onClick={() => setImportModalOpen(true)}
35+
>
36+
<DownloadIcon className="size-4" />
37+
Import contract
38+
</Button>
39+
<Button asChild className="gap-2">
40+
<Link href="/explore">
41+
<PlusIcon className="size-4" />
42+
Deploy contract
43+
</Link>
44+
</Button>
45+
</div>
46+
</div>
47+
</div>
48+
);
49+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Spinner } from "@/components/ui/Spinner/Spinner";
2+
import { Suspense } from "react";
3+
import { ClientOnly } from "../../../../../../components/ClientOnly/ClientOnly";
4+
import { DeployedContractsPageHeader } from "../DeployedContractsPageHeader";
5+
import { DeployedContractsTable } from "./DeployedContractsTable";
6+
import { GetStartedWithContractsDeploy } from "./GetStartedWithContractsDeploy";
7+
import { getSortedDeployedContracts } from "./getSortedDeployedContracts";
8+
9+
export function DeployedContractsPage(props: {
10+
address: string;
11+
className?: string;
12+
}) {
13+
return (
14+
<div className={props.className}>
15+
<DeployedContractsPageHeader />
16+
<div className="h-6" />
17+
<Suspense fallback={<Loading />}>
18+
<DeployedContractsPageAsync {...props} />
19+
</Suspense>
20+
</div>
21+
);
22+
}
23+
24+
async function DeployedContractsPageAsync(props: {
25+
address: string;
26+
}) {
27+
const deployedContracts = await getSortedDeployedContracts({
28+
address: props.address,
29+
});
30+
31+
if (deployedContracts.length === 0) {
32+
return <GetStartedWithContractsDeploy />;
33+
}
34+
35+
return (
36+
<ClientOnly ssr={<Loading />}>
37+
<DeployedContractsTable contracts={deployedContracts} />
38+
</ClientOnly>
39+
);
40+
}
41+
42+
function Loading() {
43+
return (
44+
<div className="flex min-h-[300px] grow items-center justify-center rounded-lg border border-border">
45+
<Spinner className="size-10" />
46+
</div>
47+
);
48+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"use client";
2+
import { useDashboardRouter } from "@/lib/DashboardRouter";
3+
import type { BasicContract } from "contract-ui/types/types";
4+
import { DeployedContracts } from "../../../../../../components/contract-components/tables/deployed-contracts";
5+
6+
export function DeployedContractsTable(props: {
7+
contracts: BasicContract[];
8+
}) {
9+
const router = useDashboardRouter();
10+
return (
11+
<DeployedContracts
12+
contractList={props.contracts}
13+
limit={50}
14+
isPending={false}
15+
onContractRemoved={router.refresh}
16+
/>
17+
);
18+
}

apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/GetStartedWithContractsDeploy.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
11
"use client";
22
import { TabButtons } from "@/components/ui/tabs";
33
import { useDashboardRouter } from "@/lib/DashboardRouter";
4-
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
54
import Image from "next/image";
65
import { useMemo, useState } from "react";
7-
import { useActiveAccount } from "thirdweb/react";
86
import { ImportModal } from "../../../../../../components/contract-components/import-contract/modal";
97
import { StepsCard } from "../../../../../../components/dashboard/StepsCard";
108
import { useTrack } from "../../../../../../hooks/analytics/useTrack";
119

1210
export function GetStartedWithContractsDeploy() {
13-
const address = useActiveAccount()?.address;
1411
const steps = useMemo(
1512
() => [
16-
{
17-
title: "Connect your wallet to get started",
18-
description:
19-
"In order to interact with your contracts you need to connect an EVM compatible wallet.",
20-
children: <CustomConnectWallet />,
21-
completed: !!address,
22-
},
23-
2413
{
2514
title: "Build, deploy or import a contract",
2615
description:
@@ -29,7 +18,7 @@ export function GetStartedWithContractsDeploy() {
2918
completed: false, // because we only show this component if the user does not have any contracts
3019
},
3120
],
32-
[address],
21+
[],
3322
);
3423

3524
return (
@@ -139,7 +128,7 @@ const DeployOptions = () => {
139128
<h4 className="text-start font-semibold text-lg">
140129
{activeTabContent.title}
141130
</h4>
142-
<p className="text-muted-foreground text-sm">
131+
<p className="text-start text-muted-foreground text-sm">
143132
{activeTabContent.description}
144133
</p>
145134
</div>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { BasicContract } from "contract-ui/types/types";
2+
import { MULTICHAIN_REGISTRY_CONTRACT } from "../../../../../../constants/contracts";
3+
import { getAllMultichainRegistry } from "../../../../../../dashboard-extensions/common/read/getAllMultichainRegistry";
4+
import { fetchChain } from "../../../../../../utils/fetchChain";
5+
6+
export async function getSortedDeployedContracts(params: {
7+
address: string;
8+
}) {
9+
const contracts = await getAllMultichainRegistry({
10+
contract: MULTICHAIN_REGISTRY_CONTRACT,
11+
address: params.address,
12+
});
13+
14+
const chainIds = Array.from(new Set(contracts.map((c) => c.chainId)));
15+
const chains = (
16+
await Promise.allSettled(
17+
chainIds.map((chainId) => fetchChain(chainId.toString())),
18+
)
19+
)
20+
.filter((c) => c.status === "fulfilled")
21+
.map((c) => c.value)
22+
.filter((c) => c !== null);
23+
24+
const mainnetContracts: BasicContract[] = [];
25+
const testnetContracts: BasicContract[] = [];
26+
27+
for (const contract of contracts) {
28+
const chain = chains.find((chain) => contract.chainId === chain.chainId);
29+
if (chain && chain.status !== "deprecated") {
30+
if (chain.testnet) {
31+
testnetContracts.push(contract);
32+
} else {
33+
mainnetContracts.push(contract);
34+
}
35+
}
36+
}
37+
38+
mainnetContracts.sort((a, b) => a.chainId - b.chainId);
39+
testnetContracts.sort((a, b) => a.chainId - b.chainId);
40+
41+
return [...mainnetContracts, ...testnetContracts];
42+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export default function Layout(props: {
22
children: React.ReactNode;
33
}) {
4-
return <div className="container">{props.children}</div>;
4+
return <div className="container flex grow flex-col">{props.children}</div>;
55
}

0 commit comments

Comments
 (0)