Skip to content

Commit ee1c2c1

Browse files
committed
update
1 parent 93a382b commit ee1c2c1

File tree

5 files changed

+309
-0
lines changed

5 files changed

+309
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Medias in this folder are used for /drop/<slug>
1.39 MB
Binary file not shown.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { AppFooter } from "@/components/blocks/app-footer";
2+
import { DashboardHeader } from "app/components/Header/DashboardHeader";
3+
import { ErrorProvider } from "contexts/error-handler";
4+
5+
export default function DashboardLayout(props: { children: React.ReactNode }) {
6+
return (
7+
<ErrorProvider>
8+
<div className="flex min-h-screen flex-col bg-background">
9+
<DashboardHeader />
10+
<div className="flex grow flex-col">{props.children}</div>
11+
<AppFooter />
12+
</div>
13+
</ErrorProvider>
14+
);
15+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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 { ClaimButton, MediaRenderer, useActiveAccount } from "thirdweb/react";
17+
type Props = {
18+
contract: ThirdwebContract;
19+
displayName: string;
20+
description: string;
21+
thumbnail: string;
22+
pricePerToken: number;
23+
currencySymbol: string | null;
24+
hideQuantitySelector?: boolean;
25+
hideMintToCustomAddress?: boolean;
26+
} & ({ type: "erc1155"; tokenId: bigint } | { type: "erc721" });
27+
28+
export function NftMint(props: Props) {
29+
const [isMinting, setIsMinting] = useState(false);
30+
const [quantity, setQuantity] = useState(1);
31+
const [useCustomAddress, setUseCustomAddress] = useState(false);
32+
const [customAddress, setCustomAddress] = useState("");
33+
const account = useActiveAccount();
34+
const client = useThirdwebClient();
35+
36+
const decreaseQuantity = () => {
37+
setQuantity((prev) => Math.max(1, prev - 1));
38+
};
39+
40+
const increaseQuantity = () => {
41+
setQuantity((prev) => prev + 1); // Assuming a max of 10 NFTs can be minted at once
42+
};
43+
44+
const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
45+
const value = Number.parseInt(e.target.value);
46+
if (!Number.isNaN(value)) {
47+
setQuantity(Math.min(Math.max(1, value)));
48+
}
49+
};
50+
return (
51+
<div className="mx-4 my-16 flex flex-col items-center justify-center transition-colors duration-200">
52+
<Card className="w-full max-w-md">
53+
<CardContent className="pt-6">
54+
<div className="relative mb-4 aspect-square overflow-hidden rounded-lg">
55+
<MediaRenderer
56+
client={client}
57+
className="h-full w-full object-cover"
58+
alt=""
59+
src={props.thumbnail}
60+
/>
61+
<Badge className="absolute top-2 right-2">
62+
{props.pricePerToken} {props.currencySymbol}/each
63+
</Badge>
64+
</div>
65+
<h2 className="mb-2 font-bold text-2xl">{props.displayName}</h2>
66+
<p className="mb-4 text-muted-foreground">{props.description}</p>
67+
{!props.hideQuantitySelector && (
68+
<div className="mb-4 flex items-center justify-between">
69+
<div className="flex items-center">
70+
<Button
71+
variant="outline"
72+
size="icon"
73+
onClick={decreaseQuantity}
74+
disabled={quantity <= 1}
75+
aria-label="Decrease quantity"
76+
className="rounded-r-none"
77+
>
78+
<MinusIcon className="h-4 w-4" />
79+
</Button>
80+
<Input
81+
type="number"
82+
value={quantity}
83+
onChange={handleQuantityChange}
84+
className="w-28 rounded-none border-x-0 pl-6 text-center"
85+
min="1"
86+
/>
87+
<Button
88+
variant="outline"
89+
size="icon"
90+
onClick={increaseQuantity}
91+
aria-label="Increase quantity"
92+
className="rounded-l-none"
93+
>
94+
<PlusIcon className="h-4 w-4" />
95+
</Button>
96+
</div>
97+
<div className="pr-1 font-semibold text-base">
98+
Total: {props.pricePerToken * quantity} {props.currencySymbol}
99+
</div>
100+
</div>
101+
)}
102+
103+
{!props.hideMintToCustomAddress && (
104+
<div className="mb-4 flex items-center space-x-2">
105+
<Switch
106+
id="custom-address"
107+
checked={useCustomAddress}
108+
onCheckedChange={setUseCustomAddress}
109+
/>
110+
<Label
111+
htmlFor="custom-address"
112+
className={`${useCustomAddress ? "" : "text-gray-400"} cursor-pointer`}
113+
>
114+
Mint to a custom address
115+
</Label>
116+
</div>
117+
)}
118+
{useCustomAddress && (
119+
<div className="mb-4">
120+
<Input
121+
id="address-input"
122+
type="text"
123+
placeholder="Enter recipient address"
124+
value={customAddress}
125+
onChange={(e) => setCustomAddress(e.target.value)}
126+
className="w-full"
127+
/>
128+
</div>
129+
)}
130+
</CardContent>
131+
<CardFooter>
132+
{account ? (
133+
<ClaimButton
134+
style={{ width: "100%" }}
135+
contractAddress={props.contract.address}
136+
chain={props.contract.chain}
137+
client={props.contract.client}
138+
claimParams={
139+
props.type === "erc1155"
140+
? {
141+
type: "ERC1155",
142+
tokenId: props.tokenId,
143+
quantity: BigInt(quantity),
144+
to: customAddress,
145+
from: account.address,
146+
}
147+
: props.type === "erc721"
148+
? {
149+
type: "ERC721",
150+
quantity: BigInt(quantity),
151+
to: customAddress,
152+
from: account.address,
153+
}
154+
: {
155+
type: "ERC20",
156+
quantity: String(quantity),
157+
to: customAddress,
158+
from: account.address,
159+
}
160+
}
161+
disabled={isMinting}
162+
onTransactionSent={() => {
163+
toast.loading("Minting NFT");
164+
setIsMinting(true);
165+
}}
166+
onTransactionConfirmed={() => {
167+
toast.success("Minted successfully");
168+
setIsMinting(false);
169+
}}
170+
onError={(err) => {
171+
toast.error(err.message);
172+
setIsMinting(false);
173+
}}
174+
>
175+
{quantity > 1 ? `Mint ${quantity} NFTs` : "Mint"}
176+
</ClaimButton>
177+
) : (
178+
<CustomConnectWallet
179+
loginRequired={false}
180+
connectButtonClassName="!w-full !rounded !bg-primary !text-primary-foreground !px-4 !py-2 !text-sm"
181+
/>
182+
)}
183+
</CardFooter>
184+
</Card>
185+
</div>
186+
);
187+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { getThirdwebClient } from "@/constants/thirdweb.server";
2+
import { notFound } from "next/navigation";
3+
import { defineChain, getContract, toTokens } from "thirdweb";
4+
import { getContractMetadata } from "thirdweb/extensions/common";
5+
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
6+
import {
7+
getActiveClaimCondition as getActiveClaimCondition721,
8+
getNFT as getNFT721,
9+
} from "thirdweb/extensions/erc721";
10+
import {
11+
getActiveClaimCondition as getActiveClaimCondition1155,
12+
getNFT as getNFT1155,
13+
} from "thirdweb/extensions/erc1155";
14+
import { NftMint } from "./mint-ui";
15+
16+
type DropPageData = {
17+
slug: string;
18+
contractAddress: string;
19+
chainId: number;
20+
hideQuantitySelector?: boolean;
21+
hideMintToCustomAddress?: boolean;
22+
// If not defined, we will use the image of the NFT or contract's image
23+
thumbnail?: string;
24+
} & ({ type: "erc1155"; tokenId: bigint } | { type: "erc721" });
25+
26+
export const DROP_PAGES: DropPageData[] = [
27+
{
28+
slug: "test",
29+
type: "erc1155",
30+
contractAddress: "0x3cf279b3248E164F3e5C341826B878d350EC6AB1",
31+
tokenId: 0n,
32+
chainId: 11155111,
33+
hideQuantitySelector: true,
34+
hideMintToCustomAddress: true,
35+
thumbnail: "/drop/thumbnails/zero-x-thirdweb.mp4",
36+
},
37+
];
38+
39+
export default async function DropPage({
40+
params,
41+
}: { params: { slug: string } }) {
42+
const { slug } = params;
43+
44+
const project = DROP_PAGES.find((p) => p.slug === slug);
45+
46+
if (!project) {
47+
return notFound();
48+
}
49+
50+
const chain = defineChain(project.chainId);
51+
const client = getThirdwebClient();
52+
53+
const contract = getContract({
54+
address: project.contractAddress,
55+
chain,
56+
client,
57+
});
58+
59+
const [nft, claimCondition, contractMetadata] = await Promise.all([
60+
project.type === "erc1155"
61+
? getNFT1155({ contract, tokenId: project.tokenId })
62+
: getNFT721({ contract, tokenId: 0n }),
63+
project.type === "erc1155"
64+
? getActiveClaimCondition1155({ contract, tokenId: project.tokenId })
65+
: getActiveClaimCondition721({ contract }),
66+
getContractMetadata({ contract }),
67+
]);
68+
69+
const currencyMetadata = claimCondition.currency
70+
? await getCurrencyMetadata({
71+
contract: getContract({
72+
address: claimCondition.currency,
73+
chain,
74+
client,
75+
}),
76+
})
77+
: undefined;
78+
79+
const currencySymbol = currencyMetadata?.symbol || "";
80+
if (!currencyMetadata?.decimals) {
81+
return notFound();
82+
}
83+
const pricePerToken = Number(
84+
toTokens(claimCondition.pricePerToken, currencyMetadata.decimals),
85+
);
86+
87+
const thumbnail =
88+
project.thumbnail || nft.metadata.image || contractMetadata.image || "";
89+
90+
const displayName = contractMetadata.name || nft.metadata.name || "";
91+
92+
const description =
93+
contractMetadata.description || nft.metadata.description || "";
94+
95+
return (
96+
<NftMint
97+
contract={contract}
98+
displayName={displayName || ""}
99+
thumbnail={thumbnail}
100+
description={description || ""}
101+
currencySymbol={currencySymbol}
102+
pricePerToken={pricePerToken}
103+
{...project}
104+
/>
105+
);
106+
}

0 commit comments

Comments
 (0)