Skip to content

Commit cbedec5

Browse files
committed
added in BatchMetadata
1 parent 2af289f commit cbedec5

File tree

5 files changed

+361
-5
lines changed

5 files changed

+361
-5
lines changed

apps/dashboard/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"scripts": {
66
"preinstall": "npx only-allow pnpm",
7-
"dev": "next dev --turbo",
7+
"dev": "next dev",
88
"build": "next build",
99
"start": "next start",
1010
"format": "biome format ./src --write",
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
"use client";
2+
import { Spinner } from "@/components/ui/Spinner/Spinner";
3+
import {
4+
Accordion,
5+
AccordionContent,
6+
AccordionItem,
7+
AccordionTrigger,
8+
} from "@/components/ui/accordion";
9+
import { Alert, AlertTitle } from "@/components/ui/alert";
10+
import { Button } from "@/components/ui/button";
11+
import {
12+
Form,
13+
FormControl,
14+
FormField,
15+
FormItem,
16+
FormLabel,
17+
FormMessage,
18+
} from "@/components/ui/form";
19+
import { Input } from "@/components/ui/input";
20+
import { Separator } from "@/components/ui/separator";
21+
import { Textarea } from "@/components/ui/textarea";
22+
import { useMutation } from "@tanstack/react-query";
23+
import { PropertiesFormControl } from "components/contract-pages/forms/properties.shared";
24+
import { CircleAlertIcon } from "lucide-react";
25+
import { useCallback } from "react";
26+
import { useForm } from "react-hook-form";
27+
import { toast } from "sonner";
28+
import { sendAndConfirmTransaction } from "thirdweb";
29+
import { BatchMetadataERC721 } from "thirdweb/modules";
30+
import type { NFTMetadataInputLimited } from "types/modified-types";
31+
import { parseAttributes } from "utils/parseAttributes";
32+
import { ModuleCardUI, type ModuleCardUIProps } from "./module-card";
33+
import type { ModuleInstanceProps } from "./module-instance";
34+
import { AdvancedNFTMetadataFormGroup } from "./nft/AdvancedNFTMetadataFormGroup";
35+
import { NFTMediaFormGroup } from "./nft/NFTMediaFormGroup";
36+
37+
export type UploadMetadataFormValues = NFTMetadataInputLimited & {
38+
supply: number;
39+
customImage: string;
40+
customAnimationUrl: string;
41+
tokenId?: string;
42+
};
43+
44+
export function BatchMetadataModule(props: ModuleInstanceProps) {
45+
const { contract, ownerAccount } = props;
46+
47+
const uploadMetadata = useCallback(
48+
async (values: UploadMetadataFormValues) => {
49+
if (!ownerAccount) {
50+
throw new Error("Not an owner account");
51+
}
52+
53+
const nft = parseAttributes(values);
54+
const uploadMetadataTx = BatchMetadataERC721.uploadMetadata({
55+
contract,
56+
metadatas: [nft],
57+
});
58+
59+
await sendAndConfirmTransaction({
60+
account: ownerAccount,
61+
transaction: uploadMetadataTx,
62+
});
63+
},
64+
[contract, ownerAccount],
65+
);
66+
67+
return (
68+
<BatchMetadataModuleUI
69+
{...props}
70+
uploadMetadata={uploadMetadata}
71+
isOwnerAccount={!!ownerAccount}
72+
/>
73+
);
74+
}
75+
76+
export function BatchMetadataModuleUI(
77+
props: Omit<ModuleCardUIProps, "children" | "updateButton"> & {
78+
isOwnerAccount: boolean;
79+
uploadMetadata: (values: UploadMetadataFormValues) => Promise<void>;
80+
},
81+
) {
82+
return (
83+
<ModuleCardUI {...props}>
84+
<div className="h-1" />
85+
86+
<div className="flex flex-col gap-4">
87+
{/* uploadMetadata NFT */}
88+
<Accordion type="single" collapsible className="-mx-1">
89+
<AccordionItem value="metadata" className="border-none">
90+
<AccordionTrigger className="border-border border-t px-1">
91+
uploadMetadata NFT
92+
</AccordionTrigger>
93+
<AccordionContent className="px-1">
94+
{props.isOwnerAccount && (
95+
<UploadMetadataNFTSection
96+
uploadMetadata={props.uploadMetadata}
97+
/>
98+
)}
99+
{!props.isOwnerAccount && (
100+
<Alert variant="info">
101+
<CircleAlertIcon className="size-5" />
102+
<AlertTitle>
103+
You don't have permission to uploadMetadata NFTs on this
104+
contract
105+
</AlertTitle>
106+
</Alert>
107+
)}
108+
</AccordionContent>
109+
</AccordionItem>
110+
</Accordion>
111+
</div>
112+
</ModuleCardUI>
113+
);
114+
}
115+
116+
function UploadMetadataNFTSection(props: {
117+
uploadMetadata: (values: UploadMetadataFormValues) => Promise<void>;
118+
}) {
119+
const form = useForm<UploadMetadataFormValues>({
120+
values: {
121+
supply: 1,
122+
customImage: "",
123+
customAnimationUrl: "",
124+
},
125+
reValidateMode: "onChange",
126+
});
127+
128+
const uploadMetadataMutation = useMutation({
129+
mutationFn: props.uploadMetadata,
130+
});
131+
132+
const onSubmit = async () => {
133+
const promise = uploadMetadataMutation.mutateAsync(form.getValues());
134+
toast.promise(promise, {
135+
success: "Successfully uploadMetadataed NFT",
136+
error: "Failed to uploadMetadata NFT",
137+
});
138+
};
139+
140+
return (
141+
<Form {...form}>
142+
<form onSubmit={form.handleSubmit(onSubmit)}>
143+
<div className="flex flex-col gap-6">
144+
<div className="flex flex-col gap-6 lg:flex-row">
145+
{/* Left */}
146+
<div className="shrink-0 lg:w-[300px]">
147+
<NFTMediaFormGroup form={form} previewMaxWidth="300px" />
148+
</div>
149+
150+
{/* Right */}
151+
<div className="flex grow flex-col gap-6">
152+
{/* name */}
153+
<FormField
154+
control={form.control}
155+
name="name"
156+
render={({ field }) => (
157+
<FormItem>
158+
<FormLabel>Name</FormLabel>
159+
<FormControl>
160+
<Input {...field} />
161+
</FormControl>
162+
163+
<FormMessage />
164+
</FormItem>
165+
)}
166+
/>
167+
168+
{/* Description */}
169+
<FormField
170+
control={form.control}
171+
name="description"
172+
render={({ field }) => (
173+
<FormItem>
174+
<FormLabel>Description</FormLabel>
175+
<FormControl>
176+
<Textarea {...field} />
177+
</FormControl>
178+
179+
<FormMessage />
180+
</FormItem>
181+
)}
182+
/>
183+
184+
{/* TODO - convert to shadcn + tailwind */}
185+
<PropertiesFormControl
186+
watch={form.watch}
187+
errors={form.formState.errors}
188+
control={form.control}
189+
register={form.register}
190+
setValue={form.setValue}
191+
/>
192+
193+
{/* Advanced options */}
194+
<Accordion
195+
type="single"
196+
collapsible={
197+
!(
198+
form.formState.errors.background_color ||
199+
form.formState.errors.external_url
200+
)
201+
}
202+
>
203+
<AccordionItem
204+
value="advanced-options"
205+
className="-mx-1 border-t border-b-0"
206+
>
207+
<AccordionTrigger className="px-1">
208+
Advanced Options
209+
</AccordionTrigger>
210+
<AccordionContent className="px-1">
211+
<AdvancedNFTMetadataFormGroup form={form} />
212+
</AccordionContent>
213+
</AccordionItem>
214+
</Accordion>
215+
</div>
216+
</div>
217+
218+
<Separator />
219+
220+
<div className="flex justify-end">
221+
<Button
222+
size="sm"
223+
className="min-w-24 gap-2"
224+
disabled={uploadMetadataMutation.isPending}
225+
type="submit"
226+
>
227+
{uploadMetadataMutation.isPending && (
228+
<Spinner className="size-4" />
229+
)}
230+
uploadMetadata
231+
</Button>
232+
</div>
233+
</div>
234+
</form>
235+
</Form>
236+
);
237+
}
238+
239+
export default BatchMetadataModule;

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Mintable.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,8 @@ function MintableModule(props: ModuleInstanceProps) {
9494
contract,
9595
to: values.recipient,
9696
amount: BigInt(values.amount),
97-
tokenId: values.useNextTokenId
98-
? undefined
99-
: // NOTE: this string should never be reached since if useNextTokenId is false, then tokenId should be defined
100-
BigInt(values.tokenId || "0"),
97+
// biome-ignore lint/style/noNonNullAssertion: if useNextTokenId is false, then tokenId should be defined
98+
tokenId: values.useNextTokenId ? undefined : BigInt(values.tokenId!),
10199
nft,
102100
});
103101
} else {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
2+
import { Checkbox } from "@/components/ui/checkbox";
3+
import type { Meta, StoryObj } from "@storybook/react";
4+
import { useMutation } from "@tanstack/react-query";
5+
import { useState } from "react";
6+
import { Toaster, toast } from "sonner";
7+
import { BadgeContainer, mobileViewport } from "stories/utils";
8+
import {
9+
BatchMetadataModuleUI,
10+
type UploadMetadataFormValues,
11+
} from "./BatchMetadata";
12+
13+
const meta = {
14+
title: "Modules/BatchMetadata",
15+
component: Component,
16+
parameters: {
17+
layout: "centered",
18+
},
19+
} satisfies Meta<typeof Component>;
20+
21+
export default meta;
22+
type Story = StoryObj<typeof meta>;
23+
24+
export const Desktop: Story = {
25+
args: {},
26+
};
27+
28+
export const Mobile: Story = {
29+
args: {},
30+
parameters: {
31+
viewport: mobileViewport("iphone14"),
32+
},
33+
};
34+
35+
function Component() {
36+
const [isOwner, setIsOwner] = useState(true);
37+
38+
async function uploadMetadataStub(values: UploadMetadataFormValues) {
39+
console.log("submitting", values);
40+
await new Promise((resolve) => setTimeout(resolve, 1000));
41+
}
42+
43+
const removeMutation = useMutation({
44+
mutationFn: async () => {
45+
await new Promise((resolve) => setTimeout(resolve, 1000));
46+
},
47+
onSuccess() {
48+
toast.success("Module removed successfully");
49+
},
50+
});
51+
52+
const contractInfo = {
53+
name: "Module Name",
54+
description:
55+
"lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod tempor incididunt ut labore ",
56+
publisher: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
57+
version: "1.0.0",
58+
};
59+
60+
// TODO - remove ChakraProviderSetup after converting the chakra components used in BatchMetadataModuleUI
61+
return (
62+
<ChakraProviderSetup>
63+
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
64+
<div className="flex items-center gap-5">
65+
<CheckboxWithLabel
66+
value={isOwner}
67+
onChange={setIsOwner}
68+
id="isOwner"
69+
label="Is Owner"
70+
/>
71+
</div>
72+
73+
<BadgeContainer label="Default">
74+
<BatchMetadataModuleUI
75+
contractInfo={contractInfo}
76+
moduleAddress="0x0000000000000000000000000000000000000000"
77+
uploadMetadata={uploadMetadataStub}
78+
uninstallButton={{
79+
onClick: async () => removeMutation.mutateAsync(),
80+
isPending: removeMutation.isPending,
81+
}}
82+
isOwnerAccount={isOwner}
83+
/>
84+
</BadgeContainer>
85+
<Toaster richColors />
86+
</div>
87+
</ChakraProviderSetup>
88+
);
89+
}
90+
91+
function CheckboxWithLabel(props: {
92+
value: boolean;
93+
onChange: (value: boolean) => void;
94+
id: string;
95+
label: string;
96+
}) {
97+
return (
98+
<div className="items-top flex space-x-2">
99+
<Checkbox
100+
id={props.id}
101+
checked={props.value}
102+
onCheckedChange={(v) => props.onChange(!!v)}
103+
/>
104+
<div className="grid gap-1.5 leading-none">
105+
<label
106+
htmlFor={props.id}
107+
className="font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
108+
>
109+
{props.label}
110+
</label>
111+
</div>
112+
</div>
113+
);
114+
}

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/module-instance.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { lazy } from "react";
44
import type { ThirdwebContract } from "thirdweb";
55
import type { Account } from "thirdweb/wallets";
6+
import { BatchMetadataModule } from "./BatchMetadata";
67
import ClaimableModule from "./Claimable";
78
import { ModuleCardUI, type ModuleCardUIProps } from "./module-card";
89

@@ -41,5 +42,9 @@ export function ModuleInstance(props: ModuleInstanceProps) {
4142
return <ClaimableModule {...props} />;
4243
}
4344

45+
if (props.contractInfo.name.includes("BatchMetadata")) {
46+
return <BatchMetadataModule {...props} />;
47+
}
48+
4449
return <ModuleCardUI {...props} isOwnerAccount={!!props.ownerAccount} />;
4550
}

0 commit comments

Comments
 (0)