← full-stack-fastapi-template  /  frontend/src/components/Items/AddItem.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 ItemCreate, ItemsService } from "@/client"
9
import { Button } from "@/components/ui/button"
10
import {
11
  Dialog,
12
  DialogClose,
13
  DialogContent,
14
  DialogDescription,
15
  DialogFooter,
16
  DialogHeader,
17
  DialogTitle,
18
  DialogTrigger,
19
} from "@/components/ui/dialog"
20
import {
21
  Form,
22
  FormControl,
23
  FormField,
24
  FormItem,
25
  FormLabel,
26
  FormMessage,
27
} from "@/components/ui/form"
28
import { Input } from "@/components/ui/input"
29
import { LoadingButton } from "@/components/ui/loading-button"
30
import useCustomToast from "@/hooks/useCustomToast"
31
import { handleError } from "@/utils"
32
33
const formSchema = z.object({
34
  title: z.string().min(1, { message: "Title is required" }),
35
  description: z.string().optional(),
36
})
37
38
type FormData = z.infer<typeof formSchema>
39
40
const AddItem = () => {
41
  const [isOpen, setIsOpen] = useState(false)
42
  const queryClient = useQueryClient()
43
  const { showSuccessToast, showErrorToast } = useCustomToast()
44
45
  const form = useForm<FormData>({
46
    resolver: zodResolver(formSchema),
47
    mode: "onBlur",
48
    criteriaMode: "all",
49
    defaultValues: {
50
      title: "",
51
      description: "",
52
    },
53
  })
54
55
  const mutation = useMutation({
56
    mutationFn: (data: ItemCreate) =>
57
      ItemsService.createItem({ requestBody: data }),
58
    onSuccess: () => {
59
      showSuccessToast("Item created successfully")
60
      form.reset()
61
      setIsOpen(false)
62
    },
63
    onError: handleError.bind(showErrorToast),
64
    onSettled: () => {
65
      queryClient.invalidateQueries({ queryKey: ["items"] })
66
    },
67
  })
68
69
  const onSubmit = (data: FormData) => {
70
    mutation.mutate(data)
71
  }
72
73
  return (
74
    <Dialog open={isOpen} onOpenChange={setIsOpen}>
75
      <DialogTrigger asChild>
76
        <Button className="my-4">
77
          <Plus className="mr-2" />
78
          Add Item
79
        </Button>
80
      </DialogTrigger>
81
      <DialogContent className="sm:max-w-md">
82
        <DialogHeader>
83
          <DialogTitle>Add Item</DialogTitle>
84
          <DialogDescription>
85
            Fill in the details to add a new item.
86
          </DialogDescription>
87
        </DialogHeader>
88
        <Form {...form}>
89
          <form onSubmit={form.handleSubmit(onSubmit)}>
90
            <div className="grid gap-4 py-4">
91
              <FormField
92
                control={form.control}
93
                name="title"
94
                render={({ field }) => (
95
                  <FormItem>
96
                    <FormLabel>
97
                      Title <span className="text-destructive">*</span>
98
                    </FormLabel>
99
                    <FormControl>
100
                      <Input
101
                        placeholder="Title"
102
                        type="text"
103
                        {...field}
104
                        required
105
                      />
106
                    </FormControl>
107
                    <FormMessage />
108
                  </FormItem>
109
                )}
110
              />
111
112
              <FormField
113
                control={form.control}
114
                name="description"
115
                render={({ field }) => (
116
                  <FormItem>
117
                    <FormLabel>Description</FormLabel>
118
                    <FormControl>
119
                      <Input placeholder="Description" type="text" {...field} />
120
                    </FormControl>
121
                    <FormMessage />
122
                  </FormItem>
123
                )}
124
              />
125
            </div>
126
127
            <DialogFooter>
128
              <DialogClose asChild>
129
                <Button variant="outline" disabled={mutation.isPending}>
130
                  Cancel
131
                </Button>
132
              </DialogClose>
133
              <LoadingButton type="submit" loading={mutation.isPending}>
134
                Save
135
              </LoadingButton>
136
            </DialogFooter>
137
          </form>
138
        </Form>
139
      </DialogContent>
140
    </Dialog>
141
  )
142
}
143
144
export default AddItem
145