feat: Enhance agent profiles with dynamic fields, granular location data, and add a verified badge asset.

This commit is contained in:
pradeepkumar
2026-02-01 22:45:56 +05:30
parent ad2d2dfbf4
commit bb7706d152
5 changed files with 204 additions and 97 deletions

View File

@@ -0,0 +1,3 @@
<svg width="14" height="15" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.3732 0.596262C10.2351 0.372368 10.0317 0.197732 9.79127 0.096662C9.55083 -0.00440788 9.28532 -0.0268823 9.0317 0.0323677L7.2317 0.451986C7.04723 0.495017 6.85555 0.495017 6.67108 0.451986L4.87107 0.0323677C4.61746 -0.0268823 4.35195 -0.00440788 4.1115 0.096662C3.87106 0.197732 3.66765 0.372368 3.52958 0.596262L2.54849 2.18533C2.44838 2.34789 2.31323 2.48505 2.15305 2.58767L0.58731 3.58338C0.367083 3.72338 0.195262 3.92948 0.0956966 4.17309C-0.00386835 4.4167 -0.0262384 4.68572 0.0316916 4.94282L0.445152 6.77167C0.487397 6.95856 0.487397 7.15273 0.445152 7.33962L0.0316916 9.16745C-0.0264635 9.4247 -0.00420607 9.69394 0.0953696 9.93776C0.194945 10.1816 0.366892 10.3879 0.58731 10.5279L2.15305 11.5236C2.31323 11.6252 2.44838 11.7624 2.54949 11.9249L3.53058 13.514C3.8129 13.9722 4.3515 14.1988 4.87107 14.0779L6.67108 13.6583C6.85555 13.6153 7.04723 13.6153 7.2317 13.6583L9.0327 14.0779C9.28617 14.1369 9.55147 14.1143 9.7917 14.0133C10.0319 13.9122 10.2352 13.7377 10.3732 13.514L11.3543 11.9249C11.4544 11.7624 11.5895 11.6252 11.7497 11.5236L13.3165 10.5279C13.5369 10.3876 13.7088 10.1811 13.8082 9.93713C13.9076 9.69312 13.9296 9.42373 13.8711 9.16644L13.4586 7.33962C13.4162 7.15241 13.4162 6.95787 13.4586 6.77065L13.8721 4.94282C13.9303 4.68568 13.9082 4.41651 13.8088 4.17271C13.7094 3.9289 13.5377 3.72256 13.3175 3.58236L11.7507 2.58666C11.5908 2.48487 11.4556 2.34766 11.3553 2.18533L10.3732 0.596262ZM9.86963 4.7894C9.93155 4.67384 9.9469 4.53842 9.91243 4.41161C9.87797 4.28481 9.79638 4.17652 9.68485 4.10952C9.57331 4.04252 9.44052 4.02204 9.31441 4.05239C9.18829 4.08274 9.0787 4.16155 9.00868 4.27224L6.39076 8.76917L4.81001 7.23294C4.76311 7.18407 4.707 7.14529 4.64501 7.11891C4.58303 7.09252 4.51645 7.07908 4.44923 7.07937C4.38202 7.07967 4.31555 7.09371 4.25379 7.12064C4.19204 7.14757 4.13626 7.18685 4.08979 7.23613C4.04331 7.28541 4.0071 7.34369 3.98329 7.40748C3.95949 7.47128 3.94859 7.53929 3.95125 7.60745C3.9539 7.67561 3.97006 7.74253 3.99875 7.80422C4.02744 7.86591 4.06807 7.92111 4.11824 7.96651L6.1545 9.94675C6.209 9.99963 6.27457 10.0393 6.34631 10.0629C6.41805 10.0865 6.49411 10.0933 6.56881 10.0828C6.6435 10.0724 6.71491 10.0449 6.77769 10.0025C6.84047 9.96015 6.893 9.90391 6.93137 9.83803L9.86963 4.7894Z" fill="#1DA1F2"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -226,7 +226,7 @@ export default function AgentProfileView() {
lastName={agentProfile.lastName} lastName={agentProfile.lastName}
isVerified={agentProfile.isVerified} isVerified={agentProfile.isVerified}
title={agentProfile.agentType?.name || 'Real Estate Agent'} title={agentProfile.agentType?.name || 'Real Estate Agent'}
location={agentProfile.serviceAreas?.[0] || 'United States'} location={agentProfile.serviceAreas?.[0] || '-'}
memberSince={formatMemberSince((agentProfile as unknown as { createdAt?: string }).createdAt)} memberSince={formatMemberSince((agentProfile as unknown as { createdAt?: string }).createdAt)}
bio={agentProfile.bio || ''} bio={agentProfile.bio || ''}
expertise={agentProfile.specializations || []} expertise={agentProfile.specializations || []}

View File

@@ -27,7 +27,7 @@ function FilterSection({ title, options, selectedOptions, onToggle, showMore }:
<h3 className="font-fractul font-semibold text-[14px] text-[#00293d]">{title}</h3> <h3 className="font-fractul font-semibold text-[14px] text-[#00293d]">{title}</h3>
<button className="text-[#00293d]"> <button className="text-[#00293d]">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"> <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2 4L6 8L10 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/> <path d="M2 4L6 8L10 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</button> </button>
</div> </div>
@@ -62,12 +62,10 @@ interface ProfileCardProps {
function ProfileCard({ profile }: ProfileCardProps) { function ProfileCard({ profile }: ProfileCardProps) {
const [showFullBio, setShowFullBio] = useState(false); const [showFullBio, setShowFullBio] = useState(false);
const [showAllTags, setShowAllTags] = useState(false);
const [avatarUrl, setAvatarUrl] = useState<string | null>(null); const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
const defaultImage = '/assets/demo_images/8f017153a7b4a239a4f0691234ef97dd55092282.jpg'; const defaultImage = '/assets/demo_images/8f017153a7b4a239a4f0691234ef97dd55092282.jpg';
const truncatedBio = profile.bio && profile.bio.length > 150
? profile.bio.slice(0, 150) + '...'
: profile.bio || '';
// Fetch presigned URL for avatar if it's an S3 key // Fetch presigned URL for avatar if it's an S3 key
useEffect(() => { useEffect(() => {
@@ -103,11 +101,102 @@ function ProfileCard({ profile }: ProfileCardProps) {
} }
}; };
// Get location string - prefer city/state/country over serviceAreas
const getLocation = () => {
if (profile.city && profile.state) {
return `${profile.city}, ${profile.state}`;
}
if (profile.city && profile.country) {
return `${profile.city}, ${profile.country}`;
}
if (profile.state && profile.country) {
return `${profile.state}, ${profile.country}`;
}
if (profile.city) {
return profile.city;
}
if (profile.state) {
return profile.state;
}
if (profile.country) {
return profile.country;
}
if (profile.serviceAreas && profile.serviceAreas.length > 0) {
return profile.serviceAreas[0];
}
return null;
};
// Extract expertise tags from fieldValues
const getExpertiseTags = (): string[] => {
const tags: string[] = [];
// Add from fieldValues (dynamic profile fields like client_specialization, property_type, loan_type)
if (profile.fieldValues && profile.fieldValues.length > 0) {
profile.fieldValues.forEach((fv) => {
const value = fv.jsonValue;
if (Array.isArray(value)) {
// For multi-select fields, add all selected values
value.forEach((v) => {
if (typeof v === 'string' && v.trim()) {
// Convert snake_case to Title Case for display
const displayValue = v
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
tags.push(displayValue);
}
});
} else if (typeof value === 'string' && value.trim()) {
tags.push(value);
}
});
}
// Add from specializations array (legacy field)
if (profile.specializations && profile.specializations.length > 0) {
profile.specializations.forEach((spec) => {
if (!tags.includes(spec)) {
tags.push(spec);
}
});
}
// Add from languages array
if (profile.languages && profile.languages.length > 0) {
profile.languages.forEach((lang) => {
if (!tags.includes(lang)) {
tags.push(lang);
}
});
}
return tags;
};
// Get description from fieldValues (description field) or fallback to profile.bio
const getDescription = (): string | null => {
if (profile.fieldValues && profile.fieldValues.length > 0) {
const descField = profile.fieldValues.find(fv => fv.field.slug === 'description');
if (descField && descField.textValue) {
return descField.textValue;
}
}
return profile.bio;
};
const location = getLocation();
const expertiseTags = getExpertiseTags();
const description = getDescription();
const truncatedDescription = description && description.length > 200
? description.slice(0, 200) + '...'
: description || '';
return ( return (
<div className="bg-white rounded-[15px] p-4 flex gap-4 shadow-[0px_4px_20px_rgba(0,0,0,0.08)]"> <div className="bg-white rounded-[20px] p-5 flex gap-5 shadow-[0px_10px_20px_rgba(217,217,217,0.5)]">
{/* Profile Image */} {/* Profile Image & View Profile Button */}
<div className="flex-shrink-0"> <div className="flex-shrink-0 flex flex-col items-center">
<div className="relative w-[120px] h-[140px] rounded-[10px] overflow-hidden"> <div className="relative w-[200px] h-[200px] rounded-[15px] overflow-hidden">
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
src={getProfileImageUrl()} src={getProfileImageUrl()}
@@ -119,8 +208,8 @@ function ProfileCard({ profile }: ProfileCardProps) {
}} }}
/> />
</div> </div>
<Link href={`/user/profile/${profile.id}`}> <Link href={`/user/profile/${profile.id}`} className="mt-4">
<button className="w-full mt-3 px-6 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">
View Profile View Profile
</button> </button>
</Link> </Link>
@@ -128,107 +217,113 @@ function ProfileCard({ profile }: ProfileCardProps) {
{/* Profile Info */} {/* Profile Info */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{/* Header */} {/* Header Row - Name, Verified, Match Badge */}
<div className="flex items-start justify-between mb-1"> <div className="flex items-start justify-between">
<div> <div>
{/* Name and Verified Badge */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="font-fractul font-bold text-[16px] text-[#00293d]"> <h3 className="font-fractul text-[18px] text-[#00293d]">
{profile.firstName} {profile.lastName} <span className="font-bold">{profile.firstName}</span>{' '}
<span className="font-normal">{profile.lastName}</span>
</h3> </h3>
{profile.isVerified && ( {profile.isVerified && (
<span className="flex items-center gap-1 text-[#e58625] font-serif text-[12px]"> <>
<Image <Image
src="/assets/icons/verified-badge-blue.svg" src="/assets/icons/verified-expert-badge.svg"
alt="Verified" alt="Verified"
width={14} width={14}
height={14} height={15}
/> />
(Verified Expert) <span className="font-serif font-medium text-[14px] text-[#638559]">
</span> (Verified Expert)
</span>
</>
)} )}
</div> </div>
<p className="font-serif text-[13px] text-[#00293d] mt-0.5"> {/* Agent Type */}
<p className="font-serif text-[14px] text-[#00293d] mt-1">
{profile.agentType?.name || 'Real Estate Professional'} {profile.agentType?.name || 'Real Estate Professional'}
</p> </p>
</div> </div>
{profile.rating && (
<div className="bg-[#e58625] text-white px-3 py-1 rounded-full font-fractul font-bold text-[12px]"> {/* Match Badge */}
{profile.rating.toFixed(1)} Rating <div className="bg-[#7d917d] text-white px-4 py-1.5 rounded-full font-serif text-[14px] whitespace-nowrap">
</div> 95% Match
)} </div>
</div> </div>
{/* Location & Member Since */} {/* Location & Member Since */}
<div className="flex items-center gap-4 mt-2"> <div className="flex items-center gap-4 mt-3">
{profile.serviceAreas && profile.serviceAreas.length > 0 && ( {location && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-2">
<Image <Image
src="/assets/icons/location-pin-orange.svg" src="/assets/icons/location-icon.svg"
alt="Location" alt="Location"
width={12} width={14}
height={15} height={16}
/> />
<span className="font-serif text-[12px] text-[#00293d]">{profile.serviceAreas[0]}</span> <span className="font-serif font-bold text-[14px] text-[#00293d]">
{location}
</span>
</div> </div>
)} )}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-2">
<Image <Image
src="/assets/icons/calendar-icon.svg" src="/assets/icons/calendar-icon.svg"
alt="Member Since" alt="Member Since"
width={14} width={18}
height={14} height={18}
/> />
<span className="font-serif text-[12px] text-[#00293d]"> <span className="font-serif font-medium text-[13px] text-[#00293d]">
Member Since {formatMemberSince(profile.createdAt)} Member Since {formatMemberSince(profile.createdAt)}
</span> </span>
</div> </div>
</div> </div>
{/* Bio */} {/* Description */}
{profile.bio && ( {description && (
<p className="font-serif text-[13px] text-[#00293d] mt-3 leading-relaxed"> <div className="mt-3">
{showFullBio ? profile.bio : truncatedBio} <p className="font-serif text-[13px] text-[#00293d] leading-[20px]">
{profile.bio.length > 150 && ( {showFullBio ? description : truncatedDescription}
<button {description.length > 200 && (
onClick={() => setShowFullBio(!showFullBio)} <>
className="text-[#e58625] ml-1 hover:underline font-medium" {' '}
> <button
{showFullBio ? 'Show Less' : 'Show More.'} onClick={() => setShowFullBio(!showFullBio)}
</button> className="font-serif font-bold text-[13px] text-[#00293d] underline"
)} >
</p> {showFullBio ? 'Show Less.' : 'Show More.'}
</button>
</>
)}
</p>
</div>
)} )}
{/* Specializations */} {/* Expertise Tags */}
{profile.specializations && profile.specializations.length > 0 && ( {expertiseTags.length > 0 && (
<div className="mt-3"> <div className="mt-3">
<p className="font-fractul font-semibold text-[12px] text-[#00293d] mb-2">Expertise:</p> <p className="font-fractul font-bold text-[14px] text-[#00293d] mb-2">Expertise:</p>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-2 items-center">
{profile.specializations.map((tag, index) => ( {(showAllTags ? expertiseTags : expertiseTags.slice(0, 8)).map((tag, index) => (
<span <span
key={index} key={index}
className="border border-[#00293d]/30 rounded-full px-3 py-1 font-serif text-[11px] text-[#00293d]" className="border border-[#00293d] rounded-[15px] px-3 py-1 font-serif text-[14px] text-[#00293d]"
> >
{tag} {tag}
</span> </span>
))} ))}
{expertiseTags.length > 8 && (
<button
onClick={() => setShowAllTags(!showAllTags)}
className="font-serif font-bold text-[14px] text-[#00293d] underline ml-1"
>
{showAllTags ? 'Show Less' : `+${expertiseTags.length - 8} more`}
</button>
)}
</div> </div>
</div> </div>
)} )}
{/* Languages */}
{profile.languages && profile.languages.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
{profile.languages.map((lang, index) => (
<span
key={index}
className="border border-[#00293d]/30 rounded-full px-3 py-1 font-serif text-[11px] text-[#00293d]"
>
{lang}
</span>
))}
</div>
)}
</div> </div>
</div> </div>
); );
@@ -398,17 +493,15 @@ function ProfilesPageContent() {
<div className="space-y-2"> <div className="space-y-2">
<button <button
onClick={() => setListingType('agents')} onClick={() => setListingType('agents')}
className={`block font-serif text-[14px] ${ className={`block font-serif text-[14px] ${listingType === 'agents' ? 'text-[#00293d] font-medium' : 'text-[#00293d]/70'
listingType === 'agents' ? 'text-[#00293d] font-medium' : 'text-[#00293d]/70' }`}
}`}
> >
Agents Agents
</button> </button>
<button <button
onClick={() => setListingType('lenders')} onClick={() => setListingType('lenders')}
className={`block font-serif text-[14px] ${ className={`block font-serif text-[14px] ${listingType === 'lenders' ? 'text-[#00293d] font-medium' : 'text-[#00293d]/70'
listingType === 'lenders' ? 'text-[#00293d] font-medium' : 'text-[#00293d]/70' }`}
}`}
> >
Lenders Lenders
</button> </button>
@@ -424,7 +517,7 @@ function ProfilesPageContent() {
className="text-[#00293d] hover:opacity-70" className="text-[#00293d] hover:opacity-70"
> >
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path d="M3 6H21M7 12H17M11 18H13" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/> <path d="M3 6H21M7 12H17M11 18H13" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg> </svg>
</button> </button>
</div> </div>
@@ -471,7 +564,7 @@ function ProfilesPageContent() {
className="absolute right-2 top-1/2 -translate-y-1/2 w-[36px] h-[36px] bg-[#e58625] rounded-[8px] flex items-center justify-center" className="absolute right-2 top-1/2 -translate-y-1/2 w-[36px] h-[36px] bg-[#e58625] rounded-[8px] flex items-center justify-center"
> >
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path d="M21 21L16.65 16.65M19 11C19 15.4183 15.4183 19 11 19C6.58172 19 3 15.4183 3 11C3 6.58172 6.58172 3 11 3C15.4183 3 19 6.58172 19 11Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> <path d="M21 21L16.65 16.65M19 11C19 15.4183 15.4183 19 11 19C6.58172 19 3 15.4183 3 11C3 6.58172 6.58172 3 11 3C15.4183 3 19 6.58172 19 11Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</button> </button>
</div> </div>
@@ -481,11 +574,10 @@ function ProfilesPageContent() {
<div className="flex flex-wrap gap-2 mb-6"> <div className="flex flex-wrap gap-2 mb-6">
<button <button
onClick={() => handleTypeChange(null)} onClick={() => handleTypeChange(null)}
className={`px-4 py-2 rounded-full font-serif text-[13px] border transition-colors ${ className={`px-4 py-2 rounded-full font-serif text-[13px] border transition-colors ${activeTypeId === null
activeTypeId === null
? 'bg-[#00293d] text-white border-[#00293d]' ? 'bg-[#00293d] text-white border-[#00293d]'
: 'bg-white text-[#00293d] border-[#00293d]/20 hover:border-[#00293d]/40' : 'bg-white text-[#00293d] border-[#00293d]/20 hover:border-[#00293d]/40'
}`} }`}
> >
All Types All Types
</button> </button>
@@ -493,24 +585,16 @@ function ProfilesPageContent() {
<button <button
key={type.id} key={type.id}
onClick={() => handleTypeChange(type.id)} onClick={() => handleTypeChange(type.id)}
className={`px-4 py-2 rounded-full font-serif text-[13px] border transition-colors ${ className={`px-4 py-2 rounded-full font-serif text-[13px] border transition-colors ${activeTypeId === type.id
activeTypeId === type.id
? 'bg-[#00293d] text-white border-[#00293d]' ? 'bg-[#00293d] text-white border-[#00293d]'
: 'bg-white text-[#00293d] border-[#00293d]/20 hover:border-[#00293d]/40' : 'bg-white text-[#00293d] border-[#00293d]/20 hover:border-[#00293d]/40'
}`} }`}
> >
{type.name} {type.name}
</button> </button>
))} ))}
</div> </div>
{/* Results Count */}
{!loading && (
<p className="font-serif text-[13px] text-[#00293d]/70 mb-4">
{totalResults} {totalResults === 1 ? 'agent' : 'agents'} found
</p>
)}
{/* Loading State */} {/* Loading State */}
{loading && ( {loading && (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">

View File

@@ -29,12 +29,12 @@ export function ContactInfo({ email, phone }: ContactInfoProps) {
return ( return (
<div className="w-full max-w-[354px] lg:max-w-none border border-[#00293d]/10 rounded-[15px] p-4"> <div className="w-full max-w-[354px] lg:max-w-none border border-[#00293d]/10 rounded-[15px] p-4">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<span className="font-bold text-[#00293d] text-sm">Email:</span> <span className="font-bold text-[#00293d] text-sm flex-shrink-0">Email:</span>
<span className="text-[#00293d] text-sm font-serif"> <span className="text-[#00293d] text-sm font-serif truncate min-w-0 flex-1">
{showEmail ? email : maskEmail(email)} {showEmail ? email : maskEmail(email)}
</span> </span>
<button <button
className="p-1 hover:bg-gray-100 rounded ml-auto" className="p-1 hover:bg-gray-100 rounded flex-shrink-0"
onClick={() => setShowEmail(!showEmail)} onClick={() => setShowEmail(!showEmail)}
> >
<Image <Image
@@ -46,12 +46,12 @@ export function ContactInfo({ email, phone }: ContactInfoProps) {
</button> </button>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-bold text-[#00293d] text-sm">Ph.No:</span> <span className="font-bold text-[#00293d] text-sm flex-shrink-0">Ph.No:</span>
<span className="text-[#00293d] text-sm font-serif"> <span className="text-[#00293d] text-sm font-serif truncate min-w-0 flex-1">
{showPhone ? phone : maskPhone(phone)} {showPhone ? phone : maskPhone(phone)}
</span> </span>
<button <button
className="p-1 hover:bg-gray-100 rounded ml-auto" className="p-1 hover:bg-gray-100 rounded flex-shrink-0"
onClick={() => setShowPhone(!showPhone)} onClick={() => setShowPhone(!showPhone)}
> >
<Image <Image

View File

@@ -83,6 +83,22 @@ export interface SearchAgentsParams {
filters?: Record<string, string[]>; filters?: Record<string, string[]>;
} }
// Field value from search response
export interface AgentFieldValue {
id: string;
fieldId: string;
textValue: string | null;
numberValue: number | null;
booleanValue: boolean | null;
jsonValue: unknown;
dateValue: string | null;
field: {
slug: string;
name: string;
fieldType: string;
};
}
// Public agent listing response // Public agent listing response
export interface PublicAgentProfile { export interface PublicAgentProfile {
id: string; id: string;
@@ -98,6 +114,9 @@ export interface PublicAgentProfile {
specializations: string[]; specializations: string[];
languages: string[]; languages: string[];
serviceAreas: string[]; serviceAreas: string[];
city: string | null;
state: string | null;
country: string | null;
rating: number | null; rating: number | null;
reviewCount: number; reviewCount: number;
isVerified: boolean; isVerified: boolean;
@@ -105,6 +124,7 @@ export interface PublicAgentProfile {
agentTypeId: string | null; agentTypeId: string | null;
agentType: AgentType | null; agentType: AgentType | null;
createdAt?: string; createdAt?: string;
fieldValues?: AgentFieldValue[];
} }
export interface SearchAgentsResponse { export interface SearchAgentsResponse {