From 367a25ca5a8fc818f0e269bd56a6ea8ff3f5927f Mon Sep 17 00:00:00 2001 From: MananTank Date: Fri, 25 Jul 2025 01:13:51 +0000 Subject: [PATCH] Dashboard: Migrate Explorer components from chakra to tailwind (#7707) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on improving the UI components and functionality of the `ContractExplorerPage` and related components in the dashboard by refining layout, enhancing accessibility, and optimizing code structure. ### Detailed summary - Updated layout for `ContractExplorerPage` to enhance UI consistency. - Improved header elements for better accessibility. - Refined tab structure in `ContractFunctionsOverview` for better navigation. - Enhanced styling of buttons and inputs for a more modern look. - Replaced `Card` components with simpler layouts where applicable. - Introduced new utility functions for class name management. - Optimized the handling of function inputs and outputs in `InteractiveAbiFunction`. - Adjusted the rendering of function and event lists for improved clarity. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- .../@/components/blocks/FormFieldSetup.tsx | 14 +- .../blocks/code/code-segment.client.tsx | 8 +- .../functions/contract-function-comment.tsx | 11 +- .../contracts/functions/contract-function.tsx | 443 +++++++----------- .../functions/contract-functions.tsx | 175 ++++--- .../functions/interactive-abi-function.tsx | 242 +++++----- .../contracts/published-contract/index.tsx | 16 +- .../components/solidity-inputs/bool-input.tsx | 6 +- .../explorer/ContractExplorerPage.tsx | 2 +- 9 files changed, 394 insertions(+), 523 deletions(-) diff --git a/apps/dashboard/src/@/components/blocks/FormFieldSetup.tsx b/apps/dashboard/src/@/components/blocks/FormFieldSetup.tsx index d1cdfdfee71..ce1871d3fe0 100644 --- a/apps/dashboard/src/@/components/blocks/FormFieldSetup.tsx +++ b/apps/dashboard/src/@/components/blocks/FormFieldSetup.tsx @@ -2,6 +2,7 @@ import { AsteriskIcon, InfoIcon } from "lucide-react"; import type React from "react"; import { Label } from "@/components/ui/label"; import { ToolTipLabel } from "@/components/ui/tooltip"; +import { cn } from "../../lib/utils"; export function FormFieldSetup(props: { htmlFor?: string; @@ -10,13 +11,22 @@ export function FormFieldSetup(props: { children: React.ReactNode; tooltip?: React.ReactNode; isRequired: boolean; + labelClassName?: string; + labelContainerClassName?: string; helperText?: React.ReactNode; className?: string; }) { return (
-
- +
+ {props.isRequired && ( diff --git a/apps/dashboard/src/@/components/blocks/code/code-segment.client.tsx b/apps/dashboard/src/@/components/blocks/code/code-segment.client.tsx index ea826517cab..78d2f4f14ad 100644 --- a/apps/dashboard/src/@/components/blocks/code/code-segment.client.tsx +++ b/apps/dashboard/src/@/components/blocks/code/code-segment.client.tsx @@ -3,6 +3,7 @@ import type React from "react"; import { type Dispatch, type SetStateAction, useMemo } from "react"; import { CodeClient } from "@/components/ui/code/code.client"; import { TabButtons } from "@/components/ui/tabs"; +import { cn } from "@/lib/utils"; export type CodeEnvironment = | "javascript" @@ -50,6 +51,7 @@ interface CodeSegmentProps { isInstallCommand?: boolean; hideTabs?: boolean; onlyTabs?: boolean; + codeContainerClassName?: string; } export const CodeSegment: React.FC = ({ @@ -59,6 +61,7 @@ export const CodeSegment: React.FC = ({ isInstallCommand, hideTabs, onlyTabs, + codeContainerClassName, }) => { const activeEnvironment: CodeEnvironment = useMemo(() => { return ( @@ -106,7 +109,10 @@ export const CodeSegment: React.FC = ({ {onlyTabs ? null : ( -

+

+

About this function Beta -

+

- +
); } diff --git a/apps/dashboard/src/@/components/contracts/functions/contract-function.tsx b/apps/dashboard/src/@/components/contracts/functions/contract-function.tsx index 409cbca40b5..ddf50ba5d71 100644 --- a/apps/dashboard/src/@/components/contracts/functions/contract-function.tsx +++ b/apps/dashboard/src/@/components/contracts/functions/contract-function.tsx @@ -1,32 +1,7 @@ "use client"; -import { - Box, - Divider, - Flex, - GridItem, - Image, - List, - SimpleGrid, - Tab, - TabList, - Table, - TabPanel, - TabPanels, - Tabs, - Tbody, - Td, - Th, - Thead, - Tr, -} from "@chakra-ui/react"; import type { AbiEvent, AbiFunction } from "abitype"; -import { Button } from "chakra/button"; -import { Card } from "chakra/card"; -import { Heading } from "chakra/heading"; -import { Text } from "chakra/text"; import { SearchIcon } from "lucide-react"; -import { usePathname, useSearchParams } from "next/navigation"; import { type Dispatch, lazy, @@ -45,12 +20,21 @@ import { type CodeEnvironment, CodeSegment, } from "@/components/blocks/code/code-segment.client"; -import { camelToTitle } from "@/components/solidity-inputs/helpers"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { CopyTextButton } from "@/components/ui/CopyTextButton"; import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { TabButtons } from "@/components/ui/tabs"; import { useContractFunctionSelectors } from "@/hooks/contract-ui/useContractFunctionSelectors"; -import { useDashboardRouter } from "@/lib/DashboardRouter"; import { cn } from "@/lib/utils"; import { COMMANDS, formatSnippet } from "../code-overview"; import { InteractiveAbiFunction } from "./interactive-abi-function"; @@ -137,25 +121,21 @@ function ContractFunctionInner(props: ContractFunctionProps) { }); return ( - - - - {camelToTitle(fn.name)} - - ({fn.name}) - - +
+
+

{fn.name}

{isFunction && {fn.stateMutability}} {functionSelector && ( )} - +
+ {isFunction && ( - +
+

Use this function in your app - - +

- +
)} - +
); } @@ -188,84 +168,46 @@ function ContractFunctionInputs(props: { fn: AbiFunction | AbiEvent }) { const isFunction = "stateMutability" in fn; return ( - - - - {camelToTitle(fn.name)} - - ({fn.name}) - - +
+
+

{fn.name}

{isFunction && {fn.stateMutability}} - +
{fn.inputs?.length ? ( - <> - - - Inputs - - - - - - - - - - {fn.inputs.map((input, idx) => ( - - - - - ))} - -
- - Name - - - - Type - -
- {input?.name ? ( - {input.name} - ) : ( - - No name defined - - )} - - {input.type} -
-
-
- +
+

Inputs

+ + + + + Name + Type + + + + {fn.inputs.map((input, idx) => ( + + + {input?.name ? ( + {input.name} + ) : ( + + No name defined + + )} + + + {input.type} + + + ))} + +
+
+
) : null} - +
); } @@ -296,6 +238,7 @@ export const ContractFunctionsPanel: React.FC = ({ }); return results; }, [fnsOrEvents]); + const writeFunctions: ExtensionFunctions[] = useMemo(() => { return functionsWithExtension .map((e) => { @@ -313,6 +256,7 @@ export const ContractFunctionsPanel: React.FC = ({ }) .filter((e) => e !== undefined) as ExtensionFunctions[]; }, [functionsWithExtension]); + const viewFunctions: ExtensionFunctions[] = useMemo(() => { return functionsWithExtension .map((e) => { @@ -336,166 +280,109 @@ export const ContractFunctionsPanel: React.FC = ({ return fnsOrEvents.filter((fn) => !("stateMutability" in fn)) as AbiEvent[]; }, [fnsOrEvents]); - // Load state from the URL - const searchParams = useSearchParams(); - const _selector = searchParams?.get("selector"); - const _item = _selector - ? fnsOrEvents.find((o) => { - if (o.type === "function") { - const selector = toFunctionSelector(o as AbiFunction); - return selector === _selector; - } - return null; - }) - : undefined; - const [selectedFunction, setSelectedFunction] = useState< AbiFunction | AbiEvent | undefined - >(_item ?? fnsOrEvents[0]); - // Set the active tab to Write or Read depends on the `_item` - const _defaultTabIndex = - _item && - "stateMutability" in _item && - (_item.stateMutability === "view" || _item.stateMutability === "pure") - ? 1 - : 0; + >(fnsOrEvents[0]); const [_keywordSearch, setKeywordSearch] = useState(""); const [keywordSearch] = useDebounce(_keywordSearch, 150); + const [activeTab, setActiveTab] = useState(0); + const functionSection = (e: ExtensionFunctions) => { const filteredFunctions = keywordSearch ? e.functions.filter((o) => o.name.toLowerCase().includes(keywordSearch.toLowerCase()), ) : e.functions; + return ( - - {e.extension ? ( - <> - - Extension detected +
+ {selectedFunction && + filteredFunctions.map((fn) => ( + - - {e.extension} - - - - - ) : ( - <> - - - Other Functions - - - - - )} - {selectedFunction && - filteredFunctions.map((fn) => ( - - ))} - + ))} +
+
); }; return ( - - - - {(writeFunctions.length > 0 || viewFunctions.length > 0) && ( - - - {writeFunctions.length > 0 && ( - - - Write - - - )} - {viewFunctions.length > 0 && ( - - - Read - - - )} - - -
-
- - setKeywordSearch(e.target.value)} - placeholder="Search" - value={_keywordSearch} - /> -
-
+
+ {/* left */} +
+ {(writeFunctions.length > 0 || viewFunctions.length > 0) && ( +
+ 0 + ? [ + { + name: "Write", + onClick: () => setActiveTab(0), + isActive: activeTab === 0, + }, + ] + : []), + ...(viewFunctions.length > 0 + ? [ + { + name: "Read", + onClick: () => setActiveTab(1), + isActive: activeTab === 1, + }, + ] + : []), + ]} + /> - - {writeFunctions.length > 0 && ( - - {writeFunctions.map((e) => functionSection(e))} - - )} - {viewFunctions.length > 0 && ( - - {viewFunctions.map((e) => functionSection(e))} - - )} - - - )} - - {events.length > 0 && selectedFunction && ( - - {events.map((fn) => ( - +
+ + setKeywordSearch(e.target.value)} + placeholder="Search" + value={_keywordSearch} /> - ))} - - )} - - - +
+
+ +
+ {activeTab === 0 && + writeFunctions.length > 0 && + writeFunctions.map((e) => functionSection(e))} + + {activeTab === 1 && + viewFunctions.length > 0 && + viewFunctions.map((e) => functionSection(e))} +
+
+ )} + {events.length > 0 && selectedFunction && ( +
+ {events.map((fn) => ( + + ))} +
+ )} +
+ + {/* right */} +
{selectedFunction && ( = ({ isLoggedIn={isLoggedIn} /> )} - - +
+
); }; @@ -524,31 +411,19 @@ const FunctionsOrEventsListItem: React.FC = ({ const isActive = selectedFunction?.name === fn.name && selectedFunction.inputs?.length === fn.inputs?.length; - const pathname = usePathname(); - const router = useDashboardRouter(); return ( -
  • - -
  • + ); }; diff --git a/apps/dashboard/src/@/components/contracts/functions/contract-functions.tsx b/apps/dashboard/src/@/components/contracts/functions/contract-functions.tsx index 8ff59d87863..4a600386b90 100644 --- a/apps/dashboard/src/@/components/contracts/functions/contract-functions.tsx +++ b/apps/dashboard/src/@/components/contracts/functions/contract-functions.tsx @@ -1,19 +1,12 @@ "use client"; -import { - Flex, - Tab, - TabList, - TabPanel, - TabPanels, - Tabs, -} from "@chakra-ui/react"; import type { Abi, AbiEvent, AbiFunction } from "abitype"; -import { Heading } from "chakra/heading"; +import { useState } from "react"; import type { ThirdwebContract } from "thirdweb"; import { SourcesPanel } from "@/components/contract-components/shared/sources-panel"; import type { SourceFile } from "@/components/contract-components/types"; import { CodeOverview } from "@/components/contracts/code-overview"; +import { TabButtons } from "@/components/ui/tabs"; import { ContractFunctionsPanel } from "./contract-function"; interface ContractFunctionsOverview { @@ -26,6 +19,8 @@ interface ContractFunctionsOverview { isLoggedIn: boolean; } +type Tab = "functions" | "events" | "code" | "sources"; + export const ContractFunctionsOverview: React.FC = ({ functions, events, @@ -35,90 +30,94 @@ export const ContractFunctionsOverview: React.FC = ({ onlyFunctions, isLoggedIn, }) => { + const [activeTab, setActiveTab] = useState(() => { + if (functions && functions.length > 0) return "functions"; + if (events && events.length > 0) return "events"; + if (abi) return "code"; + if (sources && sources.length > 0) return "sources"; + return undefined; + }); + if (onlyFunctions) { - return ( - - {functions && functions.length > 0 && ( + if (functions && functions.length > 0) { + return ( + + ); + } + return null; + } + + // Tab index: 0 = Functions, 1 = Events, 2 = Code, 3 = Sources + const tabOptions = [ + ...(functions && functions.length > 0 + ? [ + { + name: "Functions", + onClick: () => setActiveTab("functions"), + isActive: activeTab === "functions", + }, + ] + : []), + ...(events && events.length > 0 + ? [ + { + name: "Events", + onClick: () => setActiveTab("events"), + isActive: activeTab === "events", + }, + ] + : []), + ...(abi + ? [ + { + name: "Code", + onClick: () => setActiveTab("code"), + isActive: activeTab === "code", + }, + ] + : []), + ...(sources && sources.length > 0 + ? [ + { + name: "Sources", + onClick: () => setActiveTab("sources"), + isActive: activeTab === "sources", + }, + ] + : []), + ]; + + return ( +
    + +
    + {functions && functions.length > 0 && activeTab === "functions" && ( +
    + +
    + )} + {events && events.length > 0 && activeTab === "events" && ( )} - - ); - } - - return ( - - - - {functions && functions.length > 0 ? ( - - - Functions - - - ) : null} - {events?.length ? ( - - - Events - - - ) : null} - {abi && ( - - - Code - - - )} - {sources && sources?.length > 0 && ( - - - Sources - - - )} - - - {functions && functions.length > 0 ? ( - - - - ) : null} - {events && events?.length > 0 ? ( - - - - ) : null} - {abi && ( - -
    - -
    -
    - )} - {(sources || abi) && ( - - - - )} -
    -
    -
    + {abi && activeTab === "code" && ( + + )} + {sources && sources.length > 0 && activeTab === "sources" && ( + + )} +
    +
    ); }; diff --git a/apps/dashboard/src/@/components/contracts/functions/interactive-abi-function.tsx b/apps/dashboard/src/@/components/contracts/functions/interactive-abi-function.tsx index f3ed55be2bd..470ad6d79f1 100644 --- a/apps/dashboard/src/@/components/contracts/functions/interactive-abi-function.tsx +++ b/apps/dashboard/src/@/components/contracts/functions/interactive-abi-function.tsx @@ -1,20 +1,13 @@ "use client"; -import { - ButtonGroup, - Divider, - Flex, - FormControl, - Input, -} from "@chakra-ui/react"; import { useMutation } from "@tanstack/react-query"; import { type AbiFunction, type AbiParameter, formatAbiItem } from "abitype"; -import { Button } from "chakra/button"; -import { Card } from "chakra/card"; -import { FormErrorMessage, FormHelperText, FormLabel } from "chakra/form"; -import { Heading } from "chakra/heading"; -import { Text } from "chakra/text"; -import { ExternalLinkIcon, InfoIcon, PlayIcon } from "lucide-react"; +import { + ExternalLinkIcon, + InfoIcon, + PlayIcon, + RefreshCcwIcon, +} from "lucide-react"; import Link from "next/link"; import { useEffect, useId, useMemo, useState } from "react"; import { FormProvider, useFieldArray, useForm } from "react-hook-form"; @@ -29,16 +22,19 @@ import { } from "thirdweb"; import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react"; import { parseAbiParams, stringify, toFunctionSelector } from "thirdweb/utils"; +import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; import { SolidityInput } from "@/components/solidity-inputs"; import { camelToTitle } from "@/components/solidity-inputs/helpers"; import { TransactionButton } from "@/components/tx-button"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { CodeClient } from "@/components/ui/code/code.client"; import { PlainTextCodeBlock } from "@/components/ui/code/plaintext-code"; import { InlineCode } from "@/components/ui/inline-code"; +import { Input } from "@/components/ui/input"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Skeleton } from "@/components/ui/skeleton"; import { ToolTipLabel } from "@/components/ui/tooltip"; -import { UnderlineLink } from "@/components/ui/UnderlineLink"; -import { replaceIpfsUrl } from "@/lib/sdk"; function formatResponseData(data: unknown): { type: "json" | "text"; @@ -122,11 +118,11 @@ function formatContractCall( return parsedParams; } -interface InteractiveAbiFunctionProps { +type InteractiveAbiFunctionProps = { abiFunction: AbiFunction; contract: ThirdwebContract; isLoggedIn: boolean; -} +}; function useAsyncRead(contract: ThirdwebContract, abiFunction: AbiFunction) { const formattedAbi = formatAbiItem({ @@ -366,97 +362,72 @@ export const InteractiveAbiFunction: React.FC = ( return ( - - - {fields.length > 0 && ( - <> - - {fields.map((item, index) => { - return ( - - - {camelToTitle(item.key)} - {item.key} - - - - { - form.getFieldState( - `params.${index}.value`, - form.formState, - ).error?.message - } - - - ); - })} - - )} +
    +
    + {fields.length > 0 && + fields.map((item, index) => { + const fieldError = form.getFieldState( + `params.${index}.value`, + form.formState, + ).error; + + return ( + + {camelToTitle(item.key)} + + {item.key} + +
    + } + errorMessage={fieldError?.message} + isRequired={false} + className="mb-2" + > + + + ); + })} {abiFunction.stateMutability === "payable" && ( - <> - - - Native Token Value - - - The native currency value (in Ether) to send with this - transaction (ex: 0.01 to send 0.01 native currency). - - - + + + )} {error ? ( <> - - Error +

    Error

    + ) : readLoading ? ( + ) : formattedResponseData ? ( <> -
    - Output +

    Output

    {/* Show the Solidity type of the function's output */} {abiFunction.outputs.length > 0 && ( @@ -466,30 +437,32 @@ export const InteractiveAbiFunction: React.FC = (
    {formattedResponseData.type === "text" ? ( - + ) : ( )} {/* If the result is an IPFS URI, show a handy link so that users can open it in a new tab */} {formattedResponseData.type === "text" && formattedResponseData.data.startsWith("ipfs://") && ( - - - Open in gateway - - + + Open In IPFS Gateway + + )} + {/* Same with the logic above but this time it's applied to traditional urls */} {((formattedResponseData.type === "text" && formattedResponseData.data.startsWith("https://")) || @@ -506,37 +479,46 @@ export const InteractiveAbiFunction: React.FC = ( )} ) : null} -
    + - - +
    {isView ? ( ) : ( <> - + + + = ( )} - - +
    +
    ); }; diff --git a/apps/dashboard/src/@/components/contracts/published-contract/index.tsx b/apps/dashboard/src/@/components/contracts/published-contract/index.tsx index 378d7f1336a..c285f55ecba 100644 --- a/apps/dashboard/src/@/components/contracts/published-contract/index.tsx +++ b/apps/dashboard/src/@/components/contracts/published-contract/index.tsx @@ -144,15 +144,13 @@ export const PublishedContract: React.FC = ({ )} {contractFunctions && ( - - - + )} diff --git a/apps/dashboard/src/@/components/solidity-inputs/bool-input.tsx b/apps/dashboard/src/@/components/solidity-inputs/bool-input.tsx index e1378be39a0..a838cadff5c 100644 --- a/apps/dashboard/src/@/components/solidity-inputs/bool-input.tsx +++ b/apps/dashboard/src/@/components/solidity-inputs/bool-input.tsx @@ -10,9 +10,9 @@ export const SolidityBoolInput: React.FC = ({ const watchInput = form.watch(inputName); return (
    -
    +