diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/InstalledModulesTable.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/InstalledModulesTable.tsx index decc22e4118..d650be6498d 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/InstalledModulesTable.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/InstalledModulesTable.tsx @@ -1,35 +1,11 @@ "use client"; -import { WalletAddress } from "@/components/blocks/wallet-address"; -import { CopyAddressButton } from "@/components/ui/CopyAddressButton"; import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow"; -import { Spinner } from "@/components/ui/Spinner/Spinner"; import { Alert, AlertTitle } from "@/components/ui/alert"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Skeleton } from "@/components/ui/skeleton"; -import { ToolTipLabel } from "@/components/ui/tooltip"; -import { useMutation } from "@tanstack/react-query"; -import { TransactionButton } from "components/buttons/TransactionButton"; -import { CircleSlash, TrashIcon } from "lucide-react"; -import { useState } from "react"; -import { toast } from "sonner"; -import { - type ContractOptions, - getContract, - sendTransaction, - waitForReceipt, -} from "thirdweb"; -import { uninstallModuleByProxy } from "thirdweb/modules"; +import { CircleSlash } from "lucide-react"; +import type { ContractOptions } from "thirdweb"; import type { Account } from "thirdweb/wallets"; -import { useModuleContractInfo } from "./moduleContractInfo"; +import { ModuleCard } from "./module-card"; export const InstalledModulesTable = (props: { contract: ContractOptions; @@ -38,7 +14,7 @@ export const InstalledModulesTable = (props: { isPending: boolean; }; refetchModules: () => void; - ownerAccount?: Account; + ownerAccount: Account | undefined; }) => { const { installedModules, ownerAccount } = props; @@ -66,249 +42,18 @@ export const InstalledModulesTable = (props: { <> {sectionTitle} - - - - Module Name - Description - Publisher Address - Module Address - Version - {ownerAccount && Remove } - - - - - {installedModules.isPending ? ( - <> - - - - - ) : ( - <> - {installedModules.data?.map((e, i) => ( - - ))} - - )} - -
+
+ {installedModules.data?.map((moduleAddress) => ( + + ))} +
); }; - -function SkeletonRow(props: { ownerAccount?: Account }) { - return ( - - - - - - - - - - - - - - - {/* Version */} - - - - - {/* Remove */} - {props.ownerAccount && ( - - - - )} - - ); -} - -function ModuleRow(props: { - moduleAddress: string; - contract: ContractOptions; - onRemoveModule: () => void; - ownerAccount?: Account; -}) { - const { contract, moduleAddress, ownerAccount } = props; - const [isUninstallModalOpen, setIsUninstallModalOpen] = useState(false); - - const contractInfo = useModuleContractInfo( - getContract({ - address: moduleAddress, - chain: contract.chain, - client: contract.client, - }), - ); - - const uninstallMutation = useMutation({ - mutationFn: async (account: Account) => { - const uninstallTransaction = uninstallModuleByProxy({ - contract, - chain: contract.chain, - client: contract.client, - moduleProxyAddress: moduleAddress, - moduleData: "0x", - }); - - const txResult = await sendTransaction({ - transaction: uninstallTransaction, - account, - }); - - await waitForReceipt(txResult); - }, - onSuccess() { - toast.success("Module Removed successfully"); - props.onRemoveModule(); - }, - onError(error) { - toast.error("Failed to remove module"); - console.error("Error during uninstallation:", error); - }, - }); - - const handleRemove = async () => { - if (!ownerAccount) { - return; - } - - setIsUninstallModalOpen(false); - uninstallMutation.mutate(ownerAccount); - }; - - if (!contractInfo) { - return ; - } - - return ( - - -

{contractInfo.name}

-
- -

{contractInfo.description || "..."}

-
- - - - - - - - {/* Version */} - -

{contractInfo.version}

-
- - {/* Remove */} - {ownerAccount && ( - -
- - - -
-
- )} - - - -
{ - e.preventDefault(); - handleRemove(); - }} - > - - Uninstall Module - - Are you sure you want to uninstall{" "} - - {contractInfo.name} - {" "} - ? - - - - - - - - Uninstall - - -
-
-
-
- ); -} - -function TableRow(props: { children: React.ReactNode }) { - return ( - - {props.children} - - ); -} - -function TableData({ children }: { children: React.ReactNode }) { - return {children}; -} - -function TableHeading(props: { children: React.ReactNode }) { - return ( - - {props.children} - - ); -} - -function TableHeadingRow({ children }: { children: React.ReactNode }) { - return {children}; -} diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/module-card.stories.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/module-card.stories.tsx new file mode 100644 index 00000000000..498dcb1cc24 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/module-card.stories.tsx @@ -0,0 +1,120 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { useMutation } from "@tanstack/react-query"; +import { Toaster } from "sonner"; +import { BadgeContainer, mobileViewport } from "stories/utils"; +import { ThirdwebProvider } from "thirdweb/react"; +import { ModuleCardUI } from "./module-card"; + +const meta = { + title: "Modules/ModuleCard", + component: Component, + parameters: { + layout: "centered", + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Desktop: Story = { + args: {}, +}; + +export const Mobile: Story = { + args: {}, + parameters: { + viewport: mobileViewport("iphone14"), + }, +}; + +function Component() { + const removeMutation = useMutation({ + mutationFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + }, + }); + + const updateMutation = useMutation({ + mutationFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + }, + }); + + const contractInfo = { + name: "Module Name", + description: + "lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod tempor incididunt ut labore ", + publisher: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024", + version: "1.0.0", + }; + + return ( + +
+ + removeMutation.mutateAsync(), + isPending: removeMutation.isPending, + }} + isOwnerAccount={true} + /> + + + + removeMutation.mutateAsync(), + isPending: removeMutation.isPending, + }} + isOwnerAccount={false} + /> + + + + removeMutation.mutateAsync(), + isPending: removeMutation.isPending, + }} + updateButton={{ + isDisabled: true, + isPending: updateMutation.isPending, + onClick: () => updateMutation.mutateAsync(), + }} + isOwnerAccount={true} + /> + + + + removeMutation.mutateAsync(), + isPending: removeMutation.isPending, + }} + updateButton={{ + isDisabled: false, + isPending: updateMutation.isPending, + onClick: () => updateMutation.mutateAsync(), + }} + isOwnerAccount={true} + > +
+ CHILDREN +
+
+
+ + +
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/module-card.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/module-card.tsx new file mode 100644 index 00000000000..112481956d5 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/module-card.tsx @@ -0,0 +1,288 @@ +import { WalletAddress } from "@/components/blocks/wallet-address"; +import { CopyAddressButton } from "@/components/ui/CopyAddressButton"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Skeleton } from "@/components/ui/skeleton"; +import { useMutation } from "@tanstack/react-query"; +import { TransactionButton } from "components/buttons/TransactionButton"; +import { InfoIcon } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { + type ContractOptions, + getContract, + sendTransaction, + waitForReceipt, +} from "thirdweb"; +import { uninstallModuleByProxy } from "thirdweb/modules"; +import type { Account } from "thirdweb/wallets"; +import { useModuleContractInfo } from "./moduleContractInfo"; + +type ModuleProps = { + moduleAddress: string; + contract: ContractOptions; + onRemoveModule: () => void; + ownerAccount: Account | undefined; +}; + +export function ModuleCard(props: ModuleProps) { + const { contract, moduleAddress, ownerAccount } = props; + const [isUninstallModalOpen, setIsUninstallModalOpen] = useState(false); + + const contractInfo = useModuleContractInfo( + getContract({ + address: moduleAddress, + chain: contract.chain, + client: contract.client, + }), + ); + + const uninstallMutation = useMutation({ + mutationFn: async (account: Account) => { + const uninstallTransaction = uninstallModuleByProxy({ + contract, + chain: contract.chain, + client: contract.client, + moduleProxyAddress: moduleAddress, + moduleData: "0x", + }); + + const txResult = await sendTransaction({ + transaction: uninstallTransaction, + account, + }); + + await waitForReceipt(txResult); + }, + onSuccess() { + toast.success("Module uninstalled successfully"); + props.onRemoveModule(); + }, + onError(error) { + toast.error("Failed to uninstall module"); + console.error(error); + }, + }); + + const handleRemove = async () => { + if (!ownerAccount) { + toast.error("Wallet is not connected"); + return; + } + + uninstallMutation.mutate(ownerAccount); + }; + + if (!contractInfo) { + return ; + } + + return ( + <> + { + setIsUninstallModalOpen(true); + }, + isPending: uninstallMutation.isPending, + }} + moduleAddress={moduleAddress} + /> + + + +
{ + e.preventDefault(); + handleRemove(); + }} + > + + Uninstall Module + + Are you sure you want to uninstall{" "} + + {contractInfo.name} + {" "} + ? + + + + + + + + Uninstall + + +
+
+
+ + ); +} + +export type ModuleCardUIProps = { + children?: React.ReactNode; + contractInfo: { + name: string; + description?: string; + version?: string; + publisher?: string; + }; + moduleAddress: string; + isOwnerAccount: boolean; + uninstallButton: { + onClick: () => void; + isPending: boolean; + }; + updateButton?: { + onClick: () => void; + isPending: boolean; + isDisabled: boolean; + }; +}; + +export function ModuleCardUI(props: ModuleCardUIProps) { + return ( +
+ {/* Header */} +
+ {/* Title */} +
+

+ {props.contractInfo.name} + + {/* Info Dialog */} + + + + + + + {props.contractInfo.name} + + {props.contractInfo.description} + + + {/* Avoid adding focus on other elements to prevent tooltips from opening on modal open */} + + +
+ +
+ {props.contractInfo.version && ( +
+

+ {" "} + Version{" "} +

+

{props.contractInfo.version}

+
+ )} + + {props.contractInfo.publisher && ( +
+

+ Published By +

+ +
+ )} + +
+

+ Module Address +

+ +
+
+ + +
+

+ + {/* Description */} +

+ {props.contractInfo.description} +

+
+ + {props.children ? ( + <> +
+ {props.children} + + ) : null} +
+ +
+ + + {props.isOwnerAccount && props.updateButton && ( + + )} +
+
+ ); +}