From cdc7d95db285592328c83a12909ea290a6e7f04d Mon Sep 17 00:00:00 2001 From: MananTank Date: Fri, 11 Jul 2025 23:48:53 +0000 Subject: [PATCH] [TOOL-4959] Dashboard: Asset page UI improvements (#7599) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on refining the UI components and enhancing the functionality of the `ERC20` public page. It includes improvements in layout, styling, and the introduction of new features related to token price data and analytics. ### Detailed summary - Deleted unused files: `useTokenPriceData.ts` and `ContractCreatorBadge.tsx`. - Updated `PageHeader` to change border style. - Added `tooltip` prop in `CopyAddressButton`. - Changed API key usage in `fetchTokenInfoFromBridge`. - Enhanced `ThirdwebAreaChart` with better props handling. - Introduced `GridPattern` component for background. - Improved `RecentTransfers` UI and pagination buttons. - Updated `PriceChart` and `TokenStats` to directly use passed props for token price data. - Refactored `ContractHeaderUI` to improve layout and social link handling. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit * **New Features** * Introduced a customizable grid background pattern component for enhanced visual design. * Added support for displaying token price data and analytics on ERC20 public pages, including conditional rendering of token statistics. * Enabled tooltip customization for the copy address button. * **Bug Fixes** * The contract creator badge icon is now visible, and spacing between elements has been improved. * **Refactor** * Simplified and unified chart headers and analytics section layouts. * Streamlined fetching and handling of token price data with a new asynchronous function and removed the previous hook. * Improved layout and container styling across ERC20 and NFT public pages, including card backgrounds, borders, and spacing. * Adjusted component structures for better visual separation and consistency. * Removed the contract creator badge component and replaced it with a simpler contract owner link. * **Style** * Updated card and header styles for better visual hierarchy and clarity. * Enhanced recent transfers and contract header sections with improved padding, borders, and rounded corners. * Removed small shadow effects from cards and refined header background and border styles. --- .../@/components/blocks/charts/area-chart.tsx | 20 +- .../src/@/components/ui/CopyAddressButton.tsx | 3 +- .../@/components/ui/background-patterns.tsx | 68 +++++++ apps/dashboard/src/@/components/ui/card.tsx | 2 +- .../public-pages/_components/PageHeader.tsx | 2 +- .../erc20/_apis/token-price-data.ts | 51 +++++ .../_components/ContractCreatorBadge.tsx | 19 -- .../erc20/_components/ContractHeader.tsx | 114 +++++++----- .../erc20/_components/PriceChart.tsx | 175 +++++++----------- .../erc20/_components/RecentTransfers.tsx | 44 +++-- .../contract-analytics/contract-analytics.tsx | 66 +++---- .../erc20/_hooks/useTokenPriceData.ts | 56 ------ .../erc20/_utils/fetch-coin-info.ts | 4 +- .../public-pages/erc20/erc20.tsx | 106 ++++++----- .../public-pages/nft/nft-page-layout.tsx | 41 ++-- 15 files changed, 410 insertions(+), 361 deletions(-) create mode 100644 apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_apis/token-price-data.ts delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractCreatorBadge.tsx delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_hooks/useTokenPriceData.ts diff --git a/apps/dashboard/src/@/components/blocks/charts/area-chart.tsx b/apps/dashboard/src/@/components/blocks/charts/area-chart.tsx index afc6a22d283..d22a737fd5c 100644 --- a/apps/dashboard/src/@/components/blocks/charts/area-chart.tsx +++ b/apps/dashboard/src/@/components/blocks/charts/area-chart.tsx @@ -29,6 +29,7 @@ type ThirdwebAreaChartProps = { title: string; description?: string; titleClassName?: string; + headerClassName?: string; }; customHeader?: React.ReactNode; // chart config @@ -52,6 +53,12 @@ type ThirdwebAreaChartProps = { toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode; toolTipValueFormatter?: (value: unknown) => React.ReactNode; emptyChartState?: React.ReactElement; + margin?: { + top?: number; + right?: number; + bottom?: number; + left?: number; + }; }; export function ThirdwebAreaChart( @@ -62,7 +69,7 @@ export function ThirdwebAreaChart( return ( {props.header && ( - + {props.header.title} @@ -85,7 +92,16 @@ export function ThirdwebAreaChart( {props.emptyChartState} ) : ( - + {props.yAxis && } ); diff --git a/apps/dashboard/src/@/components/ui/background-patterns.tsx b/apps/dashboard/src/@/components/ui/background-patterns.tsx index e9d0cbd0484..dce537223b1 100644 --- a/apps/dashboard/src/@/components/ui/background-patterns.tsx +++ b/apps/dashboard/src/@/components/ui/background-patterns.tsx @@ -1,3 +1,4 @@ +import { useId } from "react"; import { cn } from "@/lib/utils"; export function DotsBackgroundPattern(props: { className?: string }) { @@ -16,3 +17,70 @@ export function DotsBackgroundPattern(props: { className?: string }) { /> ); } + +interface GridPatternProps extends React.SVGProps { + width?: number; + height?: number; + x?: number; + y?: number; + squares?: Array<[x: number, y: number]>; + strokeDasharray?: string; + className?: string; + [key: string]: unknown; +} + +export function GridPattern({ + width = 40, + height = 40, + x = -1, + y = -1, + strokeDasharray = "0", + squares, + className, + ...props +}: GridPatternProps) { + const id = useId(); + + return ( + + ); +} diff --git a/apps/dashboard/src/@/components/ui/card.tsx b/apps/dashboard/src/@/components/ui/card.tsx index 4927d2439e0..ae7a0598ea6 100644 --- a/apps/dashboard/src/@/components/ui/card.tsx +++ b/apps/dashboard/src/@/components/ui/card.tsx @@ -8,7 +8,7 @@ const Card = React.forwardRef< >(({ className, ...props }, ref) => (
+
; +}; + +export async function getTokenPriceData(params: { + chainId: number; + contractAddress: string; +}) { + try { + const url = new URL( + `https://insight.${isProd ? "thirdweb" : "thirdweb-dev"}.com/v1/tokens/price`, + ); + + url.searchParams.set("include_historical_prices", "true"); + url.searchParams.set("chain_id", params.chainId.toString()); + url.searchParams.set("address", params.contractAddress); + url.searchParams.set("include_holders", "true"); + + const res = await fetch(url, { + headers: { + "x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY, + }, + }); + if (!res.ok) { + console.error("Failed to fetch token price data", await res.text()); + return undefined; + } + + const json = await res.json(); + const priceData = json.data[0] as TokenPriceData | undefined; + + return priceData; + } catch { + return undefined; + } +} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractCreatorBadge.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractCreatorBadge.tsx deleted file mode 100644 index 7bef42d4f05..00000000000 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractCreatorBadge.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { ThirdwebContract } from "thirdweb"; -import { WalletAddress } from "@/components/blocks/wallet-address"; - -export function ContractCreatorBadge(props: { - contractCreator: string; - clientContract: ThirdwebContract; -}) { - return ( -
- By - -
- ); -} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractHeader.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractHeader.tsx index 28bb90f2014..0c926fa4a3a 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractHeader.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractHeader.tsx @@ -1,4 +1,9 @@ -import { ExternalLinkIcon, GlobeIcon, Settings2Icon } from "lucide-react"; +import { + ExternalLinkIcon, + GlobeIcon, + Settings2Icon, + UserIcon, +} from "lucide-react"; import Link from "next/link"; import { useMemo } from "react"; import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb"; @@ -19,7 +24,6 @@ import { YoutubeIcon } from "@/icons/brand-icons/YoutubeIcon"; import { ChainIconClient } from "@/icons/ChainIcon"; import { cn } from "@/lib/utils"; import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler"; -import { ContractCreatorBadge } from "./ContractCreatorBadge"; const platformToIcons: Record> = { discord: DiscordIcon, @@ -44,6 +48,7 @@ export function ContractHeaderUI(props: { socialUrls: object; imageClassName?: string; contractCreator: string | null; + className?: string; }) { const socialUrls = useMemo(() => { const socialUrlsValue: { name: string; href: string }[] = []; @@ -64,41 +69,45 @@ export function ContractHeaderUI(props: { ?.replace("Mainnet", "") .trim(); - const explorersToShow = getExplorersToShow(props.chainMetadata); + const validBlockExplorer = getExplorerToShow(props.chainMetadata); return ( -
- {props.image && ( - - {props.name[0]} -
- } - src={ - props.image - ? resolveSchemeWithErrorHandler({ - client: props.clientContract.client, - uri: props.image, - }) - : "" - } - /> +
+
+
+ + {props.name[0]} +
+ } + src={ + props.image + ? resolveSchemeWithErrorHandler({ + client: props.clientContract.client, + uri: props.image, + }) + : "" + } + /> +
+
-
+
{/* top row */}
-

+

{props.name}

-
+
- + {props.contractCreator && + validBlockExplorer && + props.contractCreator !== ZERO_ADDRESS && ( + + )} {socialUrls .toSorted((a, b) => { @@ -148,13 +160,14 @@ export function ContractHeaderUI(props: {
{/* bottom row */} -
- {props.contractCreator && props.contractCreator !== ZERO_ADDRESS && ( - - )} +
+ - {explorersToShow?.map((validBlockExplorer) => ( + {validBlockExplorer && ( - ))} + )} {/* TODO - render social links here */}
@@ -204,19 +217,22 @@ function isValidUrl(url: string) { } } -function getExplorersToShow(chainMetadata: ChainMetadata) { - const validBlockExplorers = chainMetadata.explorers - ?.filter((e) => e.standard === "EIP3091") - ?.slice(0, 2); +function getExplorerToShow(chainMetadata: ChainMetadata) { + const validBlockExplorers = chainMetadata.explorers?.filter( + (e) => e.standard === "EIP3091", + ); - return validBlockExplorers?.slice(0, 1); + return validBlockExplorers?.[0]; } -function BadgeLink(props: { name: string; href: string }) { +function BadgeLink(props: { name: string; href: string; className?: string }) { return (