mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 05:31:16 -05:00
Refactor: Update user list item interfaces
This commit is contained in:
277
src/components/search/AdvancedRideFilters.tsx
Normal file
277
src/components/search/AdvancedRideFilters.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import { useState } from 'react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { ChevronDown, Plus, X } from 'lucide-react';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
|
||||
export interface TechnicalSpecFilter {
|
||||
spec_name: string;
|
||||
spec_value?: string;
|
||||
min_value?: number;
|
||||
max_value?: number;
|
||||
operator: 'equals' | 'contains' | 'range' | 'has_spec';
|
||||
}
|
||||
|
||||
export interface CoasterStatFilter {
|
||||
stat_name: string;
|
||||
min_value?: number;
|
||||
max_value?: number;
|
||||
}
|
||||
|
||||
interface AdvancedRideFiltersProps {
|
||||
technicalSpecFilters: TechnicalSpecFilter[];
|
||||
coasterStatFilters: CoasterStatFilter[];
|
||||
onTechnicalSpecFiltersChange: (filters: TechnicalSpecFilter[]) => void;
|
||||
onCoasterStatFiltersChange: (filters: CoasterStatFilter[]) => void;
|
||||
}
|
||||
|
||||
const COMMON_TECH_SPECS = [
|
||||
'Track Material',
|
||||
'Manufacturer Model',
|
||||
'Train Configuration',
|
||||
'Launch System',
|
||||
'Braking System',
|
||||
'Control System',
|
||||
'Safety System',
|
||||
];
|
||||
|
||||
const COMMON_COASTER_STATS = [
|
||||
'Max Speed',
|
||||
'Height',
|
||||
'Drop Height',
|
||||
'Length',
|
||||
'Duration',
|
||||
'Inversions',
|
||||
'G-Force',
|
||||
'Capacity',
|
||||
];
|
||||
|
||||
export function AdvancedRideFilters({
|
||||
technicalSpecFilters,
|
||||
coasterStatFilters,
|
||||
onTechnicalSpecFiltersChange,
|
||||
onCoasterStatFiltersChange,
|
||||
}: AdvancedRideFiltersProps) {
|
||||
const [isSpecsOpen, setIsSpecsOpen] = useState(false);
|
||||
const [isStatsOpen, setIsStatsOpen] = useState(false);
|
||||
|
||||
const addTechnicalSpecFilter = () => {
|
||||
onTechnicalSpecFiltersChange([
|
||||
...technicalSpecFilters,
|
||||
{ spec_name: '', operator: 'has_spec' },
|
||||
]);
|
||||
};
|
||||
|
||||
const updateTechnicalSpecFilter = (index: number, updates: Partial<TechnicalSpecFilter>) => {
|
||||
const updated = [...technicalSpecFilters];
|
||||
updated[index] = { ...updated[index], ...updates };
|
||||
onTechnicalSpecFiltersChange(updated);
|
||||
};
|
||||
|
||||
const removeTechnicalSpecFilter = (index: number) => {
|
||||
onTechnicalSpecFiltersChange(technicalSpecFilters.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const addCoasterStatFilter = () => {
|
||||
onCoasterStatFiltersChange([
|
||||
...coasterStatFilters,
|
||||
{ stat_name: '' },
|
||||
]);
|
||||
};
|
||||
|
||||
const updateCoasterStatFilter = (index: number, updates: Partial<CoasterStatFilter>) => {
|
||||
const updated = [...coasterStatFilters];
|
||||
updated[index] = { ...updated[index], ...updates };
|
||||
onCoasterStatFiltersChange(updated);
|
||||
};
|
||||
|
||||
const removeCoasterStatFilter = (index: number) => {
|
||||
onCoasterStatFiltersChange(coasterStatFilters.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Technical Specifications Filters */}
|
||||
<Collapsible open={isSpecsOpen} onOpenChange={setIsSpecsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<div className="flex items-center justify-between cursor-pointer p-3 bg-muted/50 rounded-lg hover:bg-muted transition-colors">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-semibold">Technical Specifications</h4>
|
||||
{technicalSpecFilters.length > 0 && (
|
||||
<Badge variant="secondary">{technicalSpecFilters.length}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown className={`w-4 h-4 transition-transform ${isSpecsOpen ? 'rotate-180' : ''}`} />
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent className="space-y-3 pt-3">
|
||||
{technicalSpecFilters.map((filter, index) => (
|
||||
<div key={index} className="p-3 border rounded-lg space-y-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label>Specification Name</Label>
|
||||
<Select
|
||||
value={filter.spec_name}
|
||||
onValueChange={(value) => updateTechnicalSpecFilter(index, { spec_name: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select specification" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{COMMON_TECH_SPECS.map((spec) => (
|
||||
<SelectItem key={spec} value={spec}>
|
||||
{spec}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Filter Type</Label>
|
||||
<Select
|
||||
value={filter.operator}
|
||||
onValueChange={(value: any) => updateTechnicalSpecFilter(index, { operator: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="has_spec">Has Specification</SelectItem>
|
||||
<SelectItem value="equals">Equals Value</SelectItem>
|
||||
<SelectItem value="contains">Contains Text</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeTechnicalSpecFilter(index)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{(filter.operator === 'equals' || filter.operator === 'contains') && (
|
||||
<div className="space-y-2">
|
||||
<Label>Value</Label>
|
||||
<Input
|
||||
value={filter.spec_value || ''}
|
||||
onChange={(e) => updateTechnicalSpecFilter(index, { spec_value: e.target.value })}
|
||||
placeholder="Enter value to match"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addTechnicalSpecFilter}
|
||||
className="w-full"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Technical Spec Filter
|
||||
</Button>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* Coaster Statistics Filters */}
|
||||
<Collapsible open={isStatsOpen} onOpenChange={setIsStatsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<div className="flex items-center justify-between cursor-pointer p-3 bg-muted/50 rounded-lg hover:bg-muted transition-colors">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-semibold">Coaster Statistics</h4>
|
||||
{coasterStatFilters.length > 0 && (
|
||||
<Badge variant="secondary">{coasterStatFilters.length}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown className={`w-4 h-4 transition-transform ${isStatsOpen ? 'rotate-180' : ''}`} />
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent className="space-y-3 pt-3">
|
||||
{coasterStatFilters.map((filter, index) => (
|
||||
<div key={index} className="p-3 border rounded-lg space-y-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label>Statistic Name</Label>
|
||||
<Select
|
||||
value={filter.stat_name}
|
||||
onValueChange={(value) => updateCoasterStatFilter(index, { stat_name: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select statistic" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{COMMON_COASTER_STATS.map((stat) => (
|
||||
<SelectItem key={stat} value={stat}>
|
||||
{stat}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label>Min Value</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={filter.min_value || ''}
|
||||
onChange={(e) =>
|
||||
updateCoasterStatFilter(index, {
|
||||
min_value: e.target.value ? parseFloat(e.target.value) : undefined,
|
||||
})
|
||||
}
|
||||
placeholder="Min"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Max Value</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={filter.max_value || ''}
|
||||
onChange={(e) =>
|
||||
updateCoasterStatFilter(index, {
|
||||
max_value: e.target.value ? parseFloat(e.target.value) : undefined,
|
||||
})
|
||||
}
|
||||
placeholder="Max"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeCoasterStatFilter(index)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addCoasterStatFilter}
|
||||
className="w-full"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Coaster Stat Filter
|
||||
</Button>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { ChevronDown, Filter, X } from 'lucide-react';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { useCountries, useStatesProvinces, useManufacturers, useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
|
||||
import { DateRangePicker, DateRange } from '@/components/ui/date-range-picker';
|
||||
import { AdvancedRideFilters, TechnicalSpecFilter, CoasterStatFilter } from './AdvancedRideFilters';
|
||||
|
||||
export interface SearchFilters {
|
||||
// Park filters
|
||||
@@ -36,6 +37,8 @@ export interface SearchFilters {
|
||||
speedMax?: number;
|
||||
intensityLevel?: string;
|
||||
rideDateRange?: DateRange;
|
||||
technicalSpecFilters?: TechnicalSpecFilter[];
|
||||
coasterStatFilters?: CoasterStatFilter[];
|
||||
|
||||
// Company filters
|
||||
companyType?: string;
|
||||
@@ -391,6 +394,16 @@ export function SearchFiltersComponent({ filters, onFiltersChange, activeTab }:
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced Ride Filters */}
|
||||
<div className="pt-4 border-t">
|
||||
<AdvancedRideFilters
|
||||
technicalSpecFilters={filters.technicalSpecFilters || []}
|
||||
coasterStatFilters={filters.coasterStatFilters || []}
|
||||
onTechnicalSpecFiltersChange={(filters) => updateFilter('technicalSpecFilters', filters)}
|
||||
onCoasterStatFiltersChange={(filters) => updateFilter('coasterStatFilters', filters)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user