mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 05:11:14 -05:00
feat: Implement comprehensive entity filtering
This commit is contained in:
175
src/components/operators/OperatorFilters.tsx
Normal file
175
src/components/operators/OperatorFilters.tsx
Normal file
@@ -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 (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">Filter Operators</h3>
|
||||
<Button variant="ghost" size="sm" onClick={resetFilters}>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<FilterSection>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FilterMultiSelectCombobox
|
||||
label="Headquarters Country"
|
||||
options={countryOptions}
|
||||
value={filters.countries}
|
||||
onChange={(value) => onFiltersChange({ ...filters, countries: value })}
|
||||
placeholder="Select countries"
|
||||
/>
|
||||
<FilterDateRangePicker
|
||||
label="Founded Date"
|
||||
fromDate={filters.foundedDateFrom}
|
||||
toDate={filters.foundedDateTo}
|
||||
onFromChange={(date) => onFiltersChange({ ...filters, foundedDateFrom: date || null })}
|
||||
onToChange={(date) => onFiltersChange({ ...filters, foundedDateTo: date || null })}
|
||||
fromPlaceholder="From year"
|
||||
toPlaceholder="To year"
|
||||
/>
|
||||
</div>
|
||||
</FilterSection>
|
||||
|
||||
<Separator />
|
||||
|
||||
<FilterSection title="Statistics">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<FilterRangeSlider
|
||||
label="Rating"
|
||||
value={[filters.minRating, filters.maxRating]}
|
||||
onChange={([min, max]) => onFiltersChange({ ...filters, minRating: min, maxRating: max })}
|
||||
min={0}
|
||||
max={5}
|
||||
step={0.1}
|
||||
formatValue={(v) => `${v.toFixed(1)} stars`}
|
||||
/>
|
||||
<FilterRangeSlider
|
||||
label="Review Count"
|
||||
value={[filters.minReviewCount, filters.maxReviewCount]}
|
||||
onChange={([min, max]) => onFiltersChange({ ...filters, minReviewCount: min, maxReviewCount: max })}
|
||||
min={0}
|
||||
max={maxReviewCount}
|
||||
step={10}
|
||||
/>
|
||||
<FilterRangeSlider
|
||||
label="Park Count"
|
||||
value={[filters.minParkCount, filters.maxParkCount]}
|
||||
onChange={([min, max]) => onFiltersChange({ ...filters, minParkCount: min, maxParkCount: max })}
|
||||
min={0}
|
||||
max={maxParkCount}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
</FilterSection>
|
||||
|
||||
<Separator />
|
||||
|
||||
<FilterSection title="Other Options">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="hasRating"
|
||||
checked={filters.hasRating}
|
||||
onCheckedChange={(checked) =>
|
||||
onFiltersChange({ ...filters, hasRating: checked as boolean })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="hasRating" className="cursor-pointer">
|
||||
Has rating
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="hasFoundedDate"
|
||||
checked={filters.hasFoundedDate}
|
||||
onCheckedChange={(checked) =>
|
||||
onFiltersChange({ ...filters, hasFoundedDate: checked as boolean })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="hasFoundedDate" className="cursor-pointer">
|
||||
Has founded date
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</FilterSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user