feat: Split profile name into first and last name fields, introduce event-based profile updates for the header, and add S3 deletion for avatars.

This commit is contained in:
pradeepkumar
2026-02-01 00:59:37 +05:30
parent c4afc421cf
commit 35ae58c0df
2 changed files with 135 additions and 65 deletions

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
import { useSession, signOut } from 'next-auth/react'; import { useSession, signOut } from 'next-auth/react';
@@ -19,6 +19,7 @@ export function CommonHeader() {
const [showProfileMenu, setShowProfileMenu] = useState(false); const [showProfileMenu, setShowProfileMenu] = useState(false);
const profileMenuRef = useRef<HTMLDivElement>(null); const profileMenuRef = useRef<HTMLDivElement>(null);
const [profileImage, setProfileImage] = useState<string | null>(null); const [profileImage, setProfileImage] = useState<string | null>(null);
const [profileName, setProfileName] = useState<string | null>(null);
// Close dropdown when clicking outside // Close dropdown when clicking outside
useEffect(() => { useEffect(() => {
@@ -37,40 +38,62 @@ export function CommonHeader() {
}; };
}, [showProfileMenu]); }, [showProfileMenu]);
// Fetch profile image from backend based on user role // Fetch profile data (image and name) from backend based on user role
useEffect(() => { const fetchProfileData = useCallback(async () => {
const fetchProfileImage = async () => { try {
try { const role = (session?.user as any)?.role;
const role = (session?.user as any)?.role; let avatar: string | null = null;
let avatar: string | null = null; let name: string | null = null;
if (role === 'AGENT') { if (role === 'AGENT') {
const profile = await agentsService.getMyProfile(); const profile = await agentsService.getMyProfile();
avatar = profile.avatar; avatar = profile.avatar;
} else if (role === 'USER') { name = profile.firstName ? `${profile.firstName} ${profile.lastName || ''}`.trim() : null;
const profile = await usersService.getMyProfile(); } else if (role === 'USER') {
avatar = profile.avatar; const profile = await usersService.getMyProfile();
} avatar = profile.avatar;
name = profile.firstName ? `${profile.firstName} ${profile.lastName || ''}`.trim() : null;
if (avatar) {
try {
const avatarUrl = await uploadService.getPresignedDownloadUrl(avatar);
setProfileImage(avatarUrl);
} catch {
setProfileImage(avatar);
}
}
} catch (err) {
console.error('Failed to fetch profile image:', err);
} }
};
if (session) { if (name) {
fetchProfileImage(); setProfileName(name);
}
if (avatar) {
try {
const avatarUrl = await uploadService.getPresignedDownloadUrl(avatar);
setProfileImage(avatarUrl);
} catch {
setProfileImage(avatar);
}
}
} catch (err) {
console.error('Failed to fetch profile data:', err);
} }
}, [session]); }, [session]);
const userName = session?.user?.name; useEffect(() => {
if (session) {
fetchProfileData();
}
}, [session, fetchProfileData]);
// Listen for profile update events to refresh the header
useEffect(() => {
const handleProfileUpdate = () => {
if (session) {
fetchProfileData();
}
};
window.addEventListener('profile-updated', handleProfileUpdate);
return () => {
window.removeEventListener('profile-updated', handleProfileUpdate);
};
}, [session, fetchProfileData]);
// Use fetched profile name, fallback to session name
const userName = profileName || session?.user?.name;
const userEmail = session?.user?.email; const userEmail = session?.user?.email;
const userRole = (session?.user as any)?.role; const userRole = (session?.user as any)?.role;
// Use fetched profile image, fallback to session image // Use fetched profile image, fallback to session image

View File

@@ -9,7 +9,8 @@ import { usersService } from '@/services/users.service';
interface ProfileSettingsFormProps { interface ProfileSettingsFormProps {
initialData?: { initialData?: {
fullName: string; firstName: string;
lastName: string;
career: string; career: string;
email: string; email: string;
phone: string; phone: string;
@@ -17,7 +18,8 @@ interface ProfileSettingsFormProps {
avatar?: string; avatar?: string;
}; };
onSave?: (data: { onSave?: (data: {
fullName: string; firstName: string;
lastName: string;
career: string; career: string;
email: string; email: string;
phone: string; phone: string;
@@ -35,7 +37,8 @@ export function ProfileSettingsForm({
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
fullName: initialData?.fullName || '', firstName: initialData?.firstName || '',
lastName: initialData?.lastName || '',
career: initialData?.career || '', career: initialData?.career || '',
email: initialData?.email || '', email: initialData?.email || '',
phone: initialData?.phone || '', phone: initialData?.phone || '',
@@ -60,7 +63,8 @@ export function ProfileSettingsForm({
if (role === 'AGENT') { if (role === 'AGENT') {
const profile = await agentsService.getMyProfile(); const profile = await agentsService.getMyProfile();
setFormData({ setFormData({
fullName: `${profile.firstName || ''} ${profile.lastName || ''}`.trim(), firstName: profile.firstName || '',
lastName: profile.lastName || '',
career: profile.agentType?.name || 'Real Estate Agent', career: profile.agentType?.name || 'Real Estate Agent',
email: profile.email || session?.user?.email || '', email: profile.email || session?.user?.email || '',
phone: profile.phone || '', phone: profile.phone || '',
@@ -78,7 +82,8 @@ export function ProfileSettingsForm({
} else if (role === 'USER') { } else if (role === 'USER') {
const profile = await usersService.getMyProfile(); const profile = await usersService.getMyProfile();
setFormData({ setFormData({
fullName: `${profile.firstName || ''} ${profile.lastName || ''}`.trim(), firstName: profile.firstName || '',
lastName: profile.lastName || '',
career: '', // Users don't have career/agent type career: '', // Users don't have career/agent type
email: profile.email || session?.user?.email || '', email: profile.email || session?.user?.email || '',
phone: profile.phone || '', phone: profile.phone || '',
@@ -98,9 +103,11 @@ export function ProfileSettingsForm({
console.error('Failed to fetch profile:', err); console.error('Failed to fetch profile:', err);
// Use session data as fallback // Use session data as fallback
if (session?.user) { if (session?.user) {
const nameParts = (session.user?.name || '').split(' ');
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
fullName: session.user?.name || prev.fullName, firstName: nameParts[0] || prev.firstName,
lastName: nameParts.slice(1).join(' ') || prev.lastName,
email: session.user?.email || prev.email, email: session.user?.email || prev.email,
})); }));
if (session.user.image) { if (session.user.image) {
@@ -190,6 +197,9 @@ export function ProfileSettingsForm({
image: presignedUrl, image: presignedUrl,
}, },
}); });
// Dispatch event to notify header to refresh profile data
window.dispatchEvent(new Event('profile-updated'));
} catch { } catch {
// If presigned URL fails, keep the local preview (don't revoke it) // If presigned URL fails, keep the local preview (don't revoke it)
console.warn('Could not get presigned URL, using local preview'); console.warn('Could not get presigned URL, using local preview');
@@ -216,7 +226,27 @@ export function ProfileSettingsForm({
const role = (session?.user as any)?.role; const role = (session?.user as any)?.role;
// Update profile to remove avatar based on role // Get current avatar key from profile to delete from S3
let avatarKey: string | null = null;
if (role === 'AGENT') {
const profile = await agentsService.getMyProfile();
avatarKey = profile.avatar;
} else {
const profile = await usersService.getMyProfile();
avatarKey = profile.avatar;
}
// Delete from S3 if avatar exists
if (avatarKey) {
try {
await uploadService.deleteFile(avatarKey);
} catch (s3Error) {
console.warn('Could not delete from S3:', s3Error);
// Continue even if S3 delete fails - still clear database reference
}
}
// Update profile to remove avatar reference from database
if (role === 'AGENT') { if (role === 'AGENT') {
await agentsService.updateProfile({ avatar: null }); await agentsService.updateProfile({ avatar: null });
} else { } else {
@@ -233,6 +263,9 @@ export function ProfileSettingsForm({
}, },
}); });
// Dispatch event to notify header to refresh profile data
window.dispatchEvent(new Event('profile-updated'));
setSuccessMessage('Profile picture removed successfully!'); setSuccessMessage('Profile picture removed successfully!');
} catch (err) { } catch (err) {
console.error('Delete failed:', err); console.error('Delete failed:', err);
@@ -249,16 +282,11 @@ export function ProfileSettingsForm({
const role = (session?.user as any)?.role; const role = (session?.user as any)?.role;
// Split full name into first and last name
const nameParts = formData.fullName.trim().split(' ');
const firstName = nameParts[0] || '';
const lastName = nameParts.slice(1).join(' ') || '';
// Update profile based on role (email is not editable - tied to auth) // Update profile based on role (email is not editable - tied to auth)
if (role === 'AGENT') { if (role === 'AGENT') {
await agentsService.updateProfile({ await agentsService.updateProfile({
firstName, firstName: formData.firstName,
lastName, lastName: formData.lastName,
phone: formData.phone, phone: formData.phone,
serviceAreas: formData.location ? [formData.location] : [], serviceAreas: formData.location ? [formData.location] : [],
}); });
@@ -266,8 +294,8 @@ export function ProfileSettingsForm({
// Parse location into city, state, country for users // Parse location into city, state, country for users
const locationParts = formData.location.split(',').map(s => s.trim()); const locationParts = formData.location.split(',').map(s => s.trim());
await usersService.updateProfile({ await usersService.updateProfile({
firstName, firstName: formData.firstName,
lastName, lastName: formData.lastName,
phone: formData.phone, phone: formData.phone,
city: locationParts[0] || undefined, city: locationParts[0] || undefined,
state: locationParts[1] || undefined, state: locationParts[1] || undefined,
@@ -276,14 +304,18 @@ export function ProfileSettingsForm({
} }
// Update session with new name // Update session with new name
const fullName = `${formData.firstName} ${formData.lastName}`.trim();
await updateSession({ await updateSession({
...session, ...session,
user: { user: {
...session?.user, ...session?.user,
name: formData.fullName, name: fullName,
}, },
}); });
// Dispatch event to notify header and other components to refresh profile data
window.dispatchEvent(new Event('profile-updated'));
if (onSave) { if (onSave) {
onSave(formData); onSave(formData);
} }
@@ -300,9 +332,11 @@ export function ProfileSettingsForm({
const handleCancel = () => { const handleCancel = () => {
// Reset to initial data or session data // Reset to initial data or session data
if (session?.user) { if (session?.user) {
const nameParts = (session.user?.name || '').split(' ');
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
fullName: session.user?.name || prev.fullName, firstName: nameParts[0] || prev.firstName,
lastName: nameParts.slice(1).join(' ') || prev.lastName,
email: session.user?.email || prev.email, email: session.user?.email || prev.email,
})); }));
} }
@@ -398,34 +432,47 @@ export function ProfileSettingsForm({
{/* Form Fields */} {/* Form Fields */}
<div className="space-y-5"> <div className="space-y-5">
{/* Row 1: Full Name & Career (Career only for agents) */} {/* Row 1: First Name & Last Name */}
<div className={`grid grid-cols-1 ${(session?.user as any)?.role === 'AGENT' ? 'sm:grid-cols-2' : ''} gap-5`}> <div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
<div> <div>
<label className="block text-[12px] font-medium text-[#00293D]/70 font-serif mb-2"> <label className="block text-[12px] font-medium text-[#00293D]/70 font-serif mb-2">
Full Name First Name
</label> </label>
<input <input
type="text" type="text"
value={formData.fullName} value={formData.firstName}
onChange={(e) => handleChange('fullName', e.target.value)} onChange={(e) => handleChange('firstName', e.target.value)}
className="w-full h-[44px] px-4 border border-[#00293D]/20 rounded-[10px] text-[14px] font-serif text-[#00293D] focus:outline-none focus:border-[#E58625]"
/>
</div>
<div>
<label className="block text-[12px] font-medium text-[#00293D]/70 font-serif mb-2">
Last Name
</label>
<input
type="text"
value={formData.lastName}
onChange={(e) => handleChange('lastName', e.target.value)}
className="w-full h-[44px] px-4 border border-[#00293D]/20 rounded-[10px] text-[14px] font-serif text-[#00293D] focus:outline-none focus:border-[#E58625]" className="w-full h-[44px] px-4 border border-[#00293D]/20 rounded-[10px] text-[14px] font-serif text-[#00293D] focus:outline-none focus:border-[#E58625]"
/> />
</div> </div>
{(session?.user as any)?.role === 'AGENT' && (
<div>
<label className="block text-[12px] font-medium text-[#00293D]/70 font-serif mb-2">
Career
</label>
<input
type="text"
value={formData.career}
onChange={(e) => handleChange('career', e.target.value)}
className="w-full h-[44px] px-4 border border-[#00293D]/20 rounded-[10px] text-[14px] font-serif text-[#00293D] focus:outline-none focus:border-[#E58625]"
/>
</div>
)}
</div> </div>
{/* Career (only for agents) */}
{(session?.user as any)?.role === 'AGENT' && (
<div>
<label className="block text-[12px] font-medium text-[#00293D]/70 font-serif mb-2">
Career
</label>
<input
type="text"
value={formData.career}
onChange={(e) => handleChange('career', e.target.value)}
className="w-full sm:w-1/2 h-[44px] px-4 border border-[#00293D]/20 rounded-[10px] text-[14px] font-serif text-[#00293D] focus:outline-none focus:border-[#E58625]"
/>
</div>
)}
{/* Row 2: Email & Phone */} {/* Row 2: Email & Phone */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
<div> <div>