feat: Add cache-busting to avatar image URLs and implement show more/less functionality for certifications.
This commit is contained in:
@@ -269,6 +269,28 @@ export default function EditProfilePage() {
|
|||||||
// Save field values to API
|
// Save field values to API
|
||||||
await agentsService.saveFieldValues(fieldValues);
|
await agentsService.saveFieldValues(fieldValues);
|
||||||
|
|
||||||
|
// Also update main profile fields (phone, email) to keep them in sync
|
||||||
|
// This ensures that when the view page checks agentProfile.phone first,
|
||||||
|
// it gets the correct (possibly empty) value
|
||||||
|
const profileUpdates: Partial<{ phone: string | null; email: string | null }> = {};
|
||||||
|
|
||||||
|
// Sync phone field - check multiple possible field slugs
|
||||||
|
// The dynamic fields might use different slugs for phone
|
||||||
|
const phoneFieldSlugs = ['phone', 'phone_number', 'cell_number', 'office_number'];
|
||||||
|
for (const slug of phoneFieldSlugs) {
|
||||||
|
const phoneValue = formData[slug];
|
||||||
|
if (phoneValue !== undefined) {
|
||||||
|
// Set to null if empty, otherwise use the value
|
||||||
|
profileUpdates.phone = phoneValue === '' ? null : (phoneValue as string);
|
||||||
|
break; // Use the first found phone field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update if there are changes to sync
|
||||||
|
if (Object.keys(profileUpdates).length > 0) {
|
||||||
|
await agentsService.updateProfile(profileUpdates);
|
||||||
|
}
|
||||||
|
|
||||||
router.push('/agent/dashboard');
|
router.push('/agent/dashboard');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to save:', err);
|
console.error('Failed to save:', err);
|
||||||
|
|||||||
@@ -158,7 +158,14 @@ 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);
|
||||||
setAvatarUrl(presignedUrl);
|
// Add cache-busting parameter to force browser to fetch fresh image
|
||||||
|
// 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
|
// Don't fail the whole page, just use default image
|
||||||
|
|||||||
@@ -73,7 +73,14 @@ function ProfileCard({ profile }: ProfileCardProps) {
|
|||||||
if (profile.avatar && !profile.avatar.startsWith('http') && !profile.avatar.startsWith('/')) {
|
if (profile.avatar && !profile.avatar.startsWith('http') && !profile.avatar.startsWith('/')) {
|
||||||
try {
|
try {
|
||||||
const presignedUrl = await uploadService.getPresignedDownloadUrl(profile.avatar);
|
const presignedUrl = await uploadService.getPresignedDownloadUrl(profile.avatar);
|
||||||
setAvatarUrl(presignedUrl);
|
// Add cache-busting parameter to force browser to fetch fresh image
|
||||||
|
// 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 (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get avatar URL:', error);
|
console.error('Failed to get avatar URL:', error);
|
||||||
}
|
}
|
||||||
@@ -82,7 +89,7 @@ function ProfileCard({ profile }: ProfileCardProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchAvatarUrl();
|
fetchAvatarUrl();
|
||||||
}, [profile.avatar]);
|
}, [profile.avatar, profile]);
|
||||||
|
|
||||||
const getProfileImageUrl = () => {
|
const getProfileImageUrl = () => {
|
||||||
if (avatarUrl) return avatarUrl;
|
if (avatarUrl) return avatarUrl;
|
||||||
|
|||||||
@@ -17,14 +17,17 @@ interface ExperienceSectionProps {
|
|||||||
|
|
||||||
const INITIAL_DISPLAY_COUNT = 6;
|
const INITIAL_DISPLAY_COUNT = 6;
|
||||||
const EXPERTISE_INITIAL_COUNT = 4;
|
const EXPERTISE_INITIAL_COUNT = 4;
|
||||||
|
const CERTIFICATIONS_INITIAL_COUNT = 2;
|
||||||
|
|
||||||
export function ExperienceSection({ experience }: ExperienceSectionProps) {
|
export function ExperienceSection({ experience }: ExperienceSectionProps) {
|
||||||
const [showAllLicensing, setShowAllLicensing] = useState(false);
|
const [showAllLicensing, setShowAllLicensing] = useState(false);
|
||||||
const [showAllExpertise, setShowAllExpertise] = useState(false);
|
const [showAllExpertise, setShowAllExpertise] = useState(false);
|
||||||
|
const [showAllCertifications, setShowAllCertifications] = useState(false);
|
||||||
|
|
||||||
// Calculate how many more items are hidden
|
// Calculate how many more items are hidden
|
||||||
const licensingRemaining = experience.licensingAreas.length - INITIAL_DISPLAY_COUNT;
|
const licensingRemaining = experience.licensingAreas.length - INITIAL_DISPLAY_COUNT;
|
||||||
const expertiseRemaining = experience.expertiseYears.length - EXPERTISE_INITIAL_COUNT;
|
const expertiseRemaining = experience.expertiseYears.length - EXPERTISE_INITIAL_COUNT;
|
||||||
|
const certificationsRemaining = experience.certifications.length - CERTIFICATIONS_INITIAL_COUNT;
|
||||||
|
|
||||||
// Get displayed items
|
// Get displayed items
|
||||||
const displayedLicensing = showAllLicensing
|
const displayedLicensing = showAllLicensing
|
||||||
@@ -33,6 +36,9 @@ export function ExperienceSection({ experience }: ExperienceSectionProps) {
|
|||||||
const displayedExpertise = showAllExpertise
|
const displayedExpertise = showAllExpertise
|
||||||
? experience.expertiseYears
|
? experience.expertiseYears
|
||||||
: experience.expertiseYears.slice(0, EXPERTISE_INITIAL_COUNT);
|
: experience.expertiseYears.slice(0, EXPERTISE_INITIAL_COUNT);
|
||||||
|
const displayedCertifications = showAllCertifications
|
||||||
|
? experience.certifications
|
||||||
|
: experience.certifications.slice(0, CERTIFICATIONS_INITIAL_COUNT);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-[20px] p-4 lg:p-5">
|
<div className="bg-white rounded-[20px] p-4 lg:p-5">
|
||||||
@@ -133,16 +139,34 @@ export function ExperienceSection({ experience }: ExperienceSectionProps) {
|
|||||||
<div>
|
<div>
|
||||||
<p className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D] mb-3">Certifications</p>
|
<p className="font-fractul font-bold text-[14px] leading-[17px] text-[#00293D] mb-3">Certifications</p>
|
||||||
{experience.certifications.length > 0 ? (
|
{experience.certifications.length > 0 ? (
|
||||||
<ul className="space-y-3">
|
<div>
|
||||||
{experience.certifications.map((cert, idx) => (
|
<ul className="space-y-3">
|
||||||
<li key={idx}>
|
{displayedCertifications.map((cert, idx) => (
|
||||||
<span className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D]">{cert.name}</span>
|
<li key={idx}>
|
||||||
{cert.org && (
|
<span className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D]">{cert.name}</span>
|
||||||
<p className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D] opacity-50 ml-3">{cert.org}</p>
|
{cert.org && (
|
||||||
)}
|
<p className="font-serif font-normal text-[14px] leading-[19px] text-[#00293D] opacity-50 ml-3">{cert.org}</p>
|
||||||
</li>
|
)}
|
||||||
))}
|
</li>
|
||||||
</ul>
|
))}
|
||||||
|
</ul>
|
||||||
|
{!showAllCertifications && certificationsRemaining > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAllCertifications(true)}
|
||||||
|
className="mt-3 inline-flex items-center justify-center h-[28px] px-3 rounded-[15px] border border-[#00293d]/10 text-[15px] font-light font-serif text-[#00293d] cursor-pointer hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
+{certificationsRemaining} More
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{showAllCertifications && experience.certifications.length > CERTIFICATIONS_INITIAL_COUNT && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAllCertifications(false)}
|
||||||
|
className="mt-3 inline-flex items-center justify-center h-[28px] px-3 rounded-[15px] border border-[#00293d]/10 text-[15px] font-light font-serif text-[#00293d] cursor-pointer hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
Show Less
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-[14px] font-serif text-[#00293D]/40">No certifications added</span>
|
<span className="text-[14px] font-serif text-[#00293D]/40">No certifications added</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user