Skip to content

Commit a880f77

Browse files
committed
[TOOL-4833] Add Manage Contract button in new public contract pages
1 parent 6aa2765 commit a880f77

File tree

16 files changed

+733
-70
lines changed

16 files changed

+733
-70
lines changed

apps/dashboard/src/@/api/getProjectContracts.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import "server-only";
2+
import { getAddress } from "thirdweb";
23
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
34

45
export type ProjectContract = {
@@ -43,3 +44,43 @@ export async function getProjectContracts(options: {
4344

4445
return data.result;
4546
}
47+
48+
export type PartialProject = {
49+
id: string;
50+
name: string;
51+
slug: string;
52+
image: string | null;
53+
};
54+
55+
/**
56+
* get a list of projects within a team that have a given contract imported
57+
*/
58+
export async function getContractImportedProjects(options: {
59+
teamId: string;
60+
authToken: string;
61+
chainId: number;
62+
contractAddress: string;
63+
}) {
64+
const url = new URL(
65+
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/projects/contracts/lookup?chainId=${options.chainId}&contractAddress=${getAddress(options.contractAddress)}`,
66+
);
67+
68+
const res = await fetch(url, {
69+
headers: {
70+
Authorization: `Bearer ${options.authToken}`,
71+
},
72+
});
73+
74+
if (!res.ok) {
75+
const errorMessage = await res.text();
76+
console.error("Error fetching: /projects/contracts/lookup");
77+
console.error(errorMessage);
78+
return [];
79+
}
80+
81+
const data = (await res.json()) as {
82+
result: PartialProject[];
83+
};
84+
85+
return data.result;
86+
}

apps/dashboard/src/@/components/contracts/import-contract/modal.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ type ImportModalProps = {
3737
onClose: () => void;
3838
teamId: string;
3939
projectId: string;
40+
projectSlug: string;
41+
teamSlug: string;
4042
client: ThirdwebClient;
4143
type: "contract" | "asset";
4244
onSuccess?: () => void;
@@ -69,7 +71,9 @@ export const ImportModal: React.FC<ImportModalProps> = (props) => {
6971
client={props.client}
7072
onSuccess={props.onSuccess}
7173
projectId={props.projectId}
74+
projectSlug={props.projectSlug}
7275
teamId={props.teamId}
76+
teamSlug={props.teamSlug}
7377
type={props.type}
7478
/>
7579
</DialogContent>
@@ -96,6 +100,8 @@ const importFormSchema = z.object({
96100
function ImportForm(props: {
97101
teamId: string;
98102
projectId: string;
103+
teamSlug: string;
104+
projectSlug: string;
99105
client: ThirdwebClient;
100106
type: "contract" | "asset";
101107
onSuccess?: () => void;
@@ -216,7 +222,7 @@ function ImportForm(props: {
216222
addContractToProject.data?.result ? (
217223
<Button asChild className="gap-2">
218224
<Link
219-
href={`/${chainSlug}/${addContractToProject.data.result.contractAddress}`}
225+
href={`/team/${props.teamSlug}/${props.projectSlug}/contract/${chainSlug}/${addContractToProject.data.result.contractAddress}`}
220226
rel="noopener noreferrer"
221227
target="_blank"
222228
>

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/primary-dashboard-button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
8383
// if user is on a project page
8484
if (projectMeta) {
8585
return (
86-
<Button asChild variant="default">
86+
<Button asChild className="rounded-full" variant="default">
8787
<Link
8888
className="gap-2"
8989
href={`/${contractInfo.chainSlug}/${contractAddress}`}

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractHeader.tsx

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ExternalLinkIcon, GlobeIcon } from "lucide-react";
1+
import { ExternalLinkIcon, GlobeIcon, Settings2Icon } from "lucide-react";
22
import Link from "next/link";
33
import { useMemo } from "react";
44
import type { ThirdwebContract } from "thirdweb";
@@ -111,6 +111,13 @@ export function ContractHeaderUI(props: {
111111
)}
112112
</Link>
113113

114+
<CopyAddressButton
115+
address={props.clientContract.address}
116+
className="rounded-full bg-card w-[30px] h-[30px] p-0 [&>span]:hidden [&>svg]:text-foreground"
117+
copyIconPosition="left"
118+
variant="outline"
119+
/>
120+
114121
{socialUrls
115122
.toSorted((a, b) => {
116123
const aIcon = platformToIcons[a.name.toLowerCase()];
@@ -140,12 +147,29 @@ export function ContractHeaderUI(props: {
140147

141148
{/* bottom row */}
142149
<div className="flex flex-row flex-wrap items-center gap-2">
143-
<CopyAddressButton
144-
address={props.clientContract.address}
145-
className="rounded-full bg-card px-2.5 py-1.5 text-xs"
146-
copyIconPosition="left"
147-
variant="outline"
148-
/>
150+
<ToolTipLabel
151+
contentClassName="max-w-[300px]"
152+
label={
153+
<>
154+
View this contract in thirdweb dashboard to view contract
155+
management interface
156+
</>
157+
}
158+
>
159+
<Button
160+
asChild
161+
className="rounded-full bg-card gap-1.5 text-xs py-1.5 px-2.5 h-auto"
162+
size="sm"
163+
variant="outline"
164+
>
165+
<Link
166+
href={`/team/~/~/contract/${props.chainMetadata.slug}/${props.clientContract.address}`}
167+
>
168+
<Settings2Icon className="size-3.5 text-muted-foreground" />
169+
Manage Contract
170+
</Link>
171+
</Button>
172+
</ToolTipLabel>
149173

150174
{explorersToShow?.map((validBlockExplorer) => (
151175
<BadgeLink

apps/dashboard/src/app/(app)/account/contracts/DeployedContractsPageHeader.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { Button } from "@/components/ui/button";
1010
export function DeployedContractsPageHeader(props: {
1111
teamId: string;
1212
projectId: string;
13+
projectSlug: string;
14+
teamSlug: string;
1315
client: ThirdwebClient;
1416
}) {
1517
const [importModalOpen, setImportModalOpen] = useState(false);
@@ -23,7 +25,9 @@ export function DeployedContractsPageHeader(props: {
2325
setImportModalOpen(false);
2426
}}
2527
projectId={props.projectId}
28+
projectSlug={props.projectSlug}
2629
teamId={props.teamId}
30+
teamSlug={props.teamSlug}
2731
type="contract"
2832
/>
2933

apps/dashboard/src/app/(app)/account/contracts/_components/DeployViaCLIOrImportCard.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { Button } from "@/components/ui/button";
1010
export function DeployViaCLIOrImportCard(props: {
1111
teamId: string;
1212
projectId: string;
13+
projectSlug: string;
14+
teamSlug: string;
1315
client: ThirdwebClient;
1416
}) {
1517
const [importModalOpen, setImportModalOpen] = useState(false);
@@ -23,7 +25,9 @@ export function DeployViaCLIOrImportCard(props: {
2325
setImportModalOpen(false);
2426
}}
2527
projectId={props.projectId}
28+
projectSlug={props.projectSlug}
2629
teamId={props.teamId}
30+
teamSlug={props.teamSlug}
2731
type="contract"
2832
/>
2933

apps/dashboard/src/app/(app)/account/contracts/_components/DeployedContractsPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ export function DeployedContractsPage(props: {
2323
<DeployViaCLIOrImportCard
2424
client={props.client}
2525
projectId={props.projectId}
26+
projectSlug={props.projectSlug}
2627
teamId={props.teamId}
28+
teamSlug={props.teamSlug}
2729
/>
2830
</div>
2931
);

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/contracts/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ export default async function Layout(props: {
4343
<DeployedContractsPageHeader
4444
client={client}
4545
projectId={project.id}
46+
projectSlug={params.project_slug}
4647
teamId={team.id}
48+
teamSlug={params.team_slug}
4749
/>
4850
<TabPathLinks
4951
links={[

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/cards.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ export function Cards(props: {
3333
reportAssetImportSuccessful();
3434
}}
3535
projectId={props.projectId}
36+
projectSlug={props.projectSlug}
3637
teamId={props.teamId}
38+
teamSlug={props.teamSlug}
3739
type="asset"
3840
/>
3941

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { Meta, StoryObj } from "@storybook/nextjs";
2+
import { teamStub } from "@/storybook/stubs";
3+
import { storybookThirdwebClient } from "@/storybook/utils";
4+
import { TeamSelectorCard } from "./team-selector";
5+
6+
const meta: Meta<typeof TeamSelectorCard> = {
7+
component: TeamSelectorCard,
8+
decorators: [
9+
(Story) => (
10+
<div className="py-20 flex justify-center items-center">
11+
<Story />
12+
</div>
13+
),
14+
],
15+
parameters: {
16+
nextjs: {
17+
appDirectory: true,
18+
},
19+
},
20+
title: "selectors/TeamSelectorCard",
21+
};
22+
23+
export default meta;
24+
type Story = StoryObj<typeof TeamSelectorCard>;
25+
26+
export const TwoTeams: Story = {
27+
args: {
28+
client: storybookThirdwebClient,
29+
paths: undefined,
30+
searchParams: "",
31+
teams: [teamStub("1", "free"), teamStub("2", "starter")],
32+
},
33+
};
34+
35+
export const FiveTeams: Story = {
36+
args: {
37+
client: storybookThirdwebClient,
38+
paths: undefined,
39+
searchParams: "",
40+
teams: [
41+
teamStub("1", "free"),
42+
teamStub("2", "starter"),
43+
teamStub("3", "growth"),
44+
teamStub("4", "pro"),
45+
teamStub("5", "scale"),
46+
],
47+
},
48+
};
49+
50+
export const WithSearchParams: Story = {
51+
args: {
52+
client: storybookThirdwebClient,
53+
paths: undefined,
54+
searchParams: "tab=overview&section=analytics",
55+
teams: [
56+
teamStub("1", "free"),
57+
teamStub("2", "starter"),
58+
teamStub("3", "growth"),
59+
],
60+
},
61+
};
62+
63+
export const WithPaths: Story = {
64+
args: {
65+
client: storybookThirdwebClient,
66+
paths: ["projects", "123", "settings"],
67+
searchParams: "",
68+
teams: [teamStub("1", "free"), teamStub("2", "starter")],
69+
},
70+
};
71+
72+
export const WithPathsAndSearchParams: Story = {
73+
args: {
74+
client: storybookThirdwebClient,
75+
paths: ["projects", "123", "settings"],
76+
searchParams: "tab=overview&section=analytics",
77+
teams: [
78+
teamStub("1", "free"),
79+
teamStub("2", "starter"),
80+
teamStub("3", "growth"),
81+
teamStub("4", "pro"),
82+
],
83+
},
84+
};
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { ChevronRightIcon, UsersIcon } from "lucide-react";
2+
import Link from "next/link";
3+
import type { ThirdwebClient } from "thirdweb";
4+
import type { Team } from "@/api/team";
5+
import { GradientAvatar } from "@/components/blocks/avatar/gradient-avatar";
6+
import { TeamPlanBadge } from "@/components/blocks/TeamPlanBadge";
7+
8+
export function createTeamLink(params: {
9+
team: Team;
10+
paths: string[] | undefined;
11+
searchParams: string | undefined;
12+
}) {
13+
const pathsSegment = params.paths?.length ? `/${params.paths.join("/")}` : "";
14+
const searchParamsSegment = params.searchParams
15+
? `?${params.searchParams}`
16+
: "";
17+
return `/team/${params.team.slug}${pathsSegment}${searchParamsSegment}`;
18+
}
19+
20+
export function TeamSelectorCard(props: {
21+
teams: Team[];
22+
client: ThirdwebClient;
23+
searchParams: string;
24+
paths: string[] | undefined;
25+
}) {
26+
return (
27+
<div className="w-full max-w-lg rounded-xl border bg-card shadow-2xl">
28+
<div className="flex flex-col border-b p-4 lg:p-6">
29+
<div className="mb-2 self-start rounded-full border p-2">
30+
<UsersIcon className="size-5 text-muted-foreground" />
31+
</div>
32+
<h1 className="mb-0.5 font-semibold text-xl tracking-tight">
33+
Select a team
34+
</h1>
35+
<p className="text-muted-foreground text-sm">
36+
You are currently a member of multiple teams
37+
<br />
38+
Select a team to view this page
39+
</p>
40+
</div>
41+
42+
<div className="flex flex-col [&>*:not(:last-child)]:border-b">
43+
{props.teams.map((team) => {
44+
return (
45+
<div
46+
className="group relative flex items-center gap-3 px-4 py-4 hover:bg-accent/50 lg:px-6"
47+
key={team.id}
48+
>
49+
<GradientAvatar
50+
className="size-8 rounded-full border"
51+
client={props.client}
52+
id={team.id}
53+
src={team.image || ""}
54+
/>
55+
<Link
56+
className="before:absolute before:inset-0"
57+
href={createTeamLink({
58+
paths: props.paths,
59+
searchParams: props.searchParams,
60+
team,
61+
})}
62+
>
63+
{team.name}
64+
</Link>
65+
<TeamPlanBadge plan={team.billingPlan} teamSlug={team.slug} />
66+
<ChevronRightIcon className="ml-auto size-4 text-muted-foreground group-hover:text-foreground" />
67+
</div>
68+
);
69+
})}
70+
</div>
71+
</div>
72+
);
73+
}

0 commit comments

Comments
 (0)