From 1ec1696db7c9720c1ed393ee5b31dd4310f9786f Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Sun, 1 Feb 2026 00:35:22 +0530 Subject: [PATCH] feat: enable user profile avatar upload and display across header and settings. --- src/components/layout/CommonHeader.tsx | 76 ++++- .../settings/ProfileSettingsForm.tsx | 299 ++++++++++++++++-- src/components/settings/SettingsSidebar.tsx | 75 ++++- 3 files changed, 389 insertions(+), 61 deletions(-) diff --git a/src/components/layout/CommonHeader.tsx b/src/components/layout/CommonHeader.tsx index bb62245..09e6fbc 100644 --- a/src/components/layout/CommonHeader.tsx +++ b/src/components/layout/CommonHeader.tsx @@ -4,6 +4,8 @@ import { useState, useEffect, useRef } from 'react'; import Link from 'next/link'; import Image from 'next/image'; import { useSession, signOut } from 'next-auth/react'; +import { agentsService } from '@/services/agents.service'; +import { uploadService } from '@/services/upload.service'; const navLinks = [ { label: 'Education', href: '/education' }, @@ -15,6 +17,7 @@ export function CommonHeader() { const { data: session } = useSession(); const [showProfileMenu, setShowProfileMenu] = useState(false); const profileMenuRef = useRef(null); + const [profileImage, setProfileImage] = useState(null); // Close dropdown when clicking outside useEffect(() => { @@ -33,9 +36,34 @@ export function CommonHeader() { }; }, [showProfileMenu]); + // Fetch profile image from backend + useEffect(() => { + const fetchProfileImage = async () => { + try { + const profile = await agentsService.getMyProfile(); + if (profile.avatar) { + try { + const avatarUrl = await uploadService.getPresignedDownloadUrl(profile.avatar); + setProfileImage(avatarUrl); + } catch { + setProfileImage(profile.avatar); + } + } + } catch (err) { + console.error('Failed to fetch profile image:', err); + } + }; + + if (session) { + fetchProfileImage(); + } + }, [session]); + const userName = session?.user?.name; const userEmail = session?.user?.email; const userRole = (session?.user as any)?.role; + // Use fetched profile image, fallback to session image + const userImage = profileImage || session?.user?.image; // Determine dashboard link based on user role const dashboardLink = userRole === 'AGENT' ? '/agent/dashboard' : '/user/dashboard'; @@ -90,14 +118,22 @@ export function CommonHeader() { className="flex items-center gap-3 hover:opacity-80 transition-opacity cursor-pointer" > {/* Avatar */} -
- Profile +
+ {userImage ? ( + Profile + ) : ( + Profile + )}
{/* Greeting Text */}
@@ -118,14 +154,22 @@ export function CommonHeader() { {/* User Profile Section */}
-
- Profile +
+ {userImage ? ( + Profile + ) : ( + Profile + )}

diff --git a/src/components/settings/ProfileSettingsForm.tsx b/src/components/settings/ProfileSettingsForm.tsx index 1a76808..7b501ef 100644 --- a/src/components/settings/ProfileSettingsForm.tsx +++ b/src/components/settings/ProfileSettingsForm.tsx @@ -1,7 +1,10 @@ 'use client'; -import { useState } from 'react'; +import { useState, useRef, useEffect } from 'react'; import Image from 'next/image'; +import { useSession, signIn } from 'next-auth/react'; +import { uploadService } from '@/services/upload.service'; +import { agentsService } from '@/services/agents.service'; interface ProfileSettingsFormProps { initialData?: { @@ -10,6 +13,7 @@ interface ProfileSettingsFormProps { email: string; phone: string; location: string; + avatar?: string; }; onSave?: (data: { fullName: string; @@ -22,34 +26,234 @@ interface ProfileSettingsFormProps { } export function ProfileSettingsForm({ - initialData = { - fullName: 'Brain Neeland', - career: 'Real Estate Agent', - email: 'Brain.Neeland1234@gmail.com', - phone: '+917483849544', - location: 'New York', - }, + initialData, onSave, descriptionText = 'This information will be displayed on your public profile page.', }: ProfileSettingsFormProps) { - const [formData, setFormData] = useState(initialData); + const { data: session, update: updateSession } = useSession(); + const fileInputRef = useRef(null); + + const [formData, setFormData] = useState({ + fullName: initialData?.fullName || '', + career: initialData?.career || '', + email: initialData?.email || '', + phone: initialData?.phone || '', + location: initialData?.location || '', + }); + + const [avatarUrl, setAvatarUrl] = useState(null); + const [isUploading, setIsUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + + // Fetch profile data on mount + useEffect(() => { + const fetchProfile = async () => { + try { + setIsLoading(true); + const profile = await agentsService.getMyProfile(); + + setFormData({ + fullName: `${profile.firstName || ''} ${profile.lastName || ''}`.trim(), + career: profile.agentType?.name || 'Real Estate Agent', + email: profile.email || session?.user?.email || '', + phone: profile.phone || '', + location: profile.serviceAreas?.[0] || '', + }); + + // Handle avatar URL - if it's an S3 key, get presigned URL + if (profile.avatar) { + try { + const presignedUrl = await uploadService.getPresignedDownloadUrl(profile.avatar); + setAvatarUrl(presignedUrl); + } catch { + // If getting presigned URL fails, the avatar might be a full URL + setAvatarUrl(profile.avatar); + } + } + } catch (err) { + console.error('Failed to fetch profile:', err); + // Use session data as fallback + if (session?.user) { + setFormData(prev => ({ + ...prev, + fullName: session.user?.name || prev.fullName, + email: session.user?.email || prev.email, + })); + if (session.user.image) { + setAvatarUrl(session.user.image); + } + } + } finally { + setIsLoading(false); + } + }; + + if (session) { + fetchProfile(); + } + }, [session]); const handleChange = (field: string, value: string) => { setFormData((prev) => ({ ...prev, [field]: value })); + setError(null); + setSuccessMessage(null); }; - const handleSave = () => { - if (onSave) { - onSave(formData); - } else { - console.log('Saving profile settings:', formData); + const handleUploadClick = () => { + fileInputRef.current?.click(); + }; + + const handleFileChange = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + // Validate file type + const validTypes = ['image/jpeg', 'image/png', 'image/gif']; + if (!validTypes.includes(file.type)) { + setError('Please upload a JPEG, PNG, or GIF image.'); + return; + } + + // Validate file size (2MB max) + const maxSize = 2 * 1024 * 1024; + if (file.size > maxSize) { + setError('File size must be less than 2MB.'); + return; + } + + try { + setIsUploading(true); + setError(null); + setUploadProgress(0); + + // Upload the avatar + const uploadedFile = await uploadService.uploadAvatar(file, (progress) => { + setUploadProgress(progress.percentage); + }); + + // Update the profile with the new avatar key + await agentsService.updateProfile({ avatar: uploadedFile.url }); + + // Get the presigned URL for display + const presignedUrl = await uploadService.getPresignedDownloadUrl(uploadedFile.url); + setAvatarUrl(presignedUrl); + + // Update the session to reflect the new avatar + await updateSession({ + ...session, + user: { + ...session?.user, + image: presignedUrl, + }, + }); + + setSuccessMessage('Profile picture updated successfully!'); + } catch (err) { + console.error('Upload failed:', err); + setError('Failed to upload profile picture. Please try again.'); + } finally { + setIsUploading(false); + setUploadProgress(0); + // Reset file input + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } + }; + + const handleDeleteAvatar = async () => { + try { + setIsUploading(true); + setError(null); + + // Update profile to remove avatar + await agentsService.updateProfile({ avatar: null }); + setAvatarUrl(null); + + // Update session + await updateSession({ + ...session, + user: { + ...session?.user, + image: null, + }, + }); + + setSuccessMessage('Profile picture removed successfully!'); + } catch (err) { + console.error('Delete failed:', err); + setError('Failed to remove profile picture. Please try again.'); + } finally { + setIsUploading(false); + } + }; + + const handleSave = async () => { + try { + setIsSaving(true); + setError(null); + + // 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 + await agentsService.updateProfile({ + firstName, + lastName, + email: formData.email, + phone: formData.phone, + serviceAreas: formData.location ? [formData.location] : [], + }); + + // Update session with new name + await updateSession({ + ...session, + user: { + ...session?.user, + name: formData.fullName, + }, + }); + + if (onSave) { + onSave(formData); + } + + setSuccessMessage('Profile settings saved successfully!'); + } catch (err) { + console.error('Save failed:', err); + setError('Failed to save profile settings. Please try again.'); + } finally { + setIsSaving(false); } }; const handleCancel = () => { - setFormData(initialData); + // Reset to initial data or session data + if (session?.user) { + setFormData(prev => ({ + ...prev, + fullName: session.user?.name || prev.fullName, + email: session.user?.email || prev.email, + })); + } + setError(null); + setSuccessMessage(null); }; + if (isLoading) { + return ( +

+
+
+ ); + } + return (
{/* Header */} @@ -62,27 +266,62 @@ export function ProfileSettingsForm({

+ {/* Error/Success Messages */} + {error && ( +
+ {error} +
+ )} + {successMessage && ( +
+ {successMessage} +
+ )} + {/* Profile Photo Section */}
-
- Profile +
+ {avatarUrl ? ( + Profile + ) : ( + Profile + )}

Public Picture

- -
@@ -175,15 +414,17 @@ export function ProfileSettingsForm({
diff --git a/src/components/settings/SettingsSidebar.tsx b/src/components/settings/SettingsSidebar.tsx index c3996dd..a2be49f 100644 --- a/src/components/settings/SettingsSidebar.tsx +++ b/src/components/settings/SettingsSidebar.tsx @@ -1,8 +1,12 @@ 'use client'; +import { useState, useEffect } from 'react'; import Image from 'next/image'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; +import { useSession } from 'next-auth/react'; +import { agentsService } from '@/services/agents.service'; +import { uploadService } from '@/services/upload.service'; interface NavItem { label: string; @@ -11,19 +15,50 @@ interface NavItem { } interface SettingsSidebarProps { - profileImage?: string; - name?: string; - title?: string; basePath?: '/agent/settings' | '/user/settings'; } export function SettingsSidebar({ - profileImage = '/assets/icons/user-placeholder-icon.svg', - name = 'Brain Neeland', - title = 'Top Real Estate Agent', basePath = '/agent/settings', }: SettingsSidebarProps) { const pathname = usePathname(); + const { data: session } = useSession(); + + const [profileData, setProfileData] = useState({ + name: session?.user?.name || 'User', + title: 'Real Estate Agent', + avatarUrl: session?.user?.image || null, + }); + + useEffect(() => { + const fetchProfile = async () => { + try { + const profile = await agentsService.getMyProfile(); + + let avatarUrl = session?.user?.image || null; + if (profile.avatar) { + try { + avatarUrl = await uploadService.getPresignedDownloadUrl(profile.avatar); + } catch { + avatarUrl = profile.avatar; + } + } + + setProfileData({ + name: `${profile.firstName || ''} ${profile.lastName || ''}`.trim() || session?.user?.name || 'User', + title: profile.agentType?.name || 'Real Estate Agent', + avatarUrl, + }); + } catch (err) { + console.error('Failed to fetch profile for sidebar:', err); + // Keep session data as fallback + } + }; + + if (session) { + fetchProfile(); + } + }, [session]); const navItems: NavItem[] = [ { @@ -53,21 +88,29 @@ export function SettingsSidebar({ {/* Profile Card */}
-
- {name} +
+ {profileData.avatarUrl ? ( + {profileData.name} + ) : ( + {profileData.name} + )}

- {name} + {profileData.name}

- {title} + {profileData.title}