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"
102import { useMutation , useQueryClient } from "@tanstack/react-query"
113import { 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"
149import {
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"
2019import useAuth from "@/hooks/useAuth"
2120import 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
2531const 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