Skip to content

[TOOL-4833] Add Manage Contract button in new public contract pages #7434

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions apps/dashboard/src/@/api/getProjectContracts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "server-only";
import { getAddress } from "thirdweb";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";

export type ProjectContract = {
Expand Down Expand Up @@ -43,3 +44,43 @@ export async function getProjectContracts(options: {

return data.result;
}

export type PartialProject = {
id: string;
name: string;
slug: string;
image: string | null;
};

/**
* get a list of projects within a team that have a given contract imported
*/
export async function getContractImportedProjects(options: {
teamId: string;
authToken: string;
chainId: number;
contractAddress: string;
}) {
const url = new URL(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/projects/contracts/lookup?chainId=${options.chainId}&contractAddress=${getAddress(options.contractAddress)}`,
);

const res = await fetch(url, {
headers: {
Authorization: `Bearer ${options.authToken}`,
},
});

if (!res.ok) {
const errorMessage = await res.text();
console.error("Error fetching: /projects/contracts/lookup");
console.error(errorMessage);
return [];
}

const data = (await res.json()) as {
result: PartialProject[];
};

return data.result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type ImportModalProps = {
onClose: () => void;
teamId: string;
projectId: string;
projectSlug: string;
teamSlug: string;
client: ThirdwebClient;
type: "contract" | "asset";
onSuccess?: () => void;
Expand Down Expand Up @@ -69,7 +71,9 @@ export const ImportModal: React.FC<ImportModalProps> = (props) => {
client={props.client}
onSuccess={props.onSuccess}
projectId={props.projectId}
projectSlug={props.projectSlug}
teamId={props.teamId}
teamSlug={props.teamSlug}
type={props.type}
/>
</DialogContent>
Expand All @@ -96,6 +100,8 @@ const importFormSchema = z.object({
function ImportForm(props: {
teamId: string;
projectId: string;
teamSlug: string;
projectSlug: string;
client: ThirdwebClient;
type: "contract" | "asset";
onSuccess?: () => void;
Expand Down Expand Up @@ -216,7 +222,7 @@ function ImportForm(props: {
addContractToProject.data?.result ? (
<Button asChild className="gap-2">
<Link
href={`/${chainSlug}/${addContractToProject.data.result.contractAddress}`}
href={`/team/${props.teamSlug}/${props.projectSlug}/contract/${chainSlug}/${addContractToProject.data.result.contractAddress}`}
rel="noopener noreferrer"
target="_blank"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
// if user is on a project page
if (projectMeta) {
return (
<Button asChild variant="default">
<Button asChild className="rounded-full" variant="default">
<Link
className="gap-2"
href={`/${contractInfo.chainSlug}/${contractAddress}`}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExternalLinkIcon, GlobeIcon } from "lucide-react";
import { ExternalLinkIcon, GlobeIcon, Settings2Icon } from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import type { ThirdwebContract } from "thirdweb";
Expand Down Expand Up @@ -111,6 +111,13 @@ export function ContractHeaderUI(props: {
)}
</Link>

<CopyAddressButton
address={props.clientContract.address}
className="rounded-full bg-card w-[30px] h-[30px] p-0 [&>span]:hidden [&>svg]:text-foreground"
copyIconPosition="left"
variant="outline"
/>

{socialUrls
.toSorted((a, b) => {
const aIcon = platformToIcons[a.name.toLowerCase()];
Expand Down Expand Up @@ -140,12 +147,29 @@ export function ContractHeaderUI(props: {

{/* bottom row */}
<div className="flex flex-row flex-wrap items-center gap-2">
<CopyAddressButton
address={props.clientContract.address}
className="rounded-full bg-card px-2.5 py-1.5 text-xs"
copyIconPosition="left"
variant="outline"
/>
<ToolTipLabel
contentClassName="max-w-[300px]"
label={
<>
View this contract in thirdweb dashboard to view contract
management interface
</>
}
>
<Button
asChild
className="rounded-full bg-card gap-1.5 text-xs py-1.5 px-2.5 h-auto"
size="sm"
variant="outline"
>
<Link
href={`/team/~/~/contract/${props.chainMetadata.slug}/${props.clientContract.address}`}
>
<Settings2Icon className="size-3.5 text-muted-foreground" />
Manage Contract
</Link>
</Button>
</ToolTipLabel>

{explorersToShow?.map((validBlockExplorer) => (
<BadgeLink
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { Button } from "@/components/ui/button";
export function DeployedContractsPageHeader(props: {
teamId: string;
projectId: string;
projectSlug: string;
teamSlug: string;
client: ThirdwebClient;
}) {
const [importModalOpen, setImportModalOpen] = useState(false);
Expand All @@ -23,7 +25,9 @@ export function DeployedContractsPageHeader(props: {
setImportModalOpen(false);
}}
projectId={props.projectId}
projectSlug={props.projectSlug}
teamId={props.teamId}
teamSlug={props.teamSlug}
type="contract"
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { Button } from "@/components/ui/button";
export function DeployViaCLIOrImportCard(props: {
teamId: string;
projectId: string;
projectSlug: string;
teamSlug: string;
client: ThirdwebClient;
}) {
const [importModalOpen, setImportModalOpen] = useState(false);
Expand All @@ -23,7 +25,9 @@ export function DeployViaCLIOrImportCard(props: {
setImportModalOpen(false);
}}
projectId={props.projectId}
projectSlug={props.projectSlug}
teamId={props.teamId}
teamSlug={props.teamSlug}
type="contract"
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export function DeployedContractsPage(props: {
<DeployViaCLIOrImportCard
client={props.client}
projectId={props.projectId}
projectSlug={props.projectSlug}
teamId={props.teamId}
teamSlug={props.teamSlug}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ export default async function Layout(props: {
<DeployedContractsPageHeader
client={client}
projectId={project.id}
projectSlug={params.project_slug}
teamId={team.id}
teamSlug={params.team_slug}
/>
<TabPathLinks
links={[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export function Cards(props: {
reportAssetImportSuccessful();
}}
projectId={props.projectId}
projectSlug={props.projectSlug}
teamId={props.teamId}
teamSlug={props.teamSlug}
type="asset"
/>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { teamStub } from "@/storybook/stubs";
import { storybookThirdwebClient } from "@/storybook/utils";
import { TeamSelectorCard } from "./team-selector";

const meta: Meta<typeof TeamSelectorCard> = {
component: TeamSelectorCard,
decorators: [
(Story) => (
<div className="py-20 flex justify-center items-center">
<Story />
</div>
),
],
parameters: {
nextjs: {
appDirectory: true,
},
},
title: "selectors/TeamSelectorCard",
};

export default meta;
type Story = StoryObj<typeof TeamSelectorCard>;

export const TwoTeams: Story = {
args: {
client: storybookThirdwebClient,
paths: undefined,
searchParams: "",
teams: [teamStub("1", "free"), teamStub("2", "starter")],
},
};

export const FiveTeams: Story = {
args: {
client: storybookThirdwebClient,
paths: undefined,
searchParams: "",
teams: [
teamStub("1", "free"),
teamStub("2", "starter"),
teamStub("3", "growth"),
teamStub("4", "pro"),
teamStub("5", "scale"),
],
},
};

export const WithSearchParams: Story = {
args: {
client: storybookThirdwebClient,
paths: undefined,
searchParams: "tab=overview&section=analytics",
teams: [
teamStub("1", "free"),
teamStub("2", "starter"),
teamStub("3", "growth"),
],
},
};

export const WithPaths: Story = {
args: {
client: storybookThirdwebClient,
paths: ["projects", "123", "settings"],
searchParams: "",
teams: [teamStub("1", "free"), teamStub("2", "starter")],
},
};

export const WithPathsAndSearchParams: Story = {
args: {
client: storybookThirdwebClient,
paths: ["projects", "123", "settings"],
searchParams: "tab=overview&section=analytics",
teams: [
teamStub("1", "free"),
teamStub("2", "starter"),
teamStub("3", "growth"),
teamStub("4", "pro"),
],
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ChevronRightIcon, UsersIcon } from "lucide-react";
import Link from "next/link";
import type { ThirdwebClient } from "thirdweb";
import type { Team } from "@/api/team";
import { GradientAvatar } from "@/components/blocks/avatar/gradient-avatar";
import { TeamPlanBadge } from "@/components/blocks/TeamPlanBadge";

export function createTeamLink(params: {
team: Team;
paths: string[] | undefined;
searchParams: string | undefined;
}) {
const pathsSegment = params.paths?.length ? `/${params.paths.join("/")}` : "";
const searchParamsSegment = params.searchParams
? `?${params.searchParams}`
: "";
return `/team/${params.team.slug}${pathsSegment}${searchParamsSegment}`;
}

export function TeamSelectorCard(props: {
teams: Team[];
client: ThirdwebClient;
searchParams: string;
paths: string[] | undefined;
}) {
return (
<div className="w-full max-w-lg rounded-xl border bg-card shadow-2xl">
<div className="flex flex-col border-b p-4 lg:p-6">
<div className="mb-2 self-start rounded-full border p-2">
<UsersIcon className="size-5 text-muted-foreground" />
</div>
<h1 className="mb-0.5 font-semibold text-xl tracking-tight">
Select a team
</h1>
<p className="text-muted-foreground text-sm">
You are currently a member of multiple teams
<br />
Select a team to view this page
</p>
</div>

<div className="flex flex-col [&>*:not(:last-child)]:border-b">
{props.teams.map((team) => {
return (
<div
className="group relative flex items-center gap-3 px-4 py-4 hover:bg-accent/50 lg:px-6"
key={team.id}
>
<GradientAvatar
className="size-8 rounded-full border"
client={props.client}
id={team.id}
src={team.image || ""}
/>
<Link
className="before:absolute before:inset-0"
href={createTeamLink({
paths: props.paths,
searchParams: props.searchParams,
team,
})}
>
{team.name}
</Link>
<TeamPlanBadge plan={team.billingPlan} teamSlug={team.slug} />
<ChevronRightIcon className="ml-auto size-4 text-muted-foreground group-hover:text-foreground" />
</div>
);
})}
</div>
</div>
);
}
Loading
Loading