Skip to content

Commit 16ad40f

Browse files
committed
update
1 parent 022e403 commit 16ad40f

File tree

7 files changed

+348
-0
lines changed

7 files changed

+348
-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.

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
@@ -36,6 +36,7 @@ export const CustomConnectWallet = (props: {
3636
connectButtonClassName?: string;
3737
signInLinkButtonClassName?: string;
3838
detailsButtonClassName?: string;
39+
chain?: Chain;
3940
}) => {
4041
const thirdwebClient = useThirdwebClient();
4142
const loginRequired =
@@ -209,6 +210,7 @@ export const CustomConnectWallet = (props: {
209210
},
210211
},
211212
}}
213+
chain={props.chain}
212214
/>
213215

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

apps/dashboard/src/components/product-pages/common/Topnav.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"use client";
2+
13
import { Box, Container, Flex, useBreakpointValue } from "@chakra-ui/react";
24
import { useScrollPosition } from "@n8tb1t/use-scroll-position";
35
import { Logo } from "components/logo";

0 commit comments

Comments
 (0)