- {/* Rating Range */}
-
-
-
-
-
- {filters.minRating.toFixed(1)} - {filters.maxRating.toFixed(1)} stars
-
-
-
-
-
- onFiltersChange({ ...filters, minRating: min, maxRating: max })
- }
- min={0}
- max={5}
- step={0.1}
- className="w-full"
- />
-
- 0 stars
- 5 stars
-
-
+
+
+ onFiltersChange({ ...filters, operators: value })}
+ placeholder="Select operators"
+ />
+ onFiltersChange({ ...filters, propertyOwners: value })}
+ placeholder="Select owners"
+ />
+
- {/* Ride Count Range */}
-
-
-
-
-
- {filters.minRides} - {filters.maxRides} rides
-
-
-
-
-
- onFiltersChange({ ...filters, minRides: min, maxRides: max })
- }
- min={0}
- max={maxRides}
- step={1}
- className="w-full"
- />
-
- 0 rides
- {maxRides} rides
-
-
+
+
+
+
+ onFiltersChange({
+ ...filters,
+ openingYearStart: date ? date.getFullYear() : null
+ })}
+ onToChange={(date) => onFiltersChange({
+ ...filters,
+ openingYearEnd: date ? date.getFullYear() : null
+ })}
+ fromPlaceholder="From year"
+ toPlaceholder="To year"
+ />
-
+
+
+
+
+
+
+ onFiltersChange({ ...filters, minRating: min, maxRating: max })}
+ min={0}
+ max={5}
+ step={0.1}
+ formatValue={(v) => `${v.toFixed(1)} stars`}
+ />
+ onFiltersChange({ ...filters, minRides: min, maxRides: max })}
+ min={0}
+ max={maxRides}
+ step={1}
+ />
+ onFiltersChange({ ...filters, minCoasters: min, maxCoasters: max })}
+ min={0}
+ max={maxCoasters}
+ step={1}
+ />
+ onFiltersChange({ ...filters, minReviews: min, maxReviews: max })}
+ min={0}
+ max={maxReviews}
+ step={10}
+ />
+
+
);
}
\ No newline at end of file
diff --git a/src/components/rides/RideFilters.tsx b/src/components/rides/RideFilters.tsx
new file mode 100644
index 00000000..b0a60b54
--- /dev/null
+++ b/src/components/rides/RideFilters.tsx
@@ -0,0 +1,438 @@
+import { useMemo } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { Button } from '@/components/ui/button';
+import { Label } from '@/components/ui/label';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Checkbox } from '@/components/ui/checkbox';
+import { Separator } from '@/components/ui/separator';
+import { RotateCcw } from 'lucide-react';
+import { supabase } from '@/integrations/supabase/client';
+import { FilterRangeSlider } from '@/components/filters/FilterRangeSlider';
+import { FilterDateRangePicker } from '@/components/filters/FilterDateRangePicker';
+import { FilterSection } from '@/components/filters/FilterSection';
+import { FilterMultiSelectCombobox } from '@/components/filters/FilterMultiSelectCombobox';
+import { MultiSelectOption } from '@/components/ui/multi-select-combobox';
+import { Ride } from '@/types/database';
+
+export interface RideFilterState {
+ categories: string[];
+ status: string;
+ countries: string[];
+ statesProvinces: string[];
+ cities: string[];
+ parks: string[];
+ manufacturers: string[];
+ designers: string[];
+ coasterTypes: string[];
+ seatingTypes: string[];
+ intensityLevels: string[];
+ trackMaterials: string[];
+ minSpeed: number;
+ maxSpeed: number;
+ minHeight: number;
+ maxHeight: number;
+ minLength: number;
+ maxLength: number;
+ minInversions: number;
+ maxInversions: number;
+ openingDateFrom: Date | null;
+ openingDateTo: Date | null;
+ hasInversions: boolean;
+ operatingOnly: boolean;
+}
+
+export const defaultRideFilters: RideFilterState = {
+ categories: [],
+ status: 'all',
+ countries: [],
+ statesProvinces: [],
+ cities: [],
+ parks: [],
+ manufacturers: [],
+ designers: [],
+ coasterTypes: [],
+ seatingTypes: [],
+ intensityLevels: [],
+ trackMaterials: [],
+ minSpeed: 0,
+ maxSpeed: 200,
+ minHeight: 0,
+ maxHeight: 150,
+ minLength: 0,
+ maxLength: 3000,
+ minInversions: 0,
+ maxInversions: 14,
+ openingDateFrom: null,
+ openingDateTo: null,
+ hasInversions: false,
+ operatingOnly: false,
+};
+
+interface RideFiltersProps {
+ filters: RideFilterState;
+ onFiltersChange: (filters: RideFilterState) => void;
+ rides: Ride[];
+}
+
+export function RideFilters({ filters, onFiltersChange, rides }: RideFiltersProps) {
+ // Fetch unique filter options
+ const { data: locations } = useQuery({
+ queryKey: ['filter-locations'],
+ queryFn: async () => {
+ const { data } = await supabase
+ .from('locations')
+ .select('country, state_province, city')
+ .not('country', 'is', null);
+ return data || [];
+ },
+ staleTime: 5 * 60 * 1000,
+ });
+
+ const { data: parks } = useQuery({
+ queryKey: ['filter-parks'],
+ queryFn: async () => {
+ const { data } = await supabase
+ .from('parks')
+ .select('id, name')
+ .order('name');
+ return data || [];
+ },
+ staleTime: 5 * 60 * 1000,
+ });
+
+ const { data: manufacturers } = useQuery({
+ queryKey: ['filter-manufacturers'],
+ queryFn: async () => {
+ const { data } = await supabase
+ .from('companies')
+ .select('id, name')
+ .eq('company_type', 'manufacturer')
+ .order('name');
+ return data || [];
+ },
+ staleTime: 5 * 60 * 1000,
+ });
+
+ const { data: designers } = useQuery({
+ queryKey: ['filter-designers'],
+ queryFn: async () => {
+ const { data } = await supabase
+ .from('companies')
+ .select('id, name')
+ .eq('company_type', 'designer')
+ .order('name');
+ return data || [];
+ },
+ staleTime: 5 * 60 * 1000,
+ });
+
+ const countryOptions: MultiSelectOption[] = useMemo(() => {
+ const countries = new Set(locations?.map(l => l.country).filter(Boolean) || []);
+ return Array.from(countries).sort().map(c => ({ label: c, value: c }));
+ }, [locations]);
+
+ const stateOptions: MultiSelectOption[] = useMemo(() => {
+ const states = new Set(locations?.map(l => l.state_province).filter(Boolean) || []);
+ return Array.from(states).sort().map(s => ({ label: s, value: s }));
+ }, [locations]);
+
+ const cityOptions: MultiSelectOption[] = useMemo(() => {
+ const cities = new Set(locations?.map(l => l.city).filter(Boolean) || []);
+ return Array.from(cities).sort().map(c => ({ label: c, value: c }));
+ }, [locations]);
+
+ const parkOptions: MultiSelectOption[] = useMemo(() => {
+ return (parks || []).map(p => ({ label: p.name, value: p.id }));
+ }, [parks]);
+
+ const manufacturerOptions: MultiSelectOption[] = useMemo(() => {
+ return (manufacturers || []).map(m => ({ label: m.name, value: m.id }));
+ }, [manufacturers]);
+
+ const designerOptions: MultiSelectOption[] = useMemo(() => {
+ return (designers || []).map(d => ({ label: d.name, value: d.id }));
+ }, [designers]);
+
+ const categoryOptions: MultiSelectOption[] = [
+ { label: 'Roller Coasters', value: 'roller_coaster' },
+ { label: 'Flat Rides', value: 'flat_ride' },
+ { label: 'Water Rides', value: 'water_ride' },
+ { label: 'Dark Rides', value: 'dark_ride' },
+ { label: 'Kiddie Rides', value: 'kiddie_ride' },
+ { label: 'Transportation', value: 'transportation' },
+ ];
+
+ const statusOptions = [
+ { value: 'all', label: 'All Status' },
+ { value: 'operating', label: 'Operating' },
+ { value: 'seasonal', label: 'Seasonal' },
+ { value: 'under_construction', label: 'Under Construction' },
+ { value: 'closed', label: 'Closed' },
+ ];
+
+ const coasterTypeOptions: MultiSelectOption[] = [
+ { label: 'Steel', value: 'steel' },
+ { label: 'Wood', value: 'wood' },
+ { label: 'Hybrid', value: 'hybrid' },
+ { label: 'Inverted', value: 'inverted' },
+ { label: 'Suspended', value: 'suspended' },
+ { label: 'Flying', value: 'flying' },
+ { label: 'Wing', value: 'wing' },
+ { label: 'Dive', value: 'dive' },
+ ];
+
+ const seatingTypeOptions: MultiSelectOption[] = [
+ { label: 'Sit Down', value: 'sit_down' },
+ { label: 'Stand Up', value: 'stand_up' },
+ { label: 'Inverted', value: 'inverted' },
+ { label: 'Flying', value: 'flying' },
+ { label: 'Floorless', value: 'floorless' },
+ ];
+
+ const intensityOptions: MultiSelectOption[] = [
+ { label: 'Family', value: 'family' },
+ { label: 'Moderate', value: 'moderate' },
+ { label: 'Thrill', value: 'thrill' },
+ { label: 'Extreme', value: 'extreme' },
+ ];
+
+ const trackMaterialOptions: MultiSelectOption[] = [
+ { label: 'Steel', value: 'steel' },
+ { label: 'Wood', value: 'wood' },
+ { label: 'Hybrid', value: 'hybrid' },
+ ];
+
+ const resetFilters = () => {
+ onFiltersChange(defaultRideFilters);
+ };
+
+ return (
+
+
+
Filter Rides
+
+
+
+ {/* Basic Filters */}
+
+
+
onFiltersChange({ ...filters, categories: value })}
+ placeholder="Select categories"
+ />
+
+
+
+
+
+
+
+
+
+
+ {/* Geographic Filters */}
+
+
+ onFiltersChange({ ...filters, countries: value })}
+ placeholder="Select countries"
+ />
+ onFiltersChange({ ...filters, statesProvinces: value })}
+ placeholder="Select states"
+ />
+ onFiltersChange({ ...filters, cities: value })}
+ placeholder="Select cities"
+ />
+
+
+
+
+
+ {/* Relationship Filters */}
+
+
+ onFiltersChange({ ...filters, parks: value })}
+ placeholder="Select parks"
+ />
+ onFiltersChange({ ...filters, manufacturers: value })}
+ placeholder="Select manufacturers"
+ />
+ onFiltersChange({ ...filters, designers: value })}
+ placeholder="Select designers"
+ />
+
+
+
+
+
+ {/* Coaster-Specific Filters */}
+
+
+ onFiltersChange({ ...filters, coasterTypes: value })}
+ placeholder="Select types"
+ />
+ onFiltersChange({ ...filters, seatingTypes: value })}
+ placeholder="Select seating"
+ />
+ onFiltersChange({ ...filters, intensityLevels: value })}
+ placeholder="Select intensity"
+ />
+ onFiltersChange({ ...filters, trackMaterials: value })}
+ placeholder="Select material"
+ />
+
+
+
+
+
+ {/* Numeric Range Filters */}
+
+
+ onFiltersChange({ ...filters, minSpeed: min, maxSpeed: max })}
+ min={0}
+ max={200}
+ step={5}
+ unit=" km/h"
+ />
+ onFiltersChange({ ...filters, minHeight: min, maxHeight: max })}
+ min={0}
+ max={150}
+ step={5}
+ unit=" m"
+ />
+ onFiltersChange({ ...filters, minLength: min, maxLength: max })}
+ min={0}
+ max={3000}
+ step={50}
+ unit=" m"
+ />
+ onFiltersChange({ ...filters, minInversions: min, maxInversions: max })}
+ min={0}
+ max={14}
+ step={1}
+ />
+
+
+
+
+
+ {/* Date Filters */}
+
+
+ onFiltersChange({ ...filters, openingDateFrom: date || null })}
+ onToChange={(date) => onFiltersChange({ ...filters, openingDateTo: date || null })}
+ fromPlaceholder="From year"
+ toPlaceholder="To year"
+ />
+
+
+
+
+
+ {/* Boolean Filters */}
+
+
+
+
+ onFiltersChange({ ...filters, hasInversions: checked as boolean })
+ }
+ />
+
+
+
+
+ onFiltersChange({ ...filters, operatingOnly: checked as boolean })
+ }
+ />
+
+
+
+
+
+ );
+}
diff --git a/src/pages/Designers.tsx b/src/pages/Designers.tsx
index 7b6338bb..3727032a 100644
--- a/src/pages/Designers.tsx
+++ b/src/pages/Designers.tsx
@@ -5,8 +5,11 @@ import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Collapsible, CollapsibleContent } from '@/components/ui/collapsible';
+import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
-import { Search, SlidersHorizontal, Ruler, Plus } from 'lucide-react';
+import { Search, SlidersHorizontal, Ruler, Plus, ChevronDown, Filter } from 'lucide-react';
+import { DesignerFilters, DesignerFilterState, defaultDesignerFilters } from '@/components/designers/DesignerFilters';
import { Company } from '@/types/database';
import { supabase } from '@/integrations/supabase/client';
import { DesignerCard } from '@/components/designers/DesignerCard';
@@ -27,6 +30,8 @@ export default function Designers() {
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState('name');
+ const [filters, setFilters] = useState
(defaultDesignerFilters);
+ const [showFilters, setShowFilters] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const handleCreateSubmit = async (data: any) => {
@@ -144,16 +149,33 @@ export default function Designers() {
/>