Skip to content

Commit dcfab1c

Browse files
committed
♻️ Refactor AddUser and AddItem components
1 parent 05000a8 commit dcfab1c

File tree

2 files changed

+267
-256
lines changed

2 files changed

+267
-256
lines changed

frontend/src/components/Admin/AddUser.tsx

Lines changed: 174 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,64 @@
1-
import {
2-
Button,
3-
DialogActionTrigger,
4-
DialogTitle,
5-
Flex,
6-
Input,
7-
Text,
8-
VStack,
9-
} from "@chakra-ui/react"
1+
import { zodResolver } from "@hookform/resolvers/zod"
102
import { useMutation, useQueryClient } from "@tanstack/react-query"
3+
import { Plus } from "lucide-react"
114
import { useState } from "react"
12-
import { Controller, type SubmitHandler, useForm } from "react-hook-form"
13-
import { FaPlus } from "react-icons/fa"
5+
import { useForm } from "react-hook-form"
6+
import { z } from "zod"
7+
148
import { type UserCreate, UsersService } from "@/client"
15-
import type { ApiError } from "@/client/core/ApiError"
16-
import useCustomToast from "@/hooks/useCustomToast"
17-
import { emailPattern, handleError } from "@/utils"
18-
import { Checkbox } from "../ui/checkbox"
9+
import { Button } from "@/components/ui/button"
10+
import { Checkbox } from "@/components/ui/checkbox"
1911
import {
20-
DialogBody,
21-
DialogCloseTrigger,
12+
Dialog,
13+
DialogClose,
2214
DialogContent,
15+
DialogDescription,
2316
DialogFooter,
2417
DialogHeader,
25-
DialogRoot,
18+
DialogTitle,
2619
DialogTrigger,
27-
} from "../ui/dialog"
28-
import { Field } from "../ui/field"
20+
} from "@/components/ui/dialog"
21+
import {
22+
Form,
23+
FormControl,
24+
FormField,
25+
FormItem,
26+
FormLabel,
27+
FormMessage,
28+
} from "@/components/ui/form"
29+
import { Input } from "@/components/ui/input"
30+
import { LoadingButton } from "@/components/ui/loading-button"
31+
import useCustomToast from "@/hooks/useCustomToast"
32+
import { handleError } from "@/utils"
2933

30-
interface UserCreateForm extends UserCreate {
31-
confirm_password: string
32-
}
34+
const formSchema = z
35+
.object({
36+
email: z.email({ message: "Invalid email address" }),
37+
full_name: z.string().optional(),
38+
password: z
39+
.string()
40+
.min(1, { message: "Password is required" })
41+
.min(8, { message: "Password must be at least 8 characters" }),
42+
confirm_password: z
43+
.string()
44+
.min(1, { message: "Please confirm your password" }),
45+
is_superuser: z.boolean(),
46+
is_active: z.boolean(),
47+
})
48+
.refine((data) => data.password === data.confirm_password, {
49+
message: "The passwords don't match",
50+
path: ["confirm_password"],
51+
})
52+
53+
type FormData = z.infer<typeof formSchema>
3354

3455
const AddUser = () => {
3556
const [isOpen, setIsOpen] = useState(false)
3657
const queryClient = useQueryClient()
37-
const { showSuccessToast } = useCustomToast()
38-
const {
39-
control,
40-
register,
41-
handleSubmit,
42-
reset,
43-
getValues,
44-
formState: { errors, isValid, isSubmitting },
45-
} = useForm<UserCreateForm>({
58+
const { showSuccessToast, showErrorToast } = useCustomToast()
59+
60+
const form = useForm<FormData>({
61+
resolver: zodResolver(formSchema),
4662
mode: "onBlur",
4763
criteriaMode: "all",
4864
defaultValues: {
@@ -60,165 +76,161 @@ const AddUser = () => {
6076
UsersService.createUser({ requestBody: data }),
6177
onSuccess: () => {
6278
showSuccessToast("User created successfully.")
63-
reset()
79+
form.reset()
6480
setIsOpen(false)
6581
},
66-
onError: (err: ApiError) => {
67-
handleError(err)
68-
},
82+
onError: handleError.bind(showErrorToast),
6983
onSettled: () => {
7084
queryClient.invalidateQueries({ queryKey: ["users"] })
7185
},
7286
})
7387

74-
const onSubmit: SubmitHandler<UserCreateForm> = (data) => {
88+
const onSubmit = (data: FormData) => {
7589
mutation.mutate(data)
7690
}
7791

7892
return (
79-
<DialogRoot
80-
size={{ base: "xs", md: "md" }}
81-
placement="center"
82-
open={isOpen}
83-
onOpenChange={({ open }) => setIsOpen(open)}
84-
>
93+
<Dialog open={isOpen} onOpenChange={setIsOpen}>
8594
<DialogTrigger asChild>
86-
<Button value="add-user" my={4}>
87-
<FaPlus fontSize="16px" />
95+
<Button className="my-4">
96+
<Plus className="mr-2" />
8897
Add User
8998
</Button>
9099
</DialogTrigger>
91-
<DialogContent>
92-
<form onSubmit={handleSubmit(onSubmit)}>
93-
<DialogHeader>
94-
<DialogTitle>Add User</DialogTitle>
95-
</DialogHeader>
96-
<DialogBody>
97-
<Text mb={4}>
98-
Fill in the form below to add a new user to the system.
99-
</Text>
100-
<VStack gap={4}>
101-
<Field
102-
required
103-
invalid={!!errors.email}
104-
errorText={errors.email?.message}
105-
label="Email"
106-
>
107-
<Input
108-
{...register("email", {
109-
required: "Email is required",
110-
pattern: emailPattern,
111-
})}
112-
placeholder="Email"
113-
type="email"
114-
/>
115-
</Field>
100+
<DialogContent className="sm:max-w-md">
101+
<DialogHeader>
102+
<DialogTitle>Add User</DialogTitle>
103+
<DialogDescription>
104+
Fill in the form below to add a new user to the system.
105+
</DialogDescription>
106+
</DialogHeader>
107+
<Form {...form}>
108+
<form onSubmit={form.handleSubmit(onSubmit)}>
109+
<div className="grid gap-4 py-4">
110+
<FormField
111+
control={form.control}
112+
name="email"
113+
render={({ field }) => (
114+
<FormItem>
115+
<FormLabel>
116+
Email <span className="text-destructive">*</span>
117+
</FormLabel>
118+
<FormControl>
119+
<Input
120+
placeholder="Email"
121+
type="email"
122+
{...field}
123+
required
124+
/>
125+
</FormControl>
126+
<FormMessage />
127+
</FormItem>
128+
)}
129+
/>
116130

117-
<Field
118-
invalid={!!errors.full_name}
119-
errorText={errors.full_name?.message}
120-
label="Full Name"
121-
>
122-
<Input
123-
{...register("full_name")}
124-
placeholder="Full name"
125-
type="text"
126-
/>
127-
</Field>
131+
<FormField
132+
control={form.control}
133+
name="full_name"
134+
render={({ field }) => (
135+
<FormItem>
136+
<FormLabel>Full Name</FormLabel>
137+
<FormControl>
138+
<Input placeholder="Full name" type="text" {...field} />
139+
</FormControl>
140+
<FormMessage />
141+
</FormItem>
142+
)}
143+
/>
128144

129-
<Field
130-
required
131-
invalid={!!errors.password}
132-
errorText={errors.password?.message}
133-
label="Set Password"
134-
>
135-
<Input
136-
{...register("password", {
137-
required: "Password is required",
138-
minLength: {
139-
value: 8,
140-
message: "Password must be at least 8 characters",
141-
},
142-
})}
143-
placeholder="Password"
144-
type="password"
145-
/>
146-
</Field>
145+
<FormField
146+
control={form.control}
147+
name="password"
148+
render={({ field }) => (
149+
<FormItem>
150+
<FormLabel>
151+
Set Password <span className="text-destructive">*</span>
152+
</FormLabel>
153+
<FormControl>
154+
<Input
155+
placeholder="Password"
156+
type="password"
157+
{...field}
158+
required
159+
/>
160+
</FormControl>
161+
<FormMessage />
162+
</FormItem>
163+
)}
164+
/>
147165

148-
<Field
149-
required
150-
invalid={!!errors.confirm_password}
151-
errorText={errors.confirm_password?.message}
152-
label="Confirm Password"
153-
>
154-
<Input
155-
{...register("confirm_password", {
156-
required: "Please confirm your password",
157-
validate: (value) =>
158-
value === getValues().password ||
159-
"The passwords do not match",
160-
})}
161-
placeholder="Password"
162-
type="password"
163-
/>
164-
</Field>
165-
</VStack>
166+
<FormField
167+
control={form.control}
168+
name="confirm_password"
169+
render={({ field }) => (
170+
<FormItem>
171+
<FormLabel>
172+
Confirm Password <span className="text-destructive">*</span>
173+
</FormLabel>
174+
<FormControl>
175+
<Input
176+
placeholder="Password"
177+
type="password"
178+
{...field}
179+
required
180+
/>
181+
</FormControl>
182+
<FormMessage />
183+
</FormItem>
184+
)}
185+
/>
166186

167-
<Flex mt={4} direction="column" gap={4}>
168-
<Controller
169-
control={control}
187+
<FormField
188+
control={form.control}
170189
name="is_superuser"
171190
render={({ field }) => (
172-
<Field disabled={field.disabled} colorPalette="teal">
173-
<Checkbox
174-
checked={field.value}
175-
onCheckedChange={({ checked }) => field.onChange(checked)}
176-
>
177-
Is superuser?
178-
</Checkbox>
179-
</Field>
191+
<FormItem className="flex items-center gap-3 space-y-0">
192+
<FormControl>
193+
<Checkbox
194+
checked={field.value}
195+
onCheckedChange={field.onChange}
196+
/>
197+
</FormControl>
198+
<FormLabel className="font-normal">Is superuser?</FormLabel>
199+
</FormItem>
180200
)}
181201
/>
182-
<Controller
183-
control={control}
202+
203+
<FormField
204+
control={form.control}
184205
name="is_active"
185206
render={({ field }) => (
186-
<Field disabled={field.disabled} colorPalette="teal">
187-
<Checkbox
188-
checked={field.value}
189-
onCheckedChange={({ checked }) => field.onChange(checked)}
190-
>
191-
Is active?
192-
</Checkbox>
193-
</Field>
207+
<FormItem className="flex items-center gap-3 space-y-0">
208+
<FormControl>
209+
<Checkbox
210+
checked={field.value}
211+
onCheckedChange={field.onChange}
212+
/>
213+
</FormControl>
214+
<FormLabel className="font-normal">Is active?</FormLabel>
215+
</FormItem>
194216
)}
195217
/>
196-
</Flex>
197-
</DialogBody>
218+
</div>
198219

199-
<DialogFooter gap={2}>
200-
<DialogActionTrigger asChild>
201-
<Button
202-
variant="subtle"
203-
colorPalette="gray"
204-
disabled={isSubmitting}
205-
>
206-
Cancel
207-
</Button>
208-
</DialogActionTrigger>
209-
<Button
210-
variant="solid"
211-
type="submit"
212-
disabled={!isValid}
213-
loading={isSubmitting}
214-
>
215-
Save
216-
</Button>
217-
</DialogFooter>
218-
</form>
219-
<DialogCloseTrigger />
220+
<DialogFooter>
221+
<DialogClose asChild>
222+
<Button variant="outline" disabled={mutation.isPending}>
223+
Cancel
224+
</Button>
225+
</DialogClose>
226+
<LoadingButton type="submit" loading={mutation.isPending}>
227+
Save
228+
</LoadingButton>
229+
</DialogFooter>
230+
</form>
231+
</Form>
220232
</DialogContent>
221-
</DialogRoot>
233+
</Dialog>
222234
)
223235
}
224236

0 commit comments

Comments
 (0)