@@ -8,13 +8,21 @@ import { CopyTextButton } from "@/components/ui/CopyTextButton";
8
8
import { Input } from "@/components/ui/input" ;
9
9
import { useDashboardRouter } from "@/lib/DashboardRouter" ;
10
10
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler" ;
11
+ import { zodResolver } from "@hookform/resolvers/zod" ;
11
12
import { useMutation } from "@tanstack/react-query" ;
12
13
import { FileInput } from "components/shared/FileInput" ;
13
14
import { useState } from "react" ;
15
+ import { useForm } from "react-hook-form" ;
14
16
import { toast } from "sonner" ;
15
17
import type { ThirdwebClient } from "thirdweb" ;
18
+ import { z } from "zod" ;
16
19
import { TeamDomainVerificationCard } from "../_components/settings-cards/domain-verification" ;
17
- import { teamSlugRegex } from "./common" ;
20
+ import {
21
+ maxTeamNameLength ,
22
+ maxTeamSlugLength ,
23
+ teamNameSchema ,
24
+ teamSlugSchema ,
25
+ } from "./common" ;
18
26
19
27
type UpdateTeamField = ( team : Partial < Team > ) => Promise < void > ;
20
28
@@ -59,111 +67,109 @@ export function TeamGeneralSettingsPageUI(props: {
59
67
) ;
60
68
}
61
69
70
+ const teamNameFormSchema = z . object ( {
71
+ name : teamNameSchema ,
72
+ } ) ;
73
+
62
74
function TeamNameFormControl ( props : {
63
75
team : Team ;
64
76
updateTeamField : UpdateTeamField ;
65
77
} ) {
66
- const [ teamName , setTeamName ] = useState ( props . team . name ) ;
67
- const maxTeamNameLength = 32 ;
68
-
78
+ const form = useForm < { name : string } > ( {
79
+ values : { name : props . team . name } ,
80
+ resolver : zodResolver ( teamNameFormSchema ) ,
81
+ } ) ;
69
82
const updateTeamMutation = useMutation ( {
70
83
mutationFn : ( name : string ) => props . updateTeamField ( { name } ) ,
71
84
} ) ;
72
85
73
- function handleSave ( ) {
74
- const promises = updateTeamMutation . mutateAsync ( teamName ) ;
75
- toast . promise ( promises , {
76
- success : "Team name updated successfully" ,
77
- error : "Failed to update team name" ,
78
- } ) ;
79
- }
80
-
81
86
return (
82
- < SettingsCard
83
- header = { {
84
- title : "Team Name" ,
85
- description : "This is your team's name on thirdweb" ,
86
- } }
87
- bottomText = { `Please use ${ maxTeamNameLength } characters at maximum.` }
88
- saveButton = { {
89
- onClick : handleSave ,
90
- disabled : teamName . length === 0 ,
91
- isPending : updateTeamMutation . isPending ,
92
- } }
93
- errorText = { undefined }
94
- noPermissionText = { undefined } // TODO
87
+ < form
88
+ onSubmit = { form . handleSubmit ( ( values ) => {
89
+ const promise = updateTeamMutation . mutateAsync ( values . name ) ;
90
+ toast . promise ( promise , {
91
+ success : "Team name updated successfully" ,
92
+ error : "Failed to update team name" ,
93
+ } ) ;
94
+ } ) }
95
95
>
96
- < Input
97
- value = { teamName }
98
- maxLength = { maxTeamNameLength }
99
- onChange = { ( e ) => {
100
- setTeamName ( e . target . value ) ;
96
+ < SettingsCard
97
+ header = { {
98
+ title : "Team Name" ,
99
+ description : "This is your team's name on thirdweb" ,
101
100
} }
102
- className = "md:w-[450px]"
103
- />
104
- </ SettingsCard >
101
+ bottomText = { `Please use ${ maxTeamNameLength } characters at maximum.` }
102
+ saveButton = { {
103
+ type : "submit" ,
104
+ disabled : ! form . formState . isDirty ,
105
+ isPending : updateTeamMutation . isPending ,
106
+ } }
107
+ errorText = { form . formState . errors . name ?. message }
108
+ noPermissionText = { undefined }
109
+ >
110
+ < Input
111
+ { ...form . register ( "name" ) }
112
+ maxLength = { maxTeamNameLength }
113
+ className = "md:w-[450px]"
114
+ />
115
+ </ SettingsCard >
116
+ </ form >
105
117
) ;
106
118
}
107
119
120
+ const teamSlugFormSchema = z . object ( {
121
+ slug : teamSlugSchema ,
122
+ } ) ;
123
+
108
124
function TeamSlugFormControl ( props : {
109
125
team : Team ;
110
126
updateTeamField : ( team : Partial < Team > ) => Promise < void > ;
111
127
} ) {
112
- const [ teamSlug , setTeamSlug ] = useState ( props . team . slug ) ;
113
- const maxTeamURLLength = 48 ;
114
- const [ errorMessage , setErrorMessage ] = useState < string | undefined > ( ) ;
115
-
128
+ const form = useForm < { slug : string } > ( {
129
+ defaultValues : { slug : props . team . slug } ,
130
+ resolver : zodResolver ( teamSlugFormSchema ) ,
131
+ } ) ;
116
132
const updateTeamMutation = useMutation ( {
117
- mutationFn : ( slug : string ) => props . updateTeamField ( { slug : slug } ) ,
133
+ mutationFn : ( slug : string ) => props . updateTeamField ( { slug } ) ,
118
134
} ) ;
119
135
120
- function handleSave ( ) {
121
- const promises = updateTeamMutation . mutateAsync ( teamSlug ) ;
122
- toast . promise ( promises , {
123
- success : "Team URL updated successfully" ,
124
- error : "Failed to update team URL" ,
125
- } ) ;
126
- }
127
-
128
136
return (
129
- < SettingsCard
130
- header = { {
131
- title : "Team URL" ,
132
- description :
133
- "This is your team's URL namespace on thirdweb. All your team's projects and settings can be accessed using this URL" ,
134
- } }
135
- bottomText = { `Please use ${ maxTeamURLLength } characters at maximum.` }
136
- errorText = { errorMessage }
137
- saveButton = { {
138
- onClick : handleSave ,
139
- disabled : errorMessage !== undefined ,
140
- isPending : updateTeamMutation . isPending ,
141
- } }
142
- noPermissionText = { undefined } // TODO
137
+ < form
138
+ onSubmit = { form . handleSubmit ( ( values ) => {
139
+ const promise = updateTeamMutation . mutateAsync ( values . slug ) ;
140
+ toast . promise ( promise , {
141
+ success : "Team URL updated successfully" ,
142
+ error : "Failed to update team URL" ,
143
+ } ) ;
144
+ } ) }
143
145
>
144
- < div className = "relative flex rounded-lg border border-border md:w-[450px]" >
145
- < div className = "flex items-center self-stretch rounded-l-lg border-border border-r bg-card px-3 font-mono text-muted-foreground/80 text-sm" >
146
- thirdweb.com/team/
146
+ < SettingsCard
147
+ header = { {
148
+ title : "Team URL" ,
149
+ description :
150
+ "This is your team's URL namespace on thirdweb. All your team's projects and settings can be accessed using this URL" ,
151
+ } }
152
+ bottomText = { `Please use ${ maxTeamSlugLength } characters at maximum.` }
153
+ errorText = { form . formState . errors . slug ?. message }
154
+ saveButton = { {
155
+ type : "submit" ,
156
+ disabled : ! form . formState . isDirty ,
157
+ isPending : updateTeamMutation . isPending ,
158
+ } }
159
+ noPermissionText = { undefined }
160
+ >
161
+ < div className = "relative flex rounded-lg border border-border md:w-[450px]" >
162
+ < div className = "flex items-center self-stretch rounded-l-lg border-border border-r bg-card px-3 font-mono text-muted-foreground/80 text-sm" >
163
+ thirdweb.com/team/
164
+ </ div >
165
+ < Input
166
+ { ...form . register ( "slug" ) }
167
+ maxLength = { maxTeamSlugLength }
168
+ className = "truncate border-0 font-mono"
169
+ />
147
170
</ div >
148
- < Input
149
- value = { teamSlug }
150
- onChange = { ( e ) => {
151
- const value = e . target . value . slice ( 0 , maxTeamURLLength ) ;
152
- setTeamSlug ( value ) ;
153
- if ( value . trim ( ) . length === 0 ) {
154
- setErrorMessage ( "Team URL can not be empty" ) ;
155
- } else if ( teamSlugRegex . test ( value ) ) {
156
- setErrorMessage (
157
- "Invalid Team URL. Only letters, numbers and hyphens are allowed" ,
158
- ) ;
159
- } else {
160
- setErrorMessage ( undefined ) ;
161
- }
162
- } }
163
- className = "truncate border-0 font-mono"
164
- />
165
- </ div >
166
- </ SettingsCard >
171
+ </ SettingsCard >
172
+ </ form >
167
173
) ;
168
174
}
169
175
@@ -186,8 +192,8 @@ function TeamAvatarFormControl(props: {
186
192
} ) ;
187
193
188
194
function handleSave ( ) {
189
- const promises = updateTeamAvatarMutation . mutateAsync ( teamAvatar ) ;
190
- toast . promise ( promises , {
195
+ const promise = updateTeamAvatarMutation . mutateAsync ( teamAvatar ) ;
196
+ toast . promise ( promise , {
191
197
success : "Team avatar updated successfully" ,
192
198
error : "Failed to update team avatar" ,
193
199
} ) ;
@@ -263,8 +269,8 @@ export function LeaveTeamCard(props: {
263
269
} ) ;
264
270
265
271
function handleLeave ( ) {
266
- const promises = leaveTeam . mutateAsync ( ) ;
267
- toast . promise ( promises , {
272
+ const promise = leaveTeam . mutateAsync ( ) ;
273
+ toast . promise ( promise , {
268
274
success : "Left team successfully" ,
269
275
error : "Failed to leave team" ,
270
276
} ) ;
@@ -308,8 +314,8 @@ export function DeleteTeamCard(props: {
308
314
} ) ;
309
315
310
316
function handleDelete ( ) {
311
- const promises = deleteTeam . mutateAsync ( ) ;
312
- toast . promise ( promises , {
317
+ const promise = deleteTeam . mutateAsync ( ) ;
318
+ toast . promise ( promise , {
313
319
success : "Team deleted successfully" ,
314
320
error : "Failed to delete team" ,
315
321
} ) ;
0 commit comments