Skip to content

Commit c97d4ca

Browse files
committed
feat: Enhance user profile management with ReadmeForm and SocialMediaForm components, update dependencies, and improve error handling
1 parent bad7cf0 commit c97d4ca

File tree

8 files changed

+195
-47
lines changed

8 files changed

+195
-47
lines changed

next.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ const nextConfig = {
3737
protocol: "https",
3838
hostname: "i.ibb.co",
3939
},
40+
{
41+
protocol: "https",
42+
hostname: "api.dicebear.com",
43+
},
4044
],
4145
},
4246
};

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "1.0.12",
44
"private": true,
55
"scripts": {
6-
"dev": "next dev",
6+
"dev": "next dev --turbo",
77
"build": "next build",
88
"start": "next start",
99
"lint": "next lint",
@@ -56,7 +56,7 @@
5656
"recharts": "^2.15.1",
5757
"schema-dts": "^1.1.5",
5858
"sonner": "^2.0.5",
59-
"sqlkit": "^1.0.13",
59+
"sqlkit": "^1.0.15",
6060
"tailwind-merge": "^3.0.2",
6161
"tw-animate-css": "^1.2.4",
6262
"use-immer": "^0.11.0",
Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,140 @@
1-
import React from "react";
1+
"use client";
22

3-
const ReadmeForm = () => {
4-
return <div>ReadmeForm</div>;
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 EditorCommandButton from "@/components/Editor/EditorCommandButton";
7+
import { useMarkdownEditor } from "@/components/Editor/useMarkdownEditor";
8+
import { Button } from "@/components/ui/button";
9+
import { useAutosizeTextArea } from "@/hooks/use-auto-resize-textarea";
10+
import { useTranslation } from "@/i18n/use-translation";
11+
import { markdocParser } from "@/utils/markdoc-parser";
12+
import { zodResolver } from "@hookform/resolvers/zod";
13+
import {
14+
FontBoldIcon,
15+
FontItalicIcon,
16+
HeadingIcon,
17+
ImageIcon,
18+
} from "@radix-ui/react-icons";
19+
import { useMutation } from "@tanstack/react-query";
20+
import { Loader } from "lucide-react";
21+
import React, { useCallback, useState } from "react";
22+
import { 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 ReadmeForm: React.FC<Props> = ({ user }) => {
31+
const { _t } = useTranslation();
32+
const [editorMode, setEditorMode] = useState<"write" | "preview">("write");
33+
const editorRef = React.useRef<HTMLTextAreaElement>(null);
34+
35+
const mutation = useMutation({
36+
mutationFn: (
37+
payload: z.infer<typeof UserActionInput.updateMyProfileInput>
38+
) => userActions.updateMyProfile(payload),
39+
onSuccess: () => {
40+
toast.success(_t("Profile readme updated successfully"));
41+
},
42+
onError: (error) => {
43+
toast.error(_t("Failed to update profile readme"));
44+
console.error("Error updating profile readme:", error);
45+
},
46+
});
47+
48+
const form = useForm({
49+
mode: "all",
50+
defaultValues: {
51+
profile_readme: user?.profile_readme || "",
52+
},
53+
resolver: zodResolver(UserActionInput.updateMyProfileInput),
54+
});
55+
56+
useAutosizeTextArea(editorRef, form.watch("profile_readme") ?? "");
57+
58+
const editor = useMarkdownEditor({
59+
ref: editorRef,
60+
onChange: (value) => form.setValue("profile_readme", value),
61+
});
62+
63+
const renderEditorToolbar = useCallback(
64+
() => (
65+
<div className="flex w-full gap-6 p-2 my-2 bg-muted">
66+
<EditorCommandButton
67+
onClick={() => editor?.executeCommand("heading")}
68+
Icon={<HeadingIcon />}
69+
/>
70+
<EditorCommandButton
71+
onClick={() => editor?.executeCommand("bold")}
72+
Icon={<FontBoldIcon />}
73+
/>
74+
<EditorCommandButton
75+
onClick={() => editor?.executeCommand("italic")}
76+
Icon={<FontItalicIcon />}
77+
/>
78+
{/* <EditorCommandButton
79+
onClick={() => editor?.executeCommand("image")}
80+
Icon={<ImageIcon />}
81+
/> */}
82+
</div>
83+
),
84+
[editor]
85+
);
86+
87+
return (
88+
<form onSubmit={form.handleSubmit((data) => mutation.mutate(data))}>
89+
<div className="flex items-center justify-between">
90+
{renderEditorToolbar()}
91+
<div className="flex items-center gap-4">
92+
{editorMode === "write" && (
93+
<Button
94+
variant={"link"}
95+
className="!px-2"
96+
type="button"
97+
onClick={() => setEditorMode("preview")}
98+
>
99+
{_t("Preview Mode")}
100+
</Button>
101+
)}
102+
{editorMode === "preview" && (
103+
<Button
104+
variant={"link"}
105+
className="!px-2"
106+
type="button"
107+
onClick={() => setEditorMode("write")}
108+
>
109+
{_t("Write Mode")}
110+
</Button>
111+
)}
112+
</div>
113+
</div>
114+
<div className="w-full">
115+
{editorMode === "write" ? (
116+
<textarea
117+
disabled={mutation.isPending}
118+
tabIndex={2}
119+
className="focus:outline-none p-2 border bg-background w-full resize-none"
120+
placeholder={_t("Write something stunning...")}
121+
ref={editorRef}
122+
value={form.watch("profile_readme")}
123+
onChange={(e) => form.setValue("profile_readme", e.target.value)}
124+
/>
125+
) : (
126+
<div className="content-typography">
127+
{markdocParser(form.watch("profile_readme") ?? "")}
128+
</div>
129+
)}
130+
</div>
131+
132+
<Button type="submit" className="mt-2" disabled={mutation.isPending}>
133+
{mutation.isPending && <Loader className="animate-spin" />}
134+
{_t("Save")}
135+
</Button>
136+
</form>
137+
);
5138
};
6139

7140
export default ReadmeForm;

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ const SocialMediaForm: React.FC<Props> = ({ user }) => {
3434
payload: z.infer<typeof UserActionInput.updateMyProfileInput>
3535
) => userActions.updateMyProfile(payload),
3636
onSuccess: () => {
37-
toast(_t("Social links updated successfully"));
37+
toast.success(_t("Social links updated successfully"));
38+
},
39+
onError: (error) => {
40+
toast.error(_t("Failed to update social links"));
41+
console.error("Error updating social links:", error);
3842
},
3943
});
4044

src/app/dashboard/settings/page.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ const SettingsPage = async () => {
1111
const current_user = await userActions.getUserById(auth_id!);
1212

1313
return (
14-
<div>
15-
<pre>{JSON.stringify(current_user, null, 2)}</pre>
14+
<>
15+
{/* <pre>{JSON.stringify(current_user, null, 2)}</pre> */}
1616
<Tabs defaultValue="general">
1717
<TabsList>
1818
<TabsTrigger value="general">{_t("General")}</TabsTrigger>
@@ -36,10 +36,14 @@ const SettingsPage = async () => {
3636
)}
3737
</TabsContent>
3838
<TabsContent value="profile_readme">
39-
<ReadmeForm />
39+
{current_user && (
40+
<div className="max-w-2xl my-10">
41+
<ReadmeForm user={current_user} />
42+
</div>
43+
)}
4044
</TabsContent>
4145
</Tabs>
42-
</div>
46+
</>
4347
);
4448
};
4549

src/backend/services/user.action.ts

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { UserActionInput } from "./inputs/user.input";
99
import { drizzleClient } from "@/backend/persistence/clients";
1010
import { usersTable } from "@/backend/persistence/schemas";
1111
import { authID } from "./session.actions";
12+
import { filterUndefined } from "@/lib/utils";
1213

1314
/**
1415
* Creates or syncs a user account from a social login provider.
@@ -83,38 +84,32 @@ export async function bootSocialUser(
8384
export async function updateMyProfile(
8485
_input: z.infer<typeof UserActionInput.updateMyProfileInput>
8586
) {
86-
try {
87-
const sessionUser = await authID();
88-
if (!sessionUser) {
89-
throw new ActionException(`User not authenticated`);
90-
}
91-
92-
const input = await UserActionInput.updateMyProfileInput.parseAsync(_input);
87+
const sessionUser = await authID();
88+
if (!sessionUser) {
89+
throw new ActionException(`User not authenticated`);
90+
}
9391

94-
console.log(input.social_links);
92+
const input = await UserActionInput.updateMyProfileInput.parseAsync(_input);
9593

96-
const updatedUser = await persistenceRepository.user.update({
97-
where: eq("id", sessionUser!),
98-
data: {
99-
name: input.name,
100-
username: input.username,
101-
email: input.email,
102-
profile_photo: input.profile_photo,
103-
education: input.education,
104-
designation: input.designation,
105-
bio: input.bio,
106-
website_url: input.websiteUrl,
107-
location: input.location,
108-
social_links: input.social_links,
109-
profile_readme: input.profile_readme,
110-
skills: input.skills,
111-
},
112-
});
94+
const updatedUser = await persistenceRepository.user.update({
95+
where: eq("id", sessionUser!),
96+
data: filterUndefined<User>({
97+
name: input.name,
98+
username: input.username,
99+
email: input.email,
100+
profile_photo: input.profile_photo,
101+
education: input.education,
102+
designation: input.designation,
103+
bio: input.bio,
104+
website_url: input.websiteUrl,
105+
location: input.location,
106+
social_links: input.social_links,
107+
profile_readme: input.profile_readme,
108+
skills: input.skills,
109+
}),
110+
});
113111

114-
return updatedUser?.rows?.[0];
115-
} catch (error) {
116-
handleActionException(error);
117-
}
112+
return updatedUser?.rows?.[0];
118113
}
119114

120115
/**

src/components/ui/sonner.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
"use client"
1+
"use client";
22

3-
import { useTheme } from "next-themes"
4-
import { Toaster as Sonner, ToasterProps } from "sonner"
3+
import { useTheme } from "next-themes";
4+
import { Toaster as Sonner, ToasterProps } from "sonner";
55

66
const Toaster = ({ ...props }: ToasterProps) => {
7-
const { theme = "system" } = useTheme()
7+
const { theme = "system" } = useTheme();
88

99
return (
1010
<Sonner
@@ -19,7 +19,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
1919
}
2020
{...props}
2121
/>
22-
)
23-
}
22+
);
23+
};
2424

25-
export { Toaster }
25+
export { Toaster };

src/lib/utils.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { clsx, type ClassValue } from "clsx";
22
import { twMerge } from "tailwind-merge";
3-
import { z } from "zod";
3+
import { z, ZodAnyDef, ZodObject } from "zod";
44

55
export function cn(...inputs: ClassValue[]) {
66
return twMerge(clsx(inputs));
@@ -102,3 +102,11 @@ export const removeNullOrUndefinedFromObject = (obj: any) => {
102102
});
103103
return newObj;
104104
};
105+
106+
export function filterUndefined<T>(
107+
mapping: Partial<Record<keyof T, any>>
108+
): Partial<Record<string, any>> {
109+
return Object.fromEntries(
110+
Object.entries(mapping).filter(([_, value]) => value !== undefined)
111+
) as Partial<Record<string, any>>;
112+
}

0 commit comments

Comments
 (0)