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,7 +4,8 @@ import { SettingsSidebar, ProfileSettingsForm } from '@/components/settings';
|
|||||||
|
|
||||||
export default function ProfileSettingsPage() {
|
export default function ProfileSettingsPage() {
|
||||||
const handleSave = (data: {
|
const handleSave = (data: {
|
||||||
fullName: string;
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
career: string;
|
career: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import { useState, useEffect, useCallback, Suspense } from 'react';
|
|||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
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 { agentsService, AgentType, PublicAgentProfile, SearchAgentsParams } from '@/services/agents.service';
|
||||||
|
import { profileSectionsService, FilterableField } from '@/services/profile-sections.service';
|
||||||
import { uploadService } from '@/services/upload.service';
|
import { uploadService } from '@/services/upload.service';
|
||||||
|
|
||||||
interface FilterSectionProps {
|
interface FilterSectionProps {
|
||||||
title: string;
|
title: string;
|
||||||
options: string[];
|
options: { label: string; value: string }[];
|
||||||
selectedOptions: string[];
|
selectedOptions: string[];
|
||||||
onToggle: (option: string) => void;
|
onToggle: (optionValue: string) => void;
|
||||||
showMore?: boolean;
|
showMore?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,14 +33,14 @@ function FilterSection({ title, options, selectedOptions, onToggle, showMore }:
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{displayOptions.map((option) => (
|
{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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedOptions.includes(option)}
|
checked={selectedOptions.includes(option.value)}
|
||||||
onChange={() => onToggle(option)}
|
onChange={() => onToggle(option.value)}
|
||||||
className="w-4 h-4 rounded border-[#00293d]/30 text-[#e58625] focus:ring-[#e58625]"
|
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>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -239,48 +240,60 @@ function ProfilesPageContent() {
|
|||||||
// State for API data
|
// State for API data
|
||||||
const [agents, setAgents] = useState<PublicAgentProfile[]>([]);
|
const [agents, setAgents] = useState<PublicAgentProfile[]>([]);
|
||||||
const [agentTypes, setAgentTypes] = useState<AgentType[]>([]);
|
const [agentTypes, setAgentTypes] = useState<AgentType[]>([]);
|
||||||
|
const [filterFields, setFilterFields] = useState<FilterField[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [totalResults, setTotalResults] = useState(0);
|
const [totalResults, setTotalResults] = useState(0);
|
||||||
|
|
||||||
// State for search and filters
|
// State for search and filters - initialize from URL params
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState(() => searchParams.get('name') || '');
|
||||||
const [activeTypeId, setActiveTypeId] = useState<string | null>(null);
|
const [activeTypeId, setActiveTypeId] = useState<string | null>(() => searchParams.get('type'));
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
const [listingType, setListingType] = useState<'agents' | 'lenders'>('agents');
|
const [listingType, setListingType] = useState<'agents' | 'lenders'>('agents');
|
||||||
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
|
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
|
||||||
const [filters, setFilters] = useState<FilterState>({
|
const [filters, setFilters] = useState<FilterState>({});
|
||||||
clientSpecialization: [],
|
|
||||||
loanType: [],
|
|
||||||
propertyType: [],
|
|
||||||
pricePoint: [],
|
|
||||||
specialInterests: [],
|
|
||||||
popularFilters: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize from URL params
|
// Mark as initialized after first render and sync with URL param changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const type = searchParams.get('type');
|
setIsInitialized(true);
|
||||||
const name = searchParams.get('name');
|
}, []);
|
||||||
const location = searchParams.get('location');
|
|
||||||
|
|
||||||
if (type) setActiveTypeId(type);
|
// Sync state when URL params change (e.g., browser back/forward)
|
||||||
if (name) setSearchQuery(name);
|
|
||||||
// Location is handled separately in search params
|
|
||||||
}, [searchParams]);
|
|
||||||
|
|
||||||
// Fetch agent types on mount
|
|
||||||
useEffect(() => {
|
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 {
|
try {
|
||||||
const types = await agentsService.getAgentTypes();
|
const [types, fields] = await Promise.all([
|
||||||
|
agentsService.getAgentTypes(),
|
||||||
|
profileSectionsService.getFilterableFields(),
|
||||||
|
]);
|
||||||
setAgentTypes(types);
|
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) {
|
} 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
|
// Fetch agents based on filters
|
||||||
@@ -325,29 +338,25 @@ function ProfilesPageContent() {
|
|||||||
}
|
}
|
||||||
}, [currentPage, searchQuery, activeTypeId, searchParams]);
|
}, [currentPage, searchQuery, activeTypeId, searchParams]);
|
||||||
|
|
||||||
// Fetch agents when filters change
|
// Fetch agents when filters change (only after initialization)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAgents();
|
if (isInitialized) {
|
||||||
}, [fetchAgents]);
|
fetchAgents();
|
||||||
|
}
|
||||||
|
}, [fetchAgents, isInitialized]);
|
||||||
|
|
||||||
const toggleFilter = (category: string, option: string) => {
|
const toggleFilter = (fieldSlug: string, optionValue: string) => {
|
||||||
setFilters(prev => ({
|
setFilters(prev => {
|
||||||
...prev,
|
const currentValues = prev[fieldSlug] || [];
|
||||||
[category]: (prev[category as keyof FilterState] || []).includes(option)
|
const newValues = currentValues.includes(optionValue)
|
||||||
? (prev[category as keyof FilterState] || []).filter((o: string) => o !== option)
|
? currentValues.filter(v => v !== optionValue)
|
||||||
: [...(prev[category as keyof FilterState] || []), option]
|
: [...currentValues, optionValue];
|
||||||
}));
|
return { ...prev, [fieldSlug]: newValues };
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearAllFilters = () => {
|
const clearAllFilters = () => {
|
||||||
setFilters({
|
setFilters({});
|
||||||
clientSpecialization: [],
|
|
||||||
loanType: [],
|
|
||||||
propertyType: [],
|
|
||||||
pricePoint: [],
|
|
||||||
specialInterests: [],
|
|
||||||
popularFilters: [],
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyFilters = () => {
|
const applyFilters = () => {
|
||||||
@@ -366,6 +375,14 @@ function ProfilesPageContent() {
|
|||||||
setCurrentPage(1);
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
@@ -409,34 +426,27 @@ function ProfilesPageContent() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FilterSection
|
{/* Dynamic Filter Sections */}
|
||||||
title="Client Specialization"
|
{sidebarFilterSlugs.map(slug => {
|
||||||
options={clientSpecializations}
|
const field = getFilterFieldBySlug(slug);
|
||||||
selectedOptions={filters.clientSpecialization}
|
if (!field) return null;
|
||||||
onToggle={(option) => toggleFilter('clientSpecialization', option)}
|
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
|
{filterFields.length === 0 && (
|
||||||
title="Loan Type"
|
<div className="py-4">
|
||||||
options={loanTypes}
|
<p className="font-serif text-[13px] text-[#00293d]/50">Loading filters...</p>
|
||||||
selectedOptions={filters.loanType}
|
</div>
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -576,6 +586,7 @@ function ProfilesPageContent() {
|
|||||||
isOpen={isFilterModalOpen}
|
isOpen={isFilterModalOpen}
|
||||||
onClose={() => setIsFilterModalOpen(false)}
|
onClose={() => setIsFilterModalOpen(false)}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
|
filterFields={filterFields}
|
||||||
onToggleFilter={toggleFilter}
|
onToggleFilter={toggleFilter}
|
||||||
onClearAll={clearAllFilters}
|
onClearAll={clearAllFilters}
|
||||||
onApply={applyFilters}
|
onApply={applyFilters}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { SettingsSidebar, ProfileSettingsForm } from '@/components/settings';
|
|||||||
|
|
||||||
export default function UserProfileSettingsPage() {
|
export default function UserProfileSettingsPage() {
|
||||||
const handleSave = (data: {
|
const handleSave = (data: {
|
||||||
fullName: string;
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
career: string;
|
career: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
|
|||||||
@@ -2,61 +2,40 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
// Filter options data
|
// Types for dynamic filter data
|
||||||
export const clientSpecializations = [
|
export interface FilterOption {
|
||||||
'Buyers', 'Sellers', 'Divorce', 'First time buyers', 'First time sellers', 'Estate'
|
label: string;
|
||||||
];
|
value: string;
|
||||||
|
|
||||||
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[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FilterField {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
fieldType: string;
|
||||||
|
options: FilterOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic filter state - keys are field slugs
|
||||||
|
export type FilterState = Record<string, string[]>;
|
||||||
|
|
||||||
interface FilterModalProps {
|
interface FilterModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
filters: FilterState;
|
filters: FilterState;
|
||||||
onToggleFilter: (category: string, option: string) => void;
|
filterFields: FilterField[];
|
||||||
|
onToggleFilter: (fieldSlug: string, optionValue: string) => void;
|
||||||
onClearAll: () => void;
|
onClearAll: () => void;
|
||||||
onApply: () => void;
|
onApply: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FilterModal({ isOpen, onClose, filters, onToggleFilter, onClearAll, onApply }: FilterModalProps) {
|
export function FilterModal({ isOpen, onClose, filters, filterFields, onToggleFilter, onClearAll, onApply }: FilterModalProps) {
|
||||||
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
|
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>(() => {
|
||||||
popularFilters: true,
|
// Initialize all sections as expanded
|
||||||
clientSpecialization: true,
|
const initial: Record<string, boolean> = {};
|
||||||
loanType: true,
|
filterFields.forEach(field => {
|
||||||
propertyType: true,
|
initial[field.slug] = true;
|
||||||
specialInterests: true,
|
});
|
||||||
pricePoint: true,
|
return initial;
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleSection = (section: string) => {
|
const toggleSection = (section: string) => {
|
||||||
@@ -84,217 +63,43 @@ export function FilterModal({ isOpen, onClose, filters, onToggleFilter, onClearA
|
|||||||
|
|
||||||
{/* Scrollable Content */}
|
{/* Scrollable Content */}
|
||||||
<div className="overflow-y-auto max-h-[calc(90vh-180px)] px-8 py-5">
|
<div className="overflow-y-auto max-h-[calc(90vh-180px)] px-8 py-5">
|
||||||
{/* Popular Filters */}
|
{filterFields.map((field, index) => (
|
||||||
<div className="mb-6">
|
<div key={field.slug}>
|
||||||
<button
|
<div className="mb-6">
|
||||||
onClick={() => toggleSection('popularFilters')}
|
<button
|
||||||
className="flex items-center gap-2 font-fractul text-[20px] text-[#00293d] mb-4"
|
onClick={() => toggleSection(field.slug)}
|
||||||
>
|
className="flex items-center gap-2 font-fractul text-[20px] text-[#00293d] mb-4"
|
||||||
Popular Filters
|
>
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" className={`transition-transform ${expandedSections.popularFilters ? 'rotate-180' : ''}`}>
|
{field.name}
|
||||||
<path d="M2 4L6 8L10 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" className={`transition-transform ${expandedSections[field.slug] ? 'rotate-180' : ''}`}>
|
||||||
</svg>
|
<path d="M2 4L6 8L10 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
</button>
|
</svg>
|
||||||
{expandedSections.popularFilters && (
|
</button>
|
||||||
<div className="flex flex-wrap gap-2">
|
{expandedSections[field.slug] && (
|
||||||
{popularFilters.map((filter, index) => (
|
<div className={`grid ${field.options.length > 6 ? 'grid-cols-3' : 'grid-cols-4'} gap-x-4 gap-y-3`}>
|
||||||
<button
|
{field.options.map((option) => (
|
||||||
key={index}
|
<label key={option.value} className="flex items-center gap-2 cursor-pointer">
|
||||||
onClick={() => onToggleFilter('popularFilters', filter)}
|
<input
|
||||||
className={`px-4 py-2.5 rounded-[15px] border border-[#00293d] font-serif text-[14px] transition-colors ${
|
type="checkbox"
|
||||||
filters.popularFilters?.includes(filter)
|
checked={(filters[field.slug] || []).includes(option.value)}
|
||||||
? 'bg-[#00293d] text-white'
|
onChange={() => onToggleFilter(field.slug, option.value)}
|
||||||
: 'bg-white text-[#00293d] hover:bg-[#00293d]/5'
|
className="w-[17px] h-[17px] rounded-[5px] border-[#00293d]/20 text-[#e58625] focus:ring-[#e58625]"
|
||||||
}`}
|
/>
|
||||||
>
|
<span className="font-serif text-[15px] text-[#00293d]">{option.label}</span>
|
||||||
{filter}
|
</label>
|
||||||
</button>
|
))}
|
||||||
))}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
{index < filterFields.length - 1 && <div className="border-t border-[#d9d9d9] my-4" />}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
<div className="border-t border-[#d9d9d9] my-4" />
|
{filterFields.length === 0 && (
|
||||||
|
<div className="text-center py-8">
|
||||||
{/* Client Specialization */}
|
<p className="font-serif text-[15px] text-[#00293d]/60">Loading filters...</p>
|
||||||
<div className="mb-6">
|
</div>
|
||||||
<button
|
)}
|
||||||
onClick={() => toggleSection('clientSpecialization')}
|
|
||||||
className="flex items-center gap-2 font-fractul text-[20px] text-[#00293d] mb-4"
|
|
||||||
>
|
|
||||||
Client Specialization
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" className={`transition-transform ${expandedSections.clientSpecialization ? 'rotate-180' : ''}`}>
|
|
||||||
<path d="M2 4L6 8L10 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{expandedSections.clientSpecialization && (
|
|
||||||
<>
|
|
||||||
<div className="grid grid-cols-4 gap-x-4 gap-y-3">
|
|
||||||
{clientSpecializations.slice(0, 5).map((option) => (
|
|
||||||
<label key={option} className="flex items-center gap-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={filters.clientSpecialization.includes(option)}
|
|
||||||
onChange={() => onToggleFilter('clientSpecialization', option)}
|
|
||||||
className="w-[17px] h-[17px] rounded-[5px] border-[#00293d]/20 text-[#e58625] focus:ring-[#e58625]"
|
|
||||||
/>
|
|
||||||
<span className="font-serif text-[15px] text-[#00293d]">{option}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button className="mt-3 font-serif text-[15px] text-[#00293d] font-light flex items-center gap-1">
|
|
||||||
Show More
|
|
||||||
<svg width="10" height="10" 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="border-t border-[#d9d9d9] my-4" />
|
|
||||||
|
|
||||||
{/* Loan Type */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<button
|
|
||||||
onClick={() => toggleSection('loanType')}
|
|
||||||
className="flex items-center gap-2 font-fractul text-[20px] text-[#00293d] mb-4"
|
|
||||||
>
|
|
||||||
Loan Type
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" className={`transition-transform ${expandedSections.loanType ? 'rotate-180' : ''}`}>
|
|
||||||
<path d="M2 4L6 8L10 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{expandedSections.loanType && (
|
|
||||||
<>
|
|
||||||
<div className="grid grid-cols-4 gap-x-4 gap-y-3">
|
|
||||||
{loanTypes.map((option) => (
|
|
||||||
<label key={option} className="flex items-center gap-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={filters.loanType.includes(option)}
|
|
||||||
onChange={() => onToggleFilter('loanType', option)}
|
|
||||||
className="w-[17px] h-[17px] rounded-[5px] border-[#00293d]/20 text-[#e58625] focus:ring-[#e58625]"
|
|
||||||
/>
|
|
||||||
<span className="font-serif text-[15px] text-[#00293d]">{option}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button className="mt-3 font-serif text-[15px] text-[#00293d] font-light flex items-center gap-1">
|
|
||||||
Show More
|
|
||||||
<svg width="10" height="10" 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="border-t border-[#d9d9d9] my-4" />
|
|
||||||
|
|
||||||
{/* Property Type */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<button
|
|
||||||
onClick={() => toggleSection('propertyType')}
|
|
||||||
className="flex items-center gap-2 font-fractul text-[20px] text-[#00293d] mb-4"
|
|
||||||
>
|
|
||||||
Property Type
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" className={`transition-transform ${expandedSections.propertyType ? 'rotate-180' : ''}`}>
|
|
||||||
<path d="M2 4L6 8L10 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{expandedSections.propertyType && (
|
|
||||||
<>
|
|
||||||
<div className="grid grid-cols-3 gap-x-4 gap-y-3">
|
|
||||||
{propertyTypes.map((option) => (
|
|
||||||
<label key={option} className="flex items-center gap-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={filters.propertyType.includes(option)}
|
|
||||||
onChange={() => onToggleFilter('propertyType', option)}
|
|
||||||
className="w-[17px] h-[17px] rounded-[5px] border-[#00293d]/20 text-[#e58625] focus:ring-[#e58625]"
|
|
||||||
/>
|
|
||||||
<span className="font-serif text-[15px] text-[#00293d]">{option}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button className="mt-3 font-serif text-[15px] text-[#00293d] font-light flex items-center gap-1">
|
|
||||||
Show More
|
|
||||||
<svg width="10" height="10" 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="border-t border-[#d9d9d9] my-4" />
|
|
||||||
|
|
||||||
{/* Special Interests and Hobbies */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<button
|
|
||||||
onClick={() => toggleSection('specialInterests')}
|
|
||||||
className="flex items-center gap-2 font-fractul text-[20px] text-[#00293d] mb-4"
|
|
||||||
>
|
|
||||||
Special Interests and Hobbies
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" className={`transition-transform ${expandedSections.specialInterests ? 'rotate-180' : ''}`}>
|
|
||||||
<path d="M2 4L6 8L10 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{expandedSections.specialInterests && (
|
|
||||||
<>
|
|
||||||
<div className="grid grid-cols-3 gap-x-4 gap-y-3">
|
|
||||||
{specialInterests.map((option) => (
|
|
||||||
<label key={option} className="flex items-center gap-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={filters.specialInterests?.includes(option) || false}
|
|
||||||
onChange={() => onToggleFilter('specialInterests', option)}
|
|
||||||
className="w-[17px] h-[17px] rounded-[5px] border-[#00293d]/20 text-[#e58625] focus:ring-[#e58625]"
|
|
||||||
/>
|
|
||||||
<span className="font-serif text-[15px] text-[#00293d]">{option}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button className="mt-3 font-serif text-[15px] text-[#00293d] font-light flex items-center gap-1">
|
|
||||||
Show More
|
|
||||||
<svg width="10" height="10" 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="border-t border-[#d9d9d9] my-4" />
|
|
||||||
|
|
||||||
{/* Price Point */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<button
|
|
||||||
onClick={() => toggleSection('pricePoint')}
|
|
||||||
className="flex items-center gap-2 font-fractul text-[20px] text-[#00293d] mb-4"
|
|
||||||
>
|
|
||||||
Price Point
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" className={`transition-transform ${expandedSections.pricePoint ? 'rotate-180' : ''}`}>
|
|
||||||
<path d="M2 4L6 8L10 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{expandedSections.pricePoint && (
|
|
||||||
<div className="grid grid-cols-3 gap-x-4 gap-y-3">
|
|
||||||
{pricePoints.map((option) => (
|
|
||||||
<label key={option} className="flex items-center gap-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={filters.pricePoint.includes(option)}
|
|
||||||
onChange={() => onToggleFilter('pricePoint', option)}
|
|
||||||
className="w-[17px] h-[17px] rounded-[5px] border-[#00293d]/20 text-[#e58625] focus:ring-[#e58625]"
|
|
||||||
/>
|
|
||||||
<span className="font-serif text-[15px] text-[#00293d]">{option}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
|
|||||||
@@ -97,6 +97,14 @@ interface ApiResponse<T> {
|
|||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filterable field structure from backend
|
||||||
|
export interface FilterableField {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
fieldType: string;
|
||||||
|
options: FieldOption[];
|
||||||
|
}
|
||||||
|
|
||||||
// Profile Sections Service for Web
|
// Profile Sections Service for Web
|
||||||
class ProfileSectionsService {
|
class ProfileSectionsService {
|
||||||
private basePath = '/profile-sections';
|
private basePath = '/profile-sections';
|
||||||
@@ -116,6 +124,14 @@ class ProfileSectionsService {
|
|||||||
const response = await api.get<ApiResponse<ProfileSection[]>>(url);
|
const response = await api.get<ApiResponse<ProfileSection[]>>(url);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all filterable fields with options (for search filters)
|
||||||
|
*/
|
||||||
|
async getFilterableFields(): Promise<FilterableField[]> {
|
||||||
|
const response = await api.get<ApiResponse<FilterableField[]>>('/profile-fields/filterable');
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const profileSectionsService = new ProfileSectionsService();
|
export const profileSectionsService = new ProfileSectionsService();
|
||||||
|
|||||||
Reference in New Issue
Block a user