feat: Implement Combobox component and Autocomplete for Country

This commit is contained in:
gpt-engineer-app[bot]
2025-09-29 15:58:09 +00:00
parent 29f687b29e
commit 8b33a0d925
3 changed files with 301 additions and 13 deletions

View File

@@ -8,6 +8,8 @@ import { Slider } from '@/components/ui/slider';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { ChevronDown, Filter, X } from 'lucide-react'; import { ChevronDown, Filter, X } from 'lucide-react';
import { Combobox } from '@/components/ui/combobox';
import { useCountries, useStatesProvinces, useManufacturers, useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
export interface SearchFilters { export interface SearchFilters {
// Park filters // Park filters
@@ -48,6 +50,12 @@ interface SearchFiltersProps {
export function SearchFiltersComponent({ filters, onFiltersChange, activeTab }: SearchFiltersProps) { export function SearchFiltersComponent({ filters, onFiltersChange, activeTab }: SearchFiltersProps) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
// Fetch autocomplete data
const { countries, loading: countriesLoading } = useCountries();
const { statesProvinces, loading: statesLoading } = useStatesProvinces(filters.country);
const { manufacturers, loading: manufacturersLoading } = useManufacturers();
const { headquarters, loading: headquartersLoading } = useCompanyHeadquarters();
const updateFilter = (key: keyof SearchFilters, value: any) => { const updateFilter = (key: keyof SearchFilters, value: any) => {
onFiltersChange({ ...filters, [key]: value }); onFiltersChange({ ...filters, [key]: value });
}; };
@@ -145,19 +153,26 @@ export function SearchFiltersComponent({ filters, onFiltersChange, activeTab }:
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>Country</Label> <Label>Country</Label>
<Input <Combobox
placeholder="Enter country" options={countries}
value={filters.country || ''} value={filters.country}
onChange={(e) => updateFilter('country', e.target.value || undefined)} onValueChange={(value) => updateFilter('country', value || undefined)}
placeholder="Select country"
searchPlaceholder="Search countries..."
loading={countriesLoading}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>State/Province</Label> <Label>State/Province</Label>
<Input <Combobox
placeholder="Enter state/province" options={statesProvinces}
value={filters.stateProvince || ''} value={filters.stateProvince}
onChange={(e) => updateFilter('stateProvince', e.target.value || undefined)} onValueChange={(value) => updateFilter('stateProvince', value || undefined)}
placeholder="Select state/province"
searchPlaceholder="Search states/provinces..."
loading={statesLoading}
disabled={!filters.country}
/> />
</div> </div>
</div> </div>
@@ -273,6 +288,18 @@ export function SearchFiltersComponent({ filters, onFiltersChange, activeTab }:
</div> </div>
</div> </div>
<div className="space-y-2">
<Label>Manufacturer</Label>
<Combobox
options={manufacturers}
value={filters.manufacturer}
onValueChange={(value) => updateFilter('manufacturer', value || undefined)}
placeholder="Select manufacturer"
searchPlaceholder="Search manufacturers..."
loading={manufacturersLoading}
/>
</div>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -343,10 +370,13 @@ export function SearchFiltersComponent({ filters, onFiltersChange, activeTab }:
<div className="space-y-2"> <div className="space-y-2">
<Label>Headquarters</Label> <Label>Headquarters</Label>
<Input <Combobox
placeholder="Enter country/location" options={headquarters}
value={filters.headquarters || ''} value={filters.headquarters}
onChange={(e) => updateFilter('headquarters', e.target.value || undefined)} onValueChange={(value) => updateFilter('headquarters', value || undefined)}
placeholder="Select headquarters"
searchPlaceholder="Search headquarters..."
loading={headquartersLoading}
/> />
</div> </div>
</div> </div>

View File

@@ -0,0 +1,96 @@
import * as React from "react";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
export interface ComboboxOption {
label: string;
value: string;
}
interface ComboboxProps {
options: ComboboxOption[];
value?: string;
onValueChange?: (value: string) => void;
placeholder?: string;
searchPlaceholder?: string;
emptyText?: string;
disabled?: boolean;
className?: string;
loading?: boolean;
}
export function Combobox({
options,
value,
onValueChange,
placeholder = "Select option...",
searchPlaceholder = "Search...",
emptyText = "No options found.",
disabled = false,
className,
loading = false,
}: ComboboxProps) {
const [open, setOpen] = React.useState(false);
const selectedOption = options.find((option) => option.value === value);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn("w-full justify-between", className)}
disabled={disabled}
>
{selectedOption ? selectedOption.label : placeholder}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder={searchPlaceholder} />
<CommandList>
<CommandEmpty>{loading ? "Loading..." : emptyText}</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={(currentValue) => {
const newValue = currentValue === value ? "" : currentValue;
onValueChange?.(newValue);
setOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === option.value ? "opacity-100" : "opacity-0"
)}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,162 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { ComboboxOption } from '@/components/ui/combobox';
export function useCountries() {
const [countries, setCountries] = useState<ComboboxOption[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
async function fetchCountries() {
setLoading(true);
try {
const { data, error } = await supabase
.from('locations')
.select('country')
.not('country', 'is', null);
if (error) throw error;
const uniqueCountries = Array.from(
new Set(data?.map(item => item.country) || [])
).sort();
setCountries(
uniqueCountries.map(country => ({
label: country,
value: country.toLowerCase().replace(/\s+/g, '_')
}))
);
} catch (error) {
console.error('Error fetching countries:', error);
setCountries([]);
} finally {
setLoading(false);
}
}
fetchCountries();
}, []);
return { countries, loading };
}
export function useStatesProvinces(country?: string) {
const [statesProvinces, setStatesProvinces] = useState<ComboboxOption[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!country) {
setStatesProvinces([]);
return;
}
async function fetchStatesProvinces() {
setLoading(true);
try {
const { data, error } = await supabase
.from('locations')
.select('state_province')
.eq('country', country)
.not('state_province', 'is', null);
if (error) throw error;
const uniqueStates = Array.from(
new Set(data?.map(item => item.state_province) || [])
).sort();
setStatesProvinces(
uniqueStates.map(state => ({
label: state,
value: state.toLowerCase().replace(/\s+/g, '_')
}))
);
} catch (error) {
console.error('Error fetching states/provinces:', error);
setStatesProvinces([]);
} finally {
setLoading(false);
}
}
fetchStatesProvinces();
}, [country]);
return { statesProvinces, loading };
}
export function useManufacturers() {
const [manufacturers, setManufacturers] = useState<ComboboxOption[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
async function fetchManufacturers() {
setLoading(true);
try {
const { data, error } = await supabase
.from('companies')
.select('name')
.eq('company_type', 'manufacturer')
.order('name');
if (error) throw error;
setManufacturers(
(data || []).map(company => ({
label: company.name,
value: company.name.toLowerCase().replace(/\s+/g, '_')
}))
);
} catch (error) {
console.error('Error fetching manufacturers:', error);
setManufacturers([]);
} finally {
setLoading(false);
}
}
fetchManufacturers();
}, []);
return { manufacturers, loading };
}
export function useCompanyHeadquarters() {
const [headquarters, setHeadquarters] = useState<ComboboxOption[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
async function fetchHeadquarters() {
setLoading(true);
try {
const { data, error } = await supabase
.from('companies')
.select('headquarters_location')
.not('headquarters_location', 'is', null);
if (error) throw error;
const uniqueHeadquarters = Array.from(
new Set(data?.map(item => item.headquarters_location) || [])
).sort();
setHeadquarters(
uniqueHeadquarters.map(hq => ({
label: hq,
value: hq.toLowerCase().replace(/\s+/g, '_')
}))
);
} catch (error) {
console.error('Error fetching headquarters:', error);
setHeadquarters([]);
} finally {
setLoading(false);
}
}
fetchHeadquarters();
}, []);
return { headquarters, loading };
}