← full-stack-fastapi-template  /  frontend/src/components/UserSettings/UserInformation.tsx

1
import { zodResolver } from "@hookform/resolvers/zod"
2
import { useMutation, useQueryClient } from "@tanstack/react-query"
3
import { useState } from "react"
4
import { useForm } from "react-hook-form"
5
import { z } from "zod"
6
7
import { UsersService, type UserUpdateMe } from "@/client"
8
import { Button } from "@/components/ui/button"
9
import {
10
  Form,
11
  FormControl,
12
  FormField,
13
  FormItem,
14
  FormLabel,
15
  FormMessage,
16
} from "@/components/ui/form"
17
import { Input } from "@/components/ui/input"
18
import { LoadingButton } from "@/components/ui/loading-button"
19
import useAuth from "@/hooks/useAuth"
20
import useCustomToast from "@/hooks/useCustomToast"
21
import { cn } from "@/lib/utils"
22
import { handleError } from "@/utils"
23
24
const formSchema = z.object({
25
  full_name: z.string().max(30).optional(),
26
  email: z.email({ message: "Invalid email address" }),
27
})
28
29
type FormData = z.infer<typeof formSchema>
30
31
const UserInformation = () => {
32
  const queryClient = useQueryClient()
33
  const { showSuccessToast, showErrorToast } = useCustomToast()
34
  const [editMode, setEditMode] = useState(false)
35
  const { user: currentUser } = useAuth()
36
37
  const form = useForm<FormData>({
38
    resolver: zodResolver(formSchema),
39
    mode: "onBlur",
40
    criteriaMode: "all",
41
    defaultValues: {
42
      full_name: currentUser?.full_name ?? undefined,
43
      email: currentUser?.email,
44
    },
45
  })
46
47
  const toggleEditMode = () => {
48
    setEditMode(!editMode)
49
  }
50
51
  const mutation = useMutation({
52
    mutationFn: (data: UserUpdateMe) =>
53
      UsersService.updateUserMe({ requestBody: data }),
54
    onSuccess: () => {
55
      showSuccessToast("User updated successfully")
56
      toggleEditMode()
57
    },
58
    onError: handleError.bind(showErrorToast),
59
    onSettled: () => {
60
      queryClient.invalidateQueries()
61
    },
62
  })
63
64
  const onSubmit = (data: FormData) => {
65
    const updateData: UserUpdateMe = {}
66
67
    // only include fields that have changed
68
    if (data.full_name !== currentUser?.full_name) {
69
      updateData.full_name = data.full_name
70
    }
71
    if (data.email !== currentUser?.email) {
72
      updateData.email = data.email
73
    }
74
75
    mutation.mutate(updateData)
76
  }
77
78
  const onCancel = () => {
79
    form.reset()
80
    toggleEditMode()
81
  }
82
83
  return (
84
    <div className="max-w-md">
85
      <h3 className="text-lg font-semibold py-4">User Information</h3>
86
      <Form {...form}>
87
        <form
88
          onSubmit={form.handleSubmit(onSubmit)}
89
          className="flex flex-col gap-4"
90
        >
91
          <FormField
92
            control={form.control}
93
            name="full_name"
94
            render={({ field }) =>
95
              editMode ? (
96
                <FormItem>
97
                  <FormLabel>Full name</FormLabel>
98
                  <FormControl>
99
                    <Input type="text" {...field} />
100
                  </FormControl>
101
                  <FormMessage />
102
                </FormItem>
103
              ) : (
104
                <FormItem>
105
                  <FormLabel>Full name</FormLabel>
106
                  <p
107
                    className={cn(
108
                      "py-2 truncate max-w-sm",
109
                      !field.value && "text-muted-foreground",
110
                    )}
111
                  >
112
                    {field.value || "N/A"}
113
                  </p>
114
                </FormItem>
115
              )
116
            }
117
          />
118
119
          <FormField
120
            control={form.control}
121
            name="email"
122
            render={({ field }) =>
123
              editMode ? (
124
                <FormItem>
125
                  <FormLabel>Email</FormLabel>
126
                  <FormControl>
127
                    <Input type="email" {...field} />
128
                  </FormControl>
129
                  <FormMessage />
130
                </FormItem>
131
              ) : (
132
                <FormItem>
133
                  <FormLabel>Email</FormLabel>
134
                  <p className="py-2 truncate max-w-sm">{field.value}</p>
135
                </FormItem>
136
              )
137
            }
138
          />
139
140
          <div className="flex gap-3">
141
            {editMode ? (
142
              <>
143
                <LoadingButton
144
                  type="submit"
145
                  loading={mutation.isPending}
146
                  disabled={!form.formState.isDirty}
147
                >
148
                  Save
149
                </LoadingButton>
150
                <Button
151
                  type="button"
152
                  variant="outline"
153
                  onClick={onCancel}
154
                  disabled={mutation.isPending}
155
                >
156
                  Cancel
157
                </Button>
158
              </>
159
            ) : (
160
              <Button type="button" onClick={toggleEditMode}>
161
                Edit
162
              </Button>
163
            )}
164
          </div>
165
        </form>
166
      </Form>
167
    </div>
168
  )
169
}
170
171
export default UserInformation
172