33import { useEffect , useState } from "react" ;
44import { useParams , useRouter } from "next/navigation" ;
55import Link from "next/link" ;
6+ import { useForm } from "react-hook-form" ;
7+ import { zodResolver } from "@hookform/resolvers/zod" ;
8+ import { z } from "zod" ;
69import { getAddress } from "@stellar/freighter-api" ;
710import { AppShell } from "@/components/app-shell" ;
811import { API_BASE_URL } from "@/lib/config" ;
@@ -23,38 +26,44 @@ type ProfileData = {
2326 acceptedAssets : Array < { code : string ; issuer ?: string | null } > ;
2427} ;
2528
26- type FieldErrors = {
27- displayName ?: string ;
28- bio ?: string ;
29- websiteUrl ?: string ;
30- twitterHandle ?: string ;
31- githubHandle ?: string ;
32- email ?: string ;
29+ const editProfileSchema = z . object ( {
30+ displayName : z
31+ . string ( )
32+ . trim ( )
33+ . min ( 1 , "Display name is required." )
34+ . max ( 64 , "Max 64 characters." ) ,
35+ bio : z . string ( ) . max ( 280 , "Max 280 characters." ) ,
36+ websiteUrl : z
37+ . string ( )
38+ . refine ( ( v ) => v === "" || / ^ h t t p s : \/ \/ .+ / . test ( v ) , "Must start with https://" ) ,
39+ twitterHandle : z
40+ . string ( )
41+ . refine (
42+ ( v ) => v === "" || / ^ [ a - z A - Z 0 - 9 _ ] { 1 , 15 } $ / . test ( v ) ,
43+ "Max 15 chars, alphanumeric and underscores only." ,
44+ ) ,
45+ githubHandle : z
46+ . string ( )
47+ . refine (
48+ ( v ) => v === "" || / ^ [ a - z A - Z 0 - 9 - ] { 1 , 39 } $ / . test ( v ) ,
49+ "Max 39 chars, alphanumeric and hyphens only." ,
50+ ) ,
51+ email : z
52+ . string ( )
53+ . refine ( ( v ) => v === "" || / ^ [ ^ \s @ ] + @ [ ^ \s @ ] + \. [ ^ \s @ ] + $ / . test ( v ) , "Enter a valid email address." ) ,
54+ } ) ;
55+
56+ type EditProfileFormValues = z . infer < typeof editProfileSchema > ;
57+
58+ const EMPTY_FORM : EditProfileFormValues = {
59+ displayName : "" ,
60+ bio : "" ,
61+ websiteUrl : "" ,
62+ twitterHandle : "" ,
63+ githubHandle : "" ,
64+ email : "" ,
3365} ;
3466
35- function validate ( form : {
36- displayName : string ;
37- bio : string ;
38- websiteUrl : string ;
39- twitterHandle : string ;
40- githubHandle : string ;
41- email : string ;
42- } ) : FieldErrors {
43- const errors : FieldErrors = { } ;
44- if ( ! form . displayName . trim ( ) ) errors . displayName = "Display name is required." ;
45- else if ( form . displayName . length > 64 ) errors . displayName = "Max 64 characters." ;
46- if ( form . bio . length > 280 ) errors . bio = "Max 280 characters." ;
47- if ( form . websiteUrl && ! / ^ h t t p s : \/ \/ .+ / . test ( form . websiteUrl ) )
48- errors . websiteUrl = "Must start with https://" ;
49- if ( form . twitterHandle && ! / ^ [ a - z A - Z 0 - 9 _ ] { 1 , 15 } $ / . test ( form . twitterHandle ) )
50- errors . twitterHandle = "Max 15 chars, alphanumeric and underscores only." ;
51- if ( form . githubHandle && ! / ^ [ a - z A - Z 0 - 9 - ] { 1 , 39 } $ / . test ( form . githubHandle ) )
52- errors . githubHandle = "Max 39 chars, alphanumeric and hyphens only." ;
53- if ( form . email && ! / ^ [ ^ \s @ ] + @ [ ^ \s @ ] + \. [ ^ \s @ ] + $ / . test ( form . email ) )
54- errors . email = "Enter a valid email address." ;
55- return errors ;
56- }
57-
5867export default function EditProfilePage ( ) {
5968 const { username } = useParams < { username : string } > ( ) ;
6069 const router = useRouter ( ) ;
@@ -65,16 +74,19 @@ export default function EditProfilePage() {
6574 const [ submitting , setSubmitting ] = useState ( false ) ;
6675 const [ authError , setAuthError ] = useState < string | null > ( null ) ;
6776 const [ walletPrompt , setWalletPrompt ] = useState < "locked" | "not-owner" | null > ( null ) ;
68- const [ fieldErrors , setFieldErrors ] = useState < FieldErrors > ( { } ) ;
69-
70- const [ form , setForm ] = useState ( {
71- displayName : "" ,
72- bio : "" ,
73- websiteUrl : "" ,
74- twitterHandle : "" ,
75- githubHandle : "" ,
76- email : "" ,
77+
78+ const {
79+ register,
80+ handleSubmit,
81+ reset,
82+ watch,
83+ formState : { errors : fieldErrors } ,
84+ } = useForm < EditProfileFormValues > ( {
85+ resolver : zodResolver ( editProfileSchema ) ,
86+ mode : "onChange" ,
87+ defaultValues : EMPTY_FORM ,
7788 } ) ;
89+ const bioValue = watch ( "bio" ) ;
7890
7991 const [ assets , setAssets ] = useState < Asset [ ] > ( [ ] ) ;
8092 const [ newAssetCode , setNewAssetCode ] = useState ( "" ) ;
@@ -108,7 +120,7 @@ export default function EditProfilePage() {
108120 }
109121
110122 setOwnershipChecked ( true ) ;
111- setForm ( {
123+ reset ( {
112124 displayName : profile . displayName ?? "" ,
113125 bio : profile . bio ?? "" ,
114126 websiteUrl : profile . websiteUrl ?? "" ,
@@ -129,14 +141,7 @@ export default function EditProfilePage() {
129141 }
130142 }
131143 init ( ) ;
132- } , [ username , router , ownershipChecked ] ) ;
133-
134- function setField ( field : keyof typeof form ) {
135- return ( e : React . ChangeEvent < HTMLInputElement | HTMLTextAreaElement > ) => {
136- setForm ( ( prev ) => ( { ...prev , [ field ] : e . target . value } ) ) ;
137- setFieldErrors ( ( prev ) => ( { ...prev , [ field ] : undefined } ) ) ;
138- } ;
139- }
144+ } , [ username , router , ownershipChecked , reset ] ) ;
140145
141146 function addAsset ( ) {
142147 const code = newAssetCode . trim ( ) . toUpperCase ( ) ;
@@ -153,23 +158,16 @@ export default function EditProfilePage() {
153158 setAssets ( ( prev ) => prev . filter ( ( a ) => a . code !== code ) ) ;
154159 }
155160
156- async function handleSubmit ( e : React . FormEvent ) {
157- e . preventDefault ( ) ;
158- const errors = validate ( form ) ;
159- if ( Object . keys ( errors ) . length > 0 ) {
160- setFieldErrors ( errors ) ;
161- return ;
162- }
163-
161+ async function onSubmit ( values : EditProfileFormValues ) {
164162 setSubmitting ( true ) ;
165163 try {
166164 const profilePayload : Record < string , string | null > = {
167- displayName : form . displayName ,
168- bio : form . bio || "" ,
169- websiteUrl : form . websiteUrl || null ,
170- twitterHandle : form . twitterHandle || null ,
171- githubHandle : form . githubHandle || null ,
172- email : form . email || null ,
165+ displayName : values . displayName ,
166+ bio : values . bio || "" ,
167+ websiteUrl : values . websiteUrl || null ,
168+ twitterHandle : values . twitterHandle || null ,
169+ githubHandle : values . githubHandle || null ,
170+ email : values . email || null ,
173171 } ;
174172
175173 const [ profileRes , assetsRes ] = await Promise . all ( [
@@ -291,7 +289,7 @@ export default function EditProfilePage() {
291289 </ Link >
292290 </ div >
293291
294- < form onSubmit = { handleSubmit } noValidate className = "space-y-8" >
292+ < form onSubmit = { handleSubmit ( onSubmit ) } noValidate className = "space-y-8" >
295293 { /* Profile fields */ }
296294 < section className = "rounded-3xl border border-white/10 bg-white/[0.02] p-6 space-y-5" >
297295 < h2 className = "text-xs font-semibold uppercase tracking-widest text-steel" >
@@ -301,69 +299,63 @@ export default function EditProfilePage() {
301299 < Field
302300 label = "Display Name"
303301 required
304- error = { fieldErrors . displayName }
302+ error = { fieldErrors . displayName ?. message }
305303 >
306304 < input
307305 type = "text"
308- value = { form . displayName }
309- onChange = { setField ( "displayName" ) }
306+ { ...register ( "displayName" ) }
310307 maxLength = { 64 }
311308 className = { inputCls ( ! ! fieldErrors . displayName ) }
312309 placeholder = "Your name"
313310 />
314311 </ Field >
315312
316- < Field label = "Bio" error = { fieldErrors . bio } >
313+ < Field label = "Bio" error = { fieldErrors . bio ?. message } >
317314 < textarea
318- value = { form . bio }
319- onChange = { setField ( "bio" ) }
315+ { ...register ( "bio" ) }
320316 maxLength = { 280 }
321317 rows = { 3 }
322318 className = { inputCls ( ! ! fieldErrors . bio ) }
323319 placeholder = "Tell supporters about yourself (max 280 chars)"
324320 />
325321 < p className = "mt-1 text-right text-[10px] text-steel" >
326- { form . bio . length } /280
322+ { bioValue . length } /280
327323 </ p >
328324 </ Field >
329325
330- < Field label = "Website URL" error = { fieldErrors . websiteUrl } >
326+ < Field label = "Website URL" error = { fieldErrors . websiteUrl ?. message } >
331327 < input
332328 type = "url"
333- value = { form . websiteUrl }
334- onChange = { setField ( "websiteUrl" ) }
329+ { ...register ( "websiteUrl" ) }
335330 className = { inputCls ( ! ! fieldErrors . websiteUrl ) }
336331 placeholder = "https://yoursite.com"
337332 />
338333 </ Field >
339334
340- < Field label = "Twitter Handle" error = { fieldErrors . twitterHandle } >
335+ < Field label = "Twitter Handle" error = { fieldErrors . twitterHandle ?. message } >
341336 < input
342337 type = "text"
343- value = { form . twitterHandle }
344- onChange = { setField ( "twitterHandle" ) }
338+ { ...register ( "twitterHandle" ) }
345339 maxLength = { 15 }
346340 className = { inputCls ( ! ! fieldErrors . twitterHandle ) }
347341 placeholder = "username (no @)"
348342 />
349343 </ Field >
350344
351- < Field label = "GitHub Handle" error = { fieldErrors . githubHandle } >
345+ < Field label = "GitHub Handle" error = { fieldErrors . githubHandle ?. message } >
352346 < input
353347 type = "text"
354- value = { form . githubHandle }
355- onChange = { setField ( "githubHandle" ) }
348+ { ...register ( "githubHandle" ) }
356349 maxLength = { 39 }
357350 className = { inputCls ( ! ! fieldErrors . githubHandle ) }
358351 placeholder = "username"
359352 />
360353 </ Field >
361354
362- < Field label = "Email" error = { fieldErrors . email } >
355+ < Field label = "Email" error = { fieldErrors . email ?. message } >
363356 < input
364357 type = "email"
365- value = { form . email }
366- onChange = { setField ( "email" ) }
358+ { ...register ( "email" ) }
367359 className = { inputCls ( ! ! fieldErrors . email ) }
368360 placeholder = "you@example.com"
369361 />
0 commit comments