1+ import {
2+ Dialog ,
3+ DialogContent ,
4+ DialogHeader ,
5+ DialogTitle ,
6+ DialogFooter ,
7+ Button ,
8+ Input ,
9+ FormControl ,
10+ FormLabel ,
11+ FormErrorMessage ,
12+ VStack ,
13+ Select ,
14+ Alert ,
15+ PasswordInput ,
16+ } from '@chakra-ui/react' ;
17+ import { useState , useCallback } from 'react' ;
18+ import { useForm } from 'react-hook-form' ;
19+ import { LoadingButton } from '@/components/ui/LoadingButton' ;
20+ import type { CreateUserRequest } from '@/api/types' ;
21+
22+ interface CreateUserModalProps {
23+ isOpen : boolean ;
24+ onClose : ( ) => void ;
25+ onCreateUser : ( request : CreateUserRequest ) => Promise < void > ;
26+ loading ?: boolean ;
27+ }
28+
29+ interface FormData extends CreateUserRequest {
30+ confirmPassword : string ;
31+ }
32+
33+ export function CreateUserModal ( {
34+ isOpen,
35+ onClose,
36+ onCreateUser,
37+ loading = false ,
38+ } : CreateUserModalProps ) {
39+ const [ submitError , setSubmitError ] = useState < string | null > ( null ) ;
40+
41+ const {
42+ register,
43+ handleSubmit,
44+ formState : { errors, isSubmitting } ,
45+ reset,
46+ watch,
47+ } = useForm < FormData > ( {
48+ defaultValues : {
49+ username : '' ,
50+ email : '' ,
51+ password : '' ,
52+ confirmPassword : '' ,
53+ role : 'User' ,
54+ } ,
55+ } ) ;
56+
57+ const password = watch ( 'password' ) ;
58+
59+ const handleClose = useCallback ( ( ) => {
60+ reset ( ) ;
61+ setSubmitError ( null ) ;
62+ onClose ( ) ;
63+ } , [ reset , onClose ] ) ;
64+
65+ const onSubmit = useCallback (
66+ async ( data : FormData ) => {
67+ try {
68+ setSubmitError ( null ) ;
69+
70+ // Create user request without confirmPassword
71+ const { confirmPassword, ...request } = data ;
72+ await onCreateUser ( request ) ;
73+
74+ handleClose ( ) ;
75+ } catch ( err ) {
76+ console . error ( 'Create user error:' , err ) ;
77+ if ( err instanceof Error ) {
78+ try {
79+ const apiError = JSON . parse ( err . message ) ;
80+ setSubmitError ( apiError . message || 'Failed to create user' ) ;
81+ } catch {
82+ setSubmitError ( err . message || 'Failed to create user' ) ;
83+ }
84+ } else {
85+ setSubmitError ( 'Failed to create user' ) ;
86+ }
87+ }
88+ } ,
89+ [ onCreateUser , handleClose ]
90+ ) ;
91+
92+ return (
93+ < Dialog . Root open = { isOpen } onOpenChange = { ( e ) => ! e . open && handleClose ( ) } >
94+ < Dialog . Backdrop />
95+ < Dialog . Positioner >
96+ < DialogContent maxW = "md" >
97+ < DialogHeader >
98+ < DialogTitle > Create New User</ DialogTitle >
99+ </ DialogHeader >
100+
101+ < form onSubmit = { handleSubmit ( onSubmit ) } >
102+ < VStack gap = { 4 } py = { 4 } >
103+ { submitError && (
104+ < Alert . Root status = "error" variant = "subtle" >
105+ < Alert . Indicator />
106+ < Alert . Title > { submitError } </ Alert . Title >
107+ </ Alert . Root >
108+ ) }
109+
110+ { /* Username */ }
111+ < FormControl isInvalid = { ! ! errors . username } >
112+ < FormLabel > Username</ FormLabel >
113+ < Input
114+ { ...register ( 'username' , {
115+ required : 'Username is required' ,
116+ maxLength : {
117+ value : 256 ,
118+ message : 'Username must be 256 characters or less' ,
119+ } ,
120+ } ) }
121+ placeholder = "Enter username"
122+ disabled = { isSubmitting || loading }
123+ />
124+ { errors . username && (
125+ < FormErrorMessage > { errors . username . message } </ FormErrorMessage >
126+ ) }
127+ </ FormControl >
128+
129+ { /* Email */ }
130+ < FormControl isInvalid = { ! ! errors . email } >
131+ < FormLabel > Email</ FormLabel >
132+ < Input
133+ type = "email"
134+ { ...register ( 'email' , {
135+ required : 'Email is required' ,
136+ pattern : {
137+ value : / ^ [ ^ \s @ ] + @ [ ^ \s @ ] + \. [ ^ \s @ ] + $ / ,
138+ message : 'Please enter a valid email address' ,
139+ } ,
140+ maxLength : {
141+ value : 256 ,
142+ message : 'Email must be 256 characters or less' ,
143+ } ,
144+ } ) }
145+ placeholder = "Enter email address"
146+ disabled = { isSubmitting || loading }
147+ />
148+ { errors . email && (
149+ < FormErrorMessage > { errors . email . message } </ FormErrorMessage >
150+ ) }
151+ </ FormControl >
152+
153+ { /* Password */ }
154+ < FormControl isInvalid = { ! ! errors . password } >
155+ < FormLabel > Password</ FormLabel >
156+ < PasswordInput
157+ { ...register ( 'password' , {
158+ required : 'Password is required' ,
159+ minLength : {
160+ value : 8 ,
161+ message : 'Password must be at least 8 characters' ,
162+ } ,
163+ maxLength : {
164+ value : 100 ,
165+ message : 'Password must be 100 characters or less' ,
166+ } ,
167+ } ) }
168+ placeholder = "Enter password"
169+ disabled = { isSubmitting || loading }
170+ />
171+ { errors . password && (
172+ < FormErrorMessage > { errors . password . message } </ FormErrorMessage >
173+ ) }
174+ </ FormControl >
175+
176+ { /* Confirm Password */ }
177+ < FormControl isInvalid = { ! ! errors . confirmPassword } >
178+ < FormLabel > Confirm Password</ FormLabel >
179+ < PasswordInput
180+ { ...register ( 'confirmPassword' , {
181+ required : 'Please confirm your password' ,
182+ validate : ( value ) =>
183+ value === password || 'Passwords do not match' ,
184+ } ) }
185+ placeholder = "Confirm password"
186+ disabled = { isSubmitting || loading }
187+ />
188+ { errors . confirmPassword && (
189+ < FormErrorMessage > { errors . confirmPassword . message } </ FormErrorMessage >
190+ ) }
191+ </ FormControl >
192+
193+ { /* Role */ }
194+ < FormControl isInvalid = { ! ! errors . role } >
195+ < FormLabel > Role</ FormLabel >
196+ < Select . Root
197+ { ...register ( 'role' , { required : 'Role is required' } ) }
198+ disabled = { isSubmitting || loading }
199+ >
200+ < Select . Trigger >
201+ < Select . ValueText placeholder = "Select role" />
202+ </ Select . Trigger >
203+ < Select . Content >
204+ < Select . Item value = "User" > User</ Select . Item >
205+ < Select . Item value = "Administrator" > Administrator</ Select . Item >
206+ </ Select . Content >
207+ </ Select . Root >
208+ { errors . role && (
209+ < FormErrorMessage > { errors . role . message } </ FormErrorMessage >
210+ ) }
211+ </ FormControl >
212+ </ VStack >
213+
214+ < DialogFooter >
215+ < Button
216+ variant = "outline"
217+ onClick = { handleClose }
218+ disabled = { isSubmitting || loading }
219+ >
220+ Cancel
221+ </ Button >
222+ < LoadingButton
223+ type = "submit"
224+ colorPalette = "blue"
225+ loading = { isSubmitting || loading }
226+ loadingText = "Creating..."
227+ >
228+ Create User
229+ </ LoadingButton >
230+ </ DialogFooter >
231+ </ form >
232+ </ DialogContent >
233+ </ Dialog . Positioner >
234+ </ Dialog . Root >
235+ ) ;
236+ }
0 commit comments