9
9
FormLabel ,
10
10
FormMessage ,
11
11
} from "@follow/components/ui/form/index.jsx"
12
- import { Input } from "@follow/components/ui/input/index.js"
12
+ import { Input , TextArea } from "@follow/components/ui/input/index.js"
13
13
import { cn } from "@follow/utils/utils"
14
14
import { zodResolver } from "@hookform/resolvers/zod"
15
15
import { useMutation } from "@tanstack/react-query"
@@ -22,12 +22,57 @@ import { setWhoami, useWhoami } from "~/atoms/user"
22
22
import { updateUser } from "~/lib/auth"
23
23
import { toastFetchError } from "~/lib/error-parser"
24
24
25
+ const socialLinksSchema = z . object ( {
26
+ twitter : z . string ( ) . max ( 64 ) . optional ( ) ,
27
+ github : z . string ( ) . max ( 64 ) . optional ( ) ,
28
+ instagram : z . string ( ) . max ( 64 ) . optional ( ) ,
29
+ facebook : z . string ( ) . max ( 64 ) . optional ( ) ,
30
+ youtube : z . string ( ) . max ( 64 ) . optional ( ) ,
31
+ discord : z . string ( ) . max ( 64 ) . optional ( ) ,
32
+ } )
33
+
25
34
const formSchema = z . object ( {
26
35
handle : z . string ( ) . max ( 50 ) . optional ( ) ,
27
36
name : z . string ( ) . min ( 3 ) . max ( 50 ) ,
28
37
image : z . string ( ) . url ( ) . or ( z . literal ( "" ) ) . optional ( ) ,
38
+ bio : z . string ( ) . max ( 256 ) . optional ( ) ,
39
+ website : z . string ( ) . url ( ) . max ( 64 ) . optional ( ) . or ( z . literal ( "" ) ) ,
40
+ socialLinks : socialLinksSchema . optional ( ) ,
29
41
} )
30
42
43
+ const socialIconClassNames = {
44
+ twitter : "i-mgc-twitter-cute-fi" ,
45
+ github : "i-mgc-github-cute-fi" ,
46
+ instagram : "i-mingcute-ins-fill" ,
47
+ facebook : "i-mingcute-facebook-fill" ,
48
+ youtube : "i-mgc-youtube-cute-fi" ,
49
+ discord : "i-mingcute-discord-fill" ,
50
+ }
51
+
52
+ const formItemLabelClassName = tw `pl-3`
53
+ // Extended user type to include the new fields
54
+ type ExtendedUser = ReturnType < typeof useWhoami > & {
55
+ bio ?: string
56
+ website ?: string
57
+ socialLinks ?: {
58
+ twitter ?: string
59
+ github ?: string
60
+ instagram ?: string
61
+ facebook ?: string
62
+ youtube ?: string
63
+ discord ?: string
64
+ }
65
+ }
66
+
67
+ const socialCopyMap = {
68
+ twitter : "Twitter" ,
69
+ github : "GitHub" ,
70
+ instagram : "Instagram" ,
71
+ facebook : "Facebook" ,
72
+ youtube : "YouTube" ,
73
+ discord : "Discord" ,
74
+ }
75
+
31
76
export const ProfileSettingForm = ( {
32
77
className,
33
78
buttonClassName,
@@ -36,14 +81,24 @@ export const ProfileSettingForm = ({
36
81
buttonClassName ?: string
37
82
} ) => {
38
83
const { t } = useTranslation ( "settings" )
39
- const user = useWhoami ( )
84
+ const user = useWhoami ( ) as ExtendedUser
40
85
41
86
const form = useForm < z . infer < typeof formSchema > > ( {
42
87
resolver : zodResolver ( formSchema ) ,
43
88
defaultValues : {
44
89
handle : user ?. handle || undefined ,
45
90
name : user ?. name || "" ,
46
91
image : user ?. image || "" ,
92
+ bio : user ?. bio || "" ,
93
+ website : user ?. website || "" ,
94
+ socialLinks : {
95
+ twitter : user ?. socialLinks ?. twitter || "" ,
96
+ github : user ?. socialLinks ?. github || "" ,
97
+ instagram : user ?. socialLinks ?. instagram || "" ,
98
+ facebook : user ?. socialLinks ?. facebook || "" ,
99
+ youtube : user ?. socialLinks ?. youtube || "" ,
100
+ discord : user ?. socialLinks ?. discord || "" ,
101
+ } ,
47
102
} ,
48
103
} )
49
104
@@ -53,6 +108,10 @@ export const ProfileSettingForm = ({
53
108
handle : values . handle ,
54
109
image : values . image ,
55
110
name : values . name ,
111
+ // @ts -expect-error
112
+ bio : values . bio ,
113
+ website : values . website ,
114
+ socialLinks : values . socialLinks ,
56
115
} ) ,
57
116
onError : ( error ) => {
58
117
toastFetchError ( error )
@@ -71,6 +130,15 @@ export const ProfileSettingForm = ({
71
130
updateMutation . mutate ( values )
72
131
}
73
132
133
+ const socialLinkFields : ( keyof z . infer < typeof socialLinksSchema > ) [ ] = [
134
+ "twitter" ,
135
+ "github" ,
136
+ "instagram" ,
137
+ "facebook" ,
138
+ "youtube" ,
139
+ "discord" ,
140
+ ]
141
+
74
142
return (
75
143
< Form { ...form } >
76
144
< form onSubmit = { form . handleSubmit ( onSubmit ) } className = { cn ( "mt-4 space-y-4" , className ) } >
@@ -79,7 +147,7 @@ export const ProfileSettingForm = ({
79
147
name = "handle"
80
148
render = { ( { field } ) => (
81
149
< FormItem >
82
- < FormLabel > { t ( "profile.handle.label" ) } </ FormLabel >
150
+ < FormLabel className = { formItemLabelClassName } > { t ( "profile.handle.label" ) } </ FormLabel >
83
151
< FormControl >
84
152
< Input { ...field } />
85
153
</ FormControl >
@@ -93,7 +161,7 @@ export const ProfileSettingForm = ({
93
161
name = "name"
94
162
render = { ( { field } ) => (
95
163
< FormItem >
96
- < FormLabel > { t ( "profile.name.label" ) } </ FormLabel >
164
+ < FormLabel className = { formItemLabelClassName } > { t ( "profile.name.label" ) } </ FormLabel >
97
165
< FormControl >
98
166
< Input { ...field } />
99
167
</ FormControl >
@@ -109,7 +177,9 @@ export const ProfileSettingForm = ({
109
177
render = { ( { field } ) => (
110
178
< div className = "flex gap-4" >
111
179
< FormItem className = "w-full" >
112
- < FormLabel > { t ( "profile.avatar.label" ) } </ FormLabel >
180
+ < FormLabel className = { formItemLabelClassName } >
181
+ { t ( "profile.avatar.label" ) }
182
+ </ FormLabel >
113
183
< FormControl >
114
184
< div className = "flex items-center gap-4" >
115
185
< Input { ...field } />
@@ -127,6 +197,78 @@ export const ProfileSettingForm = ({
127
197
) }
128
198
/>
129
199
200
+ < FormField
201
+ control = { form . control }
202
+ name = "bio"
203
+ render = { ( { field } ) => (
204
+ < FormItem >
205
+ < FormLabel className = { formItemLabelClassName } > { t ( "profile.profile.bio" ) } </ FormLabel >
206
+ < FormControl >
207
+ < TextArea
208
+ rounded = "lg"
209
+ { ...field }
210
+ placeholder = "Tell us about yourself..."
211
+ className = "placeholder:text-text-tertiary min-h-[80px] resize-none p-3 text-sm"
212
+ />
213
+ </ FormControl >
214
+ < FormMessage />
215
+ </ FormItem >
216
+ ) }
217
+ />
218
+
219
+ < FormField
220
+ control = { form . control }
221
+ name = "website"
222
+ render = { ( { field } ) => (
223
+ < FormItem >
224
+ < FormLabel className = { formItemLabelClassName } >
225
+ { t ( "profile.profile.website" ) }
226
+ </ FormLabel >
227
+ < FormControl >
228
+ < Input type = "url" { ...field } placeholder = "https://your-website.com" />
229
+ </ FormControl >
230
+ < FormMessage />
231
+ </ FormItem >
232
+ ) }
233
+ />
234
+
235
+ < div >
236
+ < FormLabel className = { cn ( formItemLabelClassName , "text-sm font-medium" ) } >
237
+ { t ( "profile.profile.social_links" ) }
238
+ </ FormLabel >
239
+ < div className = "mt-2 grid grid-cols-2 gap-2" >
240
+ { socialLinkFields . map ( ( social ) => (
241
+ < FormField
242
+ key = { social }
243
+ control = { form . control }
244
+ name = { `socialLinks.${ social } ` }
245
+ render = { ( { field } ) => (
246
+ < FormItem >
247
+ < FormControl >
248
+ < label
249
+ className = { cn (
250
+ "ring-accent/20 focus-within:border-accent/80 duration-200 focus-within:outline-none focus-within:ring-2" ,
251
+ "border-border bg-theme-background hover:bg-accent/5 flex cursor-text items-center gap-2 rounded-md border px-3 py-2 transition-colors dark:bg-zinc-700/[0.15]" ,
252
+ ) }
253
+ >
254
+ < i
255
+ className = { `${ socialIconClassNames [ social ] } text-text-secondary shrink-0 text-base` }
256
+ />
257
+ < input
258
+ { ...field }
259
+ placeholder = { socialCopyMap [ social ] }
260
+ className = "placeholder:text-text-tertiary border-0 !bg-transparent p-0 text-sm focus-visible:ring-0"
261
+ />
262
+ </ label >
263
+ </ FormControl >
264
+ < FormMessage />
265
+ </ FormItem >
266
+ ) }
267
+ />
268
+ ) ) }
269
+ </ div >
270
+ </ div >
271
+
130
272
< div className = { cn ( "text-right" , buttonClassName ) } >
131
273
< Button type = "submit" isLoading = { updateMutation . isPending } >
132
274
{ t ( "profile.submit" ) }
0 commit comments