From 6b74ea1f4b46fd8219e44870129d925f3422814e Mon Sep 17 00:00:00 2001 From: Derick Gomez Date: Sun, 30 Nov 2025 23:05:49 -0600 Subject: [PATCH 1/3] Profile Page Created --- src/actions/update-profile.ts | 43 ++++++++++ src/app/profile/ProfileForm.tsx | 137 ++++++++++++++++++++++++++++++++ src/app/profile/page.tsx | 55 +++++++++++++ 3 files changed, 235 insertions(+) create mode 100644 src/actions/update-profile.ts create mode 100644 src/app/profile/ProfileForm.tsx create mode 100644 src/app/profile/page.tsx diff --git a/src/actions/update-profile.ts b/src/actions/update-profile.ts new file mode 100644 index 0000000..1a9f9cc --- /dev/null +++ b/src/actions/update-profile.ts @@ -0,0 +1,43 @@ +// src/actions/update-profile.ts +"use server"; + +import { headers } from "next/headers"; +import { revalidatePath } from "next/cache"; + +import { auth } from "@/lib/auth"; +import { db } from "@/db"; +import { users } from "@/db/schema"; +import { eq } from "drizzle-orm"; + +export async function updateProfile(formData: FormData) { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session?.user) { + throw new Error("Not authenticated"); + } + + // ✅ same fix – no Number(...) + const userId = session.user.id as any; + + const name = formData.get("name"); + const image = formData.get("image"); + + if (typeof name !== "string" || name.trim().length < 2) { + throw new Error("Name must be at least 2 characters"); + } + + await db + .update(users) + .set({ + name: name.trim(), + image: + typeof image === "string" && image.trim().length > 0 + ? image.trim() + : null, + }) + .where(eq(users.id, userId)); + + revalidatePath("/profile"); +} diff --git a/src/app/profile/ProfileForm.tsx b/src/app/profile/ProfileForm.tsx new file mode 100644 index 0000000..8b1081d --- /dev/null +++ b/src/app/profile/ProfileForm.tsx @@ -0,0 +1,137 @@ +// src/app/profile/ProfileForm.tsx + +"use client"; + +import { useTransition, FormEvent } from "react"; +import { Loader2 } from "lucide-react"; + +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Avatar, + AvatarImage, + AvatarFallback, +} from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { toast } from "sonner"; +import { updateProfile } from "@/actions/update-profile"; + +type ProfileFormProps = { + name: string; + email: string; + image: string | null; + initials: string; +}; + +export function ProfileForm({ name, email, image, initials }: ProfileFormProps) { + const [isPending, startTransition] = useTransition(); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + + startTransition(async () => { + try { + await updateProfile(formData); + toast.success("Profile updated", { + description: "Your changes have been saved.", + }); + } catch (err) { + console.error(err); + toast.error("Update failed", { + description: "Something went wrong while saving your profile.", + }); + } + }); + }; + + return ( + + +
+ + {image ? ( + + ) : ( + <> + + {initials} + + )} + + +
+ Your Profile + + View and update your account information. + +
+
+
+ +
+ + {/* Name (editable) */} +
+ + +
+ + {/* Email (read-only) */} +
+ + +

+ Email is managed by authentication and cannot be changed here. +

+
+ + {/* Avatar URL (editable) */} +
+ + +

+ Paste a direct image URL. Later you can replace this with an + upload flow using your S3/blob system. +

+
+
+ + + + +
+
+ ); +} diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx new file mode 100644 index 0000000..a6e1a89 --- /dev/null +++ b/src/app/profile/page.tsx @@ -0,0 +1,55 @@ +// src/app/profile/page.tsx +import { redirect } from "next/navigation"; +import { headers } from "next/headers"; + +import { auth } from "@/lib/auth"; +import { db } from "@/db"; +import { users } from "@/db/schema"; +import { eq } from "drizzle-orm"; +import { ProfileForm } from "./ProfileForm"; + +export default async function ProfilePage() { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session?.user) { + redirect("/sign-in"); + } + + const userId = session.user.id as any; + + const [user] = await db + .select() + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + if (!user) { + redirect("/"); + } + + const displayName = user.name ?? ""; + const email = user.email ?? ""; + const image = user.image ?? null; + + const initials = + displayName && displayName.trim().length > 0 + ? displayName + .split(" ") + .map((part: string) => part[0]) + .join("") + .toUpperCase() + : "U"; + + return ( +
+ +
+ ); +} From 99b2d0bdde87badf23ee8c1578563e8ccefa2a3d Mon Sep 17 00:00:00 2001 From: Derick Gomez Date: Sun, 30 Nov 2025 23:09:42 -0600 Subject: [PATCH 2/3] Created Profile Page --- src/actions/update-profile.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/actions/update-profile.ts b/src/actions/update-profile.ts index 1a9f9cc..197467f 100644 --- a/src/actions/update-profile.ts +++ b/src/actions/update-profile.ts @@ -18,7 +18,6 @@ export async function updateProfile(formData: FormData) { throw new Error("Not authenticated"); } - // ✅ same fix – no Number(...) const userId = session.user.id as any; const name = formData.get("name"); From e52cbb500626611170bc041ff1417bba344540b2 Mon Sep 17 00:00:00 2001 From: adanrsantos Date: Mon, 1 Dec 2025 12:20:53 -0600 Subject: [PATCH 3/3] change file location --- src/app/profile/page.tsx | 2 +- src/{app/profile => components}/ProfileForm.tsx | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{app/profile => components}/ProfileForm.tsx (100%) diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index a6e1a89..e75d4e4 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -6,7 +6,7 @@ import { auth } from "@/lib/auth"; import { db } from "@/db"; import { users } from "@/db/schema"; import { eq } from "drizzle-orm"; -import { ProfileForm } from "./ProfileForm"; +import { ProfileForm } from "@/components/ProfileForm"; export default async function ProfilePage() { const session = await auth.api.getSession({ diff --git a/src/app/profile/ProfileForm.tsx b/src/components/ProfileForm.tsx similarity index 100% rename from src/app/profile/ProfileForm.tsx rename to src/components/ProfileForm.tsx