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"
102import { useMutation , useQueryClient } from "@tanstack/react-query"
3+ import { Plus } from "lucide-react"
114import { 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+
148import { 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"
1911import {
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
3455const 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