|
|
|
|
@@ -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">
|
|
|
|
|
|