diff --git a/src/components/home/HeroSection.tsx b/src/components/home/HeroSection.tsx index 9fec360..8aed144 100644 --- a/src/components/home/HeroSection.tsx +++ b/src/components/home/HeroSection.tsx @@ -70,6 +70,10 @@ export function HeroSection() { setValidationError(null); // Clear error when user starts typing }; + const handleClearError = () => { + setValidationError(null); + }; + return (
{/* Background Image */} @@ -169,9 +173,18 @@ export function HeroSection() { {/* Validation Error Message */} {validationError && (
-

- {validationError} -

+
+ {validationError} + +
)} diff --git a/src/components/layout/CommonHeader.tsx b/src/components/layout/CommonHeader.tsx index 09e6fbc..ea125f8 100644 --- a/src/components/layout/CommonHeader.tsx +++ b/src/components/layout/CommonHeader.tsx @@ -5,6 +5,7 @@ import Link from 'next/link'; import Image from 'next/image'; import { useSession, signOut } from 'next-auth/react'; import { agentsService } from '@/services/agents.service'; +import { usersService } from '@/services/users.service'; import { uploadService } from '@/services/upload.service'; const navLinks = [ @@ -36,17 +37,27 @@ export function CommonHeader() { }; }, [showProfileMenu]); - // Fetch profile image from backend + // Fetch profile image from backend based on user role useEffect(() => { const fetchProfileImage = async () => { try { - const profile = await agentsService.getMyProfile(); - if (profile.avatar) { + const role = (session?.user as any)?.role; + let avatar: string | null = null; + + if (role === 'AGENT') { + const profile = await agentsService.getMyProfile(); + avatar = profile.avatar; + } else if (role === 'USER') { + const profile = await usersService.getMyProfile(); + avatar = profile.avatar; + } + + if (avatar) { try { - const avatarUrl = await uploadService.getPresignedDownloadUrl(profile.avatar); + const avatarUrl = await uploadService.getPresignedDownloadUrl(avatar); setProfileImage(avatarUrl); } catch { - setProfileImage(profile.avatar); + setProfileImage(avatar); } } } catch (err) { diff --git a/src/components/settings/ProfileSettingsForm.tsx b/src/components/settings/ProfileSettingsForm.tsx index 7b501ef..3f5f2a8 100644 --- a/src/components/settings/ProfileSettingsForm.tsx +++ b/src/components/settings/ProfileSettingsForm.tsx @@ -5,6 +5,7 @@ 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'; interface ProfileSettingsFormProps { initialData?: { @@ -49,29 +50,48 @@ export function ProfileSettingsForm({ const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); - // Fetch profile data on mount + // Fetch profile data on mount based on user role useEffect(() => { const fetchProfile = async () => { try { setIsLoading(true); - const profile = await agentsService.getMyProfile(); + const role = (session?.user as any)?.role; - 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] || '', - }); + if (role === 'AGENT') { + 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); + 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(); + setFormData({ + fullName: `${profile.firstName || ''} ${profile.lastName || ''}`.trim(), + career: '', // Users don't have career/agent type + email: profile.email || session?.user?.email || '', + phone: profile.phone || '', + location: [profile.city, profile.state, profile.country].filter(Boolean).join(', '), + }); + + if (profile.avatar) { + try { + const presignedUrl = await uploadService.getPresignedDownloadUrl(profile.avatar); + setAvatarUrl(presignedUrl); + } catch { + setAvatarUrl(profile.avatar); + } } } } catch (err) { @@ -130,26 +150,50 @@ export function ProfileSettingsForm({ setError(null); setUploadProgress(0); - // Upload the avatar - const uploadedFile = await uploadService.uploadAvatar(file, (progress) => { - setUploadProgress(progress.percentage); - }); + // Show local preview immediately for instant feedback + const localPreviewUrl = URL.createObjectURL(file); + setAvatarUrl(localPreviewUrl); - // Update the profile with the new avatar key - await agentsService.updateProfile({ avatar: uploadedFile.url }); + 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 - const presignedUrl = await uploadService.getPresignedDownloadUrl(uploadedFile.url); - setAvatarUrl(presignedUrl); + try { + const presignedUrl = await uploadService.getPresignedDownloadUrl(uploadedFile.url); + setAvatarUrl(presignedUrl); + // Clean up local preview URL only after successful presigned URL + URL.revokeObjectURL(localPreviewUrl); - // Update the session to reflect the new avatar - await updateSession({ - ...session, - user: { - ...session?.user, - image: presignedUrl, - }, - }); + // Update the session to reflect the new avatar + await updateSession({ + ...session, + user: { + ...session?.user, + image: presignedUrl, + }, + }); + } catch { + // If presigned URL fails, keep the local preview (don't revoke it) + console.warn('Could not get presigned URL, using local preview'); + } setSuccessMessage('Profile picture updated successfully!'); } catch (err) { @@ -170,8 +214,14 @@ export function ProfileSettingsForm({ setIsUploading(true); setError(null); - // Update profile to remove avatar - await agentsService.updateProfile({ avatar: null }); + const role = (session?.user as any)?.role; + + // Update profile to remove avatar based on role + if (role === 'AGENT') { + await agentsService.updateProfile({ avatar: null }); + } else { + await usersService.updateProfile({ avatar: null }); + } setAvatarUrl(null); // Update session @@ -197,19 +247,33 @@ export function ProfileSettingsForm({ setIsSaving(true); setError(null); + 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 - await agentsService.updateProfile({ - firstName, - lastName, - email: formData.email, - phone: formData.phone, - serviceAreas: formData.location ? [formData.location] : [], - }); + // Update profile based on role (email is not editable - tied to auth) + if (role === 'AGENT') { + await agentsService.updateProfile({ + firstName, + lastName, + phone: formData.phone, + serviceAreas: formData.location ? [formData.location] : [], + }); + } else { + // Parse location into city, state, country for users + const locationParts = formData.location.split(',').map(s => s.trim()); + await usersService.updateProfile({ + firstName, + lastName, + phone: formData.phone, + city: locationParts[0] || undefined, + state: locationParts[1] || undefined, + country: locationParts[2] || undefined, + }); + } // Update session with new name await updateSession({ @@ -334,8 +398,8 @@ export function ProfileSettingsForm({ {/* Form Fields */}
- {/* Row 1: Full Name & Career */} -
+ {/* Row 1: Full Name & Career (Career only for agents) */} +
-
- - 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]" - /> -
+ {(session?.user as any)?.role === 'AGENT' && ( +
+ + 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]" + /> +
+ )}
{/* Row 2: Email & Phone */} @@ -369,8 +435,8 @@ export function ProfileSettingsForm({ handleChange('email', 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]" + disabled + className="w-full h-[44px] px-4 border border-[#00293D]/20 rounded-[10px] text-[14px] font-serif text-[#00293D]/60 bg-gray-50 cursor-not-allowed" />
diff --git a/src/components/settings/SettingsSidebar.tsx b/src/components/settings/SettingsSidebar.tsx index a2be49f..1f31963 100644 --- a/src/components/settings/SettingsSidebar.tsx +++ b/src/components/settings/SettingsSidebar.tsx @@ -6,6 +6,7 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { useSession } from 'next-auth/react'; import { agentsService } from '@/services/agents.service'; +import { usersService } from '@/services/users.service'; import { uploadService } from '@/services/upload.service'; interface NavItem { @@ -33,22 +34,38 @@ export function SettingsSidebar({ useEffect(() => { const fetchProfile = async () => { try { - const profile = await agentsService.getMyProfile(); - + const role = (session?.user as any)?.role; let avatarUrl = session?.user?.image || null; - if (profile.avatar) { - try { - avatarUrl = await uploadService.getPresignedDownloadUrl(profile.avatar); - } catch { - avatarUrl = profile.avatar; + let name = session?.user?.name || 'User'; + let title = 'User'; + + if (role === 'AGENT') { + const profile = await agentsService.getMyProfile(); + name = `${profile.firstName || ''} ${profile.lastName || ''}`.trim() || name; + title = profile.agentType?.name || 'Real Estate Agent'; + + if (profile.avatar) { + try { + avatarUrl = await uploadService.getPresignedDownloadUrl(profile.avatar); + } catch { + avatarUrl = profile.avatar; + } + } + } else if (role === 'USER') { + const profile = await usersService.getMyProfile(); + name = `${profile.firstName || ''} ${profile.lastName || ''}`.trim() || name; + title = 'Member'; + + 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, - }); + setProfileData({ name, title, avatarUrl }); } catch (err) { console.error('Failed to fetch profile for sidebar:', err); // Keep session data as fallback diff --git a/src/services/upload.service.ts b/src/services/upload.service.ts index cd02af2..a809f8a 100644 --- a/src/services/upload.service.ts +++ b/src/services/upload.service.ts @@ -148,7 +148,8 @@ class UploadService { } /** - * Upload avatar image (uses agent profile ID as filename for auto-replacement) + * Upload avatar image for AGENTS (uses agent profile ID as filename for auto-replacement) + * S3 path: {env}/agents/{agentId}/avatar.{ext} * Returns the S3 key (not full URL) for storage in database */ async uploadAvatar( @@ -179,6 +180,39 @@ class UploadService { }; } + /** + * Upload avatar image for regular USERS (uses user ID as folder) + * S3 path: {env}/users/{userId}/avatar.{ext} + * Returns the S3 key (not full URL) for storage in database + */ + async uploadUserAvatar( + file: File, + onProgress?: (progress: UploadProgress) => void + ): Promise { + // Step 1: Get presigned URL from backend (uses user ID as folder) + const response = await api.post<{ data: PresignedUrlResponse }>( + `${this.basePath}/user-avatar-presigned-url`, + { + filename: file.name, + contentType: file.type, + } + ); + const { uploadUrl, key } = response.data.data; + + // Step 2: Upload directly to S3 + await this.uploadToS3(uploadUrl, file, onProgress); + + // Step 3: Return uploaded file info with KEY (not full URL) for database storage + return { + id: key, + name: file.name, + size: file.size, + type: file.type, + url: key, // Store the S3 key, not the full URL + uploadedAt: new Date().toISOString(), + }; + } + /** * Get a presigned download URL for an S3 key */ diff --git a/src/services/users.service.ts b/src/services/users.service.ts new file mode 100644 index 0000000..21d4f74 --- /dev/null +++ b/src/services/users.service.ts @@ -0,0 +1,50 @@ +import api from './api'; + +export interface UserProfile { + id: string; + userId: string; + email: string; + firstName: string | null; + lastName: string | null; + phone: string | null; + avatar: string | null; + city: string | null; + state: string | null; + country: string | null; + createdAt: string; + updatedAt: string; +} + +export interface UpdateUserProfileData { + firstName?: string; + lastName?: string; + phone?: string; + avatar?: string | null; + city?: string; + state?: string; + country?: string; +} + +interface ApiResponse { + success: boolean; + data: T; + message?: string; +} + +export const usersService = { + /** + * Get current user's profile + */ + async getMyProfile(): Promise { + const response = await api.get>('/users/profile/me'); + return response.data.data; + }, + + /** + * Update current user's profile + */ + async updateProfile(data: UpdateUserProfileData): Promise { + const response = await api.patch>('/users/profile/me', data); + return response.data.data; + }, +};