diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/list-button.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/list-button.tsx
index 6bc97b71620..3b6e4240920 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/list-button.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/list-button.tsx
@@ -18,7 +18,7 @@ import { CreateListingsForm } from "./list-form";
interface CreateListingButtonProps {
contract: ThirdwebContract;
createText?: string;
- type?: "direct-listings" | "english-auctions";
+ type: "direct-listings" | "english-auctions";
}
export const CreateListingButton: React.FC = ({
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/list-form.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/list-form.tsx
index 50507f5b34a..1c9473f6ed7 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/list-form.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/list-form.tsx
@@ -1,4 +1,5 @@
import { Alert, AlertTitle } from "@/components/ui/alert";
+import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { useDashboardOwnedNFTs } from "@3rdweb-sdk/react/hooks/useDashboardOwnedNFTs";
@@ -23,11 +24,13 @@ import { isSimpleHashSupported } from "lib/wallet/nfts/simpleHash";
import type { WalletNFT } from "lib/wallet/nfts/types";
import { CircleAlertIcon, InfoIcon } from "lucide-react";
import Link from "next/link";
-import { type Dispatch, type SetStateAction, useMemo, useState } from "react";
-import { useForm } from "react-hook-form";
+import { useMemo, useState } from "react";
+import { type UseFormReturn, useForm } from "react-hook-form";
import { toast } from "sonner";
import {
+ type Chain,
NATIVE_TOKEN_ADDRESS,
+ type ThirdwebClient,
type ThirdwebContract,
getContract,
toUnits,
@@ -35,10 +38,12 @@ import {
} from "thirdweb";
import { decimals } from "thirdweb/extensions/erc20";
import {
+ getNFT as getNFT721,
isApprovedForAll as isApprovedForAll721,
setApprovalForAll as setApprovalForAll721,
} from "thirdweb/extensions/erc721";
import {
+ getNFT as getNFT1155,
isApprovedForAll as isApprovedForAll1155,
setApprovalForAll as setApprovalForAll1155,
} from "thirdweb/extensions/erc1155";
@@ -47,7 +52,12 @@ import type {
CreateAuctionParams,
CreateListingParams,
} from "thirdweb/extensions/marketplace";
-import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
+import {
+ MediaRenderer,
+ useActiveAccount,
+ useReadContract,
+ useSendAndConfirmTransaction,
+} from "thirdweb/react";
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
import { shortenIfAddress } from "utils/usedapp-external";
@@ -76,8 +86,13 @@ type ListForm =
type CreateListingsFormProps = {
contract: ThirdwebContract;
actionText: string;
- setOpen: Dispatch>;
- type?: "direct-listings" | "english-auctions";
+ setOpen: (isOpen: boolean) => void;
+ type: "direct-listings" | "english-auctions";
+ prefilledNFT?: {
+ id: string;
+ type: "ERC721" | "ERC1155";
+ contractAddress: string;
+ };
};
const auctionTimes = [
@@ -95,32 +110,21 @@ export const CreateListingsForm: React.FC = ({
type,
actionText,
setOpen,
+ prefilledNFT,
}) => {
const trackEvent = useTrack();
const chainId = contract.chain.id;
const { idToChain } = useAllChainsData();
const network = idToChain.get(chainId);
const [isFormLoading, setIsFormLoading] = useState(false);
-
- const isSupportedChain =
- chainId &&
- (isSimpleHashSupported(chainId) ||
- isAlchemySupported(chainId) ||
- isMoralisSupported(chainId));
-
- const account = useActiveAccount();
-
- const { data: walletNFTs, isPending: isWalletNFTsLoading } = useWalletNFTs({
- chainId,
- walletAddress: account?.address,
- });
const sendAndConfirmTx = useSendAndConfirmTransaction();
+ const account = useActiveAccount();
const form = useForm({
defaultValues:
type === "direct-listings"
? {
- selected: undefined,
+ selected: prefilledNFT,
currencyContractAddress: NATIVE_TOKEN_ADDRESS,
quantity: "1",
pricePerToken: "0",
@@ -130,7 +134,7 @@ export const CreateListingsForm: React.FC = ({
listingDurationInSeconds: (60 * 60 * 24 * 30).toString(),
}
: {
- selected: undefined,
+ selected: prefilledNFT,
currencyContractAddress: NATIVE_TOKEN_ADDRESS,
quantity: "1",
buyoutPricePerToken: "0",
@@ -142,70 +146,22 @@ export const CreateListingsForm: React.FC = ({
},
});
- const selectedContract = form.watch("selected.contractAddress")
+ const selectedNFT = prefilledNFT || form.watch("selected");
+
+ const selectedContract = selectedNFT?.contractAddress
? getContract({
- address: form.watch("selected.contractAddress"),
+ address: selectedNFT.contractAddress,
chain: contract.chain,
client: contract.client,
})
: undefined;
- const { data: ownedNFTs, isPending: isOwnedNFTsLoading } =
- useDashboardOwnedNFTs({
- contract: selectedContract,
- owner: account?.address,
- // Only run this hook as the last resort if this chain is not supported by the API services we are using
- disabled:
- !selectedContract ||
- isSupportedChain ||
- isWalletNFTsLoading ||
- (walletNFTs?.result || []).length > 0,
- });
-
- const isSelected = (nft: WalletNFT) => {
- return (
- form.watch("selected")?.id === nft.id &&
- form.watch("selected")?.contractAddress === nft.contractAddress
- );
- };
-
- const ownedWalletNFTs: WalletNFT[] = useMemo(() => {
- return ownedNFTs?.map((nft) => {
- if (nft.type === "ERC721") {
- return {
- id: String(nft.id),
- metadata: nft.metadata,
- supply: "1",
- contractAddress: form.watch("selected.contractAddress"),
- tokenId: nft.id.toString(),
- owner: nft.owner,
- type: "ERC721",
- tokenURI: nft.tokenURI,
- };
- }
- return {
- id: String(nft.id),
- metadata: nft.metadata,
- supply: String(nft.supply),
- contractAddress: form.watch("selected.contractAddress"),
- tokenId: nft.id.toString(),
- owner: nft.owner,
- type: "ERC1155",
- tokenURI: nft.tokenURI,
- };
- }) as WalletNFT[];
- }, [ownedNFTs, form]);
-
- const nfts = ownedWalletNFTs || walletNFTs?.result;
-
- const noNfts = !nfts?.length;
-
return (
+ );
+};
+
+// todo: Change this component to use the new headless UI once published
+function PrefilledNFTInfo({
+ prefilledNFT,
+ chain,
+ client,
+}: {
+ prefilledNFT: {
+ id: string;
+ type: "ERC721" | "ERC1155";
+ contractAddress: string;
+ };
+ chain: Chain;
+ client: ThirdwebClient;
+}) {
+ const nftQuery = useReadContract(
+ prefilledNFT.type === "ERC1155" ? getNFT1155 : getNFT721,
+ {
+ contract: getContract({
+ address: prefilledNFT.contractAddress,
+ chain,
+ client,
+ }),
+ tokenId: BigInt(prefilledNFT.id),
+ },
+ );
+ const src =
+ nftQuery.data?.metadata.animation_url || nftQuery.data?.metadata.image;
+ return (
+
+
Selected NFT
+
+ {nftQuery.data?.metadata.name && (
+
+ {nftQuery.data.metadata.name} {prefilledNFT.type}
+
+ )}
+
+ );
+}
+
+type NFTPickerProps = {
+ // biome-ignore lint/suspicious/noExplicitAny:
+ form: UseFormReturn;
+ marketplaceContract: ThirdwebContract;
+};
+
+function NFTPicker(props: NFTPickerProps) {
+ const chainId = props.marketplaceContract.chain.id;
+ const isSupportedChain =
+ chainId &&
+ (isSimpleHashSupported(chainId) ||
+ isAlchemySupported(chainId) ||
+ isMoralisSupported(chainId));
+
+ const account = useActiveAccount();
+
+ const { data: walletNFTs, isPending: isWalletNFTsLoading } = useWalletNFTs({
+ chainId,
+ walletAddress: account?.address,
+ });
+ const selectedContract = props.form.watch("selected.contractAddress")
+ ? getContract({
+ address: props.form.watch("selected.contractAddress"),
+ chain: props.marketplaceContract.chain,
+ client: props.marketplaceContract.client,
+ })
+ : undefined;
+
+ const { data: ownedNFTs, isPending: isOwnedNFTsLoading } =
+ useDashboardOwnedNFTs({
+ contract: selectedContract,
+ owner: account?.address,
+ // Only run this hook as the last resort if this chain is not supported by the API services we are using
+ disabled:
+ !selectedContract ||
+ isSupportedChain ||
+ isWalletNFTsLoading ||
+ (walletNFTs?.result || []).length > 0,
+ });
+
+ const isSelected = (nft: WalletNFT) => {
+ return (
+ props.form.watch("selected")?.id === nft.id &&
+ props.form.watch("selected")?.contractAddress === nft.contractAddress
+ );
+ };
+ const ownedWalletNFTs: WalletNFT[] = useMemo(() => {
+ return ownedNFTs?.map((nft) => {
+ if (nft.type === "ERC721") {
+ return {
+ id: String(nft.id),
+ metadata: nft.metadata,
+ supply: "1",
+ contractAddress: props.form.watch("selected.contractAddress"),
+ tokenId: nft.id.toString(),
+ owner: nft.owner,
+ type: "ERC721",
+ tokenURI: nft.tokenURI,
+ };
+ }
+ return {
+ id: String(nft.id),
+ metadata: nft.metadata,
+ supply: String(nft.supply),
+ contractAddress: props.form.watch("selected.contractAddress"),
+ tokenId: nft.id.toString(),
+ owner: nft.owner,
+ type: "ERC1155",
+ tokenURI: nft.tokenURI,
+ };
+ }) as WalletNFT[];
+ }, [ownedNFTs, props.form]);
+
+ const nfts = ownedWalletNFTs || walletNFTs?.result;
+
+ return (
+ <>
Select NFT
@@ -365,19 +542,21 @@ export const CreateListingsForm: React.FC = ({
Contract address
- {form.formState.errors.selected?.contractAddress?.message}
+ {props.form.formState.errors.selected?.contractAddress?.message}
This will display all the NFTs you own from this contract.
@@ -388,7 +567,7 @@ export const CreateListingsForm: React.FC = ({
{isWalletNFTsLoading ||
(isOwnedNFTsLoading &&
!isSupportedChain &&
- form.watch("selected.contractAddress")) ? (
+ props.form.watch("selected.contractAddress")) ? (
@@ -427,8 +606,8 @@ export const CreateListingsForm: React.FC = ({
cursor="pointer"
onClick={() =>
isSelected(nft)
- ? form.setValue("selected", undefined)
- : form.setValue("selected", nft)
+ ? props.form.setValue("selected", undefined)
+ : props.form.setValue("selected", nft)
}
outline={isSelected(nft) ? "3px solid" : undefined}
outlineColor={isSelected(nft) ? "purple.500" : undefined}
@@ -459,93 +638,6 @@ export const CreateListingsForm: React.FC = ({
) : null}
-
- Listing Currency
-
- form.setValue("currencyContractAddress", e.target.value)
- }
- />
-
- The currency you want to sell your tokens for.
-
-
-
-
- {form.watch("listingType") === "auction"
- ? "Buyout Price Per Token"
- : "Listing Price"}
-
-
-
- {form.watch("listingType") === "auction"
- ? "The price per token a buyer can pay to instantly buyout the auction."
- : "The price of each token you are listing for sale."}
-
-
- {form.watch("selected")?.type?.toLowerCase() !== "erc721" && (
-
-
- Quantity
-
-
-
- The number of tokens to list for sale.
-
-
- )}
- {form.watch("listingType") === "auction" && (
- <>
-
- Reserve Price Per Token
-
-
- The minimum price per token necessary to bid on this auction
-
-
-
- Auction Duration
-
- {auctionTimes.map((time) => (
-
- {time.label}
-
- ))}
-
- The duration of this auction.
-
- >
- )}
-
- {!form.watch("selected.id") && (
-
-
- No NFT selected
-
- )}
-
- {/* Need to pin these at the bottom because this is a very long form */}
-
- setOpen(false)}
- >
- Cancel
-
-
- {actionText}
-
-
-
+ >
);
-};
+}
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/useNftDrawerTabs.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/useNftDrawerTabs.tsx
index e2b0c639637..df0b48aca29 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/useNftDrawerTabs.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/useNftDrawerTabs.tsx
@@ -6,6 +6,7 @@ import type { ThirdwebContract } from "thirdweb";
import * as ERC721Ext from "thirdweb/extensions/erc721";
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
import { useActiveAccount, useReadContract } from "thirdweb/react";
+import { ListMarketplaceButton } from "../components/list-marketplace-button";
import type { NFTDrawerTab } from "./types";
type UseNFTDrawerTabsParams = {
@@ -114,6 +115,8 @@ export function useNFTDrawerTabs({
return false;
})();
+ const isListable = isERC1155 || isERC721;
+
let tabs: NFTDrawerTab[] = [];
if (hasERC1155ClaimConditions) {
tabs = tabs.concat([
@@ -200,6 +203,22 @@ export function useNFTDrawerTabs({
]);
}
+ if (isListable) {
+ tabs = tabs.concat([
+ {
+ title: "Marketplace",
+ isDisabled: false,
+ children: (
+
+ ),
+ },
+ ]);
+ }
+
return tabs;
}, [
isERC1155,
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/list-marketplace-button.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/list-marketplace-button.tsx
new file mode 100644
index 00000000000..3104b9009ff
--- /dev/null
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/list-marketplace-button.tsx
@@ -0,0 +1,142 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet";
+import { useThirdwebClient } from "@/constants/thirdweb.client";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { type ThirdwebContract, getContract, isAddress } from "thirdweb";
+import { z } from "zod";
+import { CreateListingsForm } from "../../(marketplace)/components/list-form";
+
+type Props = {
+ type: "ERC1155" | "ERC721";
+ contract: ThirdwebContract;
+ tokenId: bigint;
+};
+
+type Action = {
+ value: "direct-listings" | "english-auctions";
+ text: string;
+};
+const actions: Action[] = [
+ {
+ value: "direct-listings",
+ text: "Create Direct Listing",
+ },
+ {
+ value: "english-auctions",
+ text: "Create English Auction",
+ },
+];
+
+const formSchema = z.object({
+ contractAddress: z.string().refine((value) => isAddress(value), {
+ message: "Invalid Ethereum address",
+ }),
+});
+
+export function ListMarketplaceButton(props: Props) {
+ const [selectedAction, setSelectedAction] =
+ useState<(typeof actions)[number]>();
+ const handleOpenChange = (isOpen: boolean) => {
+ setSelectedAction(isOpen ? actions[0] : undefined);
+ };
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ contractAddress: "",
+ },
+ });
+ const client = useThirdwebClient();
+ const [marketplaceContract, setMarketplaceContract] =
+ useState();
+
+ const handleSubmit = (action: (typeof actions)[number]) => {
+ form.handleSubmit((d) => {
+ const marketplaceContract = getContract({
+ address: d.contractAddress,
+ chain: props.contract.chain,
+ client,
+ });
+ setMarketplaceContract(marketplaceContract);
+ setSelectedAction(action);
+ })();
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {selectedAction?.text}
+
+
+ {marketplaceContract && selectedAction && (
+
+ )}
+
+
+ >
+ );
+}