Skip to content

Commit 34a24ce

Browse files
committed
update
1 parent d8e7fae commit 34a24ce

File tree

5 files changed

+310
-0
lines changed

5 files changed

+310
-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: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { getThirdwebClient } from "@/constants/thirdweb.server";
2+
import { defineDashboardChain } from "lib/defineDashboardChain";
3+
import { notFound } from "next/navigation";
4+
import { getContract, toTokens } from "thirdweb";
5+
import { getContractMetadata } from "thirdweb/extensions/common";
6+
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
7+
import {
8+
getActiveClaimCondition as getActiveClaimCondition721,
9+
getNFT as getNFT721,
10+
} from "thirdweb/extensions/erc721";
11+
import {
12+
getActiveClaimCondition as getActiveClaimCondition1155,
13+
getNFT as getNFT1155,
14+
} from "thirdweb/extensions/erc1155";
15+
import { NftMint } from "./mint-ui";
16+
17+
type DropPageData = {
18+
slug: string;
19+
contractAddress: string;
20+
chainId: number;
21+
hideQuantitySelector?: boolean;
22+
hideMintToCustomAddress?: boolean;
23+
// If not defined, we will use the image of the NFT or contract's image
24+
thumbnail?: string;
25+
} & ({ type: "erc1155"; tokenId: bigint } | { type: "erc721" });
26+
27+
export const DROP_PAGES: DropPageData[] = [
28+
{
29+
slug: "test",
30+
type: "erc1155",
31+
contractAddress: "0xBD9d7f15f3C850B35c30b8F9F698B511c20b7263",
32+
tokenId: 0n,
33+
chainId: 11155111,
34+
hideQuantitySelector: true,
35+
hideMintToCustomAddress: true,
36+
thumbnail: "/drop/thumbnails/zero-x-thirdweb.mp4",
37+
},
38+
];
39+
40+
export default async function DropPage({
41+
params,
42+
}: { params: { slug: string } }) {
43+
const { slug } = params;
44+
45+
const project = DROP_PAGES.find((p) => p.slug === slug);
46+
47+
if (!project) {
48+
return notFound();
49+
}
50+
// eslint-disable-next-line no-restricted-syntax
51+
const chain = defineDashboardChain(project.chainId, undefined);
52+
const client = getThirdwebClient();
53+
54+
const contract = getContract({
55+
address: project.contractAddress,
56+
chain,
57+
client,
58+
});
59+
60+
const [nft, claimCondition, contractMetadata] = await Promise.all([
61+
project.type === "erc1155"
62+
? getNFT1155({ contract, tokenId: project.tokenId })
63+
: getNFT721({ contract, tokenId: 0n }),
64+
project.type === "erc1155"
65+
? getActiveClaimCondition1155({ contract, tokenId: project.tokenId })
66+
: getActiveClaimCondition721({ contract }),
67+
getContractMetadata({ contract }),
68+
]);
69+
70+
const currencyMetadata = claimCondition.currency
71+
? await getCurrencyMetadata({
72+
contract: getContract({
73+
address: claimCondition.currency,
74+
chain,
75+
client,
76+
}),
77+
})
78+
: undefined;
79+
80+
const currencySymbol = currencyMetadata?.symbol || "";
81+
if (!currencyMetadata?.decimals) {
82+
return notFound();
83+
}
84+
const pricePerToken = Number(
85+
toTokens(claimCondition.pricePerToken, currencyMetadata.decimals),
86+
);
87+
88+
const thumbnail =
89+
project.thumbnail || nft.metadata.image || contractMetadata.image || "";
90+
91+
const displayName = contractMetadata.name || nft.metadata.name || "";
92+
93+
const description =
94+
contractMetadata.description || nft.metadata.description || "";
95+
96+
return (
97+
<NftMint
98+
contract={contract}
99+
displayName={displayName || ""}
100+
thumbnail={thumbnail}
101+
description={description || ""}
102+
currencySymbol={currencySymbol}
103+
pricePerToken={pricePerToken}
104+
{...project}
105+
/>
106+
);
107+
}

0 commit comments

Comments
 (0)