← full-stack-fastapi-template  /  frontend/src/components/Admin/AddUser.tsx

1
import { zodResolver } from "@hookform/resolvers/zod"
2
import { useMutation, useQueryClient } from "@tanstack/react-query"
3
import { Plus } from "lucide-react"
4
import { useState } from "react"
5
import { useForm } from "react-hook-form"
6
import { z } from "zod"
7
8
import { type UserCreate, UsersService } from "@/client"
9
import { Button } from "@/components/ui/button"
10
import { Checkbox } from "@/components/ui/checkbox"
11
import {
12
  Dialog,
13
  DialogClose,
14
  DialogContent,
15
  DialogDescription,
16
  DialogFooter,
17
  DialogHeader,
18
  DialogTitle,
19
  DialogTrigger,
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"
33
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>
54
55
const AddUser = () => {
56
  const [isOpen, setIsOpen] = useState(false)
57
  const queryClient = useQueryClient()
58
  const { showSuccessToast, showErrorToast } = useCustomToast()
59
60
  const form = useForm<FormData>({
61
    resolver: zodResolver(formSchema),
62
    mode: "onBlur",
63
    criteriaMode: "all",
64
    defaultValues: {
65
      email: "",
66
      full_name: "",
67
      password: "",
68
      confirm_password: "",
69
      is_superuser: false,
70
      is_active: false,
71
    },
72
  })
73
74
  const mutation = useMutation({
75
    mutationFn: (data: UserCreate) =>
76
      UsersService.createUser({ requestBody: data }),
77
    onSuccess: () => {
78
      showSuccessToast("User created successfully")
79
      form.reset()
80
      setIsOpen(false)
81
    },
82
    onError: handleError.bind(showErrorToast),
83
    onSettled: () => {
84
      queryClient.invalidateQueries({ queryKey: ["users"] })
85
    },
86
  })
87
88
  const onSubmit = (data: FormData) => {
89
    mutation.mutate(data)
90
  }
91
92
  return (
93
    <Dialog open={isOpen} onOpenChange={setIsOpen}>
94
      <DialogTrigger asChild>
95
        <Button className="my-4">
96
          <Plus className="mr-2" />
97
          Add User
98
        </Button>
99
      </DialogTrigger>
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
              />
130
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
              />
144
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
              />
165
166
              <FormField
167
                control={form.control}
168
                name="confirm_password"
169
                render={({ field }) => (
170
                  <FormItem>
171
                    <FormLabel>
172
                      Confirm Password{" "}
173
                      <span className="text-destructive">*</span>
174
                    </FormLabel>
175
                    <FormControl>
176
                      <Input
177
                        placeholder="Password"
178
                        type="password"
179
                        {...field}
180
                        required
181
                      />
182
                    </FormControl>
183
                    <FormMessage />
184
                  </FormItem>
185
                )}
186
              />
187
188
              <FormField
189
                control={form.control}
190
                name="is_superuser"
191
                render={({ field }) => (
192
                  <FormItem className="flex items-center gap-3 space-y-0">
193
                    <FormControl>
194
                      <Checkbox
195
                        checked={field.value}
196
                        onCheckedChange={field.onChange}
197
                      />
198
                    </FormControl>
199
                    <FormLabel className="font-normal">Is superuser?</FormLabel>
200
                  </FormItem>
201
                )}
202
              />
203
204
              <FormField
205
                control={form.control}
206
                name="is_active"
207
                render={({ field }) => (
208
                  <FormItem className="flex items-center gap-3 space-y-0">
209
                    <FormControl>
210
                      <Checkbox
211
                        checked={field.value}
212
                        onCheckedChange={field.onChange}
213
                      />
214
                    </FormControl>
215
                    <FormLabel className="font-normal">Is active?</FormLabel>
216
                  </FormItem>
217
                )}
218
              />
219
            </div>
220
221
            <DialogFooter>
222
              <DialogClose asChild>
223
                <Button variant="outline" disabled={mutation.isPending}>
224
                  Cancel
225
                </Button>
226
              </DialogClose>
227
              <LoadingButton type="submit" loading={mutation.isPending}>
228
                Save
229
              </LoadingButton>
230
            </DialogFooter>
231
          </form>
232
        </Form>
233
      </DialogContent>
234
    </Dialog>
235
  )
236
}
237
238
export default AddUser
239