Skip to content

Commit 1315501

Browse files
committed
created mintable module UI and story
1 parent b3d3f1f commit 1315501

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { Spinner } from "@/components/ui/Spinner/Spinner";
2+
import { Button } from "@/components/ui/button";
3+
import {
4+
Form,
5+
FormControl,
6+
FormField,
7+
FormItem,
8+
FormLabel,
9+
} from "@/components/ui/form";
10+
import { Input } from "@/components/ui/input";
11+
import { Skeleton } from "@/components/ui/skeleton";
12+
import { zodResolver } from "@hookform/resolvers/zod";
13+
import { useMutation } from "@tanstack/react-query";
14+
import { useForm } from "react-hook-form";
15+
import { toast } from "sonner";
16+
import { type ContractOptions, waitForReceipt } from "thirdweb";
17+
import { MintableERC721 } from "thirdweb/modules";
18+
import {
19+
useActiveAccount,
20+
useReadContract,
21+
useSendTransaction,
22+
} from "thirdweb/react";
23+
import { z } from "zod";
24+
import type { UninstallButtonProps } from "./module-card";
25+
26+
const formSchema = z.object({
27+
primarySaleRecipient: z.string(),
28+
});
29+
30+
export type MintableModuleFormValues = z.infer<typeof formSchema>;
31+
32+
export function MintableModule(props: {
33+
contract: ContractOptions;
34+
uninstallButton: UninstallButtonProps;
35+
isOwnerAccount: boolean;
36+
}) {
37+
const { contract } = props;
38+
const account = useActiveAccount();
39+
const { mutateAsync: sendTransaction } = useSendTransaction();
40+
const { data: primarySaleRecipient, isLoading } = useReadContract(
41+
MintableERC721.getSaleConfig,
42+
{
43+
contract,
44+
},
45+
);
46+
47+
async function update(values: MintableModuleFormValues) {
48+
// isRestricted is the opposite of transferEnabled
49+
const setSaleConfigTransaction = MintableERC721.setSaleConfig({
50+
contract,
51+
primarySaleRecipient: values.primarySaleRecipient,
52+
});
53+
54+
const setSaleConfigTxResult = await sendTransaction(
55+
setSaleConfigTransaction,
56+
);
57+
58+
try {
59+
await waitForReceipt(setSaleConfigTxResult);
60+
toast.success("Successfully updated primary sale recipient");
61+
} catch (_) {
62+
toast.error("Failed to update the primary sale recipient");
63+
}
64+
}
65+
66+
return (
67+
<MintableModuleUI
68+
isPending={isLoading}
69+
primarySaleRecipient={primarySaleRecipient || ""}
70+
adminAddress={account?.address || ""}
71+
update={update}
72+
{...props}
73+
/>
74+
);
75+
}
76+
77+
export function MintableModuleUI(props: {
78+
primarySaleRecipient: string;
79+
isPending: boolean;
80+
adminAddress: string;
81+
isOwnerAccount: boolean;
82+
update: (values: MintableModuleFormValues) => Promise<void>;
83+
uninstallButton: UninstallButtonProps;
84+
}) {
85+
const form = useForm<MintableModuleFormValues>({
86+
resolver: zodResolver(formSchema),
87+
values: {
88+
primarySaleRecipient: props.primarySaleRecipient,
89+
},
90+
reValidateMode: "onChange",
91+
});
92+
93+
const updateMutation = useMutation({
94+
mutationFn: props.update,
95+
});
96+
97+
const onSubmit = async () => {
98+
const _values = form.getValues();
99+
const values = { ..._values };
100+
101+
await updateMutation.mutateAsync(values);
102+
};
103+
104+
if (props.isPending) {
105+
return <Skeleton className="h-36" />;
106+
}
107+
108+
return (
109+
<Form {...form}>
110+
<form onSubmit={form.handleSubmit(onSubmit)}>
111+
<div className="flex gap-4">
112+
{/* Switch */}
113+
<FormField
114+
control={form.control}
115+
name="primarySaleRecipient"
116+
render={({ field }) => (
117+
<FormItem className="flex flex-1 flex-col gap-3">
118+
<FormLabel>Primary Sale Recipient</FormLabel>
119+
<FormControl>
120+
<Input
121+
placeholder="0x..."
122+
{...field}
123+
disabled={!props.isOwnerAccount}
124+
/>
125+
</FormControl>
126+
</FormItem>
127+
)}
128+
/>
129+
</div>
130+
131+
<div className="h-3" />
132+
133+
<div className="flex flex-row justify-end gap-3 border-border border-t py-4">
134+
<Button
135+
size="sm"
136+
onClick={props.uninstallButton.onClick}
137+
variant="destructive"
138+
className="min-w-24 gap-2"
139+
disabled={!props.isOwnerAccount}
140+
>
141+
{props.uninstallButton.isPending && <Spinner className="size-4" />}
142+
Uninstall
143+
</Button>
144+
145+
{props.isOwnerAccount && (
146+
<Button
147+
size="sm"
148+
className="min-w-24 gap-2"
149+
type="submit"
150+
disabled={
151+
props.isPending ||
152+
!props.isOwnerAccount ||
153+
!form.formState.isDirty ||
154+
updateMutation.isPending
155+
}
156+
>
157+
{updateMutation.isPending && <Spinner className="size-4" />}
158+
Update
159+
</Button>
160+
)}
161+
</div>
162+
</form>
163+
</Form>
164+
);
165+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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 { type MintableModuleFormValues, MintableModuleUI } from "./Mintable";
8+
9+
const meta = {
10+
title: "Modules/Mintable",
11+
component: Component,
12+
parameters: {
13+
layout: "centered",
14+
},
15+
} satisfies Meta<typeof Component>;
16+
17+
export default meta;
18+
type Story = StoryObj<typeof meta>;
19+
20+
export const Desktop: Story = {
21+
args: {},
22+
};
23+
24+
export const Mobile: Story = {
25+
args: {},
26+
parameters: {
27+
viewport: mobileViewport("iphone14"),
28+
},
29+
};
30+
31+
const testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
32+
33+
function Component() {
34+
const [isOwner, setIsOwner] = useState(true);
35+
async function updateStub(values: MintableModuleFormValues) {
36+
console.log("submitting", values);
37+
await new Promise((resolve) => setTimeout(resolve, 1000));
38+
}
39+
40+
const removeMutation = useMutation({
41+
mutationFn: async () => {
42+
await new Promise((resolve) => setTimeout(resolve, 1000));
43+
},
44+
onSuccess() {
45+
toast.success("Module removed successfully");
46+
},
47+
});
48+
49+
return (
50+
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
51+
<div className="items-top flex space-x-2">
52+
<Checkbox
53+
id="terms1"
54+
checked={isOwner}
55+
onCheckedChange={(v) => setIsOwner(!!v)}
56+
/>
57+
<div className="grid gap-1.5 leading-none">
58+
<label
59+
htmlFor="terms1"
60+
className="font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
61+
>
62+
Is Owner
63+
</label>
64+
</div>
65+
</div>
66+
67+
<BadgeContainer label="Empty Primary Sale Recipient">
68+
<MintableModuleUI
69+
isPending={false}
70+
primarySaleRecipient={""}
71+
adminAddress={testAddress1}
72+
update={updateStub}
73+
uninstallButton={{
74+
onClick: async () => removeMutation.mutateAsync(),
75+
isPending: removeMutation.isPending,
76+
}}
77+
isOwnerAccount={isOwner}
78+
/>
79+
</BadgeContainer>
80+
81+
<BadgeContainer label="Filled Primary Sale Recipient">
82+
<MintableModuleUI
83+
isPending={false}
84+
primarySaleRecipient={testAddress1}
85+
adminAddress={testAddress1}
86+
update={updateStub}
87+
uninstallButton={{
88+
onClick: () => removeMutation.mutateAsync(),
89+
isPending: removeMutation.isPending,
90+
}}
91+
isOwnerAccount={isOwner}
92+
/>
93+
</BadgeContainer>
94+
95+
<BadgeContainer label="Pending">
96+
<MintableModuleUI
97+
isPending={true}
98+
adminAddress={testAddress1}
99+
primarySaleRecipient={testAddress1}
100+
update={updateStub}
101+
uninstallButton={{
102+
onClick: () => removeMutation.mutateAsync(),
103+
isPending: removeMutation.isPending,
104+
}}
105+
isOwnerAccount={isOwner}
106+
/>
107+
</BadgeContainer>
108+
109+
<Toaster richColors />
110+
</div>
111+
);
112+
}

0 commit comments

Comments
 (0)