diff --git a/src/app/(agent)/agent/network/component/ConnectionCard.tsx b/src/app/(agent)/agent/network/component/ConnectionCard.tsx index 99e85eb..5043b0e 100644 --- a/src/app/(agent)/agent/network/component/ConnectionCard.tsx +++ b/src/app/(agent)/agent/network/component/ConnectionCard.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState } from 'react'; import Image from 'next/image'; import Link from 'next/link'; @@ -26,17 +27,33 @@ export function ConnectionCard({ connectedAt, onMessage, }: ConnectionCardProps) { + const [avatarLoaded, setAvatarLoaded] = useState(false); + const placeholder = '/assets/icons/user-placeholder-icon.svg'; + const isPlaceholder = !avatar || avatar === placeholder; + return (
{/* Avatar */}
- {name} +
+ {(!avatar || !avatarLoaded || isPlaceholder) && ( + {name} + )} + {avatar && !isPlaceholder && ( + {name} setAvatarLoaded(true)} + /> + )} +
{/* Content */} diff --git a/src/app/(agent)/agent/network/component/InvitationCard.tsx b/src/app/(agent)/agent/network/component/InvitationCard.tsx index 349eab4..c7d532a 100644 --- a/src/app/(agent)/agent/network/component/InvitationCard.tsx +++ b/src/app/(agent)/agent/network/component/InvitationCard.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState } from 'react'; import Image from 'next/image'; interface InvitationCardProps { @@ -40,17 +41,33 @@ export function InvitationCard({ onAccept, onIgnore, }: InvitationCardProps) { + const [avatarLoaded, setAvatarLoaded] = useState(false); + const placeholder = '/assets/icons/user-placeholder-icon.svg'; + const isPlaceholder = !avatar || avatar === placeholder; + return (
{/* Avatar */}
- {name} +
+ {(!avatar || !avatarLoaded || isPlaceholder) && ( + {name} + )} + {avatar && !isPlaceholder && ( + {name} setAvatarLoaded(true)} + /> + )} +
{/* Content */} diff --git a/src/app/globals.css b/src/app/globals.css index 404eb48..bc12085 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -111,9 +111,6 @@ body { } /* Smooth scrolling */ -html { - scroll-behavior: smooth; -} /* Selection styling */ ::selection { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5f5c584..fbf8af0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -130,7 +130,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + diff --git a/src/components/layout/CommonHeader.tsx b/src/components/layout/CommonHeader.tsx index 4dc4758..19473d6 100644 --- a/src/components/layout/CommonHeader.tsx +++ b/src/components/layout/CommonHeader.tsx @@ -26,6 +26,7 @@ export function CommonHeader() { const mobileMenuRef = useRef(null); const [profileImage, setProfileImage] = useState(null); const [profileName, setProfileName] = useState(null); + const [avatarLoaded, setAvatarLoaded] = useState(false); const [notificationCount, setNotificationCount] = useState(0); // Close dropdowns when clicking outside @@ -73,6 +74,7 @@ export function CommonHeader() { } if (avatar) { + setAvatarLoaded(false); try { const avatarUrl = await uploadService.getPresignedDownloadUrl(avatar); setProfileImage(avatarUrl); @@ -150,6 +152,7 @@ export function CommonHeader() { alt="RE-QuestN" width={150} height={40} + priority className="h-8 md:h-10 w-auto" /> @@ -198,23 +201,25 @@ export function CommonHeader() { className="flex items-center gap-2 md:gap-3 hover:opacity-80 transition-opacity cursor-pointer" > {/* Avatar */} -
- {userImage ? ( - Profile - ) : ( +
+ {(!userImage || !avatarLoaded) && ( Profile )} + {userImage && ( + Profile setAvatarLoaded(true)} + /> + )}
{/* Greeting Text - hidden on mobile */}
@@ -233,23 +238,25 @@ export function CommonHeader() {
-
- {userImage ? ( - Profile - ) : ( +
+ {(!userImage || !avatarLoaded) && ( Profile )} + {userImage && ( + Profile setAvatarLoaded(true)} + /> + )}

diff --git a/src/components/message/ChatHeader.tsx b/src/components/message/ChatHeader.tsx index 4fba68c..6227e5f 100644 --- a/src/components/message/ChatHeader.tsx +++ b/src/components/message/ChatHeader.tsx @@ -33,6 +33,10 @@ export function ChatHeader({ }: ChatHeaderProps) { const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMuted, setIsMuted] = useState(false); + const [avatarLoaded, setAvatarLoaded] = useState(false); + + const placeholder = '/assets/icons/user-placeholder-icon.svg'; + const isPlaceholderAvatar = !avatar || avatar === placeholder; const menuItems: DropdownMenuItem[] = [ { @@ -118,15 +122,24 @@ export function ChatHeader({

{/* Avatar */}
-
- {name} +
+ {(!avatar || !avatarLoaded || isPlaceholderAvatar) && ( + {name} + )} + {avatar && !isPlaceholderAvatar && ( + {name} setAvatarLoaded(true)} + /> + )}
{isOnline && (
diff --git a/src/components/message/MessagingPage.tsx b/src/components/message/MessagingPage.tsx index 412cd58..9fd47a5 100644 --- a/src/components/message/MessagingPage.tsx +++ b/src/components/message/MessagingPage.tsx @@ -80,6 +80,44 @@ function formatLastSeen(lastSeenAt: string | null | undefined, isOnline: boolean return `Last seen ${diffDays}d ago`; } +// Avatar component with placeholder-until-loaded pattern +function AvatarWithPlaceholder({ src, alt, size }: { src: string; alt: string; size: number }) { + const [loaded, setLoaded] = useState(false); + const placeholder = '/assets/icons/user-placeholder-icon.svg'; + const isPlaceholder = !src || src === placeholder; + + return ( + <> + {(!src || !loaded) && ( + {alt} + )} + {src && !isPlaceholder && ( + {alt} setLoaded(true)} + /> + )} + {src && isPlaceholder && ( + {alt} + )} + + ); +} + // Connected agent with avatar URL interface ConnectedAgent { agentProfileId: string; @@ -589,14 +627,11 @@ export function MessagingPage() { > {/* Avatar */}
-
- +
@@ -659,14 +694,11 @@ export function MessagingPage() { {/* Avatar with online indicator */}
-
- +
{conversation.otherParty?.isOnline && ( diff --git a/src/components/profile/ProfileCard.tsx b/src/components/profile/ProfileCard.tsx index ecebae2..ab07b49 100644 --- a/src/components/profile/ProfileCard.tsx +++ b/src/components/profile/ProfileCard.tsx @@ -50,6 +50,14 @@ export function ProfileCard({ const router = useRouter(); const pathname = usePathname(); const [isUnlinking, setIsUnlinking] = useState(false); + const [showLocationModal, setShowLocationModal] = useState(false); + + // Split location into parts and limit display + const locationParts = location.split(',').map(s => s.trim()).filter(Boolean); + const maxVisibleLocations = 4; + const visibleLocations = locationParts.slice(0, maxVisibleLocations).join(', '); + const hasMoreLocations = locationParts.length > maxVisibleLocations; + const remainingCount = locationParts.length - maxVisibleLocations; // Handle unlink click const handleUnlinkClick = async () => { @@ -113,26 +121,35 @@ export function ProfileCard({ )}

{title}

-
+
Location - {location} + {visibleLocations} + {hasMoreLocations && ( + + )} - + Calendar - + Member Since {memberSince} @@ -260,6 +277,33 @@ export function ProfileCard({ ))}
+ + {/* Location Modal */} + {showLocationModal && ( +
setShowLocationModal(false)}> +
e.stopPropagation()}> +
+

Service Areas

+ +
+
+ {locationParts.map((loc, idx) => ( + + {loc} + + ))} +
+
+
+ )}
); } diff --git a/src/components/settings/ProfileSettingsForm.tsx b/src/components/settings/ProfileSettingsForm.tsx index ee6e869..fddae23 100644 --- a/src/components/settings/ProfileSettingsForm.tsx +++ b/src/components/settings/ProfileSettingsForm.tsx @@ -46,12 +46,18 @@ export function ProfileSettingsForm({ }); 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); + // 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 @@ -382,20 +388,22 @@ export function ProfileSettingsForm({ {/* Profile Photo Section */}
-
- {avatarUrl ? ( - Profile - ) : ( +
+ {(!avatarUrl || !avatarLoaded) && ( Profile + )} + {avatarUrl && ( + Profile setAvatarLoaded(true)} /> )}
diff --git a/src/components/settings/SettingsSidebar.tsx b/src/components/settings/SettingsSidebar.tsx index 93abe5f..4bcb0e8 100644 --- a/src/components/settings/SettingsSidebar.tsx +++ b/src/components/settings/SettingsSidebar.tsx @@ -25,6 +25,8 @@ export function SettingsSidebar({ const pathname = usePathname(); const { data: session } = useSession(); + const [avatarLoaded, setAvatarLoaded] = useState(false); + const [profileData, setProfileData] = useState({ name: session?.user?.name || 'User', title: 'Real Estate Agent', @@ -65,6 +67,7 @@ export function SettingsSidebar({ } setProfileData({ name, title, avatarUrl }); + setAvatarLoaded(false); } catch (err) { console.error('Failed to fetch profile for sidebar:', err); } @@ -133,20 +136,22 @@ export function SettingsSidebar({ {/* Profile Card */}
-
- {profileData.avatarUrl ? ( - {profileData.name} - ) : ( +
+ {(!profileData.avatarUrl || !avatarLoaded) && ( {profileData.name} + )} + {profileData.avatarUrl && ( + {profileData.name} setAvatarLoaded(true)} /> )}
diff --git a/src/utils/profileDataMapper.ts b/src/utils/profileDataMapper.ts index e29f3b4..62f557c 100644 --- a/src/utils/profileDataMapper.ts +++ b/src/utils/profileDataMapper.ts @@ -360,8 +360,9 @@ export function mapFieldValuesToSpecializationFields(fieldValues: FieldValueResp return sectionSlug === 'specialization' || sectionSlug.includes('specialization'); }); - // Create a card for each field + // Create a card for each field, deduplicating by fieldSlug const fields: SpecializationFieldItem[] = []; + const seenSlugs = new Set(); for (const field of specializationFields) { const { fieldSlug, fieldName, value } = field; @@ -369,6 +370,10 @@ export function mapFieldValuesToSpecializationFields(fieldValues: FieldValueResp // Skip if no field info or no value if (!fieldSlug || !fieldName) continue; + // Skip duplicate fieldSlugs (API may return duplicates) + if (seenSlugs.has(fieldSlug)) continue; + seenSlugs.add(fieldSlug); + const values = valueToStringArray(value); if (values.length === 0) continue;