diff --git a/src/App.tsx b/src/App.tsx
index 5b805c7d..48ed432e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -15,6 +15,7 @@ import Rides from "./pages/Rides";
import Manufacturers from "./pages/Manufacturers";
import Designers from "./pages/Designers";
import ParkOwners from "./pages/ParkOwners";
+import Operators from "./pages/Operators";
import Auth from "./pages/Auth";
import Profile from "./pages/Profile";
import UserSettings from "./pages/UserSettings";
@@ -47,6 +48,7 @@ function AppContent() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/src/components/operators/OperatorCard.tsx b/src/components/operators/OperatorCard.tsx
new file mode 100644
index 00000000..05344120
--- /dev/null
+++ b/src/components/operators/OperatorCard.tsx
@@ -0,0 +1,113 @@
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Card, CardContent } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Building, Star, MapPin } from 'lucide-react';
+import { Company } from '@/types/database';
+
+interface OperatorCardProps {
+ company: Company;
+}
+
+const OperatorCard = ({ company }: OperatorCardProps) => {
+ const navigate = useNavigate();
+
+ const handleClick = () => {
+ navigate(`/operators/${company.slug}/parks/`);
+ };
+
+ const getCompanyIcon = () => {
+ return ;
+ };
+
+ return (
+
+ {/* Logo/Image Section */}
+
+
+
+ {/* Park Operator Badge */}
+
+
+ Park Operator
+
+
+
+ {/* Logo Display */}
+
+ {company.logo_url ? (
+
+

+
+ ) : (
+
+ {getCompanyIcon()}
+
+ )}
+
+
+
+
+ {/* Company Name */}
+
+ {company.name}
+
+
+ {/* Description */}
+ {company.description && (
+
+ {company.description}
+
+ )}
+
+ {/* Company Info */}
+
+ {company.founded_year && (
+
+ Founded:
+ {company.founded_year}
+
+ )}
+
+ {company.headquarters_location && (
+
+
+
+ {company.headquarters_location}
+
+
+ )}
+
+
+ {/* Rating */}
+ {company.average_rating > 0 && (
+
+
+ {company.average_rating.toFixed(1)}
+ ({company.review_count} reviews)
+
+ )}
+
+ {/* Park Count Stats */}
+
+ {(company as any).park_count > 0 && (
+
+
+ {(company as any).park_count}
+ parks operated
+
+ )}
+
+
+
+ );
+};
+
+export default OperatorCard;
diff --git a/src/pages/Operators.tsx b/src/pages/Operators.tsx
new file mode 100644
index 00000000..e47a698d
--- /dev/null
+++ b/src/pages/Operators.tsx
@@ -0,0 +1,185 @@
+import React, { useState } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { Header } from '@/components/layout/Header';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Search, Filter, Building } from 'lucide-react';
+import { supabase } from '@/integrations/supabase/client';
+import OperatorCard from '@/components/operators/OperatorCard';
+import { Company } from '@/types/database';
+
+const Operators = () => {
+ const [searchTerm, setSearchTerm] = useState('');
+ const [sortBy, setSortBy] = useState('name');
+ const [filterBy, setFilterBy] = useState('all');
+
+ const { data: operators, isLoading } = useQuery({
+ queryKey: ['operators'],
+ queryFn: async () => {
+ // Get companies that are park operators with park counts
+ const { data, error } = await supabase
+ .from('companies')
+ .select(`
+ *,
+ parks:parks!operator_id(count)
+ `)
+ .in('id',
+ await supabase
+ .from('parks')
+ .select('operator_id')
+ .not('operator_id', 'is', null)
+ .then(({ data }) => data?.map(park => park.operator_id) || [])
+ )
+ .order('name');
+
+ if (error) throw error;
+
+ // Transform the data to include park_count
+ const transformedData = data?.map(company => ({
+ ...company,
+ park_count: company.parks?.[0]?.count || 0
+ })) || [];
+
+ return transformedData as (Company & { park_count: number })[];
+ },
+ });
+
+ const filteredAndSortedOperators = React.useMemo(() => {
+ if (!operators) return [];
+
+ let filtered = operators.filter(operator => {
+ const matchesSearch = operator.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ (operator.description && operator.description.toLowerCase().includes(searchTerm.toLowerCase()));
+
+ if (filterBy === 'all') return matchesSearch;
+ if (filterBy === 'with-rating') return matchesSearch && operator.average_rating > 0;
+ if (filterBy === 'established') return matchesSearch && operator.founded_year;
+
+ return matchesSearch;
+ });
+
+ // Sort
+ filtered.sort((a, b) => {
+ switch (sortBy) {
+ case 'name':
+ return a.name.localeCompare(b.name);
+ case 'rating':
+ return (b.average_rating || 0) - (a.average_rating || 0);
+ case 'founded':
+ return (b.founded_year || 0) - (a.founded_year || 0);
+ case 'reviews':
+ return (b.review_count || 0) - (a.review_count || 0);
+ default:
+ return 0;
+ }
+ });
+
+ return filtered;
+ }, [operators, searchTerm, sortBy, filterBy]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
Park Operators
+
+ Discover companies that operate theme parks
+
+
+
+
+ {/* Search and Filters */}
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10"
+ />
+
+
+
+
+
+
+
+ {/* Results Count */}
+
+
+
+ {filteredAndSortedOperators?.length || 0} park operators
+
+ {searchTerm && (
+
+ Searching: "{searchTerm}"
+
+ )}
+
+
+
+ {/* Loading State */}
+ {isLoading && (
+
+
+
Loading park operators...
+
+ )}
+
+ {/* Operators Grid */}
+ {!isLoading && filteredAndSortedOperators && (
+
+ {filteredAndSortedOperators.map((operator) => (
+
+ ))}
+
+ )}
+
+ {/* Empty State */}
+ {!isLoading && filteredAndSortedOperators?.length === 0 && (
+
+
+
No park operators found
+
+ {searchTerm
+ ? "Try adjusting your search terms or filters"
+ : "No park operators are currently available"
+ }
+
+
+ )}
+
+
+ );
+};
+
+export default Operators;