From d812cc370b9b696e6ae08c04662792206c72ed75 Mon Sep 17 00:00:00 2001 From: MananTank Date: Thu, 7 Aug 2025 20:45:31 +0000 Subject: [PATCH] [MNY-92] Dashboard: Add tokens section in explore (#7812) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on improving the project selection and token creation flow within the dashboard application. It includes the addition of new components, updates to existing ones, and enhancements to user notifications and alerts. ### Detailed summary - Updated import paths for `project-selector` and `team-selector`. - Added `TokenBanner` component for displaying token creation options. - Enhanced `DismissibleAlert` to support conditional rendering based on visibility state. - Introduced `GenericProjectSelector` for improved project selection. - Modified `ExplorePage` layout and styling for better presentation. - Added `TokensSection` component with links for launching tokens and NFTs. - Updated `reportTokenUpsellClicked` function for event tracking. - Improved UI elements for better user experience and responsiveness. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit * **New Features** * Introduced a "Tokens" section on the Explore page, showcasing options for creating ERC-20 tokens (Coins) and NFT Collections. * Added a promotional token banner on deploy pages for select published token contracts. * Launched a new project selection page within the team context, streamlining project navigation and selection. * Added a generic project selector component to enhance project selection flexibility. * Added tracking for clicks on token creation cards and token upsell banner interactions to improve analytics. * **Improvements** * Enhanced layout and styling of the Explore page for better readability and organization. * Updated project and team selector components for more flexible descriptions and improved navigation. * Updated team page alert to preserve dismissal state across sessions. * **Chores** * Refined import paths for improved code organization and maintainability. * Refactored dismissible alert component to support optional state persistence. --- apps/dashboard/src/@/analytics/report.ts | 14 +++ .../@/components/blocks/dismissible-alert.tsx | 82 +++++++++++-- .../app/(app)/(dashboard)/explore/page.tsx | 13 +- .../(dashboard)/explore/tokens-section.tsx | 111 ++++++++++++++++++ .../components/publish-based-deploy.tsx | 22 ++++ .../components/token-banner.tsx | 63 ++++++++++ .../(app)/team/[team_slug]/(team)/page.tsx | 1 + .../app/(app)/team/~/[[...paths]]/page.tsx | 2 +- .../TeamAndProjectSelectorCard.stories.tsx | 0 .../TeamAndProjectSelectorCard.tsx | 14 ++- .../project-selector.tsx | 26 +++- .../team-selector.stories.tsx | 0 .../team-selector.tsx | 0 .../[chain]/[contractAddress]/page.tsx | 2 +- .../team/~/~project/[[...paths]]/page.tsx | 76 ++++++++++++ 15 files changed, 401 insertions(+), 25 deletions(-) create mode 100644 apps/dashboard/src/app/(app)/(dashboard)/explore/tokens-section.tsx create mode 100644 apps/dashboard/src/app/(app)/(dashboard)/published-contract/components/token-banner.tsx rename apps/dashboard/src/app/(app)/team/~/{components => _components}/TeamAndProjectSelectorCard.stories.tsx (100%) rename apps/dashboard/src/app/(app)/team/~/{components => _components}/TeamAndProjectSelectorCard.tsx (92%) rename apps/dashboard/src/app/(app)/team/~/{~/contract/[chain]/[contractAddress]/components => _components}/project-selector.tsx (75%) rename apps/dashboard/src/app/(app)/team/~/{[[...paths]]/components => _components}/team-selector.stories.tsx (100%) rename apps/dashboard/src/app/(app)/team/~/{[[...paths]]/components => _components}/team-selector.tsx (100%) create mode 100644 apps/dashboard/src/app/(app)/team/~/~project/[[...paths]]/page.tsx 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} +
+
+ +
+ ); +}