feat: Introduce dynamic profile filtering with a new service and update user and agent profiles to use separate first and last name fields.
This commit is contained in:
@@ -4,15 +4,16 @@ import { useState, useEffect, useCallback, Suspense } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { FilterModal, FilterState, clientSpecializations, loanTypes, propertyTypes, pricePoints } from '@/components/profiles/FilterModal';
|
||||
import { FilterModal, FilterState, FilterField } from '@/components/profiles/FilterModal';
|
||||
import { agentsService, AgentType, PublicAgentProfile, SearchAgentsParams } from '@/services/agents.service';
|
||||
import { profileSectionsService, FilterableField } from '@/services/profile-sections.service';
|
||||
import { uploadService } from '@/services/upload.service';
|
||||
|
||||
interface FilterSectionProps {
|
||||
title: string;
|
||||
options: string[];
|
||||
options: { label: string; value: string }[];
|
||||
selectedOptions: string[];
|
||||
onToggle: (option: string) => void;
|
||||
onToggle: (optionValue: string) => void;
|
||||
showMore?: boolean;
|
||||
}
|
||||
|
||||
@@ -32,14 +33,14 @@ function FilterSection({ title, options, selectedOptions, onToggle, showMore }:
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{displayOptions.map((option) => (
|
||||
<label key={option} className="flex items-center gap-2 cursor-pointer">
|
||||
<label key={option.value} className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedOptions.includes(option)}
|
||||
onChange={() => onToggle(option)}
|
||||
checked={selectedOptions.includes(option.value)}
|
||||
onChange={() => onToggle(option.value)}
|
||||
className="w-4 h-4 rounded border-[#00293d]/30 text-[#e58625] focus:ring-[#e58625]"
|
||||
/>
|
||||
<span className="font-serif text-[13px] text-[#00293d]">{option}</span>
|
||||
<span className="font-serif text-[13px] text-[#00293d]">{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -239,48 +240,60 @@ function ProfilesPageContent() {
|
||||
// State for API data
|
||||
const [agents, setAgents] = useState<PublicAgentProfile[]>([]);
|
||||
const [agentTypes, setAgentTypes] = useState<AgentType[]>([]);
|
||||
const [filterFields, setFilterFields] = useState<FilterField[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalResults, setTotalResults] = useState(0);
|
||||
|
||||
// State for search and filters
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [activeTypeId, setActiveTypeId] = useState<string | null>(null);
|
||||
// State for search and filters - initialize from URL params
|
||||
const [searchQuery, setSearchQuery] = useState(() => searchParams.get('name') || '');
|
||||
const [activeTypeId, setActiveTypeId] = useState<string | null>(() => searchParams.get('type'));
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [listingType, setListingType] = useState<'agents' | 'lenders'>('agents');
|
||||
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
|
||||
const [filters, setFilters] = useState<FilterState>({
|
||||
clientSpecialization: [],
|
||||
loanType: [],
|
||||
propertyType: [],
|
||||
pricePoint: [],
|
||||
specialInterests: [],
|
||||
popularFilters: [],
|
||||
});
|
||||
const [filters, setFilters] = useState<FilterState>({});
|
||||
|
||||
// Initialize from URL params
|
||||
// Mark as initialized after first render and sync with URL param changes
|
||||
useEffect(() => {
|
||||
const type = searchParams.get('type');
|
||||
const name = searchParams.get('name');
|
||||
const location = searchParams.get('location');
|
||||
setIsInitialized(true);
|
||||
}, []);
|
||||
|
||||
if (type) setActiveTypeId(type);
|
||||
if (name) setSearchQuery(name);
|
||||
// Location is handled separately in search params
|
||||
}, [searchParams]);
|
||||
|
||||
// Fetch agent types on mount
|
||||
// Sync state when URL params change (e.g., browser back/forward)
|
||||
useEffect(() => {
|
||||
const fetchAgentTypes = async () => {
|
||||
if (isInitialized) {
|
||||
const type = searchParams.get('type');
|
||||
const name = searchParams.get('name');
|
||||
|
||||
setActiveTypeId(type);
|
||||
setSearchQuery(name || '');
|
||||
}
|
||||
}, [searchParams, isInitialized]);
|
||||
|
||||
// Fetch agent types and filter fields on mount
|
||||
useEffect(() => {
|
||||
const fetchInitialData = async () => {
|
||||
try {
|
||||
const types = await agentsService.getAgentTypes();
|
||||
const [types, fields] = await Promise.all([
|
||||
agentsService.getAgentTypes(),
|
||||
profileSectionsService.getFilterableFields(),
|
||||
]);
|
||||
setAgentTypes(types);
|
||||
|
||||
// Convert FilterableField to FilterField format
|
||||
const filterFieldsData: FilterField[] = fields.map((f: FilterableField) => ({
|
||||
slug: f.slug,
|
||||
name: f.name,
|
||||
fieldType: f.fieldType,
|
||||
options: f.options,
|
||||
}));
|
||||
setFilterFields(filterFieldsData);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch agent types:', error);
|
||||
console.error('Failed to fetch initial data:', error);
|
||||
}
|
||||
};
|
||||
fetchAgentTypes();
|
||||
fetchInitialData();
|
||||
}, []);
|
||||
|
||||
// Fetch agents based on filters
|
||||
@@ -325,29 +338,25 @@ function ProfilesPageContent() {
|
||||
}
|
||||
}, [currentPage, searchQuery, activeTypeId, searchParams]);
|
||||
|
||||
// Fetch agents when filters change
|
||||
// Fetch agents when filters change (only after initialization)
|
||||
useEffect(() => {
|
||||
fetchAgents();
|
||||
}, [fetchAgents]);
|
||||
if (isInitialized) {
|
||||
fetchAgents();
|
||||
}
|
||||
}, [fetchAgents, isInitialized]);
|
||||
|
||||
const toggleFilter = (category: string, option: string) => {
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
[category]: (prev[category as keyof FilterState] || []).includes(option)
|
||||
? (prev[category as keyof FilterState] || []).filter((o: string) => o !== option)
|
||||
: [...(prev[category as keyof FilterState] || []), option]
|
||||
}));
|
||||
const toggleFilter = (fieldSlug: string, optionValue: string) => {
|
||||
setFilters(prev => {
|
||||
const currentValues = prev[fieldSlug] || [];
|
||||
const newValues = currentValues.includes(optionValue)
|
||||
? currentValues.filter(v => v !== optionValue)
|
||||
: [...currentValues, optionValue];
|
||||
return { ...prev, [fieldSlug]: newValues };
|
||||
});
|
||||
};
|
||||
|
||||
const clearAllFilters = () => {
|
||||
setFilters({
|
||||
clientSpecialization: [],
|
||||
loanType: [],
|
||||
propertyType: [],
|
||||
pricePoint: [],
|
||||
specialInterests: [],
|
||||
popularFilters: [],
|
||||
});
|
||||
setFilters({});
|
||||
};
|
||||
|
||||
const applyFilters = () => {
|
||||
@@ -366,6 +375,14 @@ function ProfilesPageContent() {
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
// Get filter field by slug for sidebar
|
||||
const getFilterFieldBySlug = (slug: string): FilterField | undefined => {
|
||||
return filterFields.find(f => f.slug === slug);
|
||||
};
|
||||
|
||||
// Define which fields to show in sidebar (main filter categories)
|
||||
const sidebarFilterSlugs = ['client_specialization', 'loan_type', 'property_type', 'price_point'];
|
||||
|
||||
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">
|
||||
@@ -409,34 +426,27 @@ function ProfilesPageContent() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<FilterSection
|
||||
title="Client Specialization"
|
||||
options={clientSpecializations}
|
||||
selectedOptions={filters.clientSpecialization}
|
||||
onToggle={(option) => toggleFilter('clientSpecialization', option)}
|
||||
/>
|
||||
{/* Dynamic Filter Sections */}
|
||||
{sidebarFilterSlugs.map(slug => {
|
||||
const field = getFilterFieldBySlug(slug);
|
||||
if (!field) return null;
|
||||
return (
|
||||
<FilterSection
|
||||
key={field.slug}
|
||||
title={field.name}
|
||||
options={field.options}
|
||||
selectedOptions={filters[field.slug] || []}
|
||||
onToggle={(optionValue) => toggleFilter(field.slug, optionValue)}
|
||||
showMore={field.options.length > 6}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<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)}
|
||||
/>
|
||||
{filterFields.length === 0 && (
|
||||
<div className="py-4">
|
||||
<p className="font-serif text-[13px] text-[#00293d]/50">Loading filters...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -576,6 +586,7 @@ function ProfilesPageContent() {
|
||||
isOpen={isFilterModalOpen}
|
||||
onClose={() => setIsFilterModalOpen(false)}
|
||||
filters={filters}
|
||||
filterFields={filterFields}
|
||||
onToggleFilter={toggleFilter}
|
||||
onClearAll={clearAllFilters}
|
||||
onApply={applyFilters}
|
||||
|
||||
Reference in New Issue
Block a user