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 && (
-
-
-
-
-
-
-
- )}
-
-
-
- );
-}
-
-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}
+ />
+
+
+ >
+ );
+}
+
+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 */}
+
+
+
+ {/* Description */}
+
+ {props.contractInfo.description}
+
+
+
+ {props.children ? (
+ <>
+
+ {props.children}
+ >
+ ) : null}
+
+
+
+
+
+ {props.isOwnerAccount && props.updateButton && (
+
+ )}
+
+
+ );
+}