Skip to content

Commit 507a525

Browse files
committed
feat(profile): add user profile with bio, website, and social links
- Added fields for user bio, website, and social links (Twitter, GitHub, Instagram, Facebook, YouTube, Discord) in the profile settings form. - Updated the user profile modal to display bio, website, and social links. - Improved email management UI for better layout and user experience. - Introduced new translations for the added fields in the settings locale. Signed-off-by: Innei <[email protected]>
1 parent f0cdcaa commit 507a525

File tree

7 files changed

+1065
-55
lines changed

7 files changed

+1065
-55
lines changed

apps/desktop/layer/renderer/src/modules/profile/email-management.tsx

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -59,43 +59,45 @@ export function EmailManagement() {
5959
{user?.emailVerified ? t("profile.email.verified") : t("profile.email.unverified")}
6060
</span>
6161
</div>
62-
<p className="text-text-secondary group flex gap-2 text-sm">
63-
{user?.email}
62+
<div className="flex items-center justify-between">
63+
<div className="text-text-secondary group flex items-center gap-2">
64+
{user?.email}
6465

65-
<AnimatedCommandButton
66-
icon={<m.i className="i-mgc-edit-cute-re size-4" />}
67-
className="size-5 p-1"
68-
variant="ghost"
69-
onClick={() => {
70-
present({
71-
title: t("profile.email.change"),
72-
content: EmailManagementForm,
73-
})
74-
}}
75-
/>
76-
{user?.email && (
77-
<CopyButton
78-
value={user.email}
79-
className={cn(
80-
"size-5 p-1 duration-300",
81-
!isMobile && "opacity-0 group-hover:opacity-100",
82-
)}
66+
<AnimatedCommandButton
67+
icon={<m.i className="i-mgc-edit-cute-re size-4" />}
68+
className="size-5 p-1"
69+
variant="ghost"
70+
onClick={() => {
71+
present({
72+
title: t("profile.email.change"),
73+
content: EmailManagementForm,
74+
})
75+
}}
8376
/>
77+
{user?.email && (
78+
<CopyButton
79+
value={user.email}
80+
className={cn(
81+
"size-5 p-1 duration-300",
82+
!isMobile && "opacity-0 group-hover:opacity-100",
83+
)}
84+
/>
85+
)}
86+
</div>
87+
{!user?.emailVerified && (
88+
<Button
89+
variant="outline"
90+
type="button"
91+
isLoading={verifyEmailMutation.isPending}
92+
onClick={() => {
93+
verifyEmailMutation.mutate()
94+
}}
95+
buttonClassName="mt-2"
96+
>
97+
{t("profile.email.send_verification")}
98+
</Button>
8499
)}
85-
</p>
86-
{!user?.emailVerified && (
87-
<Button
88-
variant="outline"
89-
type="button"
90-
isLoading={verifyEmailMutation.isPending}
91-
onClick={() => {
92-
verifyEmailMutation.mutate()
93-
}}
94-
buttonClassName="mt-2"
95-
>
96-
{t("profile.email.send_verification")}
97-
</Button>
98-
)}
100+
</div>
99101
</>
100102
)
101103
}

apps/desktop/layer/renderer/src/modules/profile/profile-setting-form.tsx

Lines changed: 147 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
FormLabel,
1010
FormMessage,
1111
} 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"
1313
import { cn } from "@follow/utils/utils"
1414
import { zodResolver } from "@hookform/resolvers/zod"
1515
import { useMutation } from "@tanstack/react-query"
@@ -22,12 +22,57 @@ import { setWhoami, useWhoami } from "~/atoms/user"
2222
import { updateUser } from "~/lib/auth"
2323
import { toastFetchError } from "~/lib/error-parser"
2424

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+
2534
const formSchema = z.object({
2635
handle: z.string().max(50).optional(),
2736
name: z.string().min(3).max(50),
2837
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(),
2941
})
3042

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+
3176
export const ProfileSettingForm = ({
3277
className,
3378
buttonClassName,
@@ -36,14 +81,24 @@ export const ProfileSettingForm = ({
3681
buttonClassName?: string
3782
}) => {
3883
const { t } = useTranslation("settings")
39-
const user = useWhoami()
84+
const user = useWhoami() as ExtendedUser
4085

4186
const form = useForm<z.infer<typeof formSchema>>({
4287
resolver: zodResolver(formSchema),
4388
defaultValues: {
4489
handle: user?.handle || undefined,
4590
name: user?.name || "",
4691
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+
},
47102
},
48103
})
49104

@@ -53,6 +108,10 @@ export const ProfileSettingForm = ({
53108
handle: values.handle,
54109
image: values.image,
55110
name: values.name,
111+
// @ts-expect-error
112+
bio: values.bio,
113+
website: values.website,
114+
socialLinks: values.socialLinks,
56115
}),
57116
onError: (error) => {
58117
toastFetchError(error)
@@ -71,6 +130,15 @@ export const ProfileSettingForm = ({
71130
updateMutation.mutate(values)
72131
}
73132

133+
const socialLinkFields: (keyof z.infer<typeof socialLinksSchema>)[] = [
134+
"twitter",
135+
"github",
136+
"instagram",
137+
"facebook",
138+
"youtube",
139+
"discord",
140+
]
141+
74142
return (
75143
<Form {...form}>
76144
<form onSubmit={form.handleSubmit(onSubmit)} className={cn("mt-4 space-y-4", className)}>
@@ -79,7 +147,7 @@ export const ProfileSettingForm = ({
79147
name="handle"
80148
render={({ field }) => (
81149
<FormItem>
82-
<FormLabel>{t("profile.handle.label")}</FormLabel>
150+
<FormLabel className={formItemLabelClassName}>{t("profile.handle.label")}</FormLabel>
83151
<FormControl>
84152
<Input {...field} />
85153
</FormControl>
@@ -93,7 +161,7 @@ export const ProfileSettingForm = ({
93161
name="name"
94162
render={({ field }) => (
95163
<FormItem>
96-
<FormLabel>{t("profile.name.label")}</FormLabel>
164+
<FormLabel className={formItemLabelClassName}>{t("profile.name.label")}</FormLabel>
97165
<FormControl>
98166
<Input {...field} />
99167
</FormControl>
@@ -109,7 +177,9 @@ export const ProfileSettingForm = ({
109177
render={({ field }) => (
110178
<div className="flex gap-4">
111179
<FormItem className="w-full">
112-
<FormLabel>{t("profile.avatar.label")}</FormLabel>
180+
<FormLabel className={formItemLabelClassName}>
181+
{t("profile.avatar.label")}
182+
</FormLabel>
113183
<FormControl>
114184
<div className="flex items-center gap-4">
115185
<Input {...field} />
@@ -127,6 +197,78 @@ export const ProfileSettingForm = ({
127197
)}
128198
/>
129199

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+
130272
<div className={cn("text-right", buttonClassName)}>
131273
<Button type="submit" isLoading={updateMutation.isPending}>
132274
{t("profile.submit")}

0 commit comments

Comments
 (0)