From 09cf44f41876c0086e0aff5c9b6cce3bb85a73c6 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Sun, 1 Feb 2026 15:04:51 +0530 Subject: [PATCH] feat: Introduce dynamic profile filtering with a new service and update user and agent profiles to use separate first and last name fields. --- src/app/(agent)/agent/settings/page.tsx | 3 +- src/app/(user)/user/profiles/page.tsx | 165 ++++++------ src/app/(user)/user/settings/page.tsx | 3 +- src/components/profiles/FilterModal.tsx | 313 +++++------------------ src/services/profile-sections.service.ts | 16 ++ 5 files changed, 167 insertions(+), 333 deletions(-) diff --git a/src/app/(agent)/agent/settings/page.tsx b/src/app/(agent)/agent/settings/page.tsx index 5234273..9ef5ac2 100644 --- a/src/app/(agent)/agent/settings/page.tsx +++ b/src/app/(agent)/agent/settings/page.tsx @@ -4,7 +4,8 @@ import { SettingsSidebar, ProfileSettingsForm } from '@/components/settings'; export default function ProfileSettingsPage() { const handleSave = (data: { - fullName: string; + firstName: string; + lastName: string; career: string; email: string; phone: string; diff --git a/src/app/(user)/user/profiles/page.tsx b/src/app/(user)/user/profiles/page.tsx index 443a9cc..db9610e 100644 --- a/src/app/(user)/user/profiles/page.tsx +++ b/src/app/(user)/user/profiles/page.tsx @@ -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 }:
{displayOptions.map((option) => ( -
@@ -239,48 +240,60 @@ function ProfilesPageContent() { // State for API data const [agents, setAgents] = useState([]); const [agentTypes, setAgentTypes] = useState([]); + const [filterFields, setFilterFields] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(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(null); + // State for search and filters - initialize from URL params + const [searchQuery, setSearchQuery] = useState(() => searchParams.get('name') || ''); + const [activeTypeId, setActiveTypeId] = useState(() => searchParams.get('type')); + const [isInitialized, setIsInitialized] = useState(false); const [listingType, setListingType] = useState<'agents' | 'lenders'>('agents'); const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); - const [filters, setFilters] = useState({ - clientSpecialization: [], - loanType: [], - propertyType: [], - pricePoint: [], - specialInterests: [], - popularFilters: [], - }); + const [filters, setFilters] = useState({}); - // 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 (
@@ -409,34 +426,27 @@ function ProfilesPageContent() {
- toggleFilter('clientSpecialization', option)} - /> + {/* Dynamic Filter Sections */} + {sidebarFilterSlugs.map(slug => { + const field = getFilterFieldBySlug(slug); + if (!field) return null; + return ( + toggleFilter(field.slug, optionValue)} + showMore={field.options.length > 6} + /> + ); + })} - toggleFilter('loanType', option)} - /> - - toggleFilter('propertyType', option)} - showMore - /> - - toggleFilter('pricePoint', option)} - /> + {filterFields.length === 0 && ( +
+

Loading filters...

+
+ )}
@@ -576,6 +586,7 @@ function ProfilesPageContent() { isOpen={isFilterModalOpen} onClose={() => setIsFilterModalOpen(false)} filters={filters} + filterFields={filterFields} onToggleFilter={toggleFilter} onClearAll={clearAllFilters} onApply={applyFilters} diff --git a/src/app/(user)/user/settings/page.tsx b/src/app/(user)/user/settings/page.tsx index 384f805..bc08b7d 100644 --- a/src/app/(user)/user/settings/page.tsx +++ b/src/app/(user)/user/settings/page.tsx @@ -4,7 +4,8 @@ import { SettingsSidebar, ProfileSettingsForm } from '@/components/settings'; export default function UserProfileSettingsPage() { const handleSave = (data: { - fullName: string; + firstName: string; + lastName: string; career: string; email: string; phone: string; diff --git a/src/components/profiles/FilterModal.tsx b/src/components/profiles/FilterModal.tsx index 703a0de..bfbf9e9 100644 --- a/src/components/profiles/FilterModal.tsx +++ b/src/components/profiles/FilterModal.tsx @@ -2,61 +2,40 @@ import { useState } from 'react'; -// Filter options data -export const clientSpecializations = [ - 'Buyers', 'Sellers', 'Divorce', 'First time buyers', 'First time sellers', 'Estate' -]; - -export const loanTypes = [ - 'Conventional', 'FHA', 'VA', 'USDA', 'Downpayment Assistance Programs' -]; - -export const propertyTypes = [ - 'SFR', 'Townhome', 'condo', 'Manufactured', 'Apartment / Multifamily', - 'Rural', 'Luxury', 'Retirement', 'Investment/ Income Producing', - 'Duplex / Tri-plex / 4-plex', 'Historical', 'Mountain Property' -]; - -export const pricePoints = [ - 'Under $100K', '$100K - $300K', '$300K - $500K', '$500K - $1M', '$1M and above' -]; - -export const specialInterests = [ - 'Workshop / Trade / Craftspace', 'Gardening/Landscape', 'Boating', - 'Automotive', 'Hobby farm', 'Home theater' -]; - -export const popularFilters = [ - 'Sellers Agent', 'First-Time Buyer', 'Buyers Agent', 'Buyers Agent', - 'Buyers Agent', 'First-Time Seller', 'Buyers Agent' -]; - -export interface FilterState { - clientSpecialization: string[]; - loanType: string[]; - propertyType: string[]; - pricePoint: string[]; - specialInterests: string[]; - popularFilters: string[]; +// Types for dynamic filter data +export interface FilterOption { + label: string; + value: string; } +export interface FilterField { + slug: string; + name: string; + fieldType: string; + options: FilterOption[]; +} + +// Dynamic filter state - keys are field slugs +export type FilterState = Record; + interface FilterModalProps { isOpen: boolean; onClose: () => void; filters: FilterState; - onToggleFilter: (category: string, option: string) => void; + filterFields: FilterField[]; + onToggleFilter: (fieldSlug: string, optionValue: string) => void; onClearAll: () => void; onApply: () => void; } -export function FilterModal({ isOpen, onClose, filters, onToggleFilter, onClearAll, onApply }: FilterModalProps) { - const [expandedSections, setExpandedSections] = useState>({ - popularFilters: true, - clientSpecialization: true, - loanType: true, - propertyType: true, - specialInterests: true, - pricePoint: true, +export function FilterModal({ isOpen, onClose, filters, filterFields, onToggleFilter, onClearAll, onApply }: FilterModalProps) { + const [expandedSections, setExpandedSections] = useState>(() => { + // Initialize all sections as expanded + const initial: Record = {}; + filterFields.forEach(field => { + initial[field.slug] = true; + }); + return initial; }); const toggleSection = (section: string) => { @@ -84,217 +63,43 @@ export function FilterModal({ isOpen, onClose, filters, onToggleFilter, onClearA {/* Scrollable Content */}
- {/* Popular Filters */} -
- - {expandedSections.popularFilters && ( -
- {popularFilters.map((filter, index) => ( - - ))} + {filterFields.map((field, index) => ( +
+
+ + {expandedSections[field.slug] && ( +
6 ? 'grid-cols-3' : 'grid-cols-4'} gap-x-4 gap-y-3`}> + {field.options.map((option) => ( + + ))} +
+ )}
- )} -
+ {index < filterFields.length - 1 &&
} +
+ ))} -
- - {/* Client Specialization */} -
- - {expandedSections.clientSpecialization && ( - <> -
- {clientSpecializations.slice(0, 5).map((option) => ( - - ))} -
- - - )} -
- -
- - {/* Loan Type */} -
- - {expandedSections.loanType && ( - <> -
- {loanTypes.map((option) => ( - - ))} -
- - - )} -
- -
- - {/* Property Type */} -
- - {expandedSections.propertyType && ( - <> -
- {propertyTypes.map((option) => ( - - ))} -
- - - )} -
- -
- - {/* Special Interests and Hobbies */} -
- - {expandedSections.specialInterests && ( - <> -
- {specialInterests.map((option) => ( - - ))} -
- - - )} -
- -
- - {/* Price Point */} -
- - {expandedSections.pricePoint && ( -
- {pricePoints.map((option) => ( - - ))} -
- )} -
+ {filterFields.length === 0 && ( +
+

Loading filters...

+
+ )}
{/* Footer */} diff --git a/src/services/profile-sections.service.ts b/src/services/profile-sections.service.ts index 5be5b75..207401e 100644 --- a/src/services/profile-sections.service.ts +++ b/src/services/profile-sections.service.ts @@ -97,6 +97,14 @@ interface ApiResponse { message?: string; } +// Filterable field structure from backend +export interface FilterableField { + slug: string; + name: string; + fieldType: string; + options: FieldOption[]; +} + // Profile Sections Service for Web class ProfileSectionsService { private basePath = '/profile-sections'; @@ -116,6 +124,14 @@ class ProfileSectionsService { const response = await api.get>(url); return response.data.data; } + + /** + * Get all filterable fields with options (for search filters) + */ + async getFilterableFields(): Promise { + const response = await api.get>('/profile-fields/filterable'); + return response.data.data; + } } export const profileSectionsService = new ProfileSectionsService();