diff --git a/apps/dashboard/public/assets/examples/example-with-ipfs.csv b/apps/dashboard/public/assets/examples/example-with-ipfs.csv deleted file mode 100644 index d46ea39a909..00000000000 --- a/apps/dashboard/public/assets/examples/example-with-ipfs.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,description,image,animation_url,external_url,background_color,youtube_url,additional,properties,can be,added -Token 0 Name,Token 0 Description,ipfs://ipfsHash/0,ipfs://ipfsHash/0,https://thirdweb.com,#0098EE,,every,row,is a ,property diff --git a/apps/dashboard/public/assets/examples/example-with-maps.csv b/apps/dashboard/public/assets/examples/example-with-maps.csv deleted file mode 100644 index fd1879f1422..00000000000 --- a/apps/dashboard/public/assets/examples/example-with-maps.csv +++ /dev/null @@ -1,4 +0,0 @@ -name,description,image,animation_url,external_url,background_color,youtube_url,additional,properties,can be,added -Token 0 Name,Token 0 Description,0.png,0.mp4,https://thirdweb.com,#0098EE,,every,row,is a ,property -Token 1 Name,Token 1 Description,0.png,0.mp4,https://thirdweb.com,#0098EE,,every,row,is a ,property -Token 2 Name,Token 2 Description,0.png,0.mp4,https://thirdweb.com,#0098EE,,every,row,is a ,property diff --git a/apps/dashboard/public/assets/examples/example.csv b/apps/dashboard/public/assets/examples/example.csv deleted file mode 100644 index 60232fb7f37..00000000000 --- a/apps/dashboard/public/assets/examples/example.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,description,external_url,background_color,youtube_url,additional,properties,can be,added -Token 0 Name,Token 0 Description,https://thirdweb.com,#0098EE,,every,row,is a ,property \ No newline at end of file diff --git a/apps/dashboard/src/@/components/batch-upload/batch-lazy-mint.tsx b/apps/dashboard/src/@/components/batch-upload/batch-lazy-mint.tsx index 73a6d1aad41..d9b184501db 100644 --- a/apps/dashboard/src/@/components/batch-upload/batch-lazy-mint.tsx +++ b/apps/dashboard/src/@/components/batch-upload/batch-lazy-mint.tsx @@ -1,23 +1,18 @@ "use client"; -import { - Alert, - AlertIcon, - Flex, - FormControl, - Input, - InputGroup, - InputRightElement, - Textarea, -} from "@chakra-ui/react"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Button } from "chakra/button"; -import { FormErrorMessage, FormHelperText, FormLabel } from "chakra/form"; -import { Heading } from "chakra/heading"; -import { Text } from "chakra/text"; -import { ChevronLeftIcon, EyeIcon, EyeOffIcon } from "lucide-react"; -import { useRef, useState } from "react"; -import { useDropzone } from "react-dropzone"; +import { + ArrowLeftIcon, + ArrowRightIcon, + EyeIcon, + EyeOffIcon, + HelpCircleIcon, + LockKeyholeIcon, + RefreshCcwIcon, + ShuffleIcon, +} from "lucide-react"; +import Link from "next/link"; +import { useState } from "react"; import { useForm } from "react-hook-form"; import type { ThirdwebClient } from "thirdweb"; import type { CreateDelayedRevealBatchParams } from "thirdweb/extensions/erc721"; @@ -25,12 +20,14 @@ import type { NFTInput } from "thirdweb/utils"; import { z } from "zod"; import { FileInput } from "@/components/blocks/FileInput"; import { TransactionButton } from "@/components/tx-button"; -import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox"; -import { UnderlineLink } from "@/components/ui/UnderlineLink"; -import type { ComponentWithChildren } from "@/types/component-with-children"; +import { Button } from "@/components/ui/button"; +import { Form, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { Textarea } from "@/components/ui/textarea"; import { processInputData, shuffleData } from "@/utils/batch"; +import { TabButtons } from "../ui/tabs"; import { BatchTable } from "./batch-table"; -import { SelectOption } from "./lazy-mint-form/select-option"; import { UploadStep } from "./upload-step"; type DelayedSubmit = { @@ -44,16 +41,6 @@ type InstantSubmit = { type SubmitType = DelayedSubmit | InstantSubmit; -interface BatchLazyMintEVMProps { - nextTokenIdToMint: bigint; - canCreateDelayedRevealBatch: boolean; - onSubmit: (formData: SubmitType) => Promise; - chainId: number; - client: ThirdwebClient; -} - -type BatchLazyMintProps = BatchLazyMintEVMProps; - const BatchLazyMintFormSchema = z .object({ confirmPassword: z @@ -73,7 +60,7 @@ const BatchLazyMintFormSchema = z name: z.string().min(1, "A name is required"), }) .optional(), - revealType: z.literal("instant").or(z.literal("delayed")).optional(), + revealType: z.literal("instant").or(z.literal("delayed")), // shared logic shuffle: z.boolean().default(false), @@ -91,18 +78,21 @@ function useBatchLazyMintForm() { return useForm({ defaultValues: { metadatas: [], - revealType: undefined, + revealType: "instant", shuffle: false, }, resolver: zodResolver(BatchLazyMintFormSchema), }); } -export const BatchLazyMint: ComponentWithChildren< - BatchLazyMintProps & { - isLoggedIn: boolean; - } -> = (props) => { +export function BatchLazyMint(props: { + nextTokenIdToMint: bigint; + canCreateDelayedRevealBatch: boolean; + onSubmit: (formData: SubmitType) => Promise; + chainId: number; + client: ThirdwebClient; + isLoggedIn: boolean; +}) { const [step, setStep] = useState(0); const form = useBatchLazyMintForm(); @@ -110,369 +100,415 @@ export const BatchLazyMint: ComponentWithChildren< const nftMetadatas = form.watch("metadatas"); const hasError = !!form.getFieldState("metadatas", form.formState).error; - const { getRootProps, getInputProps, isDragActive } = useDropzone({ - onDrop: async (acceptedFiles) => { - try { - await processInputData(acceptedFiles, (data) => - form.setValue("metadatas", data), - ); - } catch { - form.setError("metadatas", { - message: "Invalid metadata files", - type: "validate", - }); - } - - if (nftMetadatas.length === 0) { - form.setError("metadatas", { - message: "Invalid metadata files", - type: "validate", - }); - } - }, - }); - - const paginationPortalRef = useRef(null); - return ( -
{ - // first shuffle - const shuffledMetadatas = data.shuffle - ? shuffleData(data.metadatas) - : data.metadatas; - - // check submit is instant - if (data.revealType === "instant") { - return props.onSubmit({ - data: { metadatas: shuffledMetadatas }, - revealType: "instant", - }); - } - // validate password - if (!data.password) { - form.setError("password", { - message: "A password is required for delayed reveal.", - type: "validate", - }); - return; - } - // validate placeholder - if (!data.placeHolder?.name) { - form.setError("placeHolder.name", { - message: "A name is required for delayed reveal.", - type: "validate", - }); - } - // submit - return props.onSubmit({ - data: { - metadata: shuffledMetadatas, - password: data.password, - placeholderMetadata: { - description: data.placeHolder?.description, - image: data.placeHolder?.image, - name: data.placeHolder?.name, + + { + // first shuffle + const shuffledMetadatas = data.shuffle + ? shuffleData(data.metadatas) + : data.metadatas; + + // check submit is instant + if (data.revealType === "instant") { + return props.onSubmit({ + data: { metadatas: shuffledMetadatas }, + revealType: "instant", + }); + } + // validate password + if (!data.password) { + form.setError("password", { + message: "A password is required for delayed reveal.", + type: "validate", + }); + return; + } + // validate placeholder + if (!data.placeHolder?.name) { + form.setError("placeHolder.name", { + message: "A name is required for delayed reveal.", + type: "validate", + }); + } + // submit + await props.onSubmit({ + data: { + metadata: shuffledMetadatas, + password: data.password, + placeholderMetadata: { + description: data.placeHolder?.description, + image: data.placeHolder?.image, + name: data.placeHolder?.name, + }, }, - }, - revealType: "delayed", - }); - })} - > - {step === 0 ? ( -
- {nftMetadatas.length > 0 ? ( - <> - -
-
-
-
- - -
+ revealType: "delayed", + }); + })} + > + {step === 0 ? ( +
+ {nftMetadatas.length > 0 ? ( +
+

+ Upload NFTs +

+ + +
+ +
- - ) : ( - - )} -
- ) : ( - <> - -
+ ) : ( + { + form.reset(); + }} + onDrop={async (acceptedFiles) => { + try { + await processInputData(acceptedFiles, (data) => + form.setValue("metadatas", data), + ); + } catch { + form.setError("metadatas", { + message: "Invalid metadata files", + type: "validate", + }); + } + + if (nftMetadatas.length === 0) { + form.setError("metadatas", { + message: "Invalid metadata files", + type: "validate", + }); + } + }} + /> + )} +
+ ) : ( +
+
- - When will you reveal your NFTs? -
- - - {form.watch("revealType") && ( - <> - - form.setValue("shuffle", !!val)} - /> -
-

Shuffle the order of the NFTs before uploading.

- This is an off-chain operation and is not provable. + +

+ Upload NFTs +

+ + + +
+ +
+ {form.watch("revealType") === "delayed" && ( + + )} + + {/* shuffle */} + form.setValue("shuffle", val)} + /> + +
+
+ + {form.formState.isSubmitting + ? `Uploading ${nftMetadatas.length} NFTs` + : `Upload ${nftMetadatas.length} NFTs`} + +
+ +
+ + + Experiencing issues uploading your files? +
- -
- - {form.formState.isSubmitting - ? `Uploading ${nftMetadatas.length} NFTs` - : `Upload ${nftMetadatas.length} NFTs`} - - {props.children}
- - - Experiencing issues uploading your files? - - - - )} - - )} - +
+
+ )} + + ); -}; +} -interface SelectRevealProps { +function ShuffleNFTsCard(props: { + isShuffleEnabled: boolean; + onShuffleChange: (val: boolean) => void; +}) { + return ( +
+
+
+ +
+
+ +

+ Shuffle NFTs +

+ +

+ Shuffle the order of the NFTs before uploading. This is an off-chain + operation and is not provable +

+ + props.onShuffleChange(!!val)} + className="absolute right-4 top-4 lg:right-6 lg:top-8" + /> +
+ ); +} + +function SelectReveal(props: { form: ReturnType; canCreateDelayedRevealBatch: boolean; client: ThirdwebClient; +}) { + const { form, canCreateDelayedRevealBatch } = props; + + return ( +
+ {canCreateDelayedRevealBatch && ( + { + form.setValue("revealType", "instant"); + // reset all fields related to delayed reveal + form.resetField("password"); + form.resetField("confirmPassword"); + form.resetField("placeHolder"); + }, + isActive: form.watch("revealType") === "instant", + }, + { + name: "Delayed Reveal", + onClick: () => form.setValue("revealType", "delayed"), + isActive: form.watch("revealType") === "delayed", + }, + ]} + /> + )} + + {form.watch("revealType") === "instant" && ( +
+

+ Reveal upon mint +

+

+ Collectors will immediately see the final NFT when they complete the + minting +

+
+ )} + + {form.watch("revealType") === "delayed" && ( +
+

+ Delayed Reveal +

+

+ Collectors will mint your placeholder image, then you reveal at a + later time +

+
+ )} +
+ ); } -const SelectReveal: React.FC = ({ - form, - client, - canCreateDelayedRevealBatch, -}) => { +function DelayedRevealConfiguration(props: { + form: ReturnType; + client: ThirdwebClient; +}) { + const { form, client } = props; const [show, setShow] = useState(false); - const imageUrl = form.watch("placeHolder.image"); return ( - - - form.setValue("revealType", "instant")} - /> - form.setValue("revealType", "delayed")} - /> - -
- {form.watch("revealType") === "delayed" && ( - <> - Let's set a password - - - You'll need this password to reveal your NFTs. Please save it - somewhere safe. - - - - - Password - - - - {show ? ( - setShow(!show)} - /> - ) : ( - setShow(!show)} - /> - )} - - - - - { - form.getFieldState("password", form.formState).error - ?.message - } - - - - Confirm password - - - { - form.getFieldState("confirmPassword", form.formState).error - ?.message - } - - - -
- Placeholder - + {/* password */} +
+
+
+ +
+
+

+ Set password +

+

+ You'll need this password to reveal your NFTs. Please save it + somewhere safe +

+ +
+ {/* select password */} + + Password +
+ + +
+ + {form.getFieldState("password", form.formState).error?.message} + +
+ + {/* confirm password */} + + Confirm password +
+ +
+ + { + form.getFieldState("confirmPassword", form.formState).error + ?.message + } + +
+
+
+ + {/* placeholder */} +
+
+
+ +
+
+

+ Set placeholder metadata +

+

+ The placeholder metadata will be displayed for all NFTs until you + reveal your NFTs +

+
+ + Image + form.setValue("placeHolder.image", file)} + value={imageUrl} + /> + + + { + form.getFieldState("placeHolder.image", form.formState).error + ?.message + } + + +
+ + Name + + + { + form.getFieldState("placeHolder.name", form.formState).error + ?.message } - isRequired - > - Name - - - { - form.getFieldState("placeHolder.name", form.formState).error - ?.message - } - - - + + + Description +