diff --git a/apps/dashboard/src/@/analytics/report.ts b/apps/dashboard/src/@/analytics/report.ts index 0e5c4485af2..849149dc0b2 100644 --- a/apps/dashboard/src/@/analytics/report.ts +++ b/apps/dashboard/src/@/analytics/report.ts @@ -503,3 +503,17 @@ export function reportFundWalletSuccessful() { export function reportFundWalletFailed(params: { errorMessage: string }) { posthog.capture("fund wallet failed", params); } + +/** + * ### Why do we need to report this event? + * - To track the conversion rate of the users choosing to create a token from new flow instead of the old flow + * + * ### Who is responsible for this event? + * @MananTank + */ +export function reportTokenUpsellClicked(params: { + assetType: "nft" | "coin"; + pageType: "explore" | "deploy-contract"; +}) { + posthog.capture("token upsell clicked", params); +} diff --git a/apps/dashboard/src/@/components/blocks/dismissible-alert.tsx b/apps/dashboard/src/@/components/blocks/dismissible-alert.tsx index 88aa4bd41b1..85f082af1ae 100644 --- a/apps/dashboard/src/@/components/blocks/dismissible-alert.tsx +++ b/apps/dashboard/src/@/components/blocks/dismissible-alert.tsx @@ -1,12 +1,39 @@ "use client"; import { XIcon } from "lucide-react"; +import { useState } from "react"; import { Button } from "@/components/ui/button"; import { useLocalStorage } from "@/hooks/useLocalStorage"; -export function DismissibleAlert(props: { +export function DismissibleAlert( + props: { + title: React.ReactNode; + header?: React.ReactNode; + className?: string; + description: React.ReactNode; + children?: React.ReactNode; + } & ( + | { + preserveState: true; + localStorageId: string; + } + | { + preserveState: false; + } + ), +) { + if (props.preserveState) { + return ; + } + + return ; +} + +function DismissibleAlertWithLocalStorage(props: { title: React.ReactNode; + header?: React.ReactNode; description: React.ReactNode; + children?: React.ReactNode; localStorageId: string; }) { const [isVisible, setIsVisible] = useLocalStorage( @@ -17,19 +44,48 @@ export function DismissibleAlert(props: { if (!isVisible) return null; + return setIsVisible(false)} />; +} + +function DismissibleAlertWithoutLocalStorage(props: { + title: React.ReactNode; + description: React.ReactNode; + children?: React.ReactNode; +}) { + const [isVisible, setIsVisible] = useState(true); + + if (!isVisible) return null; + + return setIsVisible(false)} />; +} + +function AlertUI(props: { + title: React.ReactNode; + header?: React.ReactNode; + description: React.ReactNode; + children?: React.ReactNode; + className?: string; + onClose: () => void; +}) { return ( -
- -
-

{props.title}

-
{props.description}
+
+
+ +
+ {props.header} +

{props.title}

+
+ {props.description} +
+ {props.children} +
); diff --git a/apps/dashboard/src/app/(app)/(dashboard)/explore/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/explore/page.tsx index 808e095f3ab..1f97e16d9eb 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/explore/page.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/explore/page.tsx @@ -4,6 +4,7 @@ import { ContractRow } from "./components/contract-row"; import { DeployUpsellCard } from "./components/upsells/deploy-your-own"; import { PublishUpsellCard } from "./components/upsells/publish-submit"; import { EXPLORE_PAGE_DATA } from "./data"; +import { TokensSection } from "./tokens-section"; const title = "List of smart contracts for EVM Developers"; const description = @@ -22,11 +23,11 @@ export default async function ExplorePage() { return (
-
-

+
+

Explore

-

+

The best place for web3 developers to explore smart contracts from world-class web3 protocols & engineers — all deployable with one click. @@ -34,7 +35,11 @@ export default async function ExplorePage() {

-
+
+ +
+ +
{EXPLORE_PAGE_DATA.map((category, idx) => ( diff --git a/apps/dashboard/src/app/(app)/(dashboard)/explore/tokens-section.tsx b/apps/dashboard/src/app/(app)/(dashboard)/explore/tokens-section.tsx new file mode 100644 index 00000000000..9bc971b9384 --- /dev/null +++ b/apps/dashboard/src/app/(app)/(dashboard)/explore/tokens-section.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { ZapIcon } from "lucide-react"; +import Link from "next/link"; +import { reportTokenUpsellClicked } from "@/analytics/report"; +import { GridPattern } from "@/components/ui/background-patterns"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; + +export function TokensSection() { + return ( +
+ + + +
+ New + + +

+ Launch Your Tokens Effortlessly +

+

+ Deploy contract and configure all settings you need to launch your token + in one seamless flow +

+ +
+ + + +
+
+ ); +} + +function CardLink(props: { + title: string; + description: string; + href: string; + assetType: "nft" | "coin"; + bullets: string[]; +}) { + return ( +
+
+
+ +
+
+ +

+ { + reportTokenUpsellClicked({ + assetType: props.assetType, + pageType: "explore", + }); + }} + > + {props.title} + +

+

{props.description}

+ +
    + {props.bullets.map((bullet) => ( +
  • {bullet}
  • + ))} +
+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/published-contract/components/publish-based-deploy.tsx b/apps/dashboard/src/app/(app)/(dashboard)/published-contract/components/publish-based-deploy.tsx index c07ca050c05..ba8523cbc47 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/published-contract/components/publish-based-deploy.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/published-contract/components/publish-based-deploy.tsx @@ -10,6 +10,7 @@ import { ZERO_FEE_VERSIONS } from "@/constants/fee-config"; import { serverThirdwebClient } from "@/constants/thirdweb-client.server"; import { PublishedContractBreadcrumbs } from "../[publisher]/[contract_id]/components/breadcrumbs.client"; import { DeployContractHeader } from "./contract-header"; +import { TokenBanner } from "./token-banner"; import { DeployFormForUri } from "./uri-based-deploy"; type PublishBasedDeployProps = { @@ -95,6 +96,10 @@ export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) { ), ]); + const isTWPublisher = + contractMetadata?.publisher === + "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024"; + return (
@@ -111,6 +116,23 @@ export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) { />
+ {isTWPublisher && + (contractMetadata.name === "DropERC20" || + contractMetadata.name === "TokenERC20") && ( + + )} + {isTWPublisher && + (contractMetadata.name === "DropERC721" || + contractMetadata.name === "TokenERC721") && ( + + )} + + {isTWPublisher && + (contractMetadata.name === "DropERC1155" || + contractMetadata.name === "TokenERC1155") && ( + + )} +
+
+ New + + } + title={title} + className="container max-w-5xl mt-8" + preserveState={false} + description={description} + > + + + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/page.tsx index c2433fd65a4..ea03f658eca 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/page.tsx @@ -67,6 +67,7 @@ export default async function Page(props: { ) : ( void; }) { const teamAndProjects = props.teamAndProjects.filter( @@ -31,10 +31,14 @@ export function ProjectAndTeamSelectorCard(props: {
-

- Select a project -

-

{props.description}

+
+

+ Select a project +

+ {props.description && ( +

{props.description}

+ )} +
); } + +export function GenericProjectSelector(props: { + client: ThirdwebClient; + paths: string[] | undefined; + description: React.ReactNode | undefined; + teamAndProjects: { + team: Team; + projects: PartialProject[]; + }[]; +}) { + const router = useDashboardRouter(); + return ( + { + router.push( + `/team/${selection.team.slug}/${selection.project.slug}${props.paths?.length ? `/${props.paths.join("/")}` : ""}`, + ); + }} + teamAndProjects={props.teamAndProjects} + /> + ); +} diff --git a/apps/dashboard/src/app/(app)/team/~/[[...paths]]/components/team-selector.stories.tsx b/apps/dashboard/src/app/(app)/team/~/_components/team-selector.stories.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/~/[[...paths]]/components/team-selector.stories.tsx rename to apps/dashboard/src/app/(app)/team/~/_components/team-selector.stories.tsx diff --git a/apps/dashboard/src/app/(app)/team/~/[[...paths]]/components/team-selector.tsx b/apps/dashboard/src/app/(app)/team/~/_components/team-selector.tsx similarity index 100% rename from apps/dashboard/src/app/(app)/team/~/[[...paths]]/components/team-selector.tsx rename to apps/dashboard/src/app/(app)/team/~/_components/team-selector.tsx diff --git a/apps/dashboard/src/app/(app)/team/~/~/contract/[chain]/[contractAddress]/page.tsx b/apps/dashboard/src/app/(app)/team/~/~/contract/[chain]/[contractAddress]/page.tsx index 0902a37ffe0..41bfac4dcb2 100644 --- a/apps/dashboard/src/app/(app)/team/~/~/contract/[chain]/[contractAddress]/page.tsx +++ b/apps/dashboard/src/app/(app)/team/~/~/contract/[chain]/[contractAddress]/page.tsx @@ -17,7 +17,7 @@ import { TeamHeader } from "../../../../../components/TeamHeader/team-header"; import { ImportAndSelectProjectForContract, SelectProjectForContract, -} from "./components/project-selector"; +} from "../../../../_components/project-selector"; export default async function Page(props: { params: Promise<{ diff --git a/apps/dashboard/src/app/(app)/team/~/~project/[[...paths]]/page.tsx b/apps/dashboard/src/app/(app)/team/~/~project/[[...paths]]/page.tsx new file mode 100644 index 00000000000..fa5cf49c5bf --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/~/~project/[[...paths]]/page.tsx @@ -0,0 +1,76 @@ +import { getValidAccount } from "@/api/account/get-account"; +import { getAuthToken } from "@/api/auth-token"; +import { getProjects } from "@/api/project/projects"; +import { getTeams } from "@/api/team/get-team"; +import { AppFooter } from "@/components/footers/app-footer"; +import { DotsBackgroundPattern } from "@/components/ui/background-patterns"; +import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; +import { loginRedirect } from "@/utils/redirects"; +import { TeamHeader } from "../../../components/TeamHeader/team-header"; +import { GenericProjectSelector } from "../../_components/project-selector"; + +export default async function Page(props: { + params: Promise<{ + paths?: string[]; + }>; +}) { + const params = await props.params; + const pagePath = `/team/~/~${params.paths?.length ? `/${params.paths.join("/")}` : ""}`; + + const [authToken, account, teams] = await Promise.all([ + getAuthToken(), + await getValidAccount(pagePath), + getTeams(), + ]); + + if (!authToken || !account || !teams) { + loginRedirect(pagePath); + } + + const client = getClientThirdwebClient({ + jwt: authToken, + teamId: undefined, + }); + + const teamAndAllProjects = await Promise.all( + teams.map(async (team) => { + return { + projects: await getProjects(team.slug).catch(() => []), + team, + }; + }), + ); + + return ( + + + + ); +} + +function ProjectSelectionLayout(props: { children: React.ReactNode }) { + return ( +
+
+ +
+ +
+ +
+ {props.children} +
+
+ +
+ ); +}