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:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user