mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 18:11:12 -05:00
Implement multi-select search combobox
This commit is contained in:
@@ -7,7 +7,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/component
|
||||
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';
|
||||
import { X, ChevronDown, MapPin, Building2, Factory, Calendar, Star, MessageSquare, Image } from 'lucide-react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
|
||||
@@ -67,6 +67,48 @@ export function RideCreditFilters({
|
||||
};
|
||||
}, [credits]);
|
||||
|
||||
const searchOptions = useMemo(() => {
|
||||
const rides = new Map<string, { name: string, park: string }>();
|
||||
const parks = new Map<string, string>();
|
||||
const manufacturers = new Map<string, string>();
|
||||
|
||||
credits.forEach(credit => {
|
||||
// Rides with their park context
|
||||
if (credit.rides?.id && credit.rides?.name) {
|
||||
rides.set(credit.rides.id, {
|
||||
name: credit.rides.name,
|
||||
park: credit.rides.parks?.name || 'Unknown Park'
|
||||
});
|
||||
}
|
||||
|
||||
// Parks
|
||||
if (credit.rides?.parks?.id && credit.rides?.parks?.name) {
|
||||
parks.set(credit.rides.parks.id, credit.rides.parks.name);
|
||||
}
|
||||
|
||||
// Manufacturers
|
||||
if (credit.rides?.manufacturer?.id && credit.rides?.manufacturer?.name) {
|
||||
manufacturers.set(credit.rides.manufacturer.id, credit.rides.manufacturer.name);
|
||||
}
|
||||
});
|
||||
|
||||
// Combine into single options array with type prefixes
|
||||
return [
|
||||
...Array.from(rides.entries()).map(([id, data]) => ({
|
||||
label: `🎢 ${data.name} (at ${data.park})`,
|
||||
value: `ride:${id}`,
|
||||
})),
|
||||
...Array.from(parks.entries()).map(([id, name]) => ({
|
||||
label: `🏰 ${name}`,
|
||||
value: `park:${id}`,
|
||||
})),
|
||||
...Array.from(manufacturers.entries()).map(([id, name]) => ({
|
||||
label: `🏭 ${name}`,
|
||||
value: `manufacturer:${id}`,
|
||||
}))
|
||||
].sort((a, b) => a.label.localeCompare(b.label));
|
||||
}, [credits]);
|
||||
|
||||
const maxRideCount = useMemo(() => {
|
||||
return Math.max(...credits.map(c => c.ride_count), 1);
|
||||
}, [credits]);
|
||||
@@ -107,26 +149,22 @@ export function RideCreditFilters({
|
||||
return (
|
||||
<div className={compact ? 'p-4' : ''}>
|
||||
<div className={spacingClass}>
|
||||
{/* Search Bar - Always Visible */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search rides, parks, manufacturers..."
|
||||
value={filters.searchQuery || ''}
|
||||
onChange={(e) => onFilterChange('searchQuery', e.target.value || undefined)}
|
||||
className="pl-10 pr-10"
|
||||
/>
|
||||
{filters.searchQuery && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7"
|
||||
onClick={() => onFilterChange('searchQuery', undefined)}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{/* Quick Search - Multi-select Combobox */}
|
||||
<div className={sectionSpacing}>
|
||||
<Label className="text-sm font-medium">Quick Search</Label>
|
||||
<MultiSelectCombobox
|
||||
options={searchOptions}
|
||||
value={filters.selectedSearchItems || []}
|
||||
onValueChange={(values) =>
|
||||
onFilterChange('selectedSearchItems', values.length > 0 ? values : undefined)
|
||||
}
|
||||
placeholder="Search for specific rides, parks, or manufacturers..."
|
||||
searchPlaceholder="Type to search..."
|
||||
emptyText="No matches found"
|
||||
maxDisplay={3}
|
||||
className={compact ? "h-9 text-sm" : ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Quick Category Chips */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -375,12 +413,21 @@ export function RideCreditFilters({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{filters.searchQuery && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
Search: {filters.searchQuery}
|
||||
<X className="w-3 h-3 cursor-pointer" onClick={() => removeFilter('searchQuery')} />
|
||||
</Badge>
|
||||
)}
|
||||
{filters.selectedSearchItems?.map(item => {
|
||||
const option = searchOptions.find(opt => opt.value === item);
|
||||
return option ? (
|
||||
<Badge key={item} variant="secondary" className="gap-1">
|
||||
{option.label}
|
||||
<X
|
||||
className="w-3 h-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
const updated = filters.selectedSearchItems!.filter(i => i !== item);
|
||||
onFilterChange('selectedSearchItems', updated.length > 0 ? updated : undefined);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
) : null;
|
||||
})}
|
||||
{filters.categories?.map(cat => (
|
||||
<Badge key={cat} variant="secondary" className="gap-1">
|
||||
{categoryOptions.find(c => c.value === cat)?.icon} {cat}
|
||||
|
||||
@@ -43,7 +43,7 @@ export function useRideCreditFilters(credits: UserRideCredit[]) {
|
||||
const filteredCredits = useMemo(() => {
|
||||
let result = [...credits];
|
||||
|
||||
// Search filter
|
||||
// Search filter (text search)
|
||||
if (debouncedSearchQuery) {
|
||||
const search = debouncedSearchQuery.toLowerCase();
|
||||
result = result.filter(credit =>
|
||||
@@ -52,6 +52,21 @@ export function useRideCreditFilters(credits: UserRideCredit[]) {
|
||||
);
|
||||
}
|
||||
|
||||
// Selected search items filter (multi-select combobox)
|
||||
if (filters.selectedSearchItems && filters.selectedSearchItems.length > 0) {
|
||||
result = result.filter(credit => {
|
||||
return filters.selectedSearchItems!.some(item => {
|
||||
const [type, id] = item.split(':');
|
||||
|
||||
if (type === 'ride' && credit.rides?.id === id) return true;
|
||||
if (type === 'park' && credit.rides?.parks?.id === id) return true;
|
||||
if (type === 'manufacturer' && credit.rides?.manufacturer?.id === id) return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Categories
|
||||
if (filters.categories && filters.categories.length > 0) {
|
||||
result = result.filter(credit =>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface RideCreditFilters {
|
||||
// Search
|
||||
searchQuery?: string;
|
||||
selectedSearchItems?: string[]; // Format: "ride:id", "park:id", "manufacturer:id"
|
||||
|
||||
// Category (Multi-select)
|
||||
categories?: string[];
|
||||
|
||||
Reference in New Issue
Block a user