From 8b33a0d925945052fd3c16789685cc82fb682646 Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Mon, 29 Sep 2025 15:58:09 +0000
Subject: [PATCH] feat: Implement Combobox component and Autocomplete for
Country
---
src/components/search/SearchFilters.tsx | 56 ++++++--
src/components/ui/combobox.tsx | 96 ++++++++++++++
src/hooks/useAutocompleteData.ts | 162 ++++++++++++++++++++++++
3 files changed, 301 insertions(+), 13 deletions(-)
create mode 100644 src/components/ui/combobox.tsx
create mode 100644 src/hooks/useAutocompleteData.ts
diff --git a/src/components/search/SearchFilters.tsx b/src/components/search/SearchFilters.tsx
index 2f28d0fe..91328f05 100644
--- a/src/components/search/SearchFilters.tsx
+++ b/src/components/search/SearchFilters.tsx
@@ -8,6 +8,8 @@ import { Slider } from '@/components/ui/slider';
import { Badge } from '@/components/ui/badge';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { ChevronDown, Filter, X } from 'lucide-react';
+import { Combobox } from '@/components/ui/combobox';
+import { useCountries, useStatesProvinces, useManufacturers, useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
export interface SearchFilters {
// Park filters
@@ -47,7 +49,13 @@ interface SearchFiltersProps {
export function SearchFiltersComponent({ filters, onFiltersChange, activeTab }: SearchFiltersProps) {
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) => {
onFiltersChange({ ...filters, [key]: value });
};
@@ -145,19 +153,26 @@ export function SearchFiltersComponent({ filters, onFiltersChange, activeTab }:
@@ -273,6 +288,18 @@ export function SearchFiltersComponent({ filters, onFiltersChange, activeTab }:
+
+
+ updateFilter('manufacturer', value || undefined)}
+ placeholder="Select manufacturer"
+ searchPlaceholder="Search manufacturers..."
+ loading={manufacturersLoading}
+ />
+
+
diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx
new file mode 100644
index 00000000..9a80a32d
--- /dev/null
+++ b/src/components/ui/combobox.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+ {loading ? "Loading..." : emptyText}
+
+ {options.map((option) => (
+ {
+ const newValue = currentValue === value ? "" : currentValue;
+ onValueChange?.(newValue);
+ setOpen(false);
+ }}
+ >
+
+ {option.label}
+
+ ))}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/hooks/useAutocompleteData.ts b/src/hooks/useAutocompleteData.ts
new file mode 100644
index 00000000..4b93e874
--- /dev/null
+++ b/src/hooks/useAutocompleteData.ts
@@ -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
([]);
+ 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([]);
+ 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([]);
+ 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([]);
+ 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 };
+}
\ No newline at end of file