diff --git a/src/components/profile/RideCreditFilters.tsx b/src/components/profile/RideCreditFilters.tsx index e522fe4a..4b0aa0e6 100644 --- a/src/components/profile/RideCreditFilters.tsx +++ b/src/components/profile/RideCreditFilters.tsx @@ -4,6 +4,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Slider } from '@/components/ui/slider'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { MultiSelectCombobox } from '@/components/ui/multi-select-combobox'; import { RideCreditFilters as FilterTypes } from '@/types/ride-credits'; import { UserRideCredit } from '@/types/database'; import { Search, X, ChevronDown, MapPin, Building2, Factory, Calendar, Star, MessageSquare, Image } from 'lucide-react'; @@ -159,38 +160,36 @@ export function RideCreditFilters({ {filterOptions.countries.length > 0 && ( -
+
-
- {filterOptions.countries.map(country => ( - toggleArrayFilter('countries', country)} - > - {country} - - ))} -
+ ({ label: c, value: c }))} + value={filters.countries || []} + onValueChange={(values) => + onFilterChange('countries', values.length > 0 ? values : undefined) + } + placeholder="Select countries..." + searchPlaceholder="Search countries..." + emptyText="No countries found" + className={compact ? "h-9 text-sm" : ""} + />
)} {filterOptions.states.length > 0 && ( -
+
-
- {filterOptions.states.map(state => ( - toggleArrayFilter('statesProvinces', state)} - > - {state} - - ))} -
+ ({ label: s, value: s }))} + value={filters.statesProvinces || []} + onValueChange={(values) => + onFilterChange('statesProvinces', values.length > 0 ? values : undefined) + } + placeholder="Select states/provinces..." + searchPlaceholder="Search states/provinces..." + emptyText="No states/provinces found" + className={compact ? "h-9 text-sm" : ""} + />
)} @@ -210,20 +209,19 @@ export function RideCreditFilters({ {filterOptions.parks.length > 0 && ( -
+
-
- {filterOptions.parks.map(park => ( - toggleArrayFilter('parks', park.id)} - > - {park.name} - - ))} -
+ ({ label: p.name, value: p.id }))} + value={filters.parks || []} + onValueChange={(values) => + onFilterChange('parks', values.length > 0 ? values : undefined) + } + placeholder="Select parks..." + searchPlaceholder="Search parks..." + emptyText="No parks found" + className={compact ? "h-9 text-sm" : ""} + />
)} @@ -243,20 +241,19 @@ export function RideCreditFilters({ {filterOptions.manufacturers.length > 0 && ( -
+
-
- {filterOptions.manufacturers.map(mfr => ( - toggleArrayFilter('manufacturers', mfr.id)} - > - {mfr.name} - - ))} -
+ ({ label: m.name, value: m.id }))} + value={filters.manufacturers || []} + onValueChange={(values) => + onFilterChange('manufacturers', values.length > 0 ? values : undefined) + } + placeholder="Select manufacturers..." + searchPlaceholder="Search manufacturers..." + emptyText="No manufacturers found" + className={compact ? "h-9 text-sm" : ""} + />
)} diff --git a/src/components/ui/multi-select-combobox.tsx b/src/components/ui/multi-select-combobox.tsx new file mode 100644 index 00000000..183a98a5 --- /dev/null +++ b/src/components/ui/multi-select-combobox.tsx @@ -0,0 +1,143 @@ +import * as React from "react"; +import { useState, useMemo } from "react"; +import { Check, ChevronsUpDown, X } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +export interface MultiSelectOption { + label: string; + value: string; +} + +interface MultiSelectComboboxProps { + options: MultiSelectOption[]; + value?: string[]; + onValueChange: (values: string[]) => void; + placeholder?: string; + searchPlaceholder?: string; + emptyText?: string; + maxDisplay?: number; + className?: string; +} + +export function MultiSelectCombobox({ + options, + value = [], + onValueChange, + placeholder = "Select options...", + searchPlaceholder = "Search...", + emptyText = "No options found.", + maxDisplay = 2, + className, +}: MultiSelectComboboxProps) { + const [open, setOpen] = useState(false); + + const handleSelect = (selectedValue: string) => { + const newValues = value.includes(selectedValue) + ? value.filter((v) => v !== selectedValue) + : [...value, selectedValue]; + onValueChange(newValues); + }; + + const handleRemove = (valueToRemove: string) => { + onValueChange(value.filter((v) => v !== valueToRemove)); + }; + + const displayText = useMemo(() => { + if (value.length === 0) return placeholder; + if (value.length <= maxDisplay) { + return value + .map((v) => options.find((o) => o.value === v)?.label) + .filter(Boolean) + .join(", "); + } + return `${value.length} selected`; + }, [value, options, maxDisplay, placeholder]); + + return ( +
+ + + + + + + + + {emptyText} + + {options.map((option) => ( + handleSelect(option.value)} + > + + {option.label} + + ))} + + + + + + + {/* Selected Items Display */} + {value.length > 0 && ( +
+ {value.map((v) => { + const option = options.find((o) => o.value === v); + return option ? ( + + {option.label} + { + e.stopPropagation(); + handleRemove(v); + }} + /> + + ) : null; + })} + {value.length > 1 && ( + + )} +
+ )} +
+ ); +}