@@ -12,7 +12,6 @@ import {
1212 DropdownTrigger ,
1313 Input ,
1414 Link ,
15- Modal ,
1615 ModalBody ,
1716 ModalContent ,
1817 ModalHeader ,
@@ -23,6 +22,7 @@ import {
2322 NavbarMenu ,
2423 NavbarMenuItem ,
2524 NavbarMenuToggle ,
25+ Spinner ,
2626 useDisclosure
2727} from '@nextui-org/react'
2828import i18n from 'i18next'
@@ -31,20 +31,89 @@ import { useTheme } from 'next-themes'
3131import NextLink from 'next/link'
3232import { usePathname , useRouter } from 'next/navigation'
3333import { FC , useEffect , useState } from 'react'
34- import { useForm } from 'react-hook-form'
34+ import { Controller , FormProvider , useForm } from 'react-hook-form'
3535import { useTranslation } from 'react-i18next'
3636import { z } from 'zod'
37+ import { useUpdateAvatarMutation , useUpdateNameMutation } from '~/apis/mutation'
3738import { useUserQuery } from '~/apis/query'
3839import { LogoText } from '~/components/LogoText'
39- import { ModalConfirmFormFooter } from '~/components/Modal'
40+ import { Modal , ModalConfirmFormFooter , ModalSubmitFormFooter } from '~/components/Modal'
4041import { updatePasswordFormDefault , useUpdatePasswordSchemaWithRefine } from '~/schemas/account'
4142
43+ const AvatarUploader : FC < { name : string } > = ( { name } ) => {
44+ const [ uploading , setUploading ] = useState ( false )
45+
46+ return (
47+ < Controller
48+ name = { name }
49+ render = { ( { field } ) => (
50+ < div className = "flex items-center justify-center" >
51+ < div className = "flex h-32 w-32 items-center justify-center" >
52+ { uploading ? < Spinner /> : < Avatar size = "lg" as = "label" htmlFor = "avatar" showFallback src = { field . value } /> }
53+ </ div >
54+
55+ < input
56+ id = "avatar"
57+ type = "file"
58+ hidden
59+ multiple = { false }
60+ onChange = { async ( e ) => {
61+ setUploading ( true )
62+
63+ const file = e . target . files ?. item ( 0 )
64+
65+ if ( ! file ) return
66+
67+ const formData = new FormData ( )
68+ formData . append ( 'avatar' , file )
69+
70+ try {
71+ const { url } = await ky . post ( '/api/avatar' , { body : formData } ) . json < { url : string } > ( )
72+
73+ field . onChange ( url )
74+ } finally {
75+ setUploading ( false )
76+ }
77+ } }
78+ />
79+ </ div >
80+ ) }
81+ />
82+ )
83+ }
84+
4285export const Header : FC = ( ) => {
4386 const { t } = useTranslation ( )
4487 const { theme : curTheme , setTheme } = useTheme ( )
88+ const pathname = usePathname ( )
89+ const router = useRouter ( )
90+
91+ const userQuery = useUserQuery ( )
4592
4693 const [ isMenuOpen , setIsMenuOpen ] = useState ( false )
4794
95+ const {
96+ isOpen : isUpdateProfileOpen ,
97+ onOpen : onUpdateProfileOpen ,
98+ onClose : onUpdateProfileClose ,
99+ onOpenChange : onUpdateProfileOpenChange
100+ } = useDisclosure ( )
101+
102+ const updateProfileSchema = z . object ( {
103+ name : z . string ( ) . min ( 4 ) . max ( 20 ) ,
104+ avatar : z . string ( ) . min ( 1 )
105+ } )
106+
107+ const updateProfileForm = useForm < z . infer < typeof updateProfileSchema > > ( {
108+ resolver : zodResolver ( updateProfileSchema ) ,
109+ defaultValues : { name : '' , avatar : '' }
110+ } )
111+
112+ const updateProfileFormDirty = Object . values ( updateProfileForm . formState . dirtyFields ) . some ( ( dirty ) => dirty )
113+
114+ const updateNameMutation = useUpdateNameMutation ( )
115+ const updateAvatarMutation = useUpdateAvatarMutation ( )
116+
48117 const {
49118 isOpen : isUpdatePasswordOpen ,
50119 onOpen : onUpdatePasswordOpen ,
@@ -59,10 +128,6 @@ export const Header: FC = () => {
59128 defaultValues : updatePasswordFormDefault
60129 } )
61130
62- const pathname = usePathname ( )
63- const router = useRouter ( )
64- const userQuery = useUserQuery ( )
65-
66131 const navigationMenus = [
67132 { name : t ( 'primitives.network' ) , route : '/network' } ,
68133 { name : t ( 'primitives.rule' ) , route : '/rule' }
@@ -107,6 +172,7 @@ export const Header: FC = () => {
107172 className = "transition-transform"
108173 color = "secondary"
109174 size = "sm"
175+ showFallback
110176 name = { userQuery . data ?. user . name || userQuery . data ?. user . username }
111177 src = { userQuery . data ?. user . avatar || '' }
112178 />
@@ -116,6 +182,13 @@ export const Header: FC = () => {
116182 aria-label = "Profile Actions"
117183 variant = "flat"
118184 onAction = { ( key ) => {
185+ if ( key === 'profile' ) {
186+ updateProfileForm . reset ( {
187+ name : userQuery . data ?. user . name || '' ,
188+ avatar : userQuery . data ?. user . avatar || ''
189+ } )
190+ onUpdateProfileOpen ( )
191+ }
119192 if ( key === 'update-password' ) onUpdatePasswordOpen ( )
120193 } }
121194 >
@@ -129,7 +202,9 @@ export const Header: FC = () => {
129202 </ DropdownItem >
130203
131204 < DropdownItem key = "update-password" textValue = "update-password" >
132- { t ( 'actions.updatePassword' ) }
205+ { t ( 'actions.update' , {
206+ resourceName : t ( 'form.fields.password' )
207+ } ) }
133208 </ DropdownItem >
134209 </ DropdownSection >
135210
@@ -197,6 +272,51 @@ export const Header: FC = () => {
197272 ) ) }
198273 </ NavbarMenu >
199274
275+ < Modal isOpen = { isUpdateProfileOpen } onOpenChange = { onUpdateProfileOpenChange } >
276+ < FormProvider { ...updateProfileForm } >
277+ < form
278+ onSubmit = { updateProfileForm . handleSubmit ( async ( values ) => {
279+ try {
280+ if ( values . name !== userQuery . data ?. user . name ) {
281+ await updateNameMutation . mutateAsync ( values . name )
282+ }
283+
284+ if ( values . avatar !== userQuery . data ?. user . avatar ) {
285+ await updateAvatarMutation . mutateAsync ( values . avatar )
286+ }
287+
288+ await userQuery . refetch ( )
289+
290+ onUpdateProfileClose ( )
291+ } catch { }
292+ } ) }
293+ >
294+ < ModalContent >
295+ < ModalHeader > { t ( 'actions.update' , { resourceName : t ( 'primitives.profile' ) } ) } </ ModalHeader >
296+
297+ < ModalBody >
298+ < div className = "flex flex-col gap-4" >
299+ < Input
300+ label = { t ( 'form.fields.name' ) }
301+ placeholder = { t ( 'form.fields.name' ) }
302+ errorMessage = { updateProfileForm . formState . errors . name ?. message }
303+ { ...updateProfileForm . register ( 'name' ) }
304+ />
305+
306+ < AvatarUploader name = "avatar" />
307+ </ div >
308+ </ ModalBody >
309+
310+ < ModalSubmitFormFooter
311+ reset = { updateProfileForm . reset }
312+ isResetDisabled = { ! updateProfileFormDirty }
313+ isSubmitting = { updateProfileForm . formState . isSubmitting }
314+ />
315+ </ ModalContent >
316+ </ form >
317+ </ FormProvider >
318+ </ Modal >
319+
200320 < Modal isOpen = { isUpdatePasswordOpen } onOpenChange = { onUpdatePasswordOpenChange } >
201321 < form
202322 onSubmit = { updatePasswordForm . handleSubmit ( async ( values ) => {
@@ -210,30 +330,30 @@ export const Header: FC = () => {
210330 } ) }
211331 >
212332 < ModalContent >
213- < ModalHeader > { t ( 'actions.updatePassword' ) } </ ModalHeader >
333+ < ModalHeader > { t ( 'actions.update' , { resourceName : t ( 'form.fields.password' ) } ) } </ ModalHeader >
214334
215335 < ModalBody >
216336 < div className = "flex flex-col gap-4" >
217337 < Input
218338 type = "password"
219- label = { t ( 'primitives .currentPassword' ) }
220- placeholder = { t ( 'primitives .currentPassword' ) }
339+ label = { t ( 'form.fields .currentPassword' ) }
340+ placeholder = { t ( 'form.fields .currentPassword' ) }
221341 errorMessage = { updatePasswordForm . formState . errors . currentPassword ?. message }
222342 { ...updatePasswordForm . register ( 'currentPassword' ) }
223343 />
224344
225345 < Input
226346 type = "password"
227- label = { t ( 'primitives .newPassword' ) }
228- placeholder = { t ( 'primitives .newPassword' ) }
347+ label = { t ( 'form.fields .newPassword' ) }
348+ placeholder = { t ( 'form.fields .newPassword' ) }
229349 errorMessage = { updatePasswordForm . formState . errors . newPassword ?. message }
230350 { ...updatePasswordForm . register ( 'newPassword' ) }
231351 />
232352
233353 < Input
234354 type = "password"
235- label = { t ( 'primitives .confirmPassword' ) }
236- placeholder = { t ( 'primitives .confirmPassword' ) }
355+ label = { t ( 'form.fields .confirmPassword' ) }
356+ placeholder = { t ( 'form.fields .confirmPassword' ) }
237357 errorMessage = { updatePasswordForm . formState . errors . confirmPassword ?. message }
238358 { ...updatePasswordForm . register ( 'confirmPassword' ) }
239359 />
0 commit comments