'use client'; 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'; import { usersService } from '@/services/users.service'; import { authService, AuthService } from '@/services/auth.service'; interface ProfileSettingsFormProps { initialData?: { firstName: string; lastName: string; career: string; email: string; phone: string; avatar?: string; }; onSave?: (data: { firstName: string; lastName: string; career: string; email: string; phone: string; }) => void; descriptionText?: string; } export function ProfileSettingsForm({ initialData, onSave, descriptionText = 'This information will be displayed on your public profile page.', }: ProfileSettingsFormProps) { const { data: session, update: updateSession } = useSession(); const fileInputRef = useRef(null); const [formData, setFormData] = useState({ firstName: initialData?.firstName || '', lastName: initialData?.lastName || '', career: initialData?.career || '', email: initialData?.email || '', phone: initialData?.phone || '', }); const [avatarUrl, setAvatarUrl] = useState(null); const [avatarLoaded, setAvatarLoaded] = useState(false); 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); const [isEmailModalOpen, setIsEmailModalOpen] = useState(false); const [emailModalData, setEmailModalData] = useState({ newEmail: '', password: '' }); const [emailModalError, setEmailModalError] = useState(null); const [emailModalSubmitting, setEmailModalSubmitting] = useState(false); const handleRequestEmailChange = async () => { setEmailModalError(null); if (!emailModalData.newEmail.trim() || !emailModalData.password) { setEmailModalError('Please enter a new email and your current password'); return; } setEmailModalSubmitting(true); try { const res = await authService.requestEmailChange({ newEmail: emailModalData.newEmail.trim(), password: emailModalData.password, }); setSuccessMessage(res.data?.message || 'Verification email sent. Please check your new inbox.'); setIsEmailModalOpen(false); setEmailModalData({ newEmail: '', password: '' }); } catch (err) { setEmailModalError(AuthService.getErrorMessage(err)); } finally { setEmailModalSubmitting(false); } }; // Reset avatar loaded state when URL changes (e.g., after upload) useEffect(() => { setAvatarLoaded(false); }, [avatarUrl]); // Store original data for cancel/reset functionality const [originalData, setOriginalData] = useState(null); // Track if initial profile fetch is done to avoid re-showing loading spinner const hasFetchedRef = useRef(false); const fetchProfile = async () => { try { // Only show loading spinner on initial fetch, not on session-triggered re-fetches if (!hasFetchedRef.current) { setIsLoading(true); } const role = (session?.user as any)?.role; if (role === 'AGENT') { const profile = await agentsService.getMyProfile(); const profileData = { firstName: profile.firstName || '', lastName: profile.lastName || '', career: profile.headline || profile.agentType?.name || 'Real Estate Agent', email: profile.email || session?.user?.email || '', phone: profile.phone || '', }; setFormData(profileData); setOriginalData(profileData); // If backend email diverges from session (email change verified in another tab), // refresh the next-auth JWT so header/other places also pick up the new address. if (profile.email && session?.user?.email && profile.email !== session.user.email) { try { await updateSession({ user: { email: profile.email } }); } catch { // updateSession is best-effort } } if (profile.avatar) { try { const presignedUrl = await uploadService.getPresignedDownloadUrl(profile.avatar); setAvatarUrl(presignedUrl); } catch { setAvatarUrl(profile.avatar); } } } else if (role === 'USER') { const profile = await usersService.getMyProfile(); const profileData = { firstName: profile.firstName || '', lastName: profile.lastName || '', career: '', email: profile.email || session?.user?.email || '', phone: profile.phone || '', }; setFormData(profileData); setOriginalData(profileData); if (profile.email && session?.user?.email && profile.email !== session.user.email) { try { await updateSession({ user: { email: profile.email } }); } catch { // best-effort } } if (profile.avatar) { try { const presignedUrl = await uploadService.getPresignedDownloadUrl(profile.avatar); setAvatarUrl(presignedUrl); } catch { setAvatarUrl(profile.avatar); } } } } catch (err) { console.error('Failed to fetch profile:', err); if (session?.user) { const nameParts = (session.user?.name || '').split(' '); setFormData(prev => ({ ...prev, firstName: nameParts[0] || prev.firstName, lastName: nameParts.slice(1).join(' ') || prev.lastName, email: session.user?.email || prev.email, })); if (session.user.image) { setAvatarUrl(session.user.image); } } } finally { setIsLoading(false); hasFetchedRef.current = true; } }; // Initial fetch on mount useEffect(() => { if (session) { if (hasFetchedRef.current) return; fetchProfile(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [session]); // Re-fetch when the tab becomes visible again (e.g. user verified email // change in another tab and returned). Ensures email/phone/avatar stay in // sync with the source of truth. useEffect(() => { const onVisible = () => { if (document.visibilityState === 'visible' && session && hasFetchedRef.current) { fetchProfile(); } }; document.addEventListener('visibilitychange', onVisible); return () => document.removeEventListener('visibilitychange', onVisible); // eslint-disable-next-line react-hooks/exhaustive-deps }, [session]); const handleChange = (field: string, value: string) => { setFormData((prev) => ({ ...prev, [field]: value })); setError(null); setSuccessMessage(null); }; 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); // Show local preview immediately for instant feedback const localPreviewUrl = URL.createObjectURL(file); setAvatarUrl(localPreviewUrl); const role = (session?.user as any)?.role; // Upload the avatar based on role let uploadedFile; if (role === 'AGENT') { uploadedFile = await uploadService.uploadAvatar(file, (progress) => { setUploadProgress(progress.percentage); }); // Update the agent profile with the new avatar key await agentsService.updateProfile({ avatar: uploadedFile.url }); } else { uploadedFile = await uploadService.uploadUserAvatar(file, (progress) => { setUploadProgress(progress.percentage); }); // Update the user profile with the new avatar key await usersService.updateProfile({ avatar: uploadedFile.url }); } // Small delay to ensure S3 has processed the upload await new Promise(resolve => setTimeout(resolve, 500)); // Get the presigned URL for display try { const presignedUrl = await uploadService.getPresignedDownloadUrl(uploadedFile.url); setAvatarUrl(presignedUrl); // Clean up local preview URL only after successful presigned URL URL.revokeObjectURL(localPreviewUrl); // Dispatch event to notify header to refresh profile data window.dispatchEvent(new Event('profile-updated')); } catch { // If presigned URL fails, keep the local preview (don't revoke it) console.warn('Could not get presigned URL, using local preview'); // Still dispatch event to refresh header window.dispatchEvent(new Event('profile-updated')); } 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); const role = (session?.user as any)?.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') { await agentsService.updateProfile({ avatar: null }); } else { await usersService.updateProfile({ avatar: null }); } setAvatarUrl(null); // Dispatch event to notify header to refresh profile data window.dispatchEvent(new Event('profile-updated')); 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); const role = (session?.user as any)?.role; // Update profile based on role (email is not editable - tied to auth) if (role === 'AGENT') { await agentsService.updateProfile({ firstName: formData.firstName, lastName: formData.lastName, phone: formData.phone, headline: formData.career, }); } else { await usersService.updateProfile({ firstName: formData.firstName, lastName: formData.lastName, phone: formData.phone, }); } // Dispatch event to notify header and other components to refresh profile data // (Don't call updateSession as it can trigger a full page reload in next-auth v5) window.dispatchEvent(new Event('profile-updated')); if (onSave) { onSave(formData); } // Update original data so Cancel will reset to the newly saved values setOriginalData(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 hasChanges = originalData ? JSON.stringify(formData) !== JSON.stringify(originalData) : false; const handleCancel = () => { if (!hasChanges) return; // Reset to last saved data if (originalData) { setFormData(originalData); } setError(null); setSuccessMessage(null); }; if (isLoading) { return (
); } return (
{/* Header */}

Public Profile

{descriptionText}

{/* Error/Success Messages */} {error && (
{error}
)} {successMessage && (
{successMessage}
)} {/* Profile Photo Section */}
{(!avatarUrl || !avatarLoaded) && (
)} {avatarUrl && ( { if (el?.complete && el.naturalWidth > 0) setAvatarLoaded(true); }} src={avatarUrl} alt="Profile" className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`} onLoad={() => setAvatarLoaded(true)} /> )}

Public Picture

Supported formats: JPEG, PNG, GIF Max file size: 2MB

{/* Form Fields */}
{/* Row 1: First Name & Last Name */}
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]" />
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]" />
{/* Career (only for agents, read-only - set during onboarding) */} {(session?.user as any)?.role === 'AGENT' && (
)} {/* Row 2: Email & Phone */}
handleChange('phone', 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]" />
{/* Action Buttons */}
{/* Change Email Modal */} {isEmailModalOpen && (
!emailModalSubmitting && setIsEmailModalOpen(false)} />

Change Email Address

A verification link will be sent to your new email. Your email changes only after you click the link.

setEmailModalData({ ...emailModalData, newEmail: e.target.value })} placeholder="new@example.com" autoComplete="off" 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]" />
setEmailModalData({ ...emailModalData, password: e.target.value })} placeholder="Enter your current password" autoComplete="current-password" 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]" />
{emailModalError && (

{emailModalError}

)}
)}
); }