Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions app/api/profile/update/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { revalidatePath } from "next/cache";
import pool from "@/backend/config/db";

export async function POST(request: NextRequest) {
try {
const session = await getServerSession();

if (!session || !session.user) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}

const body = await request.json();
const { name, phone, address, city, country } = body;
const userEmail = session.user.email;

// Validate phone number: only digits and max 11 characters
if (phone && !/^\d{0,11}$/.test(phone)) {
return NextResponse.json(
{ error: "Phone number must contain only digits" },
{ status: 400 }
);
}

// Update user in database
const query = `
UPDATE users
SET name = $2,
updated_at = CURRENT_TIMESTAMP
WHERE email = $1
RETURNING user_id, email, name, picture, created_at, updated_at
`;

const result = await pool.query(query, [userEmail, name]);

if (result.rows.length === 0) {
return NextResponse.json(
{ error: "User not found" },
{ status: 404 }
);
}

const updatedUser = result.rows[0];
console.log("✅ User updated in database:", updatedUser);

// Revalidate the profile page to clear cache
revalidatePath("/profile");
revalidatePath("/profile/edit");

return NextResponse.json(
{
message: "Profile updated successfully",
data: {
name: updatedUser.name,
email: updatedUser.email,
id: updatedUser.user_id
}
},
{ status: 200 }
);
} catch (error) {
console.error("❌ Profile update error:", error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Failed to update profile" },
{ status: 500 }
);
}
}
274 changes: 274 additions & 0 deletions app/profile/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
"use client";

import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { User, Mail, Phone, MapPin, ChevronLeft } from "lucide-react";
import SidebarLayout from "@/Components/SidebarLayout";
import Link from "next/link";
import toast from "react-hot-toast";

const EditProfilePage = () => {
const { data: session, status } = useSession();
const router = useRouter();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
name: "",
email: "",
phone: "",
address: "",
city: "",
country: "",
});

useEffect(() => {
if (status === "unauthenticated") {
router.push("/login?callbackUrl=/profile/edit");
return;
}

if (session?.user) {
setFormData({
name: session.user.name || "",
email: session.user.email || "",
phone: "",
address: "",
city: "",
country: "",
});
}
}, [session, status, router]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;

// For phone field: only allow digits and max 11 characters
if (name === "phone") {
const digitsOnly = value.replace(/\D/g, "").slice(0, 11);
setFormData((prev) => ({
...prev,
[name]: digitsOnly,
}));
return;
}

// Capitalize first letter of each word for name and other text fields
let processedValue = value;
if (name === "name" || name === "address" || name === "city" || name === "country") {
processedValue = value
.split(" ")
.map((word) => {
if (word.length === 0) return word;
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
})
.join(" ");
}

setFormData((prev) => ({
...prev,
[name]: processedValue,
}));
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);

try {
const response = await fetch("/api/profile/update", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});

const data = await response.json();

if (!response.ok) {
throw new Error(data.error || "Failed to update profile");
}

console.log("✅ Profile updated:", data);
toast.success("Profile updated successfully!");

// Navigate to profile page and force full reload to get fresh session
window.location.href = "/profile";
} catch (error) {
toast.error("Failed to update profile. Please try again.");
console.error("❌ Error updating profile:", error);
setLoading(false);
}
};

if (status === "loading") {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand-primary mx-auto mb-4"></div>
<p className="text-gray-600">Loading...</p>
</div>
</div>
);
}

if (status === "unauthenticated") {
return null;
}

return (
<SidebarLayout>
{/* Hero Section */}
<div className="relative bg-gradient-to-br from-gray-100 via-gray-50 to-orange-50 dark:from-gray-800 dark:via-gray-700 dark:to-gray-800 text-gray-900 dark:text-white py-12 overflow-hidden border-b border-gray-200 dark:border-gray-700">
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 bg-brand-primary/10 dark:bg-brand-primary/20 backdrop-blur-sm rounded-full mb-6 border border-brand-primary/20 dark:border-brand-primary/30">
<User className="w-8 h-8 text-brand-primary" />
</div>
<h1 className="text-4xl md:text-5xl font-bold mb-4">Edit Profile</h1>
<p className="text-lg md:text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
Update your account information
</p>
</div>
</div>

{/* Main Content */}
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Back Button */}
<Link
href="/profile"
className="inline-flex items-center gap-2 text-brand-primary hover:text-brand-primaryDark mb-6 font-medium transition-colors"
>
<ChevronLeft className="w-5 h-5" />
Back to Profile
</Link>

{/* Edit Form */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-8">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Full Name */}
<div>
<label className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
Full Name
</label>
<div className="relative">
<User className="absolute left-3 top-3.5 w-5 h-5 text-gray-400" />
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary dark:bg-gray-700 dark:text-white"
placeholder="Your full name"
/>
</div>
</div>

{/* Email */}
<div>
<label className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
Email Address
</label>
<div className="relative">
<Mail className="absolute left-3 top-3.5 w-5 h-5 text-gray-400" />
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
disabled
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary dark:bg-gray-700 dark:text-white bg-gray-100 dark:bg-gray-600 cursor-not-allowed"
/>
</div>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">Email cannot be changed</p>
</div>

{/* Phone */}
<div>
<label className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
Phone Number
</label>
<div className="relative">
<Phone className="absolute left-3 top-3.5 w-5 h-5 text-gray-400" />
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleChange}
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary dark:bg-gray-700 dark:text-white"
placeholder="Your phone number"
/>
</div>
</div>

{/* Address */}
<div>
<label className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
Street Address
</label>
<div className="relative">
<MapPin className="absolute left-3 top-3.5 w-5 h-5 text-gray-400" />
<input
type="text"
name="address"
value={formData.address}
onChange={handleChange}
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary dark:bg-gray-700 dark:text-white"
placeholder="Your street address"
/>
</div>
</div>

{/* City and Country */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
City
</label>
<input
type="text"
name="city"
value={formData.city}
onChange={handleChange}
className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary dark:bg-gray-700 dark:text-white"
placeholder="Your city"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
Country
</label>
<input
type="text"
name="country"
value={formData.country}
onChange={handleChange}
className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary dark:bg-gray-700 dark:text-white"
placeholder="Your country"
/>
</div>
</div>

{/* Action Buttons */}
<div className="flex gap-4 pt-6 border-t border-gray-200 dark:border-gray-700">
<button
type="submit"
disabled={loading}
className="flex-1 px-6 py-3 bg-brand-primary hover:bg-brand-primaryDark text-white font-semibold rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? "Saving..." : "Save changes"}
</button>
<Link
href="/profile"
className="flex-1 px-6 py-3 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-900 dark:text-white font-semibold rounded-lg transition-colors text-center"
>
Cancel
</Link>
</div>
</form>
</div>
</div>
</SidebarLayout>
);
};

export default EditProfilePage;
2 changes: 1 addition & 1 deletion app/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ const ProfilePage = () => {
</p>
<Link
href="/profile/settings"
className="inline-block px-6 py-2 boo4g-brand-primary/10 hover:bg-brand-primary/20 text-brand-primary font-medium rounded-lg transition-colors"
className="inline-block px-6 py-2 bg-brand-primary/10 hover:bg-brand-primary/20 text-brand-primary font-medium rounded-lg transition-colors"
>
Manage Settings
</Link>
Expand Down
Loading