|
| 1 | +"use client"; |
| 2 | +import { |
| 3 | + Accordion, |
| 4 | + AccordionContent, |
| 5 | + AccordionItem, |
| 6 | + AccordionTrigger, |
| 7 | +} from "@/components/ui/accordion"; |
| 8 | +import { Alert, AlertTitle } from "@/components/ui/alert"; |
| 9 | +import { |
| 10 | + Form, |
| 11 | + FormControl, |
| 12 | + FormField, |
| 13 | + FormItem, |
| 14 | + FormLabel, |
| 15 | + FormMessage, |
| 16 | +} from "@/components/ui/form"; |
| 17 | +import { Input } from "@/components/ui/input"; |
| 18 | +import { Textarea } from "@/components/ui/textarea"; |
| 19 | +import { zodResolver } from "@hookform/resolvers/zod"; |
| 20 | +import { useMutation } from "@tanstack/react-query"; |
| 21 | +import { TransactionButton } from "components/buttons/TransactionButton"; |
| 22 | +import { useTxNotifications } from "hooks/useTxNotifications"; |
| 23 | +import { CircleAlertIcon } from "lucide-react"; |
| 24 | +import { useCallback } from "react"; |
| 25 | +import { useForm } from "react-hook-form"; |
| 26 | +import { sendAndConfirmTransaction } from "thirdweb"; |
| 27 | +import { BatchMetadataERC721, BatchMetadataERC1155 } from "thirdweb/modules"; |
| 28 | +import { parseAttributes } from "utils/parseAttributes"; |
| 29 | +import { z } from "zod"; |
| 30 | +import { fileBufferOrStringSchema } from "../zod-schemas"; |
| 31 | +import { ModuleCardUI, type ModuleCardUIProps } from "./module-card"; |
| 32 | +import type { ModuleInstanceProps } from "./module-instance"; |
| 33 | +import { AdvancedNFTMetadataFormGroup } from "./nft/AdvancedNFTMetadataFormGroup"; |
| 34 | +import { NFTMediaFormGroup } from "./nft/NFTMediaFormGroup"; |
| 35 | +import { PropertiesFormControl } from "./nft/PropertiesFormControl"; |
| 36 | + |
| 37 | +const uploadMetadataFormSchema = z.object({ |
| 38 | + name: z.string().min(1), |
| 39 | + description: z.string().optional(), |
| 40 | + image: fileBufferOrStringSchema.optional(), |
| 41 | + animationUri: fileBufferOrStringSchema.optional(), |
| 42 | + external_url: fileBufferOrStringSchema.optional(), |
| 43 | + customImage: z.string().optional(), |
| 44 | + customAnimationUrl: z.string().optional(), |
| 45 | + background_color: z |
| 46 | + .string() |
| 47 | + .refine( |
| 48 | + (c) => { |
| 49 | + return /^#[0-9a-f]{6}$/i.test(c.toLowerCase()); |
| 50 | + }, |
| 51 | + { |
| 52 | + message: "Invalid Hex Color", |
| 53 | + }, |
| 54 | + ) |
| 55 | + .optional(), |
| 56 | + attributes: z.array( |
| 57 | + z.object({ |
| 58 | + trait_type: z.string().min(1), |
| 59 | + value: z.string().min(1), |
| 60 | + }), |
| 61 | + ), |
| 62 | +}); |
| 63 | + |
| 64 | +export type UploadMetadataFormValues = z.infer<typeof uploadMetadataFormSchema>; |
| 65 | + |
| 66 | +function BatchMetadataModule(props: ModuleInstanceProps) { |
| 67 | + const { contract, ownerAccount } = props; |
| 68 | + |
| 69 | + const isErc721 = props.contractInfo.name === "BatchMetadataERC721"; |
| 70 | + |
| 71 | + const uploadMetadata = useCallback( |
| 72 | + async (values: UploadMetadataFormValues) => { |
| 73 | + if (!ownerAccount) { |
| 74 | + throw new Error("Not an owner account"); |
| 75 | + } |
| 76 | + |
| 77 | + const nft = parseAttributes(values); |
| 78 | + const uploadMetadata = isErc721 |
| 79 | + ? BatchMetadataERC721.uploadMetadata |
| 80 | + : BatchMetadataERC1155.uploadMetadata; |
| 81 | + const uploadMetadataTx = uploadMetadata({ |
| 82 | + contract, |
| 83 | + metadatas: [nft], |
| 84 | + }); |
| 85 | + |
| 86 | + await sendAndConfirmTransaction({ |
| 87 | + account: ownerAccount, |
| 88 | + transaction: uploadMetadataTx, |
| 89 | + }); |
| 90 | + }, |
| 91 | + [contract, ownerAccount], |
| 92 | + ); |
| 93 | + |
| 94 | + return ( |
| 95 | + <BatchMetadataModuleUI |
| 96 | + {...props} |
| 97 | + uploadMetadata={uploadMetadata} |
| 98 | + isOwnerAccount={!!ownerAccount} |
| 99 | + contractChainId={contract.chain.id} |
| 100 | + /> |
| 101 | + ); |
| 102 | +} |
| 103 | + |
| 104 | +export function BatchMetadataModuleUI( |
| 105 | + props: Omit<ModuleCardUIProps, "children" | "updateButton"> & { |
| 106 | + isOwnerAccount: boolean; |
| 107 | + uploadMetadata: (values: UploadMetadataFormValues) => Promise<void>; |
| 108 | + contractChainId: number; |
| 109 | + }, |
| 110 | +) { |
| 111 | + return ( |
| 112 | + <ModuleCardUI {...props}> |
| 113 | + <div className="flex flex-col gap-4"> |
| 114 | + <Accordion type="single" collapsible className="-mx-1"> |
| 115 | + {/* uploadMetadata */} |
| 116 | + <AccordionItem value="metadata" className="border-none"> |
| 117 | + <AccordionTrigger className="border-border border-t px-1"> |
| 118 | + Upload NFT Metadata |
| 119 | + </AccordionTrigger> |
| 120 | + <AccordionContent className="px-1"> |
| 121 | + {props.isOwnerAccount && ( |
| 122 | + <UploadMetadataNFTSection |
| 123 | + uploadMetadata={props.uploadMetadata} |
| 124 | + contractChainId={props.contractChainId} |
| 125 | + /> |
| 126 | + )} |
| 127 | + {!props.isOwnerAccount && ( |
| 128 | + <Alert variant="info"> |
| 129 | + <CircleAlertIcon className="size-5" /> |
| 130 | + <AlertTitle> |
| 131 | + You don't have permission to upload metadata on this |
| 132 | + contract |
| 133 | + </AlertTitle> |
| 134 | + </Alert> |
| 135 | + )} |
| 136 | + </AccordionContent> |
| 137 | + </AccordionItem> |
| 138 | + |
| 139 | + {/* batchMetadata */} |
| 140 | + <AccordionItem value="batch-metadata" className="border-none"> |
| 141 | + <AccordionTrigger className="border-border border-t px-1"> |
| 142 | + Batch Upload NFT Metadata |
| 143 | + </AccordionTrigger> |
| 144 | + <AccordionContent className="px-1"> |
| 145 | + {props.isOwnerAccount && <BatchMetadataNFTSection />} |
| 146 | + {!props.isOwnerAccount && ( |
| 147 | + <Alert variant="info"> |
| 148 | + <CircleAlertIcon className="size-5" /> |
| 149 | + <AlertTitle> |
| 150 | + You don't have permission to upload metadata on this |
| 151 | + contract |
| 152 | + </AlertTitle> |
| 153 | + </Alert> |
| 154 | + )} |
| 155 | + </AccordionContent> |
| 156 | + </AccordionItem> |
| 157 | + </Accordion> |
| 158 | + </div> |
| 159 | + </ModuleCardUI> |
| 160 | + ); |
| 161 | +} |
| 162 | + |
| 163 | +function UploadMetadataNFTSection(props: { |
| 164 | + uploadMetadata: (values: UploadMetadataFormValues) => Promise<void>; |
| 165 | + contractChainId: number; |
| 166 | +}) { |
| 167 | + const form = useForm<UploadMetadataFormValues>({ |
| 168 | + resolver: zodResolver(uploadMetadataFormSchema), |
| 169 | + values: { |
| 170 | + name: "", |
| 171 | + attributes: [], |
| 172 | + }, |
| 173 | + reValidateMode: "onChange", |
| 174 | + }); |
| 175 | + |
| 176 | + const uploadNotifications = useTxNotifications( |
| 177 | + "NFT metadata uploaded successfully", |
| 178 | + "Failed to uploadMetadata NFT metadata", |
| 179 | + ); |
| 180 | + |
| 181 | + const uploadMetadataMutation = useMutation({ |
| 182 | + mutationFn: props.uploadMetadata, |
| 183 | + onSuccess: uploadNotifications.onSuccess, |
| 184 | + onError: uploadNotifications.onError, |
| 185 | + }); |
| 186 | + |
| 187 | + const onSubmit = async () => { |
| 188 | + uploadMetadataMutation.mutateAsync(form.getValues()); |
| 189 | + }; |
| 190 | + |
| 191 | + return ( |
| 192 | + <Form {...form}> |
| 193 | + <form onSubmit={form.handleSubmit(onSubmit)}> |
| 194 | + <div className="flex flex-col gap-6"> |
| 195 | + <div className="flex flex-col gap-6 lg:flex-row"> |
| 196 | + {/* Left */} |
| 197 | + <div className="shrink-0 lg:w-[300px]"> |
| 198 | + <NFTMediaFormGroup form={form} previewMaxWidth="300px" /> |
| 199 | + </div> |
| 200 | + |
| 201 | + {/* Right */} |
| 202 | + <div className="flex grow flex-col gap-6"> |
| 203 | + {/* name */} |
| 204 | + <FormField |
| 205 | + control={form.control} |
| 206 | + name="name" |
| 207 | + render={({ field }) => ( |
| 208 | + <FormItem> |
| 209 | + <FormLabel>Name</FormLabel> |
| 210 | + <FormControl> |
| 211 | + <Input {...field} /> |
| 212 | + </FormControl> |
| 213 | + |
| 214 | + <FormMessage /> |
| 215 | + </FormItem> |
| 216 | + )} |
| 217 | + /> |
| 218 | + |
| 219 | + {/* Description */} |
| 220 | + <FormField |
| 221 | + control={form.control} |
| 222 | + name="description" |
| 223 | + render={({ field }) => ( |
| 224 | + <FormItem> |
| 225 | + <FormLabel>Description</FormLabel> |
| 226 | + <FormControl> |
| 227 | + <Textarea {...field} /> |
| 228 | + </FormControl> |
| 229 | + |
| 230 | + <FormMessage /> |
| 231 | + </FormItem> |
| 232 | + )} |
| 233 | + /> |
| 234 | + |
| 235 | + <PropertiesFormControl form={form} /> |
| 236 | + |
| 237 | + {/* Advanced options */} |
| 238 | + <Accordion |
| 239 | + type="single" |
| 240 | + collapsible={ |
| 241 | + !( |
| 242 | + form.formState.errors.background_color || |
| 243 | + form.formState.errors.external_url |
| 244 | + ) |
| 245 | + } |
| 246 | + > |
| 247 | + <AccordionItem |
| 248 | + value="advanced-options" |
| 249 | + className="-mx-1 border-y" |
| 250 | + > |
| 251 | + <AccordionTrigger className="px-1"> |
| 252 | + Advanced Options |
| 253 | + </AccordionTrigger> |
| 254 | + <AccordionContent className="px-1"> |
| 255 | + <AdvancedNFTMetadataFormGroup form={form} /> |
| 256 | + </AccordionContent> |
| 257 | + </AccordionItem> |
| 258 | + </Accordion> |
| 259 | + </div> |
| 260 | + </div> |
| 261 | + |
| 262 | + <div className="flex justify-end"> |
| 263 | + <TransactionButton |
| 264 | + size="sm" |
| 265 | + className="min-w-24" |
| 266 | + disabled={uploadMetadataMutation.isPending} |
| 267 | + type="submit" |
| 268 | + isLoading={uploadMetadataMutation.isPending} |
| 269 | + txChainID={props.contractChainId} |
| 270 | + transactionCount={1} |
| 271 | + colorScheme="primary" |
| 272 | + > |
| 273 | + Upload |
| 274 | + </TransactionButton> |
| 275 | + </div> |
| 276 | + </div> |
| 277 | + </form> |
| 278 | + </Form> |
| 279 | + ); |
| 280 | +} |
| 281 | + |
| 282 | +function BatchMetadataNFTSection() { |
| 283 | + return ( |
| 284 | + <Alert variant="info"> |
| 285 | + <CircleAlertIcon className="size-5" /> |
| 286 | + <AlertTitle>Coming soon!</AlertTitle> |
| 287 | + </Alert> |
| 288 | + ); |
| 289 | +} |
| 290 | + |
| 291 | +export default BatchMetadataModule; |
0 commit comments