Skip to content

Commit 7d34a8f

Browse files
committed
Add Drop page for minting NFTs with partners (#5307)
## Problem solved Short description of the bug fixed or feature added <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the `Dashboard` layout and functionality for NFT drops, including improved metadata handling, wallet connection, and minting features. ### Detailed summary - Added `AppFooter` and `ErrorProvider` in `DashboardLayout`. - Introduced `DropPageData` type in `data.ts`. - Enhanced `NftMint` component with quantity selection and custom address options. - Integrated OpenGraph image generation for NFT drops. - Updated `CustomConnectWallet` to accept `chain` prop. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 9584885 commit 7d34a8f

File tree

8 files changed

+581
-0
lines changed

8 files changed

+581
-0
lines changed
1.39 MB
Binary file not shown.

apps/dashboard/src/@3rdweb-sdk/react/components/connect-wallet/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const CustomConnectWallet = (props: {
3535
connectButtonClassName?: string;
3636
signInLinkButtonClassName?: string;
3737
detailsButtonClassName?: string;
38+
chain?: Chain;
3839
}) => {
3940
const thirdwebClient = useThirdwebClient();
4041
const loginRequired =
@@ -204,6 +205,7 @@ export const CustomConnectWallet = (props: {
204205
},
205206
},
206207
}}
208+
chain={props.chain}
207209
/>
208210

209211
<LazyConfigureNetworkModal
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { Metadata } from "next";
2+
3+
type DropPageData = {
4+
slug: string;
5+
contractAddress: string;
6+
chainId: number;
7+
hideQuantitySelector?: boolean;
8+
hideMintToCustomAddress?: boolean;
9+
// If not defined, we will use the image of the NFT or contract's image
10+
thumbnail?: string;
11+
metadata: Metadata;
12+
} & ({ type: "erc1155"; tokenId: bigint } | { type: "erc721" });
13+
14+
export const DROP_PAGES: DropPageData[] = [
15+
{
16+
slug: "test",
17+
type: "erc1155",
18+
contractAddress: "0xBD9d7f15f3C850B35c30b8F9F698B511c20b7263",
19+
tokenId: 0n,
20+
chainId: 11155111,
21+
hideQuantitySelector: true,
22+
hideMintToCustomAddress: true,
23+
thumbnail: "/drops/zerion.mp4",
24+
metadata: {
25+
title: "Test mint page",
26+
description: "none",
27+
},
28+
},
29+
{
30+
slug: "zero-chain-announcement",
31+
type: "erc1155",
32+
contractAddress: "0x78264a0af02d894f2d9ae3e11E4a503b352CC437",
33+
tokenId: 0n,
34+
chainId: 543210,
35+
hideMintToCustomAddress: true,
36+
hideQuantitySelector: true,
37+
thumbnail: "/drops/zerion.mp4",
38+
metadata: {
39+
title: "ZERO x thirdweb",
40+
description:
41+
"This exclusive commemorative NFT marks the official launch of ZERϴ's mainnet and our exciting partnership with thirdweb. Own a piece of this milestone in blockchain history as we make onchain transactions free with zero.network",
42+
openGraph: {
43+
title: "ZERO x thirdweb",
44+
description:
45+
"This exclusive commemorative NFT marks the official launch of ZERϴ's mainnet and our exciting partnership with thirdweb. Own a piece of this milestone in blockchain history as we make onchain transactions free with zero.network",
46+
},
47+
},
48+
},
49+
50+
// Add more chains here
51+
];
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { AppFooter } from "@/components/blocks/app-footer";
2+
import { ErrorProvider } from "contexts/error-handler";
3+
4+
export default function DashboardLayout(props: { children: React.ReactNode }) {
5+
return (
6+
<ErrorProvider>
7+
<div className="flex min-h-screen flex-col bg-background">
8+
{/* <DashboardHeader /> */}
9+
<div className="flex grow flex-col">{props.children}</div>
10+
<AppFooter />
11+
</div>
12+
</ErrorProvider>
13+
);
14+
}
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
"use client";
2+
3+
import { Badge } from "@/components/ui/badge";
4+
import { Button } from "@/components/ui/button";
5+
import { Card, CardContent, CardFooter } from "@/components/ui/card";
6+
import { Input } from "@/components/ui/input";
7+
import { Label } from "@/components/ui/label";
8+
import { Switch } from "@/components/ui/switch";
9+
import { useThirdwebClient } from "@/constants/thirdweb.client";
10+
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
11+
import { MinusIcon, PlusIcon } from "lucide-react";
12+
import { useState } from "react";
13+
import type React from "react";
14+
import { toast } from "sonner";
15+
import type { ThirdwebContract } from "thirdweb";
16+
import { balanceOf as balanceOfERC721 } from "thirdweb/extensions/erc721";
17+
import { balanceOf as balanceOfERC1155 } from "thirdweb/extensions/erc1155";
18+
import {
19+
ClaimButton,
20+
MediaRenderer,
21+
useActiveAccount,
22+
useReadContract,
23+
} from "thirdweb/react";
24+
25+
type Props = {
26+
contract: ThirdwebContract;
27+
displayName: string;
28+
description: string;
29+
thumbnail: string;
30+
hideQuantitySelector?: boolean;
31+
hideMintToCustomAddress?: boolean;
32+
} & ({ type: "erc1155"; tokenId: bigint } | { type: "erc721" }) &
33+
(
34+
| {
35+
pricePerToken: number;
36+
currencySymbol: string | null;
37+
noActiveClaimCondition: false;
38+
quantityLimitPerWallet: bigint;
39+
}
40+
| { noActiveClaimCondition: true }
41+
);
42+
43+
export function NftMint(props: Props) {
44+
const [isMinting, setIsMinting] = useState(false);
45+
const [quantity, setQuantity] = useState(1);
46+
const [useCustomAddress, setUseCustomAddress] = useState(false);
47+
const [customAddress, setCustomAddress] = useState("");
48+
const account = useActiveAccount();
49+
const client = useThirdwebClient();
50+
51+
const decreaseQuantity = () => {
52+
setQuantity((prev) => Math.max(1, prev - 1));
53+
};
54+
55+
const increaseQuantity = () => {
56+
setQuantity((prev) => prev + 1); // Assuming a max of 10 NFTs can be minted at once
57+
};
58+
59+
const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
60+
const value = Number.parseInt(e.target.value);
61+
if (!Number.isNaN(value)) {
62+
setQuantity(Math.min(Math.max(1, value)));
63+
}
64+
};
65+
66+
const balance721Query = useReadContract(balanceOfERC721, {
67+
contract: props.contract,
68+
owner: account?.address || "",
69+
queryOptions: {
70+
enabled: props.type === "erc721" && !!account?.address,
71+
},
72+
});
73+
74+
const balance1155Query = useReadContract(balanceOfERC1155, {
75+
contract: props.contract,
76+
owner: account?.address || "",
77+
tokenId: props.type === "erc1155" ? props.tokenId : 0n,
78+
queryOptions: {
79+
enabled: props.type === "erc1155" && !!account?.address,
80+
},
81+
});
82+
83+
const ownedAmount =
84+
props.type === "erc1155"
85+
? balance1155Query.data || 0n
86+
: balance721Query.data || 0n;
87+
88+
const fullyMinted =
89+
props.noActiveClaimCondition === false &&
90+
props.quantityLimitPerWallet === ownedAmount;
91+
92+
return (
93+
<div className="mx-4 my-16 flex flex-col items-center justify-center transition-colors duration-200">
94+
<Card className="w-full max-w-md">
95+
<CardContent className="pt-6">
96+
<div className="relative mb-4 aspect-square overflow-hidden rounded-lg">
97+
<MediaRenderer
98+
client={client}
99+
className="h-full w-full object-cover"
100+
alt=""
101+
src={props.thumbnail}
102+
/>
103+
{!props.noActiveClaimCondition && (
104+
<Badge className="absolute top-2 right-2">
105+
{props.pricePerToken === 0
106+
? "Free"
107+
: `${props.pricePerToken} ${props.currencySymbol}/each`}
108+
</Badge>
109+
)}
110+
</div>
111+
<h2 className="mb-2 font-bold text-2xl">{props.displayName}</h2>
112+
<p className="mb-4 text-muted-foreground">{props.description}</p>
113+
{!props.hideQuantitySelector && !props.noActiveClaimCondition && (
114+
<div className="mb-4 flex items-center justify-between">
115+
<div className="flex items-center">
116+
<Button
117+
variant="outline"
118+
size="icon"
119+
onClick={decreaseQuantity}
120+
disabled={quantity <= 1}
121+
aria-label="Decrease quantity"
122+
className="rounded-r-none"
123+
>
124+
<MinusIcon className="h-4 w-4" />
125+
</Button>
126+
<Input
127+
type="number"
128+
value={quantity}
129+
onChange={handleQuantityChange}
130+
className="w-28 rounded-none border-x-0 pl-6 text-center"
131+
min="1"
132+
/>
133+
<Button
134+
variant="outline"
135+
size="icon"
136+
onClick={increaseQuantity}
137+
aria-label="Increase quantity"
138+
className="rounded-l-none"
139+
>
140+
<PlusIcon className="h-4 w-4" />
141+
</Button>
142+
</div>
143+
<div className="pr-1 font-semibold text-base">
144+
Total: {props.pricePerToken * quantity} {props.currencySymbol}
145+
</div>
146+
</div>
147+
)}
148+
149+
{!props.hideMintToCustomAddress && (
150+
<div className="mb-4 flex items-center space-x-2">
151+
<Switch
152+
id="custom-address"
153+
checked={useCustomAddress}
154+
onCheckedChange={setUseCustomAddress}
155+
/>
156+
<Label
157+
htmlFor="custom-address"
158+
className={`${useCustomAddress ? "" : "text-gray-400"} cursor-pointer`}
159+
>
160+
Mint to a custom address
161+
</Label>
162+
</div>
163+
)}
164+
{useCustomAddress && (
165+
<div className="mb-4">
166+
<Input
167+
id="address-input"
168+
type="text"
169+
placeholder="Enter recipient address"
170+
value={customAddress}
171+
onChange={(e) => setCustomAddress(e.target.value)}
172+
className="w-full"
173+
/>
174+
</div>
175+
)}
176+
</CardContent>
177+
<CardFooter>
178+
{account ? (
179+
<ClaimButton
180+
style={{ width: "100%" }}
181+
contractAddress={props.contract.address}
182+
chain={props.contract.chain}
183+
client={props.contract.client}
184+
claimParams={
185+
props.type === "erc1155"
186+
? {
187+
type: "ERC1155",
188+
tokenId: props.tokenId,
189+
quantity: BigInt(quantity),
190+
to: customAddress || account.address,
191+
from: account.address,
192+
}
193+
: {
194+
type: "ERC721",
195+
quantity: BigInt(quantity),
196+
to: customAddress || account.address,
197+
from: account.address,
198+
}
199+
}
200+
disabled={
201+
isMinting || props.noActiveClaimCondition || fullyMinted
202+
}
203+
onTransactionSent={() => {
204+
toast.loading("Minting NFT", { id: "toastId" });
205+
setIsMinting(true);
206+
}}
207+
onTransactionConfirmed={() => {
208+
toast.success("Minted successfully", { id: "toastId" });
209+
setIsMinting(false);
210+
}}
211+
onError={(err) => {
212+
toast.error(err.message, { id: "toastId" });
213+
setIsMinting(false);
214+
}}
215+
>
216+
{fullyMinted
217+
? "Minted"
218+
: props.noActiveClaimCondition
219+
? "Minting not ready"
220+
: `${quantity > 1 ? `Mint ${quantity} NFTs` : "Mint"}`}
221+
</ClaimButton>
222+
) : (
223+
<CustomConnectWallet
224+
loginRequired={false}
225+
connectButtonClassName="!w-full !rounded !bg-primary !text-primary-foreground !px-4 !py-2 !text-sm"
226+
chain={props.contract.chain}
227+
/>
228+
)}
229+
</CardFooter>
230+
</Card>
231+
</div>
232+
);
233+
}

0 commit comments

Comments
 (0)