diff --git a/src/components/designers/DesignerFilters.tsx b/src/components/designers/DesignerFilters.tsx new file mode 100644 index 00000000..4710fafe --- /dev/null +++ b/src/components/designers/DesignerFilters.tsx @@ -0,0 +1,194 @@ +import { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Label } from '@/components/ui/label'; +import { Separator } from '@/components/ui/separator'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +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 { Company } from '@/types/database'; + +export interface DesignerFilterState { + personType: string; + countries: string[]; + minRating: number; + maxRating: number; + minReviewCount: number; + maxReviewCount: number; + foundedDateFrom: Date | null; + foundedDateTo: Date | null; + hasRating: boolean; + hasFoundedDate: boolean; + isIndividual: boolean; +} + +export const defaultDesignerFilters: DesignerFilterState = { + personType: 'all', + countries: [], + minRating: 0, + maxRating: 5, + minReviewCount: 0, + maxReviewCount: 1000, + foundedDateFrom: null, + foundedDateTo: null, + hasRating: false, + hasFoundedDate: false, + isIndividual: false, +}; + +interface DesignerFiltersProps { + filters: DesignerFilterState; + onFiltersChange: (filters: DesignerFilterState) => void; + designers: Company[]; +} + +export function DesignerFilters({ filters, onFiltersChange, designers }: DesignerFiltersProps) { + const { data: locations } = useQuery({ + queryKey: ['filter-locations'], + queryFn: async () => { + const { data } = await supabase + .from('locations') + .select('country') + .not('country', 'is', null); + 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 maxReviewCount = useMemo(() => { + return Math.max(...designers.map(d => d.review_count || 0), 1000); + }, [designers]); + + const resetFilters = () => { + onFiltersChange({ ...defaultDesignerFilters, maxReviewCount }); + }; + + return ( +
+
+

Filter Designers

+ +
+ + +
+
+ + +
+ + onFiltersChange({ ...filters, countries: value })} + placeholder="Select countries" + /> + + onFiltersChange({ ...filters, foundedDateFrom: date || null })} + onToChange={(date) => onFiltersChange({ ...filters, foundedDateTo: date || 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, minReviewCount: min, maxReviewCount: max })} + min={0} + max={maxReviewCount} + step={10} + /> +
+
+ + + + +
+
+ + onFiltersChange({ ...filters, hasRating: checked as boolean }) + } + /> + +
+
+ + onFiltersChange({ ...filters, hasFoundedDate: checked as boolean }) + } + /> + +
+
+ + onFiltersChange({ ...filters, isIndividual: checked as boolean }) + } + /> + +
+
+
+
+ ); +} diff --git a/src/components/filters/FilterDateRangePicker.tsx b/src/components/filters/FilterDateRangePicker.tsx new file mode 100644 index 00000000..93e1bb1c --- /dev/null +++ b/src/components/filters/FilterDateRangePicker.tsx @@ -0,0 +1,48 @@ +import { Label } from '@/components/ui/label'; +import { MonthYearPicker } from '@/components/ui/month-year-picker'; + +interface FilterDateRangePickerProps { + label: string; + fromDate: Date | null; + toDate: Date | null; + onFromChange: (date: Date | undefined) => void; + onToChange: (date: Date | undefined) => void; + fromYear?: number; + toYear?: number; + fromPlaceholder?: string; + toPlaceholder?: string; +} + +export function FilterDateRangePicker({ + label, + fromDate, + toDate, + onFromChange, + onToChange, + fromYear = 1900, + toYear = new Date().getFullYear(), + fromPlaceholder = 'From', + toPlaceholder = 'To' +}: FilterDateRangePickerProps) { + return ( +
+ +
+ + +
+
+ ); +} diff --git a/src/components/filters/FilterMultiSelectCombobox.tsx b/src/components/filters/FilterMultiSelectCombobox.tsx new file mode 100644 index 00000000..538d11e0 --- /dev/null +++ b/src/components/filters/FilterMultiSelectCombobox.tsx @@ -0,0 +1,36 @@ +import { Label } from '@/components/ui/label'; +import { MultiSelectCombobox, MultiSelectOption } from '@/components/ui/multi-select-combobox'; + +interface FilterMultiSelectComboboxProps { + label: string; + options: MultiSelectOption[]; + value: string[]; + onChange: (value: string[]) => void; + placeholder?: string; + emptyText?: string; + maxDisplay?: number; +} + +export function FilterMultiSelectCombobox({ + label, + options, + value, + onChange, + placeholder = 'Select...', + emptyText = 'No options found', + maxDisplay = 2 +}: FilterMultiSelectComboboxProps) { + return ( +
+ + +
+ ); +} diff --git a/src/components/filters/FilterRangeSlider.tsx b/src/components/filters/FilterRangeSlider.tsx new file mode 100644 index 00000000..f5d98d0e --- /dev/null +++ b/src/components/filters/FilterRangeSlider.tsx @@ -0,0 +1,52 @@ +import { Label } from '@/components/ui/label'; +import { Slider } from '@/components/ui/slider'; +import { Badge } from '@/components/ui/badge'; + +interface FilterRangeSliderProps { + label: string; + value: [number, number]; + onChange: (value: [number, number]) => void; + min: number; + max: number; + step?: number; + unit?: string; + formatValue?: (value: number) => string; +} + +export function FilterRangeSlider({ + label, + value, + onChange, + min, + max, + step = 1, + unit = '', + formatValue +}: FilterRangeSliderProps) { + const format = formatValue || ((v: number) => `${v}${unit}`); + + return ( +
+
+ + + {format(value[0])} - {format(value[1])} + +
+
+ +
+ {format(min)} + {format(max)} +
+
+
+ ); +} diff --git a/src/components/filters/FilterSection.tsx b/src/components/filters/FilterSection.tsx new file mode 100644 index 00000000..96b70330 --- /dev/null +++ b/src/components/filters/FilterSection.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from 'react'; +import { Separator } from '@/components/ui/separator'; + +interface FilterSectionProps { + title?: string; + children: ReactNode; + showSeparator?: boolean; +} + +export function FilterSection({ title, children, showSeparator = false }: FilterSectionProps) { + return ( +
+ {title &&

{title}

} + {children} + {showSeparator && } +
+ ); +} diff --git a/src/components/manufacturers/ManufacturerFilters.tsx b/src/components/manufacturers/ManufacturerFilters.tsx new file mode 100644 index 00000000..530001d3 --- /dev/null +++ b/src/components/manufacturers/ManufacturerFilters.tsx @@ -0,0 +1,194 @@ +import { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Label } from '@/components/ui/label'; +import { Separator } from '@/components/ui/separator'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +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 { Company } from '@/types/database'; + +export interface ManufacturerFilterState { + personType: string; + countries: string[]; + minRating: number; + maxRating: number; + minReviewCount: number; + maxReviewCount: number; + foundedDateFrom: Date | null; + foundedDateTo: Date | null; + hasRating: boolean; + hasFoundedDate: boolean; + isIndividual: boolean; +} + +export const defaultManufacturerFilters: ManufacturerFilterState = { + personType: 'all', + countries: [], + minRating: 0, + maxRating: 5, + minReviewCount: 0, + maxReviewCount: 1000, + foundedDateFrom: null, + foundedDateTo: null, + hasRating: false, + hasFoundedDate: false, + isIndividual: false, +}; + +interface ManufacturerFiltersProps { + filters: ManufacturerFilterState; + onFiltersChange: (filters: ManufacturerFilterState) => void; + manufacturers: Company[]; +} + +export function ManufacturerFilters({ filters, onFiltersChange, manufacturers }: ManufacturerFiltersProps) { + const { data: locations } = useQuery({ + queryKey: ['filter-locations'], + queryFn: async () => { + const { data } = await supabase + .from('locations') + .select('country') + .not('country', 'is', null); + 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 maxReviewCount = useMemo(() => { + return Math.max(...manufacturers.map(m => m.review_count || 0), 1000); + }, [manufacturers]); + + const resetFilters = () => { + onFiltersChange({ ...defaultManufacturerFilters, maxReviewCount }); + }; + + return ( +
+
+

Filter Manufacturers

+ +
+ + +
+
+ + +
+ + onFiltersChange({ ...filters, countries: value })} + placeholder="Select countries" + /> + + onFiltersChange({ ...filters, foundedDateFrom: date || null })} + onToChange={(date) => onFiltersChange({ ...filters, foundedDateTo: date || 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, minReviewCount: min, maxReviewCount: max })} + min={0} + max={maxReviewCount} + step={10} + /> +
+
+ + + + +
+
+ + onFiltersChange({ ...filters, hasRating: checked as boolean }) + } + /> + +
+
+ + onFiltersChange({ ...filters, hasFoundedDate: checked as boolean }) + } + /> + +
+
+ + onFiltersChange({ ...filters, isIndividual: checked as boolean }) + } + /> + +
+
+
+
+ ); +} diff --git a/src/components/operators/OperatorFilters.tsx b/src/components/operators/OperatorFilters.tsx new file mode 100644 index 00000000..ad1416bd --- /dev/null +++ b/src/components/operators/OperatorFilters.tsx @@ -0,0 +1,175 @@ +import { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Label } from '@/components/ui/label'; +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 { Company } from '@/types/database'; + +export interface OperatorFilterState { + countries: string[]; + minRating: number; + maxRating: number; + minReviewCount: number; + maxReviewCount: number; + minParkCount: number; + maxParkCount: number; + foundedDateFrom: Date | null; + foundedDateTo: Date | null; + hasRating: boolean; + hasFoundedDate: boolean; +} + +export const defaultOperatorFilters: OperatorFilterState = { + countries: [], + minRating: 0, + maxRating: 5, + minReviewCount: 0, + maxReviewCount: 1000, + minParkCount: 0, + maxParkCount: 100, + foundedDateFrom: null, + foundedDateTo: null, + hasRating: false, + hasFoundedDate: false, +}; + +interface OperatorFiltersProps { + filters: OperatorFilterState; + onFiltersChange: (filters: OperatorFilterState) => void; + operators: (Company & { park_count?: number })[]; +} + +export function OperatorFilters({ filters, onFiltersChange, operators }: OperatorFiltersProps) { + const { data: locations } = useQuery({ + queryKey: ['filter-locations'], + queryFn: async () => { + const { data } = await supabase + .from('locations') + .select('country') + .not('country', 'is', null); + 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 maxParkCount = useMemo(() => { + return Math.max(...operators.map(o => o.park_count || 0), 100); + }, [operators]); + + const maxReviewCount = useMemo(() => { + return Math.max(...operators.map(o => o.review_count || 0), 1000); + }, [operators]); + + const resetFilters = () => { + onFiltersChange({ ...defaultOperatorFilters, maxParkCount, maxReviewCount }); + }; + + return ( +
+
+

Filter Operators

+ +
+ + +
+ onFiltersChange({ ...filters, countries: value })} + placeholder="Select countries" + /> + onFiltersChange({ ...filters, foundedDateFrom: date || null })} + onToChange={(date) => onFiltersChange({ ...filters, foundedDateTo: date || 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, minReviewCount: min, maxReviewCount: max })} + min={0} + max={maxReviewCount} + step={10} + /> + onFiltersChange({ ...filters, minParkCount: min, maxParkCount: max })} + min={0} + max={maxParkCount} + step={1} + /> +
+
+ + + + +
+
+ + onFiltersChange({ ...filters, hasRating: checked as boolean }) + } + /> + +
+
+ + onFiltersChange({ ...filters, hasFoundedDate: checked as boolean }) + } + /> + +
+
+
+
+ ); +} diff --git a/src/components/parks/ParkFilters.tsx b/src/components/parks/ParkFilters.tsx index 92df64d7..e36abbd8 100644 --- a/src/components/parks/ParkFilters.tsx +++ b/src/components/parks/ParkFilters.tsx @@ -1,15 +1,18 @@ -import { useState, useMemo } from 'react'; +import { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Slider } from '@/components/ui/slider'; -import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import { RotateCcw } from 'lucide-react'; +import { supabase } from '@/integrations/supabase/client'; import { Park } from '@/types/database'; import { FilterState } from '@/pages/Parks'; -import { MonthYearPicker } from '@/components/ui/month-year-picker'; +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'; interface ParkFiltersProps { filters: FilterState; @@ -18,15 +21,66 @@ interface ParkFiltersProps { } export function ParkFilters({ filters, onFiltersChange, parks }: ParkFiltersProps) { - const countries = useMemo(() => { - const countrySet = new Set(); - parks.forEach(park => { - if (park.location?.country) { - countrySet.add(park.location.country); - } - }); - return Array.from(countrySet).sort(); - }, [parks]); + 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: operators } = useQuery({ + queryKey: ['filter-operators'], + queryFn: async () => { + const { data } = await supabase + .from('companies') + .select('id, name') + .eq('company_type', 'operator') + .order('name'); + return data || []; + }, + staleTime: 5 * 60 * 1000, + }); + + const { data: propertyOwners } = useQuery({ + queryKey: ['filter-property-owners'], + queryFn: async () => { + const { data } = await supabase + .from('companies') + .select('id, name') + .eq('company_type', 'property_owner') + .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 operatorOptions: MultiSelectOption[] = useMemo(() => { + return (operators || []).map(o => ({ label: o.name, value: o.id })); + }, [operators]); + + const propertyOwnerOptions: MultiSelectOption[] = useMemo(() => { + return (propertyOwners || []).map(p => ({ label: p.name, value: p.id })); + }, [propertyOwners]); const parkTypes = [ { value: 'all', label: 'All Types' }, @@ -48,8 +102,13 @@ export function ParkFilters({ filters, onFiltersChange, parks }: ParkFiltersProp return Math.max(...parks.map(p => p.ride_count || 0), 100); }, [parks]); - const currentYear = new Date().getFullYear(); - const minYear = 1900; + const maxCoasters = useMemo(() => { + return Math.max(...parks.map(p => p.coaster_count || 0), 50); + }, [parks]); + + const maxReviews = useMemo(() => { + return Math.max(...parks.map(p => p.review_count || 0), 1000); + }, [parks]); const resetFilters = () => { onFiltersChange({ @@ -57,10 +116,18 @@ export function ParkFilters({ filters, onFiltersChange, parks }: ParkFiltersProp parkType: 'all', status: 'all', country: 'all', + states: [], + cities: [], + operators: [], + propertyOwners: [], minRating: 0, maxRating: 5, minRides: 0, maxRides: maxRides, + minCoasters: 0, + maxCoasters: maxCoasters, + minReviews: 0, + maxReviews: maxReviews, openingYearStart: null, openingYearEnd: null, }); @@ -76,155 +143,169 @@ export function ParkFilters({ filters, onFiltersChange, parks }: ParkFiltersProp -
- {/* Park Type */} -
- - -
- - {/* Status */} -
- - -
- - {/* Country */} -
- - -
- - {/* Opening Year Range */} -
- -
- onFiltersChange({ - ...filters, - openingYearStart: date ? date.getFullYear() : null - })} - placeholder="From year" - fromYear={minYear} - toYear={currentYear} - /> - onFiltersChange({ - ...filters, - openingYearEnd: date ? date.getFullYear() : null - })} - placeholder="To year" - fromYear={minYear} - toYear={currentYear} - /> + +
+ {/* Park Type */} +
+ +
+ + {/* Status */} +
+ + +
+ + {/* Country */} +
+ + +
+ + onFiltersChange({ ...filters, states: value })} + placeholder="Select states" + /> + + onFiltersChange({ ...filters, cities: value })} + placeholder="Select cities" + />
-
+ -
- {/* 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() { />
- +
+ + +
+ + + + + + + + + +
{/* Companies Grid */} diff --git a/src/pages/Manufacturers.tsx b/src/pages/Manufacturers.tsx index faa17c70..58a0fd9a 100644 --- a/src/pages/Manufacturers.tsx +++ b/src/pages/Manufacturers.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, Factory, Plus } from 'lucide-react'; +import { Search, SlidersHorizontal, Factory, Plus, ChevronDown, Filter } from 'lucide-react'; +import { ManufacturerFilters, ManufacturerFilterState, defaultManufacturerFilters } from '@/components/manufacturers/ManufacturerFilters'; import { Company } from '@/types/database'; import { supabase } from '@/integrations/supabase/client'; import { ManufacturerCard } from '@/components/manufacturers/ManufacturerCard'; @@ -27,6 +30,8 @@ export default function Manufacturers() { const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [sortBy, setSortBy] = useState('name'); + const [filters, setFilters] = useState(defaultManufacturerFilters); + const [showFilters, setShowFilters] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); @@ -157,16 +162,33 @@ export default function Manufacturers() { />
- +
+ + +
+ + + + + + + + + + {/* Companies Grid */} diff --git a/src/pages/Operators.tsx b/src/pages/Operators.tsx index fcb40932..149534d6 100644 --- a/src/pages/Operators.tsx +++ b/src/pages/Operators.tsx @@ -7,10 +7,13 @@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Search, Filter, Building, Plus } from 'lucide-react'; +import { Collapsible, CollapsibleContent } from '@/components/ui/collapsible'; +import { Card, CardContent } from '@/components/ui/card'; +import { Search, Filter, Building, Plus, ChevronDown } from 'lucide-react'; import { supabase } from '@/integrations/supabase/client'; import OperatorCard from '@/components/operators/OperatorCard'; import { OperatorForm } from '@/components/admin/OperatorForm'; +import { OperatorFilters, OperatorFilterState, defaultOperatorFilters } from '@/components/operators/OperatorFilters'; import { Company } from '@/types/database'; import { useAuth } from '@/hooks/useAuth'; import { useUserRole } from '@/hooks/useUserRole'; @@ -26,7 +29,8 @@ const Operators = () => { const { requireAuth } = useAuthModal(); const [searchTerm, setSearchTerm] = useState(''); const [sortBy, setSortBy] = useState('name'); - const [filterBy, setFilterBy] = useState('all'); + const [filters, setFilters] = useState(defaultOperatorFilters); + const [showFilters, setShowFilters] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const { data: operators, isLoading } = useQuery({ @@ -170,7 +174,7 @@ const Operators = () => {
- +
+ + + + + + + + + + diff --git a/src/pages/Parks.tsx b/src/pages/Parks.tsx index 5cb3ade9..2cba7886 100644 --- a/src/pages/Parks.tsx +++ b/src/pages/Parks.tsx @@ -38,10 +38,18 @@ export interface FilterState { parkType: string; status: string; country: string; + states?: string[]; + cities?: string[]; + operators?: string[]; + propertyOwners?: string[]; minRating: number; maxRating: number; minRides: number; maxRides: number; + minCoasters?: number; + maxCoasters?: number; + minReviews?: number; + maxReviews?: number; openingYearStart: number | null; openingYearEnd: number | null; } @@ -56,10 +64,18 @@ const initialFilters: FilterState = { parkType: 'all', status: 'all', country: 'all', + states: [], + cities: [], + operators: [], + propertyOwners: [], minRating: 0, maxRating: 5, minRides: 0, maxRides: 1000, + minCoasters: 0, + maxCoasters: 100, + minReviews: 0, + maxReviews: 1000, openingYearStart: null, openingYearEnd: null, }; @@ -144,6 +160,34 @@ export default function Parks() { return false; } + // States filter + if (filters.states && filters.states.length > 0) { + if (!park.location?.state_province || !filters.states.includes(park.location.state_province)) { + return false; + } + } + + // Cities filter + if (filters.cities && filters.cities.length > 0) { + if (!park.location?.city || !filters.cities.includes(park.location.city)) { + return false; + } + } + + // Operators filter + if (filters.operators && filters.operators.length > 0) { + if (!park.operator?.id || !filters.operators.includes(park.operator.id)) { + return false; + } + } + + // Property owners filter + if (filters.propertyOwners && filters.propertyOwners.length > 0) { + if (!park.property_owner?.id || !filters.propertyOwners.includes(park.property_owner.id)) { + return false; + } + } + // Rating filter const rating = park.average_rating || 0; if (rating < filters.minRating || rating > filters.maxRating) { @@ -156,6 +200,22 @@ export default function Parks() { return false; } + // Coaster count filter + if (filters.minCoasters !== undefined && filters.maxCoasters !== undefined) { + const coasterCount = park.coaster_count || 0; + if (coasterCount < filters.minCoasters || coasterCount > filters.maxCoasters) { + return false; + } + } + + // Review count filter + if (filters.minReviews !== undefined && filters.maxReviews !== undefined) { + const reviewCount = park.review_count || 0; + if (reviewCount < filters.minReviews || reviewCount > filters.maxReviews) { + return false; + } + } + // Opening year filter if (filters.openingYearStart || filters.openingYearEnd) { const openingYear = park.opening_date ? parseInt(park.opening_date.split('-')[0]) : null; diff --git a/src/pages/Rides.tsx b/src/pages/Rides.tsx index 1db6ba58..2de12eed 100644 --- a/src/pages/Rides.tsx +++ b/src/pages/Rides.tsx @@ -5,10 +5,13 @@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Filter, SlidersHorizontal, FerrisWheel, Plus } from 'lucide-react'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { Card, CardContent } from '@/components/ui/card'; +import { Filter, SlidersHorizontal, FerrisWheel, Plus, ChevronDown } from 'lucide-react'; import { AutocompleteSearch } from '@/components/search/AutocompleteSearch'; import { RideCard } from '@/components/rides/RideCard'; import { RideForm } from '@/components/admin/RideForm'; +import { RideFilters, RideFilterState, defaultRideFilters } from '@/components/rides/RideFilters'; import { Ride } from '@/types/database'; import { supabase } from '@/integrations/supabase/client'; import { useAuth } from '@/hooks/useAuth'; @@ -26,51 +29,25 @@ export default function Rides() { const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [sortBy, setSortBy] = useState('name'); - const [filterCategory, setFilterCategory] = useState('all'); - const [filterStatus, setFilterStatus] = useState('all'); + const [filters, setFilters] = useState(defaultRideFilters); + const [showFilters, setShowFilters] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); useEffect(() => { fetchRides(); - }, [sortBy, filterCategory, filterStatus]); + }, []); const fetchRides = async () => { try { - let query = supabase + const { data } = await supabase .from('rides') .select(` *, park:parks!inner(name, slug, location:locations(*)), - manufacturer:companies!rides_manufacturer_id_fkey(*) - `); - - // Apply filters - if (filterCategory !== 'all') { - query = query.eq('category', filterCategory); - } - if (filterStatus !== 'all') { - query = query.eq('status', filterStatus); - } - - // Apply sorting - switch (sortBy) { - case 'rating': - query = query.order('average_rating', { ascending: false }); - break; - case 'speed': - query = query.order('max_speed_kmh', { ascending: false, nullsFirst: false }); - break; - case 'height': - query = query.order('max_height_meters', { ascending: false, nullsFirst: false }); - break; - case 'reviews': - query = query.order('review_count', { ascending: false }); - break; - default: - query = query.order('name'); - } - - const { data } = await query; + manufacturer:companies!rides_manufacturer_id_fkey(*), + designer:companies!rides_designer_id_fkey(*) + `) + .order('name'); setRides(data || []); } catch (error) { console.error('Error fetching rides:', error); @@ -207,34 +184,27 @@ export default function Rides() { - - - + + + + + + + + + + + {/* Rides Grid */}