Skip to content

Commit b5650f9

Browse files
committed
added in OpenEditionMetadataModule
1 parent cbedec5 commit b5650f9

File tree

3 files changed

+317
-0
lines changed

3 files changed

+317
-0
lines changed
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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 { zodResolver } from "@hookform/resolvers/zod";
21+
import { useMutation } from "@tanstack/react-query";
22+
import { CircleAlertIcon } from "lucide-react";
23+
import { useCallback } from "react";
24+
import { useForm } from "react-hook-form";
25+
import { toast } from "sonner";
26+
import { sendAndConfirmTransaction } from "thirdweb";
27+
import { OpenEditionMetadataERC721 } from "thirdweb/modules";
28+
import { z } from "zod";
29+
import { ModuleCardUI, type ModuleCardUIProps } from "./module-card";
30+
import type { ModuleInstanceProps } from "./module-instance";
31+
32+
const formSchema = z.object({
33+
name: z.string().min(1),
34+
description: z.string().min(1),
35+
imageUri: z.string().optional(),
36+
animationUri: z.string().optional(),
37+
});
38+
39+
export type SetSharedMetadataFormValues = z.infer<typeof formSchema>;
40+
41+
function OpenEditionMetadataModule(props: ModuleInstanceProps) {
42+
const { contract, ownerAccount } = props;
43+
44+
const setSharedMetadata = useCallback(
45+
async (values: SetSharedMetadataFormValues) => {
46+
if (!ownerAccount) {
47+
throw new Error("Not an Owner account");
48+
}
49+
50+
const setSharedMetadataTx = OpenEditionMetadataERC721.setSharedMetadata({
51+
contract,
52+
metadata: {
53+
name: values.name,
54+
description: values.description,
55+
imageURI: values.imageUri || "",
56+
animationURI: values.animationUri || "",
57+
},
58+
});
59+
60+
await sendAndConfirmTransaction({
61+
transaction: setSharedMetadataTx,
62+
account: ownerAccount,
63+
});
64+
},
65+
[contract, ownerAccount],
66+
);
67+
68+
return (
69+
<OpenEditionMetadataModuleUI
70+
{...props}
71+
setSharedMetadata={setSharedMetadata}
72+
isOwnerAccount={!!props.ownerAccount}
73+
/>
74+
);
75+
}
76+
77+
export function OpenEditionMetadataModuleUI(
78+
props: Omit<ModuleCardUIProps, "children" | "updateButton"> & {
79+
isOwnerAccount: boolean;
80+
setSharedMetadata: (values: SetSharedMetadataFormValues) => Promise<void>;
81+
},
82+
) {
83+
return (
84+
<ModuleCardUI {...props}>
85+
<div className="h-1" />
86+
<Accordion type="single" collapsible className="-mx-1">
87+
<AccordionItem value="metadata" className="border-none">
88+
<AccordionTrigger className="border-border border-t px-1">
89+
Set Shared Metadata
90+
</AccordionTrigger>
91+
<AccordionContent className="px-1">
92+
{props.isOwnerAccount && (
93+
<SetSharedMetadataSection
94+
setSharedMetadata={props.setSharedMetadata}
95+
/>
96+
)}
97+
{!props.isOwnerAccount && (
98+
<Alert variant="info">
99+
<CircleAlertIcon className="size-5" />
100+
<AlertTitle>
101+
You don't have permission to set shared metadata on this
102+
contract
103+
</AlertTitle>
104+
</Alert>
105+
)}
106+
</AccordionContent>
107+
</AccordionItem>
108+
</Accordion>
109+
</ModuleCardUI>
110+
);
111+
}
112+
113+
export function SetSharedMetadataSection(props: {
114+
setSharedMetadata: (values: SetSharedMetadataFormValues) => Promise<void>;
115+
}) {
116+
const form = useForm<SetSharedMetadataFormValues>({
117+
resolver: zodResolver(formSchema),
118+
values: {
119+
name: "",
120+
description: "",
121+
imageUri: "",
122+
animationUri: "",
123+
},
124+
reValidateMode: "onChange",
125+
});
126+
127+
const setSharedMetadataMutation = useMutation({
128+
mutationFn: props.setSharedMetadata,
129+
});
130+
131+
const onSubmit = async () => {
132+
const promise = setSharedMetadataMutation.mutateAsync(form.getValues());
133+
toast.promise(promise, {
134+
success: "Successfully set shared metadata",
135+
error: "Failed to set shared metadata",
136+
});
137+
};
138+
139+
return (
140+
<Form {...form}>
141+
<form onSubmit={form.handleSubmit(onSubmit)}>
142+
<div className="flex flex-col gap-6">
143+
<div className="flex gap-4">
144+
<FormField
145+
control={form.control}
146+
name="name"
147+
render={({ field }) => (
148+
<FormItem className="flex-1">
149+
<FormLabel>Name</FormLabel>
150+
<FormControl>
151+
<Input {...field} />
152+
</FormControl>
153+
<FormMessage />
154+
</FormItem>
155+
)}
156+
/>
157+
<FormField
158+
control={form.control}
159+
name="description"
160+
render={({ field }) => (
161+
<FormItem className="flex-1">
162+
<FormLabel>Description</FormLabel>
163+
<FormControl>
164+
<Input {...field} />
165+
</FormControl>
166+
<FormMessage />
167+
</FormItem>
168+
)}
169+
/>
170+
</div>
171+
172+
<div className="flex gap-4">
173+
<FormField
174+
control={form.control}
175+
name="imageUri"
176+
render={({ field }) => (
177+
<FormItem className="flex-1">
178+
<FormLabel>Image URL</FormLabel>
179+
<FormControl>
180+
<Input {...field} />
181+
</FormControl>
182+
<FormMessage />
183+
</FormItem>
184+
)}
185+
/>
186+
<FormField
187+
control={form.control}
188+
name="animationUri"
189+
render={({ field }) => (
190+
<FormItem className="flex-1">
191+
<FormLabel>Animation URL</FormLabel>
192+
<FormControl>
193+
<Input {...field} />
194+
</FormControl>
195+
<FormMessage />
196+
</FormItem>
197+
)}
198+
/>
199+
</div>
200+
201+
<div className="flex justify-end">
202+
<Button
203+
size="sm"
204+
className="min-w-24 gap-2"
205+
disabled={setSharedMetadataMutation.isPending}
206+
type="submit"
207+
>
208+
{setSharedMetadataMutation.isPending && (
209+
<Spinner className="size-4" />
210+
)}
211+
Set Shared Metadata
212+
</Button>
213+
</div>
214+
</div>
215+
</form>
216+
</Form>
217+
);
218+
}
219+
220+
export default OpenEditionMetadataModule;

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
@@ -5,6 +5,7 @@ import type { ThirdwebContract } from "thirdweb";
55
import type { Account } from "thirdweb/wallets";
66
import { BatchMetadataModule } from "./BatchMetadata";
77
import ClaimableModule from "./Claimable";
8+
import OpenEditionMetadataModule from "./OpenEditionMetadata";
89
import { ModuleCardUI, type ModuleCardUIProps } from "./module-card";
910

1011
const MintableModule = lazy(() => import("./Mintable"));
@@ -46,5 +47,9 @@ export function ModuleInstance(props: ModuleInstanceProps) {
4647
return <BatchMetadataModule {...props} />;
4748
}
4849

50+
if (props.contractInfo.name.includes("OpenEditionMetadata")) {
51+
return <OpenEditionMetadataModule {...props} />;
52+
}
53+
4954
return <ModuleCardUI {...props} isOwnerAccount={!!props.ownerAccount} />;
5055
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { Checkbox } from "@/components/ui/checkbox";
2+
import type { Meta, StoryObj } from "@storybook/react";
3+
import { useMutation } from "@tanstack/react-query";
4+
import { useState } from "react";
5+
import { Toaster, toast } from "sonner";
6+
import { BadgeContainer, mobileViewport } from "stories/utils";
7+
import {
8+
OpenEditionMetadataModuleUI,
9+
type SetSharedMetadataFormValues,
10+
} from "./OpenEditionMetadata";
11+
12+
const meta = {
13+
title: "Modules/OpenEditionMetadata",
14+
component: Component,
15+
parameters: {
16+
layout: "centered",
17+
},
18+
} satisfies Meta<typeof Component>;
19+
20+
export default meta;
21+
type Story = StoryObj<typeof meta>;
22+
23+
export const Desktop: Story = {
24+
args: {},
25+
};
26+
27+
export const Mobile: Story = {
28+
args: {},
29+
parameters: {
30+
viewport: mobileViewport("iphone14"),
31+
},
32+
};
33+
34+
function Component() {
35+
const [isOwner, setIsOwner] = useState(true);
36+
async function setSharedMetadataStub(values: SetSharedMetadataFormValues) {
37+
console.log("submitting", values);
38+
await new Promise((resolve) => setTimeout(resolve, 1000));
39+
}
40+
41+
const removeMutation = useMutation({
42+
mutationFn: async () => {
43+
await new Promise((resolve) => setTimeout(resolve, 1000));
44+
},
45+
onSuccess() {
46+
toast.success("Module removed successfully");
47+
},
48+
});
49+
50+
const contractInfo = {
51+
name: "Module Name",
52+
description:
53+
"lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod tempor incididunt ut labore ",
54+
publisher: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
55+
version: "1.0.0",
56+
};
57+
58+
return (
59+
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
60+
<div className="flex gap-2">
61+
<Checkbox
62+
id="terms1"
63+
checked={isOwner}
64+
onCheckedChange={(v) => setIsOwner(!!v)}
65+
/>
66+
<div className="grid gap-1.5 leading-none">
67+
<label
68+
htmlFor="terms1"
69+
className="font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
70+
>
71+
Is Owner
72+
</label>
73+
</div>
74+
</div>
75+
76+
<BadgeContainer label="Default">
77+
<OpenEditionMetadataModuleUI
78+
contractInfo={contractInfo}
79+
moduleAddress="0x0000000000000000000000000000000000000000"
80+
setSharedMetadata={setSharedMetadataStub}
81+
uninstallButton={{
82+
onClick: async () => removeMutation.mutateAsync(),
83+
isPending: removeMutation.isPending,
84+
}}
85+
isOwnerAccount={isOwner}
86+
/>
87+
</BadgeContainer>
88+
89+
<Toaster richColors />
90+
</div>
91+
);
92+
}

0 commit comments

Comments
 (0)