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 ? ( +
+ {`${company.name} +
+ ) : ( +
+ {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;