mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 21:51:14 -05:00
feat: Implement comprehensive entity filtering
This commit is contained in:
@@ -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<DesignerFilterState>(defaultDesignerFilters);
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
|
||||
const handleCreateSubmit = async (data: any) => {
|
||||
@@ -144,16 +149,33 @@ export default function Designers() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select value={sortBy} onValueChange={setSortBy}>
|
||||
<SelectTrigger className="w-full h-10">
|
||||
<SlidersHorizontal className="h-4 w-4 mr-2" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="name">Name A-Z</SelectItem>
|
||||
<SelectItem value="founded">Founded (Newest)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex gap-2">
|
||||
<Select value={sortBy} onValueChange={setSortBy}>
|
||||
<SelectTrigger className="w-[180px] h-10">
|
||||
<SlidersHorizontal className="h-4 w-4 mr-2" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="name">Name A-Z</SelectItem>
|
||||
<SelectItem value="founded">Founded (Newest)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" onClick={() => setShowFilters(!showFilters)} className="gap-2">
|
||||
<Filter className="w-4 h-4" />
|
||||
Filters
|
||||
<ChevronDown className={`w-4 h-4 transition-transform ${showFilters ? 'rotate-180' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Collapsible open={showFilters} onOpenChange={setShowFilters}>
|
||||
<CollapsibleContent>
|
||||
<Card className="mt-4">
|
||||
<CardContent className="pt-6">
|
||||
<DesignerFilters filters={filters} onFiltersChange={setFilters} designers={companies} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
{/* Companies Grid */}
|
||||
|
||||
@@ -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<ManufacturerFilterState>(defaultManufacturerFilters);
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
|
||||
|
||||
@@ -157,16 +162,33 @@ export default function Manufacturers() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select value={sortBy} onValueChange={setSortBy}>
|
||||
<SelectTrigger className="w-full h-10">
|
||||
<SlidersHorizontal className="h-4 w-4 mr-2" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="name">Name A-Z</SelectItem>
|
||||
<SelectItem value="founded">Founded (Newest)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex gap-2">
|
||||
<Select value={sortBy} onValueChange={setSortBy}>
|
||||
<SelectTrigger className="w-[180px] h-10">
|
||||
<SlidersHorizontal className="h-4 w-4 mr-2" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="name">Name A-Z</SelectItem>
|
||||
<SelectItem value="founded">Founded (Newest)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" onClick={() => setShowFilters(!showFilters)} className="gap-2">
|
||||
<Filter className="w-4 h-4" />
|
||||
Filters
|
||||
<ChevronDown className={`w-4 h-4 transition-transform ${showFilters ? 'rotate-180' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Collapsible open={showFilters} onOpenChange={setShowFilters}>
|
||||
<CollapsibleContent>
|
||||
<Card className="mt-4">
|
||||
<CardContent className="pt-6">
|
||||
<ManufacturerFilters filters={filters} onFiltersChange={setFilters} manufacturers={companies} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
{/* Companies Grid */}
|
||||
|
||||
@@ -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<OperatorFilterState>(defaultOperatorFilters);
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
|
||||
const { data: operators, isLoading } = useQuery({
|
||||
@@ -170,7 +174,7 @@ const Operators = () => {
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Select value={sortBy} onValueChange={setSortBy}>
|
||||
<SelectTrigger className="flex-1 h-10">
|
||||
<SelectTrigger className="w-[180px] h-10">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -181,18 +185,22 @@ const Operators = () => {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={filterBy} onValueChange={setFilterBy}>
|
||||
<SelectTrigger className="flex-1 h-10">
|
||||
<Filter className="h-4 w-4 mr-2" />
|
||||
<SelectValue placeholder="Filter" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All</SelectItem>
|
||||
<SelectItem value="with-rating">Rated</SelectItem>
|
||||
<SelectItem value="established">Est.</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" onClick={() => setShowFilters(!showFilters)} className="gap-2">
|
||||
<Filter className="w-4 h-4" />
|
||||
Filters
|
||||
<ChevronDown className={`w-4 h-4 transition-transform ${showFilters ? 'rotate-180' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Collapsible open={showFilters} onOpenChange={setShowFilters}>
|
||||
<CollapsibleContent>
|
||||
<Card className="mt-4">
|
||||
<CardContent className="pt-6">
|
||||
<OperatorFilters filters={filters} onFiltersChange={setFilters} operators={operators || []} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<RideFilterState>(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() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={filterCategory} onValueChange={setFilterCategory}>
|
||||
<SelectTrigger className="w-[160px]">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map(category => (
|
||||
<SelectItem key={category.value} value={category.value}>
|
||||
{category.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={filterStatus} onValueChange={setFilterStatus}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{statusOptions.map(status => (
|
||||
<SelectItem key={status.value} value={status.value}>
|
||||
{status.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className="gap-2"
|
||||
>
|
||||
<Filter className="w-4 h-4" />
|
||||
Filters
|
||||
<ChevronDown className={`w-4 h-4 transition-transform ${showFilters ? 'rotate-180' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Collapsible open={showFilters} onOpenChange={setShowFilters}>
|
||||
<CollapsibleContent>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<RideFilters filters={filters} onFiltersChange={setFilters} rides={rides} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
{/* Rides Grid */}
|
||||
|
||||
Reference in New Issue
Block a user