feat: Replace hardcoded default profile image with a dynamic placeholder and remove cache-busting from presigned URLs.
This commit is contained in:
@@ -86,8 +86,6 @@ export default function AgentProfileView() {
|
|||||||
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus | null>(null);
|
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus | null>(null);
|
||||||
const [connectionRequestId, setConnectionRequestId] = useState<string | null>(null);
|
const [connectionRequestId, setConnectionRequestId] = useState<string | null>(null);
|
||||||
|
|
||||||
const defaultImage = '/assets/demo_images/8f017153a7b4a239a4f0691234ef97dd55092282.jpg';
|
|
||||||
|
|
||||||
// Helper to check if avatar is an S3 key (not a full URL or local path)
|
// Helper to check if avatar is an S3 key (not a full URL or local path)
|
||||||
const isS3Key = (avatar: string | null | undefined): boolean => {
|
const isS3Key = (avatar: string | null | undefined): boolean => {
|
||||||
if (!avatar) return false;
|
if (!avatar) return false;
|
||||||
@@ -149,20 +147,11 @@ export default function AgentProfileView() {
|
|||||||
if (profile.avatar && isS3Key(profile.avatar)) {
|
if (profile.avatar && isS3Key(profile.avatar)) {
|
||||||
try {
|
try {
|
||||||
const presignedUrl = await uploadService.getPresignedDownloadUrl(profile.avatar);
|
const presignedUrl = await uploadService.getPresignedDownloadUrl(profile.avatar);
|
||||||
// Add cache-busting parameter to force browser to fetch fresh image
|
setAvatarUrl(presignedUrl);
|
||||||
// This prevents showing old cached image when avatar is updated
|
|
||||||
const profileWithTimestamp = profile as unknown as { updatedAt?: string };
|
|
||||||
const cacheBuster = profileWithTimestamp.updatedAt
|
|
||||||
? new Date(profileWithTimestamp.updatedAt).getTime()
|
|
||||||
: Date.now();
|
|
||||||
const urlWithCacheBuster = `${presignedUrl}&_t=${cacheBuster}`;
|
|
||||||
setAvatarUrl(urlWithCacheBuster);
|
|
||||||
} catch (avatarErr) {
|
} catch (avatarErr) {
|
||||||
console.error('Failed to get avatar URL:', avatarErr);
|
console.error('Failed to get avatar URL:', avatarErr);
|
||||||
// Don't fail the whole page, just use default image
|
|
||||||
}
|
}
|
||||||
} else if (profile.avatar) {
|
} else if (profile.avatar) {
|
||||||
// It's already a full URL or local path
|
|
||||||
setAvatarUrl(profile.avatar);
|
setAvatarUrl(profile.avatar);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -213,9 +202,9 @@ export default function AgentProfileView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getProfileImageUrl = () => {
|
const getProfileImageUrl = () => {
|
||||||
// If image failed to load, return default
|
// If image failed to load, return null
|
||||||
if (imageError) {
|
if (imageError) {
|
||||||
return defaultImage;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a presigned URL from S3, use it
|
// If we have a presigned URL from S3, use it
|
||||||
@@ -225,7 +214,7 @@ export default function AgentProfileView() {
|
|||||||
|
|
||||||
// Check for null, undefined, or empty string
|
// Check for null, undefined, or empty string
|
||||||
if (!agentProfile?.avatar || agentProfile.avatar.trim() === '') {
|
if (!agentProfile?.avatar || agentProfile.avatar.trim() === '') {
|
||||||
return defaultImage;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For relative paths (local assets), return as-is
|
// For relative paths (local assets), return as-is
|
||||||
@@ -233,8 +222,8 @@ export default function AgentProfileView() {
|
|||||||
return agentProfile.avatar;
|
return agentProfile.avatar;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default fallback
|
// No image available
|
||||||
return defaultImage;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format member since date
|
// Format member since date
|
||||||
@@ -293,18 +282,26 @@ export default function AgentProfileView() {
|
|||||||
<div className="w-full lg:w-[280px] flex-shrink-0 space-y-4 flex flex-col items-center lg:items-start">
|
<div className="w-full lg:w-[280px] flex-shrink-0 space-y-4 flex flex-col items-center lg:items-start">
|
||||||
{/* Profile Image */}
|
{/* Profile Image */}
|
||||||
<div className="relative w-[200px] lg:w-[260px]">
|
<div className="relative w-[200px] lg:w-[260px]">
|
||||||
<div className="w-[200px] h-[200px] lg:w-[260px] lg:h-[260px] rounded-[15px] overflow-hidden">
|
<div className="w-[200px] h-[200px] lg:w-[260px] lg:h-[260px] rounded-[15px] overflow-hidden bg-[#e8e8e8]">
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{getProfileImageUrl() ? (
|
||||||
|
/* eslint-disable-next-line @next/next/no-img-element */
|
||||||
<img
|
<img
|
||||||
src={getProfileImageUrl()}
|
src={getProfileImageUrl()!}
|
||||||
alt="Profile"
|
alt="Profile"
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
onError={(e) => {
|
onError={() => setImageError(true)}
|
||||||
const target = e.target as HTMLImageElement;
|
|
||||||
target.src = defaultImage;
|
|
||||||
setImageError(true);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/user-placeholder-icon.svg"
|
||||||
|
alt="Profile"
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
|
className="opacity-40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Gradient Overlay */}
|
{/* Gradient Overlay */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-black/20 to-transparent pointer-events-none" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-black/20 to-transparent pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,12 +64,10 @@ interface ProfileCardProps {
|
|||||||
function ProfileCard({ profile, resolvedAvatarUrl }: ProfileCardProps) {
|
function ProfileCard({ profile, resolvedAvatarUrl }: ProfileCardProps) {
|
||||||
const [showFullBio, setShowFullBio] = useState(false);
|
const [showFullBio, setShowFullBio] = useState(false);
|
||||||
const [showAllTags, setShowAllTags] = useState(false);
|
const [showAllTags, setShowAllTags] = useState(false);
|
||||||
const defaultImage = '/assets/demo_images/8f017153a7b4a239a4f0691234ef97dd55092282.jpg';
|
const getProfileImageUrl = (): string | null => {
|
||||||
|
|
||||||
const getProfileImageUrl = () => {
|
|
||||||
if (resolvedAvatarUrl) return resolvedAvatarUrl;
|
if (resolvedAvatarUrl) return resolvedAvatarUrl;
|
||||||
if (profile.avatar?.startsWith('/')) return profile.avatar;
|
if (profile.avatar?.startsWith('/')) return profile.avatar;
|
||||||
return defaultImage;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format member since date
|
// Format member since date
|
||||||
@@ -237,17 +235,29 @@ function ProfileCard({ profile, resolvedAvatarUrl }: ProfileCardProps) {
|
|||||||
<div className="bg-white rounded-[20px] p-5 flex gap-5 shadow-[0px_10px_20px_rgba(217,217,217,0.5)]">
|
<div className="bg-white rounded-[20px] p-5 flex gap-5 shadow-[0px_10px_20px_rgba(217,217,217,0.5)]">
|
||||||
{/* Profile Image & View Profile Button */}
|
{/* Profile Image & View Profile Button */}
|
||||||
<div className="flex-shrink-0 flex flex-col items-center">
|
<div className="flex-shrink-0 flex flex-col items-center">
|
||||||
<div className="relative w-[200px] h-[200px] rounded-[15px] overflow-hidden">
|
<div className="relative w-[200px] h-[200px] rounded-[15px] overflow-hidden bg-[#e8e8e8]">
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{getProfileImageUrl() ? (
|
||||||
|
/* eslint-disable-next-line @next/next/no-img-element */
|
||||||
<img
|
<img
|
||||||
src={getProfileImageUrl()}
|
src={getProfileImageUrl()!}
|
||||||
alt={`${profile.firstName} ${profile.lastName}`}
|
alt={`${profile.firstName} ${profile.lastName}`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
const target = e.target as HTMLImageElement;
|
const target = e.target as HTMLImageElement;
|
||||||
target.src = defaultImage;
|
target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/user-placeholder-icon.svg"
|
||||||
|
alt={`${profile.firstName} ${profile.lastName}`}
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
className="opacity-40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Link href={`/user/profile/${profile.id}`} className="mt-4">
|
<Link href={`/user/profile/${profile.id}`} className="mt-4">
|
||||||
<button className="w-[113px] py-2.5 bg-[#e58625] hover:bg-[#d47720] text-[#00293d] font-fractul font-bold text-[14px] rounded-[15px] transition-colors">
|
<button className="w-[113px] py-2.5 bg-[#e58625] hover:bg-[#d47720] text-[#00293d] font-fractul font-bold text-[14px] rounded-[15px] transition-colors">
|
||||||
@@ -469,19 +479,15 @@ function ProfilesPageContent() {
|
|||||||
|
|
||||||
const response = await agentsService.searchAgents(params);
|
const response = await agentsService.searchAgents(params);
|
||||||
|
|
||||||
// Resolve all avatar presigned URLs in parallel before rendering
|
// Resolve all avatar URLs via presigned-download-url API
|
||||||
const urlMap: Record<string, string> = {};
|
const urlMap: Record<string, string> = {};
|
||||||
const avatarPromises = response.data.map(async (agent) => {
|
const avatarPromises = response.data.map(async (agent) => {
|
||||||
if (agent.avatar && !agent.avatar.startsWith('http') && !agent.avatar.startsWith('/')) {
|
if (agent.avatar && !agent.avatar.startsWith('http') && !agent.avatar.startsWith('/')) {
|
||||||
try {
|
try {
|
||||||
const presignedUrl = await uploadService.getPresignedDownloadUrl(agent.avatar);
|
const presignedUrl = await uploadService.getPresignedDownloadUrl(agent.avatar);
|
||||||
const agentWithTimestamp = agent as unknown as { updatedAt?: string };
|
urlMap[agent.id] = presignedUrl;
|
||||||
const cacheBuster = agentWithTimestamp.updatedAt
|
|
||||||
? new Date(agentWithTimestamp.updatedAt).getTime()
|
|
||||||
: Date.now();
|
|
||||||
urlMap[agent.id] = `${presignedUrl}&_t=${cacheBuster}`;
|
|
||||||
} catch {
|
} catch {
|
||||||
// Skip failed URLs, card will show default image
|
// Skip failed URLs, card will show placeholder
|
||||||
}
|
}
|
||||||
} else if (agent.avatar) {
|
} else if (agent.avatar) {
|
||||||
urlMap[agent.id] = agent.avatar;
|
urlMap[agent.id] = agent.avatar;
|
||||||
|
|||||||
Reference in New Issue
Block a user