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}
isVerified={agentProfile.isVerified}
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)}
bio={agentProfile.bio || ''}
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>
<button className="text-[#00293d]">
<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>
</button>
</div>
@@ -62,12 +62,10 @@ interface ProfileCardProps {
function ProfileCard({ profile }: ProfileCardProps) {
const [showFullBio, setShowFullBio] = useState(false);
const [showAllTags, setShowAllTags] = useState(false);
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
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
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 (
<div className="bg-white rounded-[15px] p-4 flex gap-4 shadow-[0px_4px_20px_rgba(0,0,0,0.08)]">
{/* Profile Image */}
<div className="flex-shrink-0">
<div className="relative w-[120px] h-[140px] rounded-[10px] overflow-hidden">
<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 */}
<div className="flex-shrink-0 flex flex-col items-center">
<div className="relative w-[200px] h-[200px] rounded-[15px] overflow-hidden">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={getProfileImageUrl()}
@@ -119,8 +208,8 @@ function ProfileCard({ profile }: ProfileCardProps) {
}}
/>
</div>
<Link href={`/user/profile/${profile.id}`}>
<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">
<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">
View Profile
</button>
</Link>
@@ -128,107 +217,113 @@ function ProfileCard({ profile }: ProfileCardProps) {
{/* Profile Info */}
<div className="flex-1 min-w-0">
{/* Header */}
<div className="flex items-start justify-between mb-1">
{/* Header Row - Name, Verified, Match Badge */}
<div className="flex items-start justify-between">
<div>
{/* Name and Verified Badge */}
<div className="flex items-center gap-2">
<h3 className="font-fractul font-bold text-[16px] text-[#00293d]">
{profile.firstName} {profile.lastName}
<h3 className="font-fractul text-[18px] text-[#00293d]">
<span className="font-bold">{profile.firstName}</span>{' '}
<span className="font-normal">{profile.lastName}</span>
</h3>
{profile.isVerified && (
<span className="flex items-center gap-1 text-[#e58625] font-serif text-[12px]">
<>
<Image
src="/assets/icons/verified-badge-blue.svg"
src="/assets/icons/verified-expert-badge.svg"
alt="Verified"
width={14}
height={14}
height={15}
/>
(Verified Expert)
</span>
<span className="font-serif font-medium text-[14px] text-[#638559]">
(Verified Expert)
</span>
</>
)}
</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'}
</p>
</div>
{profile.rating && (
<div className="bg-[#e58625] text-white px-3 py-1 rounded-full font-fractul font-bold text-[12px]">
{profile.rating.toFixed(1)} Rating
</div>
)}
{/* Match Badge */}
<div className="bg-[#7d917d] text-white px-4 py-1.5 rounded-full font-serif text-[14px] whitespace-nowrap">
95% Match
</div>
</div>
{/* Location & Member Since */}
<div className="flex items-center gap-4 mt-2">
{profile.serviceAreas && profile.serviceAreas.length > 0 && (
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-4 mt-3">
{location && (
<div className="flex items-center gap-2">
<Image
src="/assets/icons/location-pin-orange.svg"
src="/assets/icons/location-icon.svg"
alt="Location"
width={12}
height={15}
width={14}
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 className="flex items-center gap-1.5">
<div className="flex items-center gap-2">
<Image
src="/assets/icons/calendar-icon.svg"
alt="Member Since"
width={14}
height={14}
width={18}
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)}
</span>
</div>
</div>
{/* Bio */}
{profile.bio && (
<p className="font-serif text-[13px] text-[#00293d] mt-3 leading-relaxed">
{showFullBio ? profile.bio : truncatedBio}
{profile.bio.length > 150 && (
<button
onClick={() => setShowFullBio(!showFullBio)}
className="text-[#e58625] ml-1 hover:underline font-medium"
>
{showFullBio ? 'Show Less' : 'Show More.'}
</button>
)}
</p>
{/* Description */}
{description && (
<div className="mt-3">
<p className="font-serif text-[13px] text-[#00293d] leading-[20px]">
{showFullBio ? description : truncatedDescription}
{description.length > 200 && (
<>
{' '}
<button
onClick={() => setShowFullBio(!showFullBio)}
className="font-serif font-bold text-[13px] text-[#00293d] underline"
>
{showFullBio ? 'Show Less.' : 'Show More.'}
</button>
</>
)}
</p>
</div>
)}
{/* Specializations */}
{profile.specializations && profile.specializations.length > 0 && (
{/* Expertise Tags */}
{expertiseTags.length > 0 && (
<div className="mt-3">
<p className="font-fractul font-semibold text-[12px] text-[#00293d] mb-2">Expertise:</p>
<div className="flex flex-wrap gap-1.5">
{profile.specializations.map((tag, index) => (
<p className="font-fractul font-bold text-[14px] text-[#00293d] mb-2">Expertise:</p>
<div className="flex flex-wrap gap-2 items-center">
{(showAllTags ? expertiseTags : expertiseTags.slice(0, 8)).map((tag, index) => (
<span
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}
</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>
)}
{/* 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>
);
@@ -398,17 +493,15 @@ function ProfilesPageContent() {
<div className="space-y-2">
<button
onClick={() => setListingType('agents')}
className={`block font-serif text-[14px] ${
listingType === 'agents' ? 'text-[#00293d] font-medium' : 'text-[#00293d]/70'
}`}
className={`block font-serif text-[14px] ${listingType === 'agents' ? 'text-[#00293d] font-medium' : 'text-[#00293d]/70'
}`}
>
Agents
</button>
<button
onClick={() => setListingType('lenders')}
className={`block font-serif text-[14px] ${
listingType === 'lenders' ? 'text-[#00293d] font-medium' : 'text-[#00293d]/70'
}`}
className={`block font-serif text-[14px] ${listingType === 'lenders' ? 'text-[#00293d] font-medium' : 'text-[#00293d]/70'
}`}
>
Lenders
</button>
@@ -424,7 +517,7 @@ function ProfilesPageContent() {
className="text-[#00293d] hover:opacity-70"
>
<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>
</button>
</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"
>
<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>
</button>
</div>
@@ -481,11 +574,10 @@ function ProfilesPageContent() {
<div className="flex flex-wrap gap-2 mb-6">
<button
onClick={() => handleTypeChange(null)}
className={`px-4 py-2 rounded-full font-serif text-[13px] border transition-colors ${
activeTypeId === null
className={`px-4 py-2 rounded-full font-serif text-[13px] border transition-colors ${activeTypeId === null
? 'bg-[#00293d] text-white border-[#00293d]'
: 'bg-white text-[#00293d] border-[#00293d]/20 hover:border-[#00293d]/40'
}`}
}`}
>
All Types
</button>
@@ -493,24 +585,16 @@ function ProfilesPageContent() {
<button
key={type.id}
onClick={() => handleTypeChange(type.id)}
className={`px-4 py-2 rounded-full font-serif text-[13px] border transition-colors ${
activeTypeId === type.id
className={`px-4 py-2 rounded-full font-serif text-[13px] border transition-colors ${activeTypeId === type.id
? 'bg-[#00293d] text-white border-[#00293d]'
: 'bg-white text-[#00293d] border-[#00293d]/20 hover:border-[#00293d]/40'
}`}
}`}
>
{type.name}
</button>
))}
</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 && (
<div className="flex items-center justify-center py-12">

View File

@@ -29,12 +29,12 @@ export function ContactInfo({ email, phone }: ContactInfoProps) {
return (
<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">
<span className="font-bold text-[#00293d] text-sm">Email:</span>
<span className="text-[#00293d] text-sm font-serif">
<span className="font-bold text-[#00293d] text-sm flex-shrink-0">Email:</span>
<span className="text-[#00293d] text-sm font-serif truncate min-w-0 flex-1">
{showEmail ? email : maskEmail(email)}
</span>
<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)}
>
<Image
@@ -46,12 +46,12 @@ export function ContactInfo({ email, phone }: ContactInfoProps) {
</button>
</div>
<div className="flex items-center gap-2">
<span className="font-bold text-[#00293d] text-sm">Ph.No:</span>
<span className="text-[#00293d] text-sm font-serif">
<span className="font-bold text-[#00293d] text-sm flex-shrink-0">Ph.No:</span>
<span className="text-[#00293d] text-sm font-serif truncate min-w-0 flex-1">
{showPhone ? phone : maskPhone(phone)}
</span>
<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)}
>
<Image

View File

@@ -83,6 +83,22 @@ export interface SearchAgentsParams {
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
export interface PublicAgentProfile {
id: string;
@@ -98,6 +114,9 @@ export interface PublicAgentProfile {
specializations: string[];
languages: string[];
serviceAreas: string[];
city: string | null;
state: string | null;
country: string | null;
rating: number | null;
reviewCount: number;
isVerified: boolean;
@@ -105,6 +124,7 @@ export interface PublicAgentProfile {
agentTypeId: string | null;
agentType: AgentType | null;
createdAt?: string;
fieldValues?: AgentFieldValue[];
}
export interface SearchAgentsResponse {