Skip to content

Commit da63536

Browse files
MananTankGWSzeto
authored andcommitted
Refactor everything
1 parent d0f2557 commit da63536

File tree

3 files changed

+347
-115
lines changed

3 files changed

+347
-115
lines changed

apps/dashboard/src/@/components/ui/transferrable.stories.tsx

Lines changed: 0 additions & 115 deletions
This file was deleted.
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import { Spinner } from "@/components/ui/Spinner/Spinner";
2+
import { Alert, AlertTitle } from "@/components/ui/alert";
3+
import { Button } from "@/components/ui/button";
4+
import {
5+
Form,
6+
FormControl,
7+
FormField,
8+
FormItem,
9+
FormLabel,
10+
FormMessage,
11+
} from "@/components/ui/form";
12+
import { Input } from "@/components/ui/input";
13+
import { Skeleton } from "@/components/ui/skeleton";
14+
import { Switch } from "@/components/ui/switch";
15+
import { ToolTipLabel } from "@/components/ui/tooltip";
16+
import { zodResolver } from "@hookform/resolvers/zod";
17+
import { useMutation } from "@tanstack/react-query";
18+
import { CircleAlertIcon, PlusIcon, Trash2Icon } from "lucide-react";
19+
import { useFieldArray, useForm } from "react-hook-form";
20+
import { toast } from "sonner";
21+
import { isAddress } from "thirdweb";
22+
import { z } from "zod";
23+
24+
const formSchema = z.object({
25+
allowList: z.array(
26+
z.object({
27+
address: z.string().refine(
28+
(v) => {
29+
// don't return `isAddress(v)` directly to avoid typecasting address as `0x${string}`
30+
if (isAddress(v)) {
31+
return true;
32+
}
33+
return false;
34+
},
35+
{
36+
message: "Invalid Address",
37+
},
38+
),
39+
}),
40+
),
41+
isRestricted: z.boolean(),
42+
});
43+
44+
export type TransferrableModuleFormValues = z.infer<typeof formSchema>;
45+
46+
export function TransferrableModuleUI(props: {
47+
allowList: string[];
48+
isRestricted: boolean;
49+
isPending: boolean;
50+
adminAddress: string;
51+
update: (values: TransferrableModuleFormValues) => Promise<void>;
52+
}) {
53+
const form = useForm<TransferrableModuleFormValues>({
54+
resolver: zodResolver(formSchema),
55+
values: {
56+
allowList: props.allowList.map((x) => ({ address: x })),
57+
isRestricted: props.isRestricted,
58+
},
59+
reValidateMode: "onChange",
60+
});
61+
62+
const updateMutation = useMutation({
63+
mutationFn: props.update,
64+
});
65+
66+
const formFields = useFieldArray({
67+
control: form.control,
68+
name: "allowList",
69+
});
70+
71+
const onSubmit = (_values: TransferrableModuleFormValues) => {
72+
const values = { ..._values };
73+
74+
// clear the allowlist if no restrictions
75+
if (!_values.isRestricted) {
76+
values.allowList = [];
77+
}
78+
79+
const promise = updateMutation.mutateAsync(values);
80+
toast.promise(promise, {
81+
success: "Successfully updated transfer restrictions",
82+
error: "Failed to update transfer restrictions",
83+
});
84+
};
85+
86+
const isRestricted = form.watch("isRestricted");
87+
88+
return (
89+
<section>
90+
<Form {...form}>
91+
<form
92+
className="rounded-lg border border-border bg-muted/50"
93+
onSubmit={form.handleSubmit(onSubmit)}
94+
>
95+
<div className="p-4 lg:p-6">
96+
{/* Title */}
97+
<h3 className="font-semibold text-xl tracking-tight">
98+
Transferrable
99+
</h3>
100+
101+
{/* Description */}
102+
<p className="text-muted-foreground">
103+
Determine who can transfer tokens on this contract
104+
</p>
105+
106+
<div className="h-5" />
107+
108+
{props.isPending ? (
109+
<Skeleton className="h-36" />
110+
) : (
111+
<>
112+
<div className="flex items-center gap-4">
113+
{/* Switch */}
114+
<FormField
115+
control={form.control}
116+
name="isRestricted"
117+
render={({ field }) => {
118+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
119+
const { value, ...restField } = field;
120+
121+
return (
122+
<FormItem className="flex items-center gap-3">
123+
<FormLabel>Restrict Transfers</FormLabel>
124+
<FormControl>
125+
<Switch
126+
{...restField}
127+
checked={field.value}
128+
className="!m-0"
129+
onCheckedChange={(v) => {
130+
field.onChange(v);
131+
132+
// if enabling restrictions and allowlist is empty, add the admin address by default
133+
if (
134+
v === true &&
135+
formFields.fields.length === 0
136+
) {
137+
formFields.append({
138+
address: props.adminAddress || "",
139+
});
140+
}
141+
}}
142+
/>
143+
</FormControl>
144+
</FormItem>
145+
);
146+
}}
147+
/>
148+
</div>
149+
150+
<div className="h-3" />
151+
152+
{isRestricted && (
153+
<div className="w-full">
154+
{/* Warning */}
155+
{formFields.fields.length === 0 ? (
156+
<Alert variant="warning">
157+
<CircleAlertIcon className="size-5" />
158+
<AlertTitle>
159+
Nobody has permission to transfer tokens on this
160+
contract
161+
</AlertTitle>
162+
</Alert>
163+
) : (
164+
<div className="flex flex-col gap-3">
165+
{/* Addresses */}
166+
{formFields.fields.map((fieldItem, index) => (
167+
<div
168+
className="flex items-start gap-3"
169+
key={fieldItem.id}
170+
>
171+
<FormField
172+
control={form.control}
173+
name={`allowList.${index}.address`}
174+
render={({ field }) => (
175+
<FormItem className="grow">
176+
<FormControl>
177+
<Input placeholder="0x..." {...field} />
178+
</FormControl>
179+
<FormMessage />
180+
</FormItem>
181+
)}
182+
/>
183+
<ToolTipLabel label="Remove address">
184+
<Button
185+
variant="destructive"
186+
onClick={() => {
187+
formFields.remove(index);
188+
}}
189+
>
190+
<Trash2Icon className="size-4" />
191+
</Button>
192+
</ToolTipLabel>
193+
</div>
194+
))}
195+
</div>
196+
)}
197+
198+
<div className="h-5" />
199+
200+
{/* Add Addresses Actions */}
201+
<div className="flex gap-3">
202+
<Button
203+
variant="outline"
204+
size="sm"
205+
onClick={() => {
206+
// add admin by default if adding the first input
207+
formFields.append({
208+
address:
209+
formFields.fields.length === 0
210+
? props.adminAddress
211+
: "",
212+
});
213+
}}
214+
className="gap-2"
215+
>
216+
<PlusIcon className="size-3" />
217+
Add Address
218+
</Button>
219+
</div>
220+
</div>
221+
)}
222+
223+
{!isRestricted && (
224+
<Alert variant="info">
225+
<CircleAlertIcon className="size-5" />
226+
<AlertTitle>
227+
Transferring tokens in this contract is not restricted.
228+
Everyone is free to transfer their tokens.
229+
</AlertTitle>
230+
</Alert>
231+
)}
232+
</>
233+
)}
234+
</div>
235+
236+
{/* Footer */}
237+
<div className="flex flex-col items-end border-border border-t px-4 py-4 lg:px-6">
238+
<Button
239+
type="submit"
240+
size="sm"
241+
className="min-w-24 gap-2"
242+
disabled={!form.formState.isDirty || props.isPending}
243+
>
244+
{updateMutation.isPending && <Spinner className="size-4" />}
245+
Update
246+
</Button>
247+
</div>
248+
</form>
249+
</Form>
250+
</section>
251+
);
252+
}

0 commit comments

Comments
 (0)