feat: Add a new user profiles page with filtering capabilities and update hero section navigation to it.

This commit is contained in:
pradeepkumar
2026-01-20 11:51:10 +05:30
parent 03d1377a88
commit 781a60f537
2 changed files with 411 additions and 2 deletions

View File

@@ -0,0 +1,409 @@
'use client';
import { useState } from 'react';
import { useSearchParams } from 'next/navigation';
import Image from 'next/image';
import Link from 'next/link';
// Sample profile data
const profilesData = [
{
id: '1',
name: 'Brian Noseland',
verified: true,
title: 'Licensed Real Estate Agent',
location: 'Colorado',
memberSince: 'March 2020',
bio: 'Brian has eight years of experience helping clients buy, sell, and invest in properties. He combines strong analytical skills with market insight to deliver smooth, data-driven real estate transactions.',
expertise: ['Buyers', 'Sellers', 'Divorce', 'Luxury', 'Retirement', 'Historical', 'condo', 'Rural'],
certifications: ['FHA', 'Conventional', 'USDA'],
matchPercentage: 95,
imageUrl: '/assets/images/professional-1.jpg',
},
{
id: '2',
name: 'Brian Noseland',
verified: true,
title: 'Licensed Real Estate Agent',
location: 'Colorado',
memberSince: 'March 2020',
bio: 'Brian has eight years of experience helping clients buy, sell, and invest in properties. He combines strong analytical skills with market insight to deliver smooth, data-driven real estate transactions.',
expertise: ['Buyers', 'Sellers', 'Divorce', 'Luxury', 'Retirement', 'Historical', 'condo'],
certifications: ['FHA', 'Conventional', 'USDA'],
matchPercentage: 95,
imageUrl: '/assets/images/professional-2.jpg',
},
{
id: '3',
name: 'Brian Noseland',
verified: true,
title: 'Licensed Real Estate Agent',
location: 'Colorado',
memberSince: 'March 2020',
bio: 'Brian has eight years of experience helping clients buy, sell, and invest in properties. He combines strong analytical skills with market insight to deliver smooth, data-driven real estate transactions.',
expertise: ['Buyers', 'Sellers', 'Divorce', 'Luxury', 'Retirement', 'Historical', 'condo', 'Rural'],
certifications: ['FHA', 'Conventional', 'USDA'],
matchPercentage: 95,
imageUrl: '/assets/images/professional-3.jpg',
},
{
id: '4',
name: 'Brian Noseland',
verified: true,
title: 'Licensed Real Estate Agent',
location: 'Colorado',
memberSince: 'March 2020',
bio: 'Brian has eight years of experience helping clients buy, sell, and invest in properties. He combines strong analytical skills with market insight to deliver smooth, data-driven real estate transactions.',
expertise: ['Buyers', 'Sellers', 'Divorce', 'Luxury', 'Retirement', 'Historical', 'condo', 'Rural'],
certifications: ['FHA', 'Conventional', 'USDA'],
matchPercentage: 95,
imageUrl: '/assets/images/professional-1.jpg',
},
{
id: '5',
name: 'Brian Noseland',
verified: true,
title: 'Licensed Real Estate Agent',
location: 'Colorado',
memberSince: 'March 2020',
bio: 'Brian has eight years of experience helping clients buy, sell, and invest in properties. He combines strong analytical skills with market insight to deliver smooth, data-driven real estate transactions.',
expertise: ['Buyers', 'Sellers', 'Divorce', 'Luxury', 'Retirement', 'Historical', 'condo', 'Rural'],
certifications: ['FHA', 'Conventional', 'USDA'],
matchPercentage: 95,
imageUrl: '/assets/images/professional-2.jpg',
},
];
// Filter options
const clientSpecializations = [
'Buyers', 'Sellers', 'Divorce', 'First time buyers', 'First time sellers', 'Estate'
];
const loanTypes = [
'Conventional', 'FHA', 'VA', 'USDA', 'Downpayment Assistance Programs'
];
const propertyTypes = [
'SFR', 'Townhome', 'condo', 'Manufactured', 'Apartment / Multifamily',
'Rural', 'Luxury', 'Retirement', 'Investment/ Income Producing',
'Duplex / Tri-plex / 4-plex', 'Historical', 'Mountain Property'
];
const pricePoints = [
'Under $100K', '$100K - $300K', '$300K - $500K', '$500K - $1M', '$1M and above'
];
const categoryTabs = [
{ id: 'residential', label: 'Residential' },
{ id: 'analytics', label: 'Analytics' },
{ id: 'commercial', label: 'Commercial' },
{ id: 'property', label: 'Property' },
];
interface FilterSectionProps {
title: string;
options: string[];
selectedOptions: string[];
onToggle: (option: string) => void;
showMore?: boolean;
}
function FilterSection({ title, options, selectedOptions, onToggle, showMore }: FilterSectionProps) {
const [expanded, setExpanded] = useState(false);
const displayOptions = expanded ? options : options.slice(0, 6);
return (
<div className="pb-4 mb-4">
<div className="flex items-center justify-between mb-3">
<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"/>
</svg>
</button>
</div>
<div className="space-y-2">
{displayOptions.map((option) => (
<label key={option} className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={selectedOptions.includes(option)}
onChange={() => onToggle(option)}
className="w-4 h-4 rounded border-[#00293d]/30 text-[#e58625] focus:ring-[#e58625]"
/>
<span className="font-serif text-[13px] text-[#00293d]">{option}</span>
</label>
))}
</div>
{showMore && options.length > 6 && (
<button
onClick={() => setExpanded(!expanded)}
className="mt-2 font-serif text-[13px] text-[#e58625] hover:underline"
>
{expanded ? 'Show Less' : 'Show More'}
</button>
)}
</div>
);
}
interface ProfileCardProps {
profile: typeof profilesData[0];
}
function ProfileCard({ profile }: ProfileCardProps) {
const [showFullBio, setShowFullBio] = useState(false);
const truncatedBio = profile.bio.length > 150 ? profile.bio.slice(0, 150) + '...' : profile.bio;
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">
<Image
src={profile.imageUrl}
alt={profile.name}
fill
className="object-cover"
/>
</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">
View Profile
</button>
</Link>
</div>
{/* Profile Info */}
<div className="flex-1 min-w-0">
{/* Header */}
<div className="flex items-start justify-between mb-1">
<div>
<div className="flex items-center gap-2">
<h3 className="font-fractul font-bold text-[16px] text-[#00293d]">{profile.name}</h3>
{profile.verified && (
<span className="flex items-center gap-1 text-[#e58625] font-serif text-[12px]">
<Image
src="/assets/icons/verified-badge-blue.svg"
alt="Verified"
width={14}
height={14}
/>
(Verified Expert)
</span>
)}
</div>
<p className="font-serif text-[13px] text-[#00293d] mt-0.5">{profile.title}</p>
</div>
<div className="bg-[#e58625] text-white px-3 py-1 rounded-full font-fractul font-bold text-[12px]">
{profile.matchPercentage}% Match
</div>
</div>
{/* Location & Member Since */}
<div className="flex items-center gap-4 mt-2">
<div className="flex items-center gap-1.5">
<Image
src="/assets/icons/location-pin-orange.svg"
alt="Location"
width={12}
height={15}
/>
<span className="font-serif text-[12px] text-[#00293d]">{profile.location}</span>
</div>
<div className="flex items-center gap-1.5">
<Image
src="/assets/icons/calendar-icon.svg"
alt="Member Since"
width={14}
height={14}
/>
<span className="font-serif text-[12px] text-[#00293d]">Member Since {profile.memberSince}</span>
</div>
</div>
{/* 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>
{/* Expertise */}
<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.expertise.map((tag, index) => (
<span
key={index}
className="border border-[#00293d]/30 rounded-full px-3 py-1 font-serif text-[11px] text-[#00293d]"
>
{tag}
</span>
))}
</div>
</div>
{/* Certifications */}
<div className="flex flex-wrap gap-1.5 mt-2">
{profile.certifications.map((cert, index) => (
<span
key={index}
className="border border-[#00293d]/30 rounded-full px-3 py-1 font-serif text-[11px] text-[#00293d]"
>
{cert}
</span>
))}
</div>
</div>
</div>
);
}
export default function ProfilesPage() {
const searchParams = useSearchParams();
const [searchQuery, setSearchQuery] = useState('');
const [activeTab, setActiveTab] = useState('residential');
const [listingType, setListingType] = useState<'agents' | 'lenders'>('agents');
const [filters, setFilters] = useState({
clientSpecialization: [] as string[],
loanType: [] as string[],
propertyType: [] as string[],
pricePoint: [] as string[],
});
const toggleFilter = (category: keyof typeof filters, option: string) => {
setFilters(prev => ({
...prev,
[category]: prev[category].includes(option)
? prev[category].filter(o => o !== option)
: [...prev[category], option]
}));
};
return (
<div className="min-h-screen bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex gap-6">
{/* Left Sidebar */}
<div className="w-[220px] flex-shrink-0">
{/* Listing Type */}
<div className="mb-6">
<h2 className="font-fractul font-bold text-[14px] text-[#00293d] mb-3">Listing Type</h2>
<div className="space-y-2">
<button
onClick={() => setListingType('agents')}
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'
}`}
>
Lenders
</button>
</div>
</div>
{/* Filters */}
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="font-fractul font-bold text-[14px] text-[#00293d]">Filters</h2>
<button className="text-[#00293d]">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path d="M3 6H21M7 12H17M11 18H13" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
</button>
</div>
<FilterSection
title="Client Specialization"
options={clientSpecializations}
selectedOptions={filters.clientSpecialization}
onToggle={(option) => toggleFilter('clientSpecialization', option)}
/>
<FilterSection
title="Loan Type"
options={loanTypes}
selectedOptions={filters.loanType}
onToggle={(option) => toggleFilter('loanType', option)}
/>
<FilterSection
title="Property Type"
options={propertyTypes}
selectedOptions={filters.propertyType}
onToggle={(option) => toggleFilter('propertyType', option)}
showMore
/>
<FilterSection
title="Price Point"
options={pricePoints}
selectedOptions={filters.pricePoint}
onToggle={(option) => toggleFilter('pricePoint', option)}
/>
</div>
</div>
{/* Main Content */}
<div className="flex-1">
{/* Search Bar */}
<div className="mb-4">
<div className="relative">
<input
type="text"
placeholder="Search Agents"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full h-[44px] px-4 pr-12 rounded-[10px] border border-[#00293d]/20 bg-white font-serif text-[14px] text-[#00293d] placeholder:text-[#00293d]/50 focus:outline-none focus:border-[#e58625]"
/>
<button 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"/>
</svg>
</button>
</div>
</div>
{/* Category Tabs */}
<div className="flex flex-wrap gap-2 mb-6">
{categoryTabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 rounded-full font-serif text-[13px] border transition-colors ${
activeTab === tab.id
? 'bg-[#00293d] text-white border-[#00293d]'
: 'bg-white text-[#00293d] border-[#00293d]/20 hover:border-[#00293d]/40'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Profile Cards */}
<div className="space-y-4">
{profilesData.map((profile) => (
<ProfileCard key={profile.id} profile={profile} />
))}
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -34,11 +34,11 @@ export function HeroSection() {
if (searchParams.name) params.set('name', searchParams.name);
if (searchParams.location) params.set('location', searchParams.location);
if (searchParams.category) params.set('category', searchParams.category);
router.push(`/agents?${params.toString()}`);
router.push(`/user/profiles?${params.toString()}`);
};
const handleSeeAllAgents = () => {
router.push('/agents');
router.push('/user/profiles');
};
const toggleDropdown = (dropdown: string) => {