feat: Add a new user profiles page with filtering capabilities and update hero section navigation to it.
This commit is contained in:
409
src/app/(user)/user/profiles/page.tsx
Normal file
409
src/app/(user)/user/profiles/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user