Skip to content

Commit da5a6b4

Browse files
committed
♻️ Refactor UserInformation component
1 parent 0685d7f commit da5a6b4

File tree

1 file changed

+126
-103
lines changed

1 file changed

+126
-103
lines changed

frontend/src/components/UserSettings/UserInformation.tsx

Lines changed: 126 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,45 @@
1-
import {
2-
Box,
3-
Button,
4-
Container,
5-
Flex,
6-
Heading,
7-
Input,
8-
Text,
9-
} from "@chakra-ui/react"
1+
import { zodResolver } from "@hookform/resolvers/zod"
102
import { useMutation, useQueryClient } from "@tanstack/react-query"
113
import { useState } from "react"
12-
import { type SubmitHandler, useForm } from "react-hook-form"
4+
import { useForm } from "react-hook-form"
5+
import { z } from "zod"
136

7+
import { UsersService, type UserUpdateMe } from "@/client"
8+
import { Button } from "@/components/ui/button"
149
import {
15-
type ApiError,
16-
type UserPublic,
17-
UsersService,
18-
type UserUpdateMe,
19-
} from "@/client"
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"
2019
import useAuth from "@/hooks/useAuth"
2120
import useCustomToast from "@/hooks/useCustomToast"
22-
import { emailPattern, handleError } from "@/utils"
23-
import { Field } from "../ui/field"
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>
2430

2531
const UserInformation = () => {
2632
const queryClient = useQueryClient()
27-
const { showSuccessToast } = useCustomToast()
33+
const { showSuccessToast, showErrorToast } = useCustomToast()
2834
const [editMode, setEditMode] = useState(false)
2935
const { user: currentUser } = useAuth()
30-
const {
31-
register,
32-
handleSubmit,
33-
reset,
34-
getValues,
35-
formState: { isSubmitting, errors, isDirty },
36-
} = useForm<UserPublic>({
36+
37+
const form = useForm<FormData>({
38+
resolver: zodResolver(formSchema),
3739
mode: "onBlur",
3840
criteriaMode: "all",
3941
defaultValues: {
40-
full_name: currentUser?.full_name,
42+
full_name: currentUser?.full_name ?? undefined,
4143
email: currentUser?.email,
4244
},
4345
})
@@ -51,97 +53,118 @@ const UserInformation = () => {
5153
UsersService.updateUserMe({ requestBody: data }),
5254
onSuccess: () => {
5355
showSuccessToast("User updated successfully.")
56+
toggleEditMode()
5457
},
55-
onError: (err: ApiError) => {
56-
handleError(err)
57-
},
58+
onError: handleError.bind(showErrorToast),
5859
onSettled: () => {
5960
queryClient.invalidateQueries()
6061
},
6162
})
6263

63-
const onSubmit: SubmitHandler<UserUpdateMe> = async (data) => {
64-
mutation.mutate(data)
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)
6576
}
6677

6778
const onCancel = () => {
68-
reset()
79+
form.reset()
6980
toggleEditMode()
7081
}
7182

7283
return (
73-
<Container maxW="full">
74-
<Heading size="sm" py={4}>
75-
User Information
76-
</Heading>
77-
<Box
78-
w={{ sm: "full", md: "sm" }}
79-
as="form"
80-
onSubmit={handleSubmit(onSubmit)}
81-
>
82-
<Field label="Full name">
83-
{editMode ? (
84-
<Input
85-
{...register("full_name", { maxLength: 30 })}
86-
type="text"
87-
size="md"
88-
/>
89-
) : (
90-
<Text
91-
fontSize="md"
92-
py={2}
93-
color={!currentUser?.full_name ? "gray" : "inherit"}
94-
truncate
95-
maxW="sm"
96-
>
97-
{currentUser?.full_name || "N/A"}
98-
</Text>
99-
)}
100-
</Field>
101-
<Field
102-
mt={4}
103-
label="Email"
104-
invalid={!!errors.email}
105-
errorText={errors.email?.message}
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"
10690
>
107-
{editMode ? (
108-
<Input
109-
{...register("email", {
110-
required: "Email is required",
111-
pattern: emailPattern,
112-
})}
113-
type="email"
114-
size="md"
115-
/>
116-
) : (
117-
<Text fontSize="md" py={2} truncate maxW="sm">
118-
{currentUser?.email}
119-
</Text>
120-
)}
121-
</Field>
122-
<Flex mt={4} gap={3}>
123-
<Button
124-
variant="solid"
125-
onClick={toggleEditMode}
126-
type={editMode ? "button" : "submit"}
127-
loading={editMode ? isSubmitting : false}
128-
disabled={editMode ? !isDirty || !getValues("email") : false}
129-
>
130-
{editMode ? "Save" : "Edit"}
131-
</Button>
132-
{editMode && (
133-
<Button
134-
variant="subtle"
135-
colorPalette="gray"
136-
onClick={onCancel}
137-
disabled={isSubmitting}
138-
>
139-
Cancel
140-
</Button>
141-
)}
142-
</Flex>
143-
</Box>
144-
</Container>
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>
145168
)
146169
}
147170

0 commit comments

Comments
 (0)