feat: Enhance agent profiles with dynamic fields, granular location data, and add a verified badge asset.
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user