diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/NFTDetails.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/NFTDetails.tsx
index 4ee134af757..153d1885318 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/NFTDetails.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/NFTDetails.tsx
@@ -7,13 +7,16 @@ import type { ThirdwebContract } from "thirdweb";
import * as ERC721 from "thirdweb/extensions/erc721";
import * as ERC1155 from "thirdweb/extensions/erc1155";
import { useReadContract } from "thirdweb/react";
+import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { NFTCards } from "../../_components/NFTCards";
+import { buildContractPagePath } from "../../_utils/contract-page-path";
type NFTDetailsProps = {
contract: ThirdwebContract;
trackingCategory: string;
isErc721: boolean;
chainSlug: string;
+ projectMeta: ProjectMeta | undefined;
};
export function NFTDetails({
@@ -21,8 +24,14 @@ export function NFTDetails({
trackingCategory,
isErc721,
chainSlug,
+ projectMeta,
}: NFTDetailsProps) {
- const nftsHref = `/${chainSlug}/${contract.address}/nfts`;
+ const nftsHref = buildContractPagePath({
+ projectMeta,
+ chainIdOrSlug: chainSlug,
+ contractAddress: contract.address,
+ subpath: "/nfts",
+ });
const nftQuery = useReadContract(
isErc721 ? ERC721.getNFTs : ERC1155.getNFTs,
@@ -58,6 +67,7 @@ export function NFTDetails({
{/* cards */}
({
...t,
contractAddress: contract.address,
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/PermissionsTable.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/PermissionsTable.tsx
index a45e97cb2d3..a55ce866362 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/PermissionsTable.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/PermissionsTable.tsx
@@ -19,12 +19,22 @@ import { ArrowRightIcon } from "lucide-react";
import { useMemo } from "react";
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
import { useReadContract } from "thirdweb/react";
+import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
+import { buildContractPagePath } from "../../_utils/contract-page-path";
export function PermissionsTable(props: {
contract: ThirdwebContract;
trackingCategory: string;
chainSlug: string;
+ projectMeta: ProjectMeta | undefined;
}) {
+ const permissionsHref = buildContractPagePath({
+ projectMeta: props.projectMeta,
+ chainIdOrSlug: props.chainSlug,
+ contractAddress: props.contract.address,
+ subpath: "/permissions",
+ });
+
const allRoleMembers = useReadContract(getAllRoleMembers, {
contract: props.contract,
});
@@ -53,7 +63,7 @@ export function PermissionsTable(props: {
);
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/contract-overview-page.client.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/contract-overview-page.client.tsx
index ad4fe37fbbe..4e0a626de01 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/contract-overview-page.client.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/contract-overview-page.client.tsx
@@ -1,6 +1,7 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
+import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractOverviewPage } from "./ContractOverviewPage";
@@ -8,8 +9,9 @@ import { ContractOverviewPage } from "./ContractOverviewPage";
export function ContractOverviewPageClient(props: {
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
+ projectMeta: ProjectMeta | undefined;
}) {
- const { contract, chainMetadata } = props;
+ const { contract, chainMetadata, projectMeta } = props;
const metadataQuery = useContractPageMetadata(contract);
if (metadataQuery.isPending) {
@@ -25,6 +27,7 @@ export function ContractOverviewPageClient(props: {
return (
;
+ params: Promise;
}) {
const params = await props.params;
- const info = await getContractPageParamsInfo(params);
-
- if (!info) {
- notFound();
- }
-
- const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
- info;
- if (isLocalhostChain) {
- return (
-
- );
- }
-
- const contractPageMetadata = await getContractPageMetadata(serverContract);
-
return (
-
-
-
- }
+
);
}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/ContractPermissionsPage.client.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/ContractPermissionsPage.client.tsx
index 11feb7a7b9d..d8de49694b0 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/ContractPermissionsPage.client.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/ContractPermissionsPage.client.tsx
@@ -2,6 +2,7 @@
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
+import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractPermissionsPage } from "./ContractPermissionsPage";
@@ -10,6 +11,7 @@ export function ContractPermissionsPageClient(props: {
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
isLoggedIn: boolean;
+ projectMeta: ProjectMeta | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
@@ -23,6 +25,7 @@ export function ContractPermissionsPageClient(props: {
return (
= ({ contract, detectedPermissionEnumerable, chainSlug, isLoggedIn }) => {
- useIsomorphicLayoutEffect(() => {
- window?.scrollTo({ top: 0, behavior: "smooth" });
- }, []);
-
- const explorerHref = `/${chainSlug}/${contract.address}/explorer`;
+> = ({
+ contract,
+ detectedPermissionEnumerable,
+ chainSlug,
+ isLoggedIn,
+ projectMeta,
+}) => {
+ const explorerHref = buildContractPagePath({
+ projectMeta,
+ chainIdOrSlug: chainSlug,
+ contractAddress: contract.address,
+ subpath: "/explorer",
+ });
if (!detectedPermissionEnumerable) {
return (
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/page.tsx
index e170fc110f6..558c38b9704 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/page.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/page.tsx
@@ -1,44 +1,16 @@
-import { notFound } from "next/navigation";
import { getRawAccount } from "../../../../../account/settings/getAccount";
-import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
-import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
-import { ContractPermissionsPage } from "./ContractPermissionsPage";
-import { ContractPermissionsPageClient } from "./ContractPermissionsPage.client";
+import type { PublicContractPageParams } from "../types";
+import { SharedPermissionsPage } from "./shared-permissions-page";
export default async function Page(props: {
- params: Promise<{
- contractAddress: string;
- chain_id: string;
- }>;
+ params: Promise;
}) {
- const params = await props.params;
- const info = await getContractPageParamsInfo(params);
-
- if (!info) {
- notFound();
- }
-
- const account = await getRawAccount();
-
- const { clientContract, serverContract, isLocalhostChain } = info;
- if (isLocalhostChain) {
- return (
-
- );
- }
-
- const { isPermissionsEnumerableSupported } =
- await getContractPageMetadata(serverContract);
-
+ const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
-
);
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/shared-permissions-page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/shared-permissions-page.tsx
new file mode 100644
index 00000000000..877506d5207
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/shared-permissions-page.tsx
@@ -0,0 +1,48 @@
+import { notFound } from "next/navigation";
+import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
+import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
+import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
+import { ContractPermissionsPage } from "./ContractPermissionsPage";
+import { ContractPermissionsPageClient } from "./ContractPermissionsPage.client";
+
+export async function SharedPermissionsPage(props: {
+ contractAddress: string;
+ chainIdOrSlug: string;
+ projectMeta: ProjectMeta | undefined;
+ isLoggedIn: boolean;
+}) {
+ const info = await getContractPageParamsInfo({
+ contractAddress: props.contractAddress,
+ chainIdOrSlug: props.chainIdOrSlug,
+ teamId: props.projectMeta?.teamId,
+ });
+
+ if (!info) {
+ notFound();
+ }
+
+ const { clientContract, serverContract, isLocalhostChain } = info;
+ if (isLocalhostChain) {
+ return (
+
+ );
+ }
+
+ const { isPermissionsEnumerableSupported } =
+ await getContractPageMetadata(serverContract);
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/proposals/ContractProposalsPage.client.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/proposals/ContractProposalsPage.client.tsx
index 95dae159ff8..7b250fb18f8 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/proposals/ContractProposalsPage.client.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/proposals/ContractProposalsPage.client.tsx
@@ -1,6 +1,7 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
+import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
@@ -9,6 +10,7 @@ import { ContractProposalsPage } from "./ContractProposalsPage";
export function ContractProposalsPageClient(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
+ projectMeta: ProjectMeta | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
@@ -21,7 +23,12 @@ export function ContractProposalsPageClient(props: {
}
if (!metadataQuery.data.isVoteContract) {
- return ;
+ return (
+
+ );
}
return (
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/proposals/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/proposals/page.tsx
index 9b972f59fee..ad4495e4f03 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/proposals/page.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/proposals/page.tsx
@@ -1,41 +1,18 @@
-import { notFound, redirect } from "next/navigation";
import { getRawAccount } from "../../../../../account/settings/getAccount";
-import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
-import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
-import { ContractProposalsPage } from "./ContractProposalsPage";
-import { ContractProposalsPageClient } from "./ContractProposalsPage.client";
+import type { PublicContractPageParams } from "../types";
+import { SharedContractProposalsPage } from "./shared-proposals-page";
export default async function Page(props: {
- params: Promise<{
- contractAddress: string;
- chain_id: string;
- }>;
+ params: Promise;
}) {
- const params = await props.params;
- const info = await getContractPageParamsInfo(params);
-
- if (!info) {
- notFound();
- }
- const account = await getRawAccount();
-
- const { clientContract, serverContract, isLocalhostChain } = info;
- if (isLocalhostChain) {
- return (
-
- );
- }
-
- const { isVoteContract } = await getContractPageMetadata(serverContract);
-
- if (!isVoteContract) {
- redirect(`/${params.chain_id}/${params.contractAddress}`);
- }
+ const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
-
+
);
}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/proposals/shared-proposals-page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/proposals/shared-proposals-page.tsx
new file mode 100644
index 00000000000..230e73dc82f
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/proposals/shared-proposals-page.tsx
@@ -0,0 +1,52 @@
+import { notFound } from "next/navigation";
+import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
+import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
+import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
+import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
+import { ContractProposalsPage } from "./ContractProposalsPage";
+import { ContractProposalsPageClient } from "./ContractProposalsPage.client";
+
+export async function SharedContractProposalsPage(props: {
+ contractAddress: string;
+ chainIdOrSlug: string;
+ projectMeta: ProjectMeta | undefined;
+ isLoggedIn: boolean;
+}) {
+ const info = await getContractPageParamsInfo({
+ contractAddress: props.contractAddress,
+ chainIdOrSlug: props.chainIdOrSlug,
+ teamId: props.projectMeta?.teamId,
+ });
+
+ if (!info) {
+ notFound();
+ }
+
+ const { clientContract, serverContract, isLocalhostChain } = info;
+ if (isLocalhostChain) {
+ return (
+
+ );
+ }
+
+ const { isVoteContract } = await getContractPageMetadata(serverContract);
+
+ if (!isVoteContract) {
+ redirectToContractLandingPage({
+ projectMeta: props.projectMeta,
+ chainIdOrSlug: props.chainIdOrSlug,
+ contractAddress: props.contractAddress,
+ });
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/page.tsx
index 0b2490b9768..ed13122da57 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/page.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/page.tsx
@@ -1,55 +1,18 @@
-import { DEFAULT_FEE_RECIPIENT } from "constants/addresses";
-import { notFound } from "next/navigation";
-import { getPlatformFeeInfo } from "thirdweb/extensions/common";
import { getRawAccount } from "../../../../../account/settings/getAccount";
-import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
-import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
-import { ContractSettingsPage } from "./ContractSettingsPage";
-import { ContractSettingsPageClient } from "./ContractSettingsPage.client";
+import type { PublicContractPageParams } from "../types";
+import { SharedContractSettingsPage } from "./shared-settings-page";
export default async function Page(props: {
- params: Promise<{
- contractAddress: string;
- chain_id: string;
- }>;
+ params: Promise;
}) {
- const params = await props.params;
- const info = await getContractPageParamsInfo(params);
-
- if (!info) {
- notFound();
- }
-
- const { clientContract, serverContract, isLocalhostChain } = info;
-
- if (isLocalhostChain) {
- const account = await getRawAccount();
- return (
-
- );
- }
-
- const [account, metadata] = await Promise.all([
- getRawAccount(),
- getContractPageMetadata(serverContract),
- ]);
-
- let hasDefaultFeeConfig = true;
- try {
- const feeInfo = await getPlatformFeeInfo({ contract: serverContract });
- hasDefaultFeeConfig =
- feeInfo[0].toLowerCase() === DEFAULT_FEE_RECIPIENT.toLowerCase();
- } catch {}
+ const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
-
);
}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/shared-settings-page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/shared-settings-page.tsx
new file mode 100644
index 00000000000..a35d46be95e
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/shared-settings-page.tsx
@@ -0,0 +1,62 @@
+import { DEFAULT_FEE_RECIPIENT } from "constants/addresses";
+import { notFound } from "next/navigation";
+import type { ThirdwebContract } from "thirdweb";
+import { getPlatformFeeInfo } from "thirdweb/extensions/common";
+import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
+import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
+import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
+import { ContractSettingsPage } from "./ContractSettingsPage";
+import { ContractSettingsPageClient } from "./ContractSettingsPage.client";
+
+export async function SharedContractSettingsPage(props: {
+ contractAddress: string;
+ chainIdOrSlug: string;
+ projectMeta: ProjectMeta | undefined;
+ isLoggedIn: boolean;
+}) {
+ const info = await getContractPageParamsInfo({
+ contractAddress: props.contractAddress,
+ chainIdOrSlug: props.chainIdOrSlug,
+ teamId: props.projectMeta?.teamId,
+ });
+
+ if (!info) {
+ notFound();
+ }
+
+ const { clientContract, serverContract, isLocalhostChain } = info;
+
+ if (isLocalhostChain) {
+ return (
+
+ );
+ }
+
+ const [metadata, hasDefaultFeeConfig] = await Promise.all([
+ getContractPageMetadata(serverContract),
+ checkDefaultFeeConfig(serverContract),
+ ]);
+
+ return (
+
+ );
+}
+
+async function checkDefaultFeeConfig(contract: ThirdwebContract) {
+ let hasDefaultFeeConfig = true;
+ try {
+ const feeInfo = await getPlatformFeeInfo({ contract });
+ hasDefaultFeeConfig =
+ feeInfo[0].toLowerCase() === DEFAULT_FEE_RECIPIENT.toLowerCase();
+ } catch {}
+
+ return hasDefaultFeeConfig;
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-layout.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-layout.tsx
new file mode 100644
index 00000000000..edea62ddeae
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-layout.tsx
@@ -0,0 +1,218 @@
+import { getProjects } from "@/api/projects";
+import { getTeams } from "@/api/team";
+import type { MinimalTeamsAndProjects } from "components/contract-components/contract-deploy-form/add-to-project-card";
+import { resolveFunctionSelectors } from "lib/selectors";
+import type { Metadata } from "next";
+import { notFound } from "next/navigation";
+import { getContractMetadata } from "thirdweb/extensions/common";
+import { isAddress, isContractDeployed } from "thirdweb/utils";
+import { shortenIfAddress } from "utils/usedapp-external";
+import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
+import { examplePrompts } from "../../../../../nebula-app/(app)/data/examplePrompts";
+import { getAuthTokenWalletAddress } from "../../../../api/lib/getAuthToken";
+import type { ProjectMeta } from "../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
+import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
+import { getContractMetadataHeaderData } from "./_layout/contract-metadata";
+import { ContractPageLayout } from "./_layout/contract-page-layout";
+import { ContractPageLayoutClient } from "./_layout/contract-page-layout.client";
+import { supportedERCs } from "./_utils/detectedFeatures/supportedERCs";
+import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
+import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
+import { getContractPageSidebarLinks } from "./_utils/getContractPageSidebarLinks";
+
+export async function SharedContractLayout(props: {
+ contractAddress: string;
+ chainIdOrSlug: string;
+ projectMeta: ProjectMeta | undefined;
+ children: React.ReactNode;
+}) {
+ if (!isAddress(props.contractAddress)) {
+ return notFound();
+ }
+
+ const [info, accountAddress, teamsAndProjects] = await Promise.all([
+ getContractPageParamsInfo({
+ contractAddress: props.contractAddress,
+ chainIdOrSlug: props.chainIdOrSlug,
+ teamId: props.projectMeta?.teamId,
+ }),
+ getAuthTokenWalletAddress(),
+ getTeamsAndProjectsIfLoggedIn(),
+ ]);
+
+ if (!info) {
+ return ;
+ }
+
+ const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
+ info;
+
+ if (chainMetadata.status === "deprecated") {
+ notFound();
+ }
+
+ if (isLocalhostChain) {
+ return (
+
+ {props.children}
+
+ );
+ }
+
+ const [
+ isValidContract,
+ contractPageMetadata,
+ { contractMetadata, externalLinks },
+ ] = await Promise.all([
+ isContractDeployed(serverContract).catch(() => false),
+ getContractPageMetadata(serverContract),
+ getContractMetadataHeaderData(serverContract),
+ ]);
+
+ // check if the contract exists
+ if (!isValidContract) {
+ // TODO - replace 404 with a better page to upsell deploy or other thirdweb products
+ notFound();
+ }
+
+ const sidebarLinks = getContractPageSidebarLinks({
+ chainSlug: chainMetadata.slug,
+ contractAddress: serverContract.address,
+ metadata: contractPageMetadata,
+ projectMeta: props.projectMeta,
+ });
+
+ const contractAddress = serverContract.address;
+ const chainName = chainMetadata.name;
+ const chainId = chainMetadata.chainId;
+
+ const contractPromptPrefix = `A user is viewing the contract address ${contractAddress} on ${chainName} (Chain ID: ${chainId}). Provide a concise summary of this contract's functionalities, such as token minting, staking, or governance mechanisms. Focus on what the contract enables users to do, avoiding transaction execution details unless requested.
+Users may be interested in how to interact with the contract. Outline common interaction patterns, such as claiming rewards, participating in governance, or transferring assets. Emphasize the contract's capabilities without guiding through transaction processes unless asked.
+Provide insights into how the contract is being used. Share information on user engagement, transaction volumes, or integration with other dApps, focusing on the contract's role within the broader ecosystem.
+Users may be considering integrating the contract into their applications. Discuss how this contract's functionalities can be leveraged within different types of dApps, highlighting potential use cases and benefits.
+
+The following is the user's message:`;
+
+ return (
+
+
+ {props.children}
+
+ );
+}
+
+async function getTeamsAndProjectsIfLoggedIn() {
+ try {
+ const teams = await getTeams();
+
+ if (!teams) {
+ return undefined;
+ }
+
+ const teamsAndProjects: MinimalTeamsAndProjects = await Promise.all(
+ teams.map(async (team) => ({
+ team: {
+ id: team.id,
+ name: team.name,
+ slug: team.slug,
+ image: team.image,
+ },
+ projects: (await getProjects(team.slug)).map((x) => ({
+ id: x.id,
+ name: x.name,
+ image: x.image,
+ })),
+ })),
+ );
+
+ return teamsAndProjects;
+ } catch {
+ return undefined;
+ }
+}
+
+export async function generateContractLayoutMetadata(params: {
+ contractAddress: string;
+ chainIdOrSlug: string;
+}): Promise {
+ try {
+ const info = await getContractPageParamsInfo({
+ contractAddress: params.contractAddress,
+ chainIdOrSlug: params.chainIdOrSlug,
+ teamId: undefined,
+ });
+
+ if (!info) {
+ throw new Error();
+ }
+
+ const [functionSelectors, contractMetadata] = await Promise.all([
+ resolveFunctionSelectors(info.serverContract),
+ getContractMetadata({
+ contract: info.serverContract,
+ }),
+ ]);
+
+ if (contractMetadata.name === null) {
+ throw new Error();
+ }
+
+ const { isERC1155, isERC20, isERC721 } = supportedERCs(functionSelectors);
+
+ const contractDisplayName = `${contractMetadata.name}${
+ contractMetadata.symbol ? ` (${contractMetadata.symbol})` : ""
+ }`;
+
+ const cleanedChainName = info?.chainMetadata?.name
+ .replace("Mainnet", "")
+ .replace("Testnet", "")
+ .trim();
+
+ const title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
+ let description = "";
+
+ if (isERC721 || isERC1155) {
+ description = `View tokens, source code, transactions, balances, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}.`;
+ } else if (isERC20) {
+ description = `View ERC20 tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
+ } else {
+ description = `View tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
+ }
+
+ return {
+ title: title,
+ description: description,
+ };
+ } catch {
+ return {
+ title: `${shortenIfAddress(params.contractAddress)} | ${params.chainIdOrSlug}`,
+ description: `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chainIdOrSlug}`,
+ };
+ }
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-overview-page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-overview-page.tsx
new file mode 100644
index 00000000000..3d3faf5e75a
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-overview-page.tsx
@@ -0,0 +1,61 @@
+import { notFound } from "next/navigation";
+import { ErrorBoundary } from "react-error-boundary";
+import type { ProjectMeta } from "../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
+import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
+import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
+import { ContractOverviewPage } from "./overview/ContractOverviewPage";
+import { PublishedBy } from "./overview/components/published-by.server";
+import { ContractOverviewPageClient } from "./overview/contract-overview-page.client";
+
+export async function SharedContractOverviewPage(props: {
+ contractAddress: string;
+ chainIdOrSlug: string;
+ projectMeta: ProjectMeta | undefined;
+}) {
+ const info = await getContractPageParamsInfo({
+ contractAddress: props.contractAddress,
+ chainIdOrSlug: props.chainIdOrSlug,
+ teamId: props.projectMeta?.teamId,
+ });
+
+ if (!info) {
+ notFound();
+ }
+
+ const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
+ info;
+ if (isLocalhostChain) {
+ return (
+
+ );
+ }
+
+ const contractPageMetadata = await getContractPageMetadata(serverContract);
+
+ return (
+
+
+
+ }
+ />
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/sources/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/sources/page.tsx
index 7f8049f3944..906d0357729 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/sources/page.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/sources/page.tsx
@@ -1,19 +1,15 @@
-import { notFound } from "next/navigation";
-import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
-import { ContractSourcesPage } from "./ContractSourcesPage";
+import type { PublicContractPageParams } from "../types";
+import { SharedContractSourcesPage } from "./shared-sources-page";
export default async function Page(props: {
- params: Promise<{
- contractAddress: string;
- chain_id: string;
- }>;
+ params: Promise;
}) {
const params = await props.params;
- const info = await getContractPageParamsInfo(params);
-
- if (!info) {
- notFound();
- }
-
- return ;
+ return (
+
+ );
}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/sources/shared-sources-page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/sources/shared-sources-page.tsx
new file mode 100644
index 00000000000..07f30aca14a
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/sources/shared-sources-page.tsx
@@ -0,0 +1,22 @@
+import { notFound } from "next/navigation";
+import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
+import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
+import { ContractSourcesPage } from "./ContractSourcesPage";
+
+export async function SharedContractSourcesPage(props: {
+ contractAddress: string;
+ chainIdOrSlug: string;
+ projectMeta: ProjectMeta | undefined;
+}) {
+ const info = await getContractPageParamsInfo({
+ contractAddress: props.contractAddress,
+ chainIdOrSlug: props.chainIdOrSlug,
+ teamId: props.projectMeta?.teamId,
+ });
+
+ if (!info) {
+ notFound();
+ }
+
+ return ;
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/ContractSplitPage.client.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/ContractSplitPage.client.tsx
index 52ad418fbea..cd5d2d7a2f8 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/ContractSplitPage.client.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/ContractSplitPage.client.tsx
@@ -1,6 +1,7 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
+import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
@@ -9,6 +10,7 @@ import { ContractSplitPage } from "./ContractSplitPage";
export function ContractSplitPageClient(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
+ projectMeta: ProjectMeta | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
@@ -21,7 +23,12 @@ export function ContractSplitPageClient(props: {
}
if (!metadataQuery.data.isSplitSupported) {
- return ;
+ return (
+
+ );
}
return (
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/page.tsx
index 8eb3c26e0f2..7631d86e62d 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/page.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/page.tsx
@@ -1,42 +1,18 @@
-import { notFound, redirect } from "next/navigation";
import { getRawAccount } from "../../../../../account/settings/getAccount";
-import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
-import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
-import { ContractSplitPage } from "./ContractSplitPage";
-import { ContractSplitPageClient } from "./ContractSplitPage.client";
+import type { PublicContractPageParams } from "../types";
+import { SharedContractSplitPage } from "./shared-split-page";
export default async function Page(props: {
- params: Promise<{
- contractAddress: string;
- chain_id: string;
- }>;
+ params: Promise;
}) {
- const params = await props.params;
- const info = await getContractPageParamsInfo(params);
-
- if (!info) {
- notFound();
- }
- const { clientContract, serverContract, isLocalhostChain } = info;
-
- const twAccount = await getRawAccount();
-
- if (isLocalhostChain) {
- return (
-
- );
- }
-
- const { isSplitSupported } = await getContractPageMetadata(serverContract);
-
- if (!isSplitSupported) {
- redirect(`/${params.chain_id}/${params.contractAddress}`);
- }
+ const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
-
+
);
}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/shared-split-page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/shared-split-page.tsx
new file mode 100644
index 00000000000..9a00561e6c7
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/shared-split-page.tsx
@@ -0,0 +1,52 @@
+import { notFound } from "next/navigation";
+import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
+import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
+import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
+import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
+import { ContractSplitPage } from "./ContractSplitPage";
+import { ContractSplitPageClient } from "./ContractSplitPage.client";
+
+export async function SharedContractSplitPage(props: {
+ contractAddress: string;
+ chainIdOrSlug: string;
+ projectMeta: ProjectMeta | undefined;
+ isLoggedIn: boolean;
+}) {
+ const info = await getContractPageParamsInfo({
+ contractAddress: props.contractAddress,
+ chainIdOrSlug: props.chainIdOrSlug,
+ teamId: props.projectMeta?.teamId,
+ });
+
+ if (!info) {
+ notFound();
+ }
+ const { clientContract, serverContract, isLocalhostChain } = info;
+
+ if (isLocalhostChain) {
+ return (
+
+ );
+ }
+
+ const { isSplitSupported } = await getContractPageMetadata(serverContract);
+
+ if (!isSplitSupported) {
+ redirectToContractLandingPage({
+ chainIdOrSlug: props.chainIdOrSlug,
+ contractAddress: props.contractAddress,
+ projectMeta: props.projectMeta,
+ });
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/page.tsx
index 69d69a3a303..721da7ea7ba 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/page.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/page.tsx
@@ -1,48 +1,17 @@
-import { notFound } from "next/navigation";
-import {
- isClaimToSupported,
- isMintToSupported,
-} from "thirdweb/extensions/erc20";
import { getRawAccount } from "../../../../../account/settings/getAccount";
-import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
-import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
-import { ContractTokensPage } from "./ContractTokensPage";
-import { ContractTokensPageClient } from "./ContractTokensPage.client";
+import type { PublicContractPageParams } from "../types";
+import { SharedContractTokensPage } from "./shared-page";
export default async function Page(props: {
- params: Promise<{
- contractAddress: string;
- chain_id: string;
- }>;
+ params: Promise;
}) {
- const params = await props.params;
- const info = await getContractPageParamsInfo(params);
-
- if (!info) {
- notFound();
- }
-
- const account = await getRawAccount();
-
- if (info.isLocalhostChain) {
- return (
-
- );
- }
-
- const { supportedERCs, functionSelectors } = await getContractPageMetadata(
- info.serverContract,
- );
+ const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
-
);
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/shared-page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/shared-page.tsx
new file mode 100644
index 00000000000..cf5e0f398b8
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/shared-page.tsx
@@ -0,0 +1,50 @@
+import { notFound } from "next/navigation";
+import {
+ isClaimToSupported,
+ isMintToSupported,
+} from "thirdweb/extensions/erc20";
+import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
+import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
+import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
+import { ContractTokensPage } from "./ContractTokensPage";
+import { ContractTokensPageClient } from "./ContractTokensPage.client";
+
+export async function SharedContractTokensPage(props: {
+ contractAddress: string;
+ chainIdOrSlug: string;
+ projectMeta: ProjectMeta | undefined;
+ isLoggedIn: boolean;
+}) {
+ const info = await getContractPageParamsInfo({
+ contractAddress: props.contractAddress,
+ chainIdOrSlug: props.chainIdOrSlug,
+ teamId: props.projectMeta?.teamId,
+ });
+
+ if (!info) {
+ notFound();
+ }
+
+ if (info.isLocalhostChain) {
+ return (
+
+ );
+ }
+
+ const { supportedERCs, functionSelectors } = await getContractPageMetadata(
+ info.serverContract,
+ );
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/types.ts b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/types.ts
new file mode 100644
index 00000000000..23e5104f0b5
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/types.ts
@@ -0,0 +1,4 @@
+export type PublicContractPageParams = {
+ contractAddress: string;
+ chain_id: string;
+};
diff --git a/apps/dashboard/src/app/(app)/account/contracts/_components/DeployedContractsPage.tsx b/apps/dashboard/src/app/(app)/account/contracts/_components/DeployedContractsPage.tsx
index cc3f9dc0c32..7aa25eafee8 100644
--- a/apps/dashboard/src/app/(app)/account/contracts/_components/DeployedContractsPage.tsx
+++ b/apps/dashboard/src/app/(app)/account/contracts/_components/DeployedContractsPage.tsx
@@ -12,6 +12,8 @@ export function DeployedContractsPage(props: {
projectId: string;
authToken: string;
client: ThirdwebClient;
+ teamSlug: string;
+ projectSlug: string;
}) {
return (
@@ -39,6 +41,8 @@ async function DeployedContractsPageAsync(props: {
projectId: string;
authToken: string;
client: ThirdwebClient;
+ teamSlug: string;
+ projectSlug: string;
}) {
const deployedContracts = await getSortedDeployedContracts({
teamId: props.teamId,
@@ -56,6 +60,8 @@ async function DeployedContractsPageAsync(props: {
teamId={props.teamId}
projectId={props.projectId}
client={props.client}
+ teamSlug={props.teamSlug}
+ projectSlug={props.projectSlug}
/>
);
diff --git a/apps/dashboard/src/app/(app)/api/lib/getAuthToken.ts b/apps/dashboard/src/app/(app)/api/lib/getAuthToken.ts
index 307659c895b..c28dc8e5695 100644
--- a/apps/dashboard/src/app/(app)/api/lib/getAuthToken.ts
+++ b/apps/dashboard/src/app/(app)/api/lib/getAuthToken.ts
@@ -29,8 +29,18 @@ export async function getAuthTokenWalletAddress() {
return null;
}
-export async function getUserThirdwebClient() {
+export async function getUserThirdwebClient(params: {
+ teamId: string | undefined;
+}) {
const authToken = await getAuthToken();
+
+ if (params.teamId) {
+ return getClientThirdwebClient({
+ jwt: authToken,
+ teamId: params.teamId,
+ });
+ }
+
const cookiesManager = await cookies();
const lastUsedTeamId = cookiesManager.get(LAST_USED_TEAM_ID)?.value;
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/page.tsx
index a013573c20d..536c199d31d 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/page.tsx
@@ -65,6 +65,8 @@ export default async function Page(props: {
projectId={project.id}
authToken={authToken}
client={client}
+ teamSlug={params.team_slug}
+ projectSlug={params.project_slug}
/>
@@ -92,6 +94,8 @@ async function AssetsPageAsync(props: {
projectId: string;
authToken: string;
client: ThirdwebClient;
+ teamSlug: string;
+ projectSlug: string;
}) {
const deployedContracts = await getSortedDeployedContracts({
teamId: props.teamId,
@@ -109,6 +113,8 @@ async function AssetsPageAsync(props: {
teamId={props.teamId}
projectId={props.projectId}
client={props.client}
+ teamSlug={props.teamSlug}
+ projectSlug={props.projectSlug}
/>
);
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/connect/account-abstraction/factories/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/connect/account-abstraction/factories/page.tsx
index 040fb0bd996..a6cd1fca3a8 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/connect/account-abstraction/factories/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/connect/account-abstraction/factories/page.tsx
@@ -50,6 +50,8 @@ export default async function Page(props: {
@@ -60,6 +62,8 @@ function YourFactoriesSection(props: {
teamId: string;
projectId: string;
authToken: string;
+ teamSlug: string;
+ projectSlug: string;
}) {
return (
@@ -98,6 +102,8 @@ function YourFactoriesSection(props: {
teamId={props.teamId}
projectId={props.projectId}
authToken={props.authToken}
+ teamSlug={props.teamSlug}
+ projectSlug={props.projectSlug}
/>
@@ -108,6 +114,8 @@ async function AsyncYourFactories(props: {
teamId: string;
projectId: string;
authToken: string;
+ teamSlug: string;
+ projectSlug: string;
}) {
const deployedContracts = await getSortedDeployedContracts({
teamId: props.teamId,
@@ -140,6 +148,8 @@ async function AsyncYourFactories(props: {
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/contracts/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/contracts/page.tsx
index 32405e550da..074cfa9495c 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/contracts/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/contracts/page.tsx
@@ -42,6 +42,8 @@ export default async function Page(props: {
projectId={project.id}
authToken={authToken}
client={client}
+ teamSlug={params.team_slug}
+ projectSlug={params.project_slug}
/>
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/(marketplace)/direct-listings/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/(marketplace)/direct-listings/page.tsx
new file mode 100644
index 00000000000..dd33fe38824
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/(marketplace)/direct-listings/page.tsx
@@ -0,0 +1,28 @@
+import { notFound } from "next/navigation";
+import { SharedDirectListingsPage } from "../../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/direct-listings/shared-direct-listings-page";
+import { getProject } from "../../../../../../../../../../@/api/projects";
+import type { ProjectContractPageParams } from "../../types";
+
+export default async function Page(props: {
+ params: Promise
;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/(marketplace)/english-auctions/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/(marketplace)/english-auctions/page.tsx
new file mode 100644
index 00000000000..c53a1c377f1
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/(marketplace)/english-auctions/page.tsx
@@ -0,0 +1,28 @@
+import { notFound } from "next/navigation";
+import { SharedEnglishAuctionsPage } from "../../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/english-auctions/shared-english-auctions-page";
+import { getProject } from "../../../../../../../../../../@/api/projects";
+import type { ProjectContractPageParams } from "../../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/account-permissions/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/account-permissions/page.tsx
new file mode 100644
index 00000000000..f9becec8ecb
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/account-permissions/page.tsx
@@ -0,0 +1,27 @@
+import { getProject } from "@/api/projects";
+import { notFound } from "next/navigation";
+import { SharedContractAccountPermissionsPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/account-permissions/shared-account-permissions-page";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/account/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/account/page.tsx
new file mode 100644
index 00000000000..4b7a14eae4a
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/account/page.tsx
@@ -0,0 +1,28 @@
+import { notFound } from "next/navigation";
+import { SharedContractAccountPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/account/shared-account-page";
+import { getProject } from "../../../../../../../../../@/api/projects";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/accounts/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/accounts/page.tsx
new file mode 100644
index 00000000000..c34234ead38
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/accounts/page.tsx
@@ -0,0 +1,28 @@
+import { getProject } from "@/api/projects";
+import { notFound } from "next/navigation";
+import { SharedAccountsPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/accounts/shared-accounts-page";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/analytics/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/analytics/page.tsx
new file mode 100644
index 00000000000..b4b597e8186
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/analytics/page.tsx
@@ -0,0 +1,27 @@
+import { getProject } from "@/api/projects";
+import { notFound } from "next/navigation";
+import { SharedAnalyticsPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/analytics/shared-analytics-page";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-conditions/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-conditions/page.tsx
new file mode 100644
index 00000000000..e2205c298c8
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-conditions/page.tsx
@@ -0,0 +1,28 @@
+import { getProject } from "@/api/projects";
+import { notFound } from "next/navigation";
+import { SharedClaimConditionsPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/claim-conditions/shared-claim-conditions-page";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/code/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/code/page.tsx
new file mode 100644
index 00000000000..395456737df
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/code/page.tsx
@@ -0,0 +1,27 @@
+import { getProject } from "@/api/projects";
+import { notFound } from "next/navigation";
+import { SharedCodePage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/code/shared-code-page";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/cross-chain/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/cross-chain/page.tsx
new file mode 100644
index 00000000000..0212eff4a42
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/cross-chain/page.tsx
@@ -0,0 +1,27 @@
+import { getProject } from "@/api/projects";
+import { notFound } from "next/navigation";
+import { SharedCrossChainPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/cross-chain/shared-cross-chain-page";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/events/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/events/page.tsx
new file mode 100644
index 00000000000..067a0590021
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/events/page.tsx
@@ -0,0 +1,27 @@
+import { getProject } from "@/api/projects";
+import { notFound } from "next/navigation";
+import { SharedEventsPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/events/shared-events-page";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/explorer/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/explorer/page.tsx
new file mode 100644
index 00000000000..ac7f11cb67d
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/explorer/page.tsx
@@ -0,0 +1,28 @@
+import { getProject } from "@/api/projects";
+import { notFound } from "next/navigation";
+import { SharedExplorerPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/explorer/shared-explorer-page";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/layout.tsx
new file mode 100644
index 00000000000..cb7a14f79e9
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/layout.tsx
@@ -0,0 +1,88 @@
+import { getProjects } from "@/api/projects";
+import { getTeams } from "@/api/team";
+import { AppFooter } from "@/components/blocks/app-footer";
+import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
+import { redirect } from "next/navigation";
+import { SaveLastUsedProject } from "../../../(sidebar)/components/SaveLastUsedProject";
+import { SharedContractLayout } from "../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-layout";
+import { getValidAccount } from "../../../../../../account/settings/getAccount";
+import {
+ getAuthToken,
+ getAuthTokenWalletAddress,
+} from "../../../../../../api/lib/getAuthToken";
+import { TeamHeaderLoggedIn } from "../../../../../components/TeamHeader/team-header-logged-in.client";
+import type { ProjectContractPageParams } from "./types";
+
+export default async function ContractLayout(props: {
+ children: React.ReactNode;
+ params: Promise;
+}) {
+ const params = await props.params;
+ const [accountAddress, teams, account, authToken] = await Promise.all([
+ getAuthTokenWalletAddress(),
+ getTeams(),
+ getValidAccount(`/team/${params.team_slug}/${params.project_slug}`),
+ getAuthToken(),
+ ]);
+
+ if (!teams || !accountAddress || !authToken) {
+ redirect("/login");
+ }
+
+ const team = teams.find(
+ (t) => t.slug === decodeURIComponent(params.team_slug),
+ );
+
+ if (!team) {
+ redirect("/team");
+ }
+
+ const teamsAndProjects = await Promise.all(
+ teams.map(async (team) => ({
+ team,
+ projects: await getProjects(team.slug),
+ })),
+ );
+
+ const project = teamsAndProjects
+ .find((t) => t.team.slug === decodeURIComponent(params.team_slug))
+ ?.projects.find((p) => p.slug === params.project_slug);
+
+ if (!project) {
+ // not a valid project, redirect back to team page
+ redirect(`/team/${params.team_slug}`);
+ }
+
+ const client = getClientThirdwebClient({
+ jwt: authToken,
+ teamId: team.id,
+ });
+
+ return (
+
+
+
+
+
+ {props.children}
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/modules/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/modules/page.tsx
new file mode 100644
index 00000000000..67de4909d5a
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/modules/page.tsx
@@ -0,0 +1,28 @@
+import { getProject } from "@/api/projects";
+import { notFound } from "next/navigation";
+import { SharedModulesPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/shared-modules-page";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/nfts/[tokenId]/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/nfts/[tokenId]/page.tsx
new file mode 100644
index 00000000000..d42f31d0fb7
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/nfts/[tokenId]/page.tsx
@@ -0,0 +1,33 @@
+import { getProject } from "@/api/projects";
+import { notFound } from "next/navigation";
+import { SharedNFTTokenPage } from "../../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/shared-nfts-token-page";
+import type { ProjectContractPageParams } from "../../types";
+
+export default async function Page(props: {
+ params: Promise<
+ ProjectContractPageParams & {
+ tokenId: string;
+ }
+ >;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/nfts/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/nfts/page.tsx
new file mode 100644
index 00000000000..b87daeaaf40
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/nfts/page.tsx
@@ -0,0 +1,28 @@
+import { getProject } from "@/api/projects";
+import { notFound } from "next/navigation";
+import { SharedNFTPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/shared-nfts-page";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/page.tsx
new file mode 100644
index 00000000000..9795c61dd78
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/page.tsx
@@ -0,0 +1,27 @@
+import { getProject } from "@/api/projects";
+import { notFound } from "next/navigation";
+import { SharedContractOverviewPage } from "../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-overview-page";
+import type { ProjectContractPageParams } from "./types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/permissions/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/permissions/page.tsx
new file mode 100644
index 00000000000..3146e2074d8
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/permissions/page.tsx
@@ -0,0 +1,28 @@
+import { getProject } from "@/api/projects";
+import { notFound } from "next/navigation";
+import { SharedPermissionsPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/shared-permissions-page";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/proposals/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/proposals/page.tsx
new file mode 100644
index 00000000000..6398577eefa
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/proposals/page.tsx
@@ -0,0 +1,28 @@
+import { getProject } from "@/api/projects";
+import { notFound } from "next/navigation";
+import { SharedContractProposalsPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/proposals/shared-proposals-page";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/settings/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/settings/page.tsx
new file mode 100644
index 00000000000..68bc5c701f3
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/settings/page.tsx
@@ -0,0 +1,28 @@
+import { notFound } from "next/navigation";
+import { SharedContractSettingsPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/shared-settings-page";
+import { getProject } from "../../../../../../../../../@/api/projects";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/sources/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/sources/page.tsx
new file mode 100644
index 00000000000..c30c3d10f27
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/sources/page.tsx
@@ -0,0 +1,27 @@
+import { getProject } from "@/api/projects";
+import { notFound } from "next/navigation";
+import { SharedContractSourcesPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/sources/shared-sources-page";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/split/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/split/page.tsx
new file mode 100644
index 00000000000..b319dd2b2db
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/split/page.tsx
@@ -0,0 +1,28 @@
+import { notFound } from "next/navigation";
+import { SharedContractSplitPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/split/shared-split-page";
+import { getProject } from "../../../../../../../../../@/api/projects";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/tokens/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/tokens/page.tsx
new file mode 100644
index 00000000000..1cb4ea75d4a
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/tokens/page.tsx
@@ -0,0 +1,28 @@
+import { notFound } from "next/navigation";
+import { SharedContractTokensPage } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/shared-page";
+import { getProject } from "../../../../../../../../../@/api/projects";
+import type { ProjectContractPageParams } from "../types";
+
+export default async function Page(props: {
+ params: Promise;
+}) {
+ const params = await props.params;
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types.ts
new file mode 100644
index 00000000000..cf638845b01
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types.ts
@@ -0,0 +1,12 @@
+export type ProjectContractPageParams = {
+ contractAddress: string;
+ chainIdOrSlug: string;
+ team_slug: string;
+ project_slug: string;
+};
+
+export type ProjectMeta = {
+ teamId: string;
+ projectSlug: string;
+ teamSlug: string;
+};
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils.ts
new file mode 100644
index 00000000000..7b35cf4d838
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils.ts
@@ -0,0 +1,18 @@
+import "server-only";
+import { redirect } from "next/navigation";
+import { buildContractPagePath } from "../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/contract-page-path";
+import type { ProjectMeta } from "./types";
+
+export function redirectToContractLandingPage(params: {
+ chainIdOrSlug: string;
+ contractAddress: string;
+ projectMeta: ProjectMeta | undefined;
+}): never {
+ redirect(
+ buildContractPagePath({
+ projectMeta: params.projectMeta,
+ chainIdOrSlug: params.chainIdOrSlug,
+ contractAddress: params.contractAddress,
+ }),
+ );
+}
diff --git a/apps/dashboard/src/components/contract-components/tables/cells.tsx b/apps/dashboard/src/components/contract-components/tables/cells.tsx
index 41dac59fa77..98305d8509c 100644
--- a/apps/dashboard/src/components/contract-components/tables/cells.tsx
+++ b/apps/dashboard/src/components/contract-components/tables/cells.tsx
@@ -18,6 +18,8 @@ export const ContractNameCell = memo(function ContractNameCell(props: {
chainId: string;
contractAddress: string;
linkOverlay?: boolean;
+ teamSlug: string;
+ projectSlug: string;
}) {
const chainSlug = useChainSlug(Number(props.chainId));
const chain = useV5DashboardChain(Number(props.chainId));
@@ -40,7 +42,7 @@ export const ContractNameCell = memo(function ContractNameCell(props: {
render={(v) => {
return (
@@ -64,6 +67,8 @@ function Story() {
contracts={[]}
removeContractFromProject={removeContractStub}
pageSize={10}
+ teamSlug={teamSlug}
+ projectSlug={projectSlug}
/>
@@ -74,6 +79,8 @@ function Story() {
contracts={[popularPolygonNFTs[0] as ProjectContract]}
pageSize={10}
removeContractFromProject={removeContractStub}
+ teamSlug={teamSlug}
+ projectSlug={projectSlug}
/>
@@ -84,6 +91,8 @@ function Story() {
contracts={popularPolygonNFTs}
pageSize={10}
removeContractFromProject={removeContractStub}
+ teamSlug={teamSlug}
+ projectSlug={projectSlug}
/>
@@ -94,6 +103,8 @@ function Story() {
contracts={[...popularPolygonNFTs, ...EthereumPopularNFTs]}
pageSize={10}
removeContractFromProject={removeContractStub}
+ teamSlug={teamSlug}
+ projectSlug={projectSlug}
/>
@@ -108,6 +119,8 @@ function Story() {
]}
pageSize={10}
removeContractFromProject={removeContractStub}
+ teamSlug={teamSlug}
+ projectSlug={projectSlug}
/>
diff --git a/apps/dashboard/src/components/contract-components/tables/contract-table.tsx b/apps/dashboard/src/components/contract-components/tables/contract-table.tsx
index 3455ebb1fb9..1065844643a 100644
--- a/apps/dashboard/src/components/contract-components/tables/contract-table.tsx
+++ b/apps/dashboard/src/components/contract-components/tables/contract-table.tsx
@@ -46,6 +46,8 @@ export function ContractTable(props: {
projectId: string;
client: ThirdwebClient;
variant: "asset" | "contract";
+ teamSlug: string;
+ projectSlug: string;
}) {
return (
{
await removeContractFromProject({
teamId: props.teamId,
@@ -70,6 +74,8 @@ export function ContractTableUI(props: {
removeContractFromProject: (contractId: string) => Promise;
client: ThirdwebClient;
variant: "asset" | "contract";
+ teamSlug: string;
+ projectSlug: string;
}) {
// instantly update the table without waiting for router refresh by adding deleted contract ids to the state
const [deletedContractIds, setDeletedContractIds] = useState([]);
@@ -168,6 +174,8 @@ export function ContractTableUI(props: {
chainId={contract.chainId}
contractAddress={contract.contractAddress}
linkOverlay
+ teamSlug={props.teamSlug}
+ projectSlug={props.projectSlug}
/>
diff --git a/apps/dashboard/src/components/smart-wallets/AccountFactories/factory-contracts.tsx b/apps/dashboard/src/components/smart-wallets/AccountFactories/factory-contracts.tsx
index ad333c50d78..e47aa217f8a 100644
--- a/apps/dashboard/src/components/smart-wallets/AccountFactories/factory-contracts.tsx
+++ b/apps/dashboard/src/components/smart-wallets/AccountFactories/factory-contracts.tsx
@@ -22,6 +22,8 @@ interface FactoryContractsProps {
contracts: ProjectContract[];
isPending: boolean;
isFetched: boolean;
+ teamSlug: string;
+ projectSlug: string;
}
function NetworkName(props: { id: number }) {
@@ -45,6 +47,8 @@ function NetworkName(props: { id: number }) {
export const FactoryContracts: React.FC = ({
contracts,
+ teamSlug,
+ projectSlug,
}) => {
return (
@@ -64,6 +68,8 @@ export const FactoryContracts: React.FC = ({