Skip to content

Commit ee48121

Browse files
authored
Merge pull request #67 from acmutsa/ProfilePage
Profile Page Creation
2 parents 0c03d71 + e52cbb5 commit ee48121

File tree

3 files changed

+234
-0
lines changed

3 files changed

+234
-0
lines changed

src/actions/update-profile.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// src/actions/update-profile.ts
2+
"use server";
3+
4+
import { headers } from "next/headers";
5+
import { revalidatePath } from "next/cache";
6+
7+
import { auth } from "@/lib/auth";
8+
import { db } from "@/db";
9+
import { users } from "@/db/schema";
10+
import { eq } from "drizzle-orm";
11+
12+
export async function updateProfile(formData: FormData) {
13+
const session = await auth.api.getSession({
14+
headers: await headers(),
15+
});
16+
17+
if (!session?.user) {
18+
throw new Error("Not authenticated");
19+
}
20+
21+
const userId = session.user.id as any;
22+
23+
const name = formData.get("name");
24+
const image = formData.get("image");
25+
26+
if (typeof name !== "string" || name.trim().length < 2) {
27+
throw new Error("Name must be at least 2 characters");
28+
}
29+
30+
await db
31+
.update(users)
32+
.set({
33+
name: name.trim(),
34+
image:
35+
typeof image === "string" && image.trim().length > 0
36+
? image.trim()
37+
: null,
38+
})
39+
.where(eq(users.id, userId));
40+
41+
revalidatePath("/profile");
42+
}

src/app/profile/page.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// src/app/profile/page.tsx
2+
import { redirect } from "next/navigation";
3+
import { headers } from "next/headers";
4+
5+
import { auth } from "@/lib/auth";
6+
import { db } from "@/db";
7+
import { users } from "@/db/schema";
8+
import { eq } from "drizzle-orm";
9+
import { ProfileForm } from "@/components/ProfileForm";
10+
11+
export default async function ProfilePage() {
12+
const session = await auth.api.getSession({
13+
headers: await headers(),
14+
});
15+
16+
if (!session?.user) {
17+
redirect("/sign-in");
18+
}
19+
20+
const userId = session.user.id as any;
21+
22+
const [user] = await db
23+
.select()
24+
.from(users)
25+
.where(eq(users.id, userId))
26+
.limit(1);
27+
28+
if (!user) {
29+
redirect("/");
30+
}
31+
32+
const displayName = user.name ?? "";
33+
const email = user.email ?? "";
34+
const image = user.image ?? null;
35+
36+
const initials =
37+
displayName && displayName.trim().length > 0
38+
? displayName
39+
.split(" ")
40+
.map((part: string) => part[0])
41+
.join("")
42+
.toUpperCase()
43+
: "U";
44+
45+
return (
46+
<div className="max-w-3xl mx-auto py-10 px-4 sm:px-6 lg:px-8">
47+
<ProfileForm
48+
name={displayName}
49+
email={email}
50+
image={image}
51+
initials={initials}
52+
/>
53+
</div>
54+
);
55+
}

src/components/ProfileForm.tsx

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// src/app/profile/ProfileForm.tsx
2+
3+
"use client";
4+
5+
import { useTransition, FormEvent } from "react";
6+
import { Loader2 } from "lucide-react";
7+
8+
import {
9+
Card,
10+
CardHeader,
11+
CardTitle,
12+
CardDescription,
13+
CardContent,
14+
CardFooter,
15+
} from "@/components/ui/card";
16+
import { Input } from "@/components/ui/input";
17+
import { Label } from "@/components/ui/label";
18+
import {
19+
Avatar,
20+
AvatarImage,
21+
AvatarFallback,
22+
} from "@/components/ui/avatar";
23+
import { Button } from "@/components/ui/button";
24+
import { toast } from "sonner";
25+
import { updateProfile } from "@/actions/update-profile";
26+
27+
type ProfileFormProps = {
28+
name: string;
29+
email: string;
30+
image: string | null;
31+
initials: string;
32+
};
33+
34+
export function ProfileForm({ name, email, image, initials }: ProfileFormProps) {
35+
const [isPending, startTransition] = useTransition();
36+
37+
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
38+
e.preventDefault();
39+
const formData = new FormData(e.currentTarget);
40+
41+
startTransition(async () => {
42+
try {
43+
await updateProfile(formData);
44+
toast.success("Profile updated", {
45+
description: "Your changes have been saved.",
46+
});
47+
} catch (err) {
48+
console.error(err);
49+
toast.error("Update failed", {
50+
description: "Something went wrong while saving your profile.",
51+
});
52+
}
53+
});
54+
};
55+
56+
return (
57+
<Card className="border border-border/60 shadow-sm">
58+
<CardHeader>
59+
<div className="flex items-center gap-4">
60+
<Avatar className="h-16 w-16">
61+
{image ? (
62+
<AvatarImage src={image} alt={name || email} />
63+
) : (
64+
<>
65+
<AvatarImage src="/user.png" alt="User avatar" />
66+
<AvatarFallback>{initials}</AvatarFallback>
67+
</>
68+
)}
69+
</Avatar>
70+
71+
<div className="space-y-1">
72+
<CardTitle className="text-xl">Your Profile</CardTitle>
73+
<CardDescription>
74+
View and update your account information.
75+
</CardDescription>
76+
</div>
77+
</div>
78+
</CardHeader>
79+
80+
<form onSubmit={handleSubmit}>
81+
<CardContent className="space-y-6">
82+
{/* Name (editable) */}
83+
<div className="grid gap-2">
84+
<Label htmlFor="name">Name</Label>
85+
<Input
86+
id="name"
87+
name="name"
88+
defaultValue={name}
89+
placeholder="Your name"
90+
required
91+
disabled={isPending}
92+
/>
93+
</div>
94+
95+
{/* Email (read-only) */}
96+
<div className="grid gap-2">
97+
<Label htmlFor="email">Email</Label>
98+
<Input
99+
id="email"
100+
value={email}
101+
disabled
102+
className="bg-muted/50"
103+
/>
104+
<p className="text-xs text-muted-foreground">
105+
Email is managed by authentication and cannot be changed here.
106+
</p>
107+
</div>
108+
109+
{/* Avatar URL (editable) */}
110+
<div className="grid gap-2">
111+
<Label htmlFor="image">Avatar URL</Label>
112+
<Input
113+
id="image"
114+
name="image"
115+
defaultValue={image ?? ""}
116+
placeholder="https://example.com/avatar.png"
117+
disabled={isPending}
118+
/>
119+
<p className="text-xs text-muted-foreground">
120+
Paste a direct image URL. Later you can replace this with an
121+
upload flow using your S3/blob system.
122+
</p>
123+
</div>
124+
</CardContent>
125+
126+
<CardFooter className="flex justify-center pt-4 gap-2">
127+
<Button type="submit" disabled={isPending}>
128+
{isPending && (
129+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
130+
)}
131+
{isPending ? "Saving..." : "Save changes"}
132+
</Button>
133+
</CardFooter>
134+
</form>
135+
</Card>
136+
);
137+
}

0 commit comments

Comments
 (0)