Skip to content

Commit 00c98cd

Browse files
committed
feat: Add social media links form and validation to user profile settings
1 parent 91525a0 commit 00c98cd

File tree

8 files changed

+282
-14
lines changed

8 files changed

+282
-14
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"react-hook-form": "^7.55.0",
5656
"recharts": "^2.15.1",
5757
"schema-dts": "^1.1.5",
58+
"sonner": "^2.0.5",
5859
"sqlkit": "^1.0.13",
5960
"tailwind-merge": "^3.0.2",
6061
"tw-animate-css": "^1.2.4",

src/app/dashboard/settings/_components/GeneralForm.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import React from "react";
2222
import { SubmitHandler, useForm } from "react-hook-form";
2323
import z from "zod";
2424
import { Loader2 } from "lucide-react";
25+
import { toast } from "sonner";
2526

2627
// fields
2728
// name -> ✅
@@ -46,6 +47,9 @@ const GeneralForm: React.FC<Props> = ({ user }) => {
4647
mutationFn: (
4748
payload: z.infer<typeof UserActionInput.updateMyProfileInput>
4849
) => userActions.updateMyProfile(payload),
50+
onSuccess: () => {
51+
toast(_t("Profile updated successfully"));
52+
},
4953
});
5054
const form = useForm({
5155
defaultValues: {
@@ -195,7 +199,10 @@ const GeneralForm: React.FC<Props> = ({ user }) => {
195199
)}
196200
/>
197201

198-
<Button type="submit" disabled={mutation.isPending}>
202+
<Button
203+
type="submit"
204+
disabled={mutation.isPending || !form.formState.isValid}
205+
>
199206
{mutation.isPending && <Loader2 className="animate-spin" />}
200207
{_t("Save")}
201208
</Button>
Lines changed: 171 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,176 @@
1+
"use client";
2+
3+
import { User } from "@/backend/models/domain-models";
4+
import { UserActionInput } from "@/backend/services/inputs/user.input";
5+
import * as userActions from "@/backend/services/user.action";
6+
import { Button } from "@/components/ui/button";
7+
import {
8+
Form,
9+
FormControl,
10+
FormDescription,
11+
FormField,
12+
FormItem,
13+
FormLabel,
14+
FormMessage,
15+
} from "@/components/ui/form";
16+
import { Input } from "@/components/ui/input";
17+
import { useTranslation } from "@/i18n/use-translation";
18+
import { zodResolver } from "@hookform/resolvers/zod";
19+
import { useMutation } from "@tanstack/react-query";
20+
import { Loader } from "lucide-react";
121
import React from "react";
22+
import { SubmitHandler, useForm } from "react-hook-form";
23+
import { toast } from "sonner";
24+
import z from "zod";
25+
26+
interface Props {
27+
user: User;
28+
}
29+
30+
const SocialMediaForm: React.FC<Props> = ({ user }) => {
31+
const { _t } = useTranslation();
32+
const mutation = useMutation({
33+
mutationFn: (
34+
payload: z.infer<typeof UserActionInput.updateMyProfileInput>
35+
) => userActions.updateMyProfile(payload),
36+
onSuccess: () => {
37+
toast(_t("Social links updated successfully"));
38+
},
39+
});
40+
41+
const form = useForm({
42+
mode: "all",
43+
defaultValues: {
44+
social_links: {
45+
github: user.social_links.github,
46+
x: user.social_links.x,
47+
linkedin: user.social_links.linkedin,
48+
facebook: user.social_links.facebook,
49+
instagram: user.social_links.instagram,
50+
youtube: user.social_links.youtube,
51+
},
52+
},
53+
resolver: zodResolver(UserActionInput.updateMyProfileInput),
54+
});
55+
56+
const onSubmit: SubmitHandler<
57+
z.infer<typeof UserActionInput.updateMyProfileInput>
58+
> = (data) => {
59+
mutation.mutate({
60+
social_links: {
61+
github: data.social_links?.github,
62+
x: data.social_links?.x,
63+
linkedin: data.social_links?.linkedin,
64+
facebook: data.social_links?.facebook,
65+
instagram: data.social_links?.instagram,
66+
youtube: data.social_links?.youtube,
67+
},
68+
});
69+
};
70+
71+
return (
72+
<Form {...form}>
73+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
74+
<FormField
75+
control={form.control}
76+
name="social_links.github"
77+
render={({ field }) => (
78+
<FormItem>
79+
<FormLabel>{_t("Github")}</FormLabel>
80+
<FormControl>
81+
<Input className="py-6" {...field} />
82+
</FormControl>
83+
<FormDescription />
84+
<FormMessage />
85+
</FormItem>
86+
)}
87+
/>
88+
89+
<FormField
90+
control={form.control}
91+
name="social_links.facebook"
92+
render={({ field }) => (
93+
<FormItem>
94+
<FormLabel>{_t("Facebook")}</FormLabel>
95+
<FormControl>
96+
<Input className="py-6" {...field} />
97+
</FormControl>
98+
<FormDescription />
99+
<FormMessage />
100+
</FormItem>
101+
)}
102+
/>
103+
104+
<FormField
105+
control={form.control}
106+
name="social_links.instagram"
107+
render={({ field }) => (
108+
<FormItem>
109+
<FormLabel>{_t("Instagram")}</FormLabel>
110+
<FormControl>
111+
<Input className="py-6" {...field} />
112+
</FormControl>
113+
<FormDescription />
114+
<FormMessage />
115+
</FormItem>
116+
)}
117+
/>
118+
119+
<FormField
120+
control={form.control}
121+
name="social_links.linkedin"
122+
render={({ field }) => (
123+
<FormItem>
124+
<FormLabel>{_t("LinkedIn")}</FormLabel>
125+
<FormControl>
126+
<Input className="py-6" {...field} />
127+
</FormControl>
128+
<FormDescription />
129+
<FormMessage />
130+
</FormItem>
131+
)}
132+
/>
133+
134+
<FormField
135+
control={form.control}
136+
name="social_links.x"
137+
render={({ field }) => (
138+
<FormItem>
139+
<FormLabel>{_t("X")}</FormLabel>
140+
<FormControl>
141+
<Input className="py-6" {...field} />
142+
</FormControl>
143+
<FormDescription />
144+
<FormMessage />
145+
</FormItem>
146+
)}
147+
/>
148+
149+
<FormField
150+
control={form.control}
151+
name="social_links.youtube"
152+
render={({ field }) => (
153+
<FormItem>
154+
<FormLabel>{_t("Youtube")}</FormLabel>
155+
<FormControl>
156+
<Input className="py-6" {...field} />
157+
</FormControl>
158+
<FormDescription />
159+
<FormMessage />
160+
</FormItem>
161+
)}
162+
/>
2163

3-
const SocialMediaForm = () => {
4-
return <div>SocialMediaForm</div>;
164+
<Button
165+
type="submit"
166+
disabled={mutation.isPending || !form.formState.isValid}
167+
>
168+
{mutation.isPending && <Loader className="animate-spin" />}
169+
{_t("Save")}
170+
</Button>
171+
</form>
172+
</Form>
173+
);
5174
};
6175

7176
export default SocialMediaForm;

src/app/dashboard/settings/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ const SettingsPage = async () => {
2828
)}
2929
</TabsContent>
3030
<TabsContent value="social">
31-
<SocialMediaForm />
31+
{current_user && (
32+
<div className="max-w-2xl my-10">
33+
<SocialMediaForm user={current_user} />
34+
</div>
35+
)}
3236
</TabsContent>
3337
<TabsContent value="profile_readme">
3438
<ReadmeForm />

src/app/layout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Metadata } from "next";
2+
import { Toaster } from "@/components/ui/sonner";
23
import "../styles/app.css";
34

45
import * as sessionActions from "@/backend/services/session.actions";
@@ -37,7 +38,10 @@ const RootLayout: React.FC<PropsWithChildren> = async ({ children }) => {
3738
<html lang="en" suppressHydrationWarning>
3839
<body style={fontKohinoorBanglaRegular.style}>
3940
<I18nProvider currentLanguage={_cookies.get("language")?.value || "en"}>
40-
<CommonProviders session={session}>{children}</CommonProviders>
41+
<CommonProviders session={session}>
42+
{children}
43+
<Toaster />
44+
</CommonProviders>
4145
</I18nProvider>
4246
</body>
4347
</html>

src/backend/models/domain-models.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@ export interface User {
99
bio: string;
1010
website_url: string;
1111
location: string;
12-
social_links: any;
12+
social_links: {
13+
github?: string;
14+
x?: string;
15+
linkedin?: string;
16+
facebook?: string;
17+
instagram?: string;
18+
youtube?: string;
19+
};
1320
profile_readme: string;
1421
skills: string;
1522
created_at: Date;
@@ -123,12 +130,12 @@ export interface Bookmark {
123130
}
124131

125132
export type REACTION_TYPE =
126-
| "LOVE" // ✅
127-
| "UNICORN" // ✅
128-
| "WOW" // ✅
129-
| "FIRE" // ✅
130-
| "CRY" // ✅
131-
| "HAHA"; // ✅
133+
| "LOVE"
134+
| "UNICORN"
135+
| "WOW"
136+
| "FIRE"
137+
| "CRY"
138+
| "HAHA";
132139

133140
export interface Reaction {
134141
resource_id: string;

src/backend/services/inputs/user.input.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const UserActionInput = {
1111
bio: z.string().optional().nullable(),
1212
}),
1313
updateMyProfileInput: z.object({
14-
name: z.string(),
14+
name: z.string().optional(),
1515
username: z.string().optional(),
1616
email: z.string().email().optional(),
1717
profile_photo: z.string().url().optional(),
@@ -20,7 +20,58 @@ export const UserActionInput = {
2020
bio: z.string().optional(),
2121
websiteUrl: z.string().url().optional(),
2222
location: z.string().optional(),
23-
social_links: z.record(z.string()).optional(),
23+
social_links: z
24+
.object({
25+
github: z
26+
.string()
27+
.url()
28+
.regex(/^https:\/\/(www\.)?github\.com\/[A-Za-z0-9_-]+\/?$/, {
29+
message: "Invalid GitHub profile URL",
30+
})
31+
.optional(),
32+
x: z
33+
.string()
34+
.url()
35+
.regex(
36+
/^https:\/\/(www\.)?(twitter\.com|x\.com)\/[A-Za-z0-9_]+\/?$/,
37+
{
38+
message: "Invalid X (Twitter) profile URL",
39+
}
40+
)
41+
.optional(),
42+
linkedin: z
43+
.string()
44+
.url()
45+
.regex(/^https:\/\/(www\.)?linkedin\.com\/in\/[A-Za-z0-9_-]+\/?$/, {
46+
message: "Invalid LinkedIn profile URL",
47+
})
48+
.optional(),
49+
facebook: z
50+
.string()
51+
.url()
52+
.regex(/^https:\/\/(www\.)?facebook\.com\/[A-Za-z0-9.]+\/?$/, {
53+
message: "Invalid Facebook profile URL",
54+
})
55+
.optional(),
56+
instagram: z
57+
.string()
58+
.url()
59+
.regex(/^https:\/\/(www\.)?instagram\.com\/[A-Za-z0-9_.]+\/?$/, {
60+
message: "Invalid Instagram profile URL",
61+
})
62+
.optional(),
63+
youtube: z
64+
.string()
65+
.url()
66+
.regex(
67+
/^https:\/\/(www\.)?youtube\.com\/(c|channel|user)\/[A-Za-z0-9_-]+\/?$/,
68+
{
69+
message: "Invalid YouTube profile URL",
70+
}
71+
)
72+
.optional(),
73+
})
74+
.optional(),
2475
profile_readme: z.string().optional(),
2576
skills: z.string().optional(),
2677
}),

src/components/ui/sonner.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use client"
2+
3+
import { useTheme } from "next-themes"
4+
import { Toaster as Sonner, ToasterProps } from "sonner"
5+
6+
const Toaster = ({ ...props }: ToasterProps) => {
7+
const { theme = "system" } = useTheme()
8+
9+
return (
10+
<Sonner
11+
theme={theme as ToasterProps["theme"]}
12+
className="toaster group"
13+
style={
14+
{
15+
"--normal-bg": "var(--popover)",
16+
"--normal-text": "var(--popover-foreground)",
17+
"--normal-border": "var(--border)",
18+
} as React.CSSProperties
19+
}
20+
{...props}
21+
/>
22+
)
23+
}
24+
25+
export { Toaster }

0 commit comments

Comments
 (0)