mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 20: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 { MultiSelectCombobox } from '@/components/ui/multi-select-combobox';
|
||||||
import { RideCreditFilters as FilterTypes } from '@/types/ride-credits';
|
import { RideCreditFilters as FilterTypes } from '@/types/ride-credits';
|
||||||
import { UserRideCredit } from '@/types/database';
|
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 { useEffect, useMemo } from 'react';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
|
||||||
@@ -67,6 +67,48 @@ export function RideCreditFilters({
|
|||||||
};
|
};
|
||||||
}, [credits]);
|
}, [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(() => {
|
const maxRideCount = useMemo(() => {
|
||||||
return Math.max(...credits.map(c => c.ride_count), 1);
|
return Math.max(...credits.map(c => c.ride_count), 1);
|
||||||
}, [credits]);
|
}, [credits]);
|
||||||
@@ -107,26 +149,22 @@ export function RideCreditFilters({
|
|||||||
return (
|
return (
|
||||||
<div className={compact ? 'p-4' : ''}>
|
<div className={compact ? 'p-4' : ''}>
|
||||||
<div className={spacingClass}>
|
<div className={spacingClass}>
|
||||||
{/* Search Bar - Always Visible */}
|
{/* Quick Search - Multi-select Combobox */}
|
||||||
<div className="relative">
|
<div className={sectionSpacing}>
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
<Label className="text-sm font-medium">Quick Search</Label>
|
||||||
<Input
|
<MultiSelectCombobox
|
||||||
placeholder="Search rides, parks, manufacturers..."
|
options={searchOptions}
|
||||||
value={filters.searchQuery || ''}
|
value={filters.selectedSearchItems || []}
|
||||||
onChange={(e) => onFilterChange('searchQuery', e.target.value || undefined)}
|
onValueChange={(values) =>
|
||||||
className="pl-10 pr-10"
|
onFilterChange('selectedSearchItems', values.length > 0 ? values : undefined)
|
||||||
/>
|
}
|
||||||
{filters.searchQuery && (
|
placeholder="Search for specific rides, parks, or manufacturers..."
|
||||||
<Button
|
searchPlaceholder="Type to search..."
|
||||||
variant="ghost"
|
emptyText="No matches found"
|
||||||
size="icon"
|
maxDisplay={3}
|
||||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7"
|
className={compact ? "h-9 text-sm" : ""}
|
||||||
onClick={() => onFilterChange('searchQuery', undefined)}
|
/>
|
||||||
>
|
</div>
|
||||||
<X className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quick Category Chips */}
|
{/* Quick Category Chips */}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -375,12 +413,21 @@ export function RideCreditFilters({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{filters.searchQuery && (
|
{filters.selectedSearchItems?.map(item => {
|
||||||
<Badge variant="secondary" className="gap-1">
|
const option = searchOptions.find(opt => opt.value === item);
|
||||||
Search: {filters.searchQuery}
|
return option ? (
|
||||||
<X className="w-3 h-3 cursor-pointer" onClick={() => removeFilter('searchQuery')} />
|
<Badge key={item} variant="secondary" className="gap-1">
|
||||||
</Badge>
|
{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 => (
|
{filters.categories?.map(cat => (
|
||||||
<Badge key={cat} variant="secondary" className="gap-1">
|
<Badge key={cat} variant="secondary" className="gap-1">
|
||||||
{categoryOptions.find(c => c.value === cat)?.icon} {cat}
|
{categoryOptions.find(c => c.value === cat)?.icon} {cat}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function useRideCreditFilters(credits: UserRideCredit[]) {
|
|||||||
const filteredCredits = useMemo(() => {
|
const filteredCredits = useMemo(() => {
|
||||||
let result = [...credits];
|
let result = [...credits];
|
||||||
|
|
||||||
// Search filter
|
// Search filter (text search)
|
||||||
if (debouncedSearchQuery) {
|
if (debouncedSearchQuery) {
|
||||||
const search = debouncedSearchQuery.toLowerCase();
|
const search = debouncedSearchQuery.toLowerCase();
|
||||||
result = result.filter(credit =>
|
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
|
// Categories
|
||||||
if (filters.categories && filters.categories.length > 0) {
|
if (filters.categories && filters.categories.length > 0) {
|
||||||
result = result.filter(credit =>
|
result = result.filter(credit =>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export interface RideCreditFilters {
|
export interface RideCreditFilters {
|
||||||
// Search
|
// Search
|
||||||
searchQuery?: string;
|
searchQuery?: string;
|
||||||
|
selectedSearchItems?: string[]; // Format: "ride:id", "park:id", "manufacturer:id"
|
||||||
|
|
||||||
// Category (Multi-select)
|
// Category (Multi-select)
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
|
|||||||
Reference in New Issue
Block a user