Skip to content

Commit 3b1e16c

Browse files
committed
[TOOL-4621] New ERC20 public contract page (#7177)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR primarily focuses on enhancing the dashboard's functionality and user interface by updating environment variable usage, improving component layouts, and refining analytics features. It also introduces new components and modifies existing ones for better data handling and presentation. ### Detailed summary - Added `NEXT_PUBLIC_DASHBOARD_CLIENT_ID` to `public-envs.ts`. - Updated `CreateTokenAssetPage` to pass `teamSlug` and `projectSlug`. - Changed `projectSlug` to use `project.slug` in `marketplace` page. - Enhanced `EmptyChartState` to accept a `type` prop. - Updated `getTokenStepTrackingData` to include "deploy" action. - Introduced `LoadingDots` component for loading states. - Refactored multiple layout components to include `TeamHeader`. - Added new public page handling logic in various components. - Updated analytics functions to utilize the new client ID. - Improved error handling and response management in analytics API calls. - Added `DecimalInput` component to replace standard input in `token-sale.tsx`. > The following files were skipped due to too many changes: `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-layout.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractHeader.stories.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/erc20.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/Analytics.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/RecentTransfers.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractHeader.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/PriceChart.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/claim-tokens/claim-tokens-ui.tsx` > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added new layout components with team headers for various dashboard sections. - Introduced `LoadingDots` component for animated loading indicators. - Added `PublicPageConnectButton` and `PageHeader` components for public pages. - Implemented public ERC-20 token page with price charts, token stats, recent transfers, and buy token embed. - Added token claim card UI with transaction step feedback for purchasing tokens. - Added hooks for fetching token price data and token transfers. - Added utility to fetch currency metadata for tokens. - Added comprehensive contract analytics overview component. - Introduced `DecimalInput` component for validated decimal input handling. - Added new social media brand icons for Discord and Telegram. - **Improvements** - Enhanced area and bar charts with customizable styles, tooltip formatting, and explicit empty state types. - Improved empty chart state visuals with new area chart gradients and replaced spinner with animated dots. - Unified environment variable usage for client IDs across API calls and client initializations. - Updated contract overview to display call-to-action banner linking to public asset pages for ERC-20 tokens. - Enhanced contract table to conditionally show contract address or asset page links based on context. - Improved primary dashboard button to link to asset pages when applicable. - Added redirects to public landing pages for unsupported contract views lacking project metadata. - Wrapped existing layouts with conditional team headers for consistent UI in public/private views. - Added pagination and detailed tables for recent token transfers. - Updated project slug usage in marketplace listings for accuracy. - Introduced dynamic time precision handling in analytics charts and tooltips. - Added optional balance check control to transaction and mismatch buttons. - Replaced local decimal input component with shared validated decimal input component. - Added optional target attribute for call-to-action links in upsell banners. - Refined accessibility and sizing of icon components. - **Bug Fixes** - Fixed environment variable naming for client IDs to ensure consistent API integration. - **Refactor** - Standardized header and layout components for a unified dashboard experience. - Replaced spinner with animated dots in loading states for smoother UX. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 1f42ad2 commit 3b1e16c

File tree

78 files changed

+2820
-244
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+2820
-244
lines changed

apps/dashboard/src/@/actions/getWalletNFTs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { WalletNFT } from "lib/wallet/nfts/types";
1212
import { getVercelEnv } from "../../lib/vercel-utils";
1313
import { isAlchemySupported } from "../../lib/wallet/nfts/isAlchemySupported";
1414
import { isMoralisSupported } from "../../lib/wallet/nfts/isMoralisSupported";
15-
import { NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID } from "../constants/public-envs";
15+
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "../constants/public-envs";
1616
import { MORALIS_API_KEY } from "../constants/server-envs";
1717

1818
type WalletNFTApiReturn =
@@ -149,7 +149,7 @@ async function getWalletNFTsFromInsight(params: {
149149

150150
const response = await fetch(url, {
151151
headers: {
152-
"x-client-id": NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID,
152+
"x-client-id": NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
153153
},
154154
});
155155

apps/dashboard/src/@/components/blocks/UpsellBannerCard.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type UpsellBannerCardProps = {
4141
cta: {
4242
text: React.ReactNode;
4343
icon?: React.ReactNode;
44+
target?: "_blank";
4445
link: string;
4546
};
4647
trackingCategory: string;
@@ -55,7 +56,7 @@ export function UpsellBannerCard(props: UpsellBannerCardProps) {
5556
return (
5657
<div
5758
className={cn(
58-
"relative overflow-hidden rounded-lg border bg-gradient-to-r p-5",
59+
"relative overflow-hidden rounded-lg border bg-gradient-to-r p-4 lg:p-6",
5960
color.border,
6061
color.bgFrom,
6162
)}
@@ -73,7 +74,7 @@ export function UpsellBannerCard(props: UpsellBannerCardProps) {
7374
{props.icon ? (
7475
<div
7576
className={cn(
76-
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-full",
77+
"mt-0.5 hidden h-9 w-9 shrink-0 items-center justify-center rounded-full lg:flex",
7778
color.iconBg,
7879
)}
7980
>
@@ -90,9 +91,7 @@ export function UpsellBannerCard(props: UpsellBannerCardProps) {
9091
>
9192
{props.title}
9293
</h3>
93-
<p className={cn("mt-0.5 text-sm", color.desc)}>
94-
{props.description}
95-
</p>
94+
<p className={cn("text-sm", color.desc)}>{props.description}</p>
9695
</div>
9796
</div>
9897

@@ -108,6 +107,7 @@ export function UpsellBannerCard(props: UpsellBannerCardProps) {
108107
href={props.cta.link}
109108
category={props.trackingCategory}
110109
label={props.trackingLabel}
110+
target={props.cta.target}
111111
>
112112
{props.cta.text}
113113
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}

apps/dashboard/src/@/components/blocks/charts/area-chart.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
4646
// chart className
4747
chartClassName?: string;
4848
isPending: boolean;
49+
className?: string;
50+
cardContentClassName?: string;
4951
hideLabel?: boolean;
5052
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
53+
toolTipValueFormatter?: (value: unknown) => React.ReactNode;
54+
emptyChartState?: React.ReactElement;
5155
};
5256

5357
export function ThirdwebAreaChart<TConfig extends ChartConfig>(
@@ -56,7 +60,7 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
5660
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
5761

5862
return (
59-
<Card>
63+
<Card className={props.className}>
6064
{props.header && (
6165
<CardHeader>
6266
<CardTitle className={cn("mb-2", props.header.titleClassName)}>
@@ -70,12 +74,16 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
7074

7175
{props.customHeader && props.customHeader}
7276

73-
<CardContent className={cn(!props.header && "pt-6")}>
77+
<CardContent
78+
className={cn(!props.header && "pt-6", props.cardContentClassName)}
79+
>
7480
<ChartContainer config={props.config} className={props.chartClassName}>
7581
{props.isPending ? (
7682
<LoadingChartState />
7783
) : props.data.length === 0 ? (
78-
<EmptyChartState />
84+
<EmptyChartState type="area">
85+
{props.emptyChartState}
86+
</EmptyChartState>
7987
) : (
8088
<AreaChart accessibilityLayer data={props.data}>
8189
<CartesianGrid vertical={false} />
@@ -100,6 +108,7 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
100108
props.hideLabel !== undefined ? props.hideLabel : true
101109
}
102110
labelFormatter={props.toolTipLabelFormatter}
111+
valueFormatter={props.toolTipValueFormatter}
103112
/>
104113
}
105114
/>

apps/dashboard/src/@/components/blocks/charts/bar-chart.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
7575
{props.isPending ? (
7676
<LoadingChartState />
7777
) : props.data.length === 0 ? (
78-
<EmptyChartState>{props.emptyChartState}</EmptyChartState>
78+
<EmptyChartState type="bar">
79+
{props.emptyChartState}
80+
</EmptyChartState>
7981
) : (
8082
<BarChart accessibilityLayer data={props.data}>
8183
<CartesianGrid vertical={false} />
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export function LoadingDots() {
2+
return (
3+
<div className="fade-in-0 flex animate-in gap-2 duration-300">
4+
<span className="sr-only">Loading...</span>
5+
<div className="size-4 animate-bounce rounded-full bg-foreground [animation-delay:-0.3s]" />
6+
<div className="size-4 animate-bounce rounded-full bg-foreground [animation-delay:-0.15s]" />
7+
<div className="size-4 animate-bounce rounded-full bg-foreground" />
8+
</div>
9+
);
10+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Input } from "./input";
2+
export function DecimalInput(props: {
3+
value: string;
4+
onChange: (value: string) => void;
5+
maxValue?: number;
6+
id?: string;
7+
className?: string;
8+
}) {
9+
return (
10+
<Input
11+
id={props.id}
12+
type="text"
13+
value={props.value}
14+
className={props.className}
15+
inputMode="decimal"
16+
onChange={(e) => {
17+
const number = Number(e.target.value);
18+
// ignore if string becomes invalid number
19+
if (Number.isNaN(number)) {
20+
return;
21+
}
22+
if (props.maxValue && number > props.maxValue) {
23+
return;
24+
}
25+
// replace leading multiple zeros with single zero
26+
let cleanedValue = e.target.value.replace(/^0+/, "0");
27+
// replace leading zero before decimal point
28+
if (!cleanedValue.includes(".")) {
29+
cleanedValue = cleanedValue.replace(/^0+/, "");
30+
}
31+
props.onChange(cleanedValue || "0");
32+
}}
33+
/>
34+
);
35+
}

apps/dashboard/src/@/constants/public-envs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID =
1+
export const NEXT_PUBLIC_DASHBOARD_CLIENT_ID =
22
process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID || "";
33

44
export const NEXT_PUBLIC_NEBULA_APP_CLIENT_ID =

apps/dashboard/src/@/constants/thirdweb.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID,
2+
NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
33
NEXT_PUBLIC_IPFS_GATEWAY_URL,
44
} from "@/constants/public-envs";
55
import {
@@ -76,7 +76,7 @@ export function getConfiguredThirdwebClient(options: {
7676
return createThirdwebClient({
7777
teamId: options.teamId,
7878
secretKey: options.secretKey,
79-
clientId: NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID,
79+
clientId: NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
8080
config: {
8181
storage: {
8282
gatewayUrl: NEXT_PUBLIC_IPFS_GATEWAY_URL,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { TeamHeader } from "../../team/components/TeamHeader/team-header";
2+
3+
export default function Layout({
4+
children,
5+
}: {
6+
children: React.ReactNode;
7+
}) {
8+
return (
9+
<div className="flex grow flex-col">
10+
<div className="border-border border-b bg-card">
11+
<TeamHeader />
12+
</div>
13+
{children}
14+
</div>
15+
);
16+
}

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/live-stats.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { CopyTextButton } from "@/components/ui/CopyTextButton";
44
import { Skeleton } from "@/components/ui/skeleton";
55
import { ToolTipLabel } from "@/components/ui/tooltip";
66
import { isProd } from "@/constants/env-utils";
7-
import { NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID } from "@/constants/public-envs";
7+
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
88
import { useQuery } from "@tanstack/react-query";
99
import { CircleCheckIcon, XIcon } from "lucide-react";
1010
import { hostnameEndsWith } from "utils/url";
@@ -14,7 +14,7 @@ function useChainStatswithRPC(_rpcUrl: string) {
1414
let rpcUrl = _rpcUrl.replace(
1515
// eslint-disable-next-line no-template-curly-in-string
1616
"${THIRDWEB_API_KEY}",
17-
NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID,
17+
NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
1818
);
1919

2020
// based on the environment hit dev or production

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
getAuthToken,
2525
getAuthTokenWalletAddress,
2626
} from "../../../../api/lib/getAuthToken";
27+
import { TeamHeader } from "../../../../team/components/TeamHeader/team-header";
2728
import { StarButton } from "../../components/client/star-button";
2829
import { getChain, getChainMetadata } from "../../utils";
2930
import { AddChainToWallet } from "./components/client/add-chain-to-wallet";
@@ -95,7 +96,10 @@ The following is the user's message:
9596
}
9697

9798
return (
98-
<>
99+
<div className="flex grow flex-col">
100+
<div className="border-border border-b bg-card">
101+
<TeamHeader />
102+
</div>
99103
<NebulaChatButton
100104
isLoggedIn={!!authToken}
101105
networks={chain.testnet ? "testnet" : "mainnet"}
@@ -225,7 +229,7 @@ The following is the user's message:
225229
{children}
226230
</div>
227231
</div>
228-
</>
232+
</div>
229233
);
230234
}
231235

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

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -82,21 +82,19 @@ export const MetadataHeader: React.FC<MetadataHeaderProps> = ({
8282
</h1>
8383
)}
8484

85-
{chain && (
86-
<Link
87-
href={`/${chain.slug}`}
88-
className="flex w-fit shrink-0 items-center gap-2 rounded-3xl border border-border bg-card px-2.5 py-1.5 hover:bg-accent"
89-
>
90-
<ChainIconClient
91-
src={chain.icon?.url}
92-
client={client}
93-
className="size-4"
94-
/>
95-
{cleanedChainName && (
96-
<span className="text-xs">{cleanedChainName}</span>
97-
)}
98-
</Link>
99-
)}
85+
<Link
86+
href={`/${chain.slug}`}
87+
className="flex w-fit shrink-0 items-center gap-2 rounded-3xl border border-border bg-card px-2.5 py-1.5 hover:bg-accent"
88+
>
89+
<ChainIconClient
90+
src={chain.icon?.url}
91+
client={client}
92+
className="size-4"
93+
/>
94+
{cleanedChainName && (
95+
<span className="text-xs">{cleanedChainName}</span>
96+
)}
97+
</Link>
10098
</div>
10199
)}
102100

@@ -115,7 +113,7 @@ export const MetadataHeader: React.FC<MetadataHeaderProps> = ({
115113
<CopyAddressButton
116114
address={address}
117115
copyIconPosition="left"
118-
className="bg-card text-xs"
116+
className="bg-card px-2.5 py-1.5 text-xs"
119117
variant="outline"
120118
/>
121119

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,17 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
8181

8282
// if user is on a project page
8383
if (projectMeta) {
84-
return null;
84+
return (
85+
<Button variant="default" asChild>
86+
<Link
87+
href={`/${contractInfo.chainSlug}/${contractAddress}`}
88+
target="_blank"
89+
className="gap-2"
90+
>
91+
View Asset Page <ExternalLinkIcon className="size-3.5" />
92+
</Link>
93+
</Button>
94+
);
8595
}
8696

8797
return (
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { resolveFunctionSelectors } from "lib/selectors";
2+
import type { ThirdwebContract } from "thirdweb";
3+
import { isERC20 } from "thirdweb/extensions/erc20";
4+
5+
export type NewPublicPageType = "erc20";
6+
7+
export async function shouldRenderNewPublicPage(
8+
contract: ThirdwebContract,
9+
): Promise<false | { type: NewPublicPageType }> {
10+
try {
11+
const functionSelectors = await resolveFunctionSelectors(contract).catch(
12+
() => undefined,
13+
);
14+
15+
if (!functionSelectors) {
16+
return false;
17+
}
18+
19+
const isERC20Contract = isERC20(functionSelectors);
20+
21+
if (isERC20Contract) {
22+
return {
23+
type: "erc20",
24+
};
25+
}
26+
27+
return false;
28+
} catch {
29+
return false;
30+
}
31+
}

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/analytics/shared-analytics-page.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]
55
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
66
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
77
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
8+
import { shouldRenderNewPublicPage } from "../_utils/newPublicPage";
89
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
910

1011
export async function SharedAnalyticsPage(props: {
@@ -22,6 +23,18 @@ export async function SharedAnalyticsPage(props: {
2223
notFound();
2324
}
2425

26+
// new public page can't show /analytics page
27+
if (!props.projectMeta) {
28+
const shouldHide = await shouldRenderNewPublicPage(info.serverContract);
29+
if (shouldHide) {
30+
redirectToContractLandingPage({
31+
contractAddress: props.contractAddress,
32+
chainIdOrSlug: props.chainIdOrSlug,
33+
projectMeta: props.projectMeta,
34+
});
35+
}
36+
}
37+
2538
const [
2639
{ eventSelectorToName, writeFnSelectorToName },
2740
{ isInsightSupported },

0 commit comments

Comments
 (0)