feat: add location autocomplete functionality to HeroSection search input
This commit is contained in:
@@ -20,6 +20,7 @@ export function HeroSection({ content }: { content?: HeroContent }) {
|
|||||||
const [agentTypes, setAgentTypes] = useState<AgentType[]>([]);
|
const [agentTypes, setAgentTypes] = useState<AgentType[]>([]);
|
||||||
const [specializationOptions, setSpecializationOptions] = useState<{ id: string; name: string }[]>([]);
|
const [specializationOptions, setSpecializationOptions] = useState<{ id: string; name: string }[]>([]);
|
||||||
const [specializationSlug, setSpecializationSlug] = useState<string>('');
|
const [specializationSlug, setSpecializationSlug] = useState<string>('');
|
||||||
|
const [locationOptions, setLocationOptions] = useState<{ label: string; value: string }[]>([]);
|
||||||
const [searchParams, setSearchParams] = useState({
|
const [searchParams, setSearchParams] = useState({
|
||||||
specialization: '',
|
specialization: '',
|
||||||
type: '',
|
type: '',
|
||||||
@@ -47,6 +48,30 @@ export function HeroSection({ content }: { content?: HeroContent }) {
|
|||||||
setSpecializationSlug(specField.slug);
|
setSpecializationSlug(specField.slug);
|
||||||
setSpecializationOptions(specField.options.map(o => ({ id: o.value, name: o.label })));
|
setSpecializationOptions(specField.options.map(o => ({ id: o.value, name: o.label })));
|
||||||
}
|
}
|
||||||
|
// Collect city + state options for location autocomplete.
|
||||||
|
// Cities in the system may carry a state prefix (e.g. "ne_adams") — strip it for display.
|
||||||
|
const formatCity = (label: string) => label.replace(/^[A-Z]{2}\s*[-–—]\s*/, '').trim();
|
||||||
|
const locs: { label: string; value: string }[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const f of fields) {
|
||||||
|
if (f.slug === 'state') {
|
||||||
|
for (const opt of f.options) {
|
||||||
|
const key = `state:${opt.label.toLowerCase()}`;
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
locs.push({ label: opt.label, value: opt.label });
|
||||||
|
}
|
||||||
|
} else if (f.slug === 'city') {
|
||||||
|
for (const opt of f.options) {
|
||||||
|
const clean = formatCity(opt.label);
|
||||||
|
const key = `city:${clean.toLowerCase()}`;
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
locs.push({ label: clean, value: clean });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLocationOptions(locs);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch specializations:', error);
|
console.error('Failed to fetch specializations:', error);
|
||||||
}
|
}
|
||||||
@@ -55,6 +80,10 @@ export function HeroSection({ content }: { content?: HeroContent }) {
|
|||||||
fetchSpecializations();
|
fetchSpecializations();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const filteredLocationOptions = searchParams.location.trim()
|
||||||
|
? locationOptions.filter(o => o.label.toLowerCase().includes(searchParams.location.trim().toLowerCase())).slice(0, 8)
|
||||||
|
: [];
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
// Validate that at least one field has a value
|
// Validate that at least one field has a value
|
||||||
const hasType = searchParams.type.trim() !== '';
|
const hasType = searchParams.type.trim() !== '';
|
||||||
@@ -182,15 +211,39 @@ export function HeroSection({ content }: { content?: HeroContent }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Location Input */}
|
{/* Location Input with autocomplete */}
|
||||||
<div className="flex-1 md:min-w-[140px]">
|
<div className="relative flex-1 md:min-w-[140px]">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Location"
|
placeholder="Location"
|
||||||
value={searchParams.location}
|
value={searchParams.location}
|
||||||
onChange={(e) => handleInputChange('location', e.target.value)}
|
onChange={(e) => {
|
||||||
|
handleInputChange('location', e.target.value);
|
||||||
|
setOpenDropdown('location');
|
||||||
|
}}
|
||||||
|
onFocus={() => setOpenDropdown('location')}
|
||||||
|
onBlur={() => setTimeout(() => setOpenDropdown((cur) => (cur === 'location' ? null : cur)), 150)}
|
||||||
|
autoComplete="off"
|
||||||
className="w-full h-[42px] px-4 rounded-[7px] border border-[#00293d]/10 bg-white font-serif text-sm text-[#00293d] placeholder:text-[#00293d] focus:outline-none"
|
className="w-full h-[42px] px-4 rounded-[7px] border border-[#00293d]/10 bg-white font-serif text-sm text-[#00293d] placeholder:text-[#00293d] focus:outline-none"
|
||||||
/>
|
/>
|
||||||
|
{openDropdown === 'location' && filteredLocationOptions.length > 0 && (
|
||||||
|
<div className="absolute top-full left-0 right-0 mt-0 bg-white rounded-[7px] border border-[#00293d]/10 shadow-lg z-20 overflow-hidden max-h-[240px] overflow-y-auto">
|
||||||
|
{filteredLocationOptions.map((option, index, arr) => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSearchParams((prev) => ({ ...prev, location: option.value }));
|
||||||
|
setOpenDropdown(null);
|
||||||
|
setValidationError(null);
|
||||||
|
}}
|
||||||
|
className={`w-full px-4 py-2.5 text-left font-serif text-sm text-[#00293d] hover:bg-[#00293d]/5 ${index < arr.length - 1 ? 'border-b border-[#00293d]/10' : ''}`}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Specialization Select */}
|
{/* Specialization Select */}
|
||||||
|
|||||||
Reference in New Issue
Block a user