diff --git a/src/App.tsx b/src/App.tsx
index 5f3e77b1..ad8b41c1 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -18,6 +18,7 @@ import ParkOwners from "./pages/ParkOwners";
import Auth from "./pages/Auth";
import Profile from "./pages/Profile";
import UserSettings from "./pages/UserSettings";
+import Search from "./pages/Search";
import NotFound from "./pages/NotFound";
import Terms from "./pages/Terms";
import Privacy from "./pages/Privacy";
@@ -43,6 +44,7 @@ function AppContent() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/src/components/search/EnhancedSearchResults.tsx b/src/components/search/EnhancedSearchResults.tsx
new file mode 100644
index 00000000..415ad90b
--- /dev/null
+++ b/src/components/search/EnhancedSearchResults.tsx
@@ -0,0 +1,242 @@
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Card, CardContent } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Star, MapPin, Zap, Factory, Clock, Users, Calendar, Ruler, Gauge, Building } from 'lucide-react';
+import { SearchResult } from '@/hooks/useSearch';
+
+interface EnhancedSearchResultsProps {
+ results: SearchResult[];
+ loading: boolean;
+ hasMore?: boolean;
+ onLoadMore?: () => void;
+}
+
+export function EnhancedSearchResults({ results, loading, hasMore, onLoadMore }: EnhancedSearchResultsProps) {
+ const navigate = useNavigate();
+
+ const handleResultClick = (result: SearchResult) => {
+ switch (result.type) {
+ case 'park':
+ navigate(`/parks/${result.slug || result.id}`);
+ break;
+ case 'ride':
+ // Need to get park slug for ride navigation
+ navigate(`/rides/${result.id}`);
+ break;
+ case 'company':
+ navigate(`/companies/${result.id}`);
+ break;
+ }
+ };
+
+ const getTypeIcon = (type: string, size = 'w-6 h-6') => {
+ switch (type) {
+ case 'park': return ;
+ case 'ride': return ;
+ case 'company': return ;
+ default: return ;
+ }
+ };
+
+ const getTypeColor = (type: string) => {
+ switch (type) {
+ case 'park':
+ return 'bg-primary/10 text-primary border-primary/20';
+ case 'ride':
+ return 'bg-secondary/10 text-secondary border-secondary/20';
+ case 'company':
+ return 'bg-accent/10 text-accent border-accent/20';
+ default:
+ return 'bg-muted text-muted-foreground';
+ }
+ };
+
+ const renderParkDetails = (result: SearchResult) => {
+ if (result.type !== 'park') return null;
+ const parkData = result.data as any; // Type assertion for park-specific properties
+
+ return (
+
+ {parkData?.ride_count && (
+
+
+ {parkData.ride_count} rides
+
+ )}
+ {parkData?.opening_date && (
+
+
+ Opened {new Date(parkData.opening_date).getFullYear()}
+
+ )}
+ {parkData?.status && (
+
+ {parkData.status.replace('_', ' ')}
+
+ )}
+
+ );
+ };
+
+ const renderRideDetails = (result: SearchResult) => {
+ if (result.type !== 'ride') return null;
+ const rideData = result.data as any; // Type assertion for ride-specific properties
+
+ return (
+
+ {rideData?.category && (
+
+ {rideData.category.replace('_', ' ')}
+
+ )}
+ {rideData?.max_height_meters && (
+
+
+ {rideData.max_height_meters}m
+
+ )}
+ {rideData?.max_speed_kmh && (
+
+
+ {rideData.max_speed_kmh} km/h
+
+ )}
+ {rideData?.intensity_level && (
+
+ {rideData.intensity_level}
+
+ )}
+
+ );
+ };
+
+ const renderCompanyDetails = (result: SearchResult) => {
+ if (result.type !== 'company') return null;
+ const companyData = result.data as any; // Type assertion for company-specific properties
+
+ return (
+
+ {companyData?.company_type && (
+
+ {companyData.company_type.replace('_', ' ')}
+
+ )}
+ {companyData?.founded_year && (
+
+
+ Founded {companyData.founded_year}
+
+ )}
+ {companyData?.headquarters_location && (
+
+
+ {companyData.headquarters_location}
+
+ )}
+
+ );
+ };
+
+ if (loading && results.length === 0) {
+ return (
+
+ );
+ }
+
+ if (results.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+ {results.map((result) => (
+
handleResultClick(result)}
+ className="group cursor-pointer hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 border-border/50 hover:border-primary/50"
+ >
+
+
+ {/* Image placeholder or actual image */}
+
+ {result.image ? (
+

+ ) : (
+
+ {getTypeIcon(result.type, 'w-8 h-8')}
+
+ )}
+
+
+ {/* Content */}
+
+
+
+
+
+ {result.title}
+
+
+ {result.type}
+
+
+
+
+ {result.subtitle}
+
+
+ {/* Type-specific details */}
+ {result.type === 'park' && renderParkDetails(result)}
+ {result.type === 'ride' && renderRideDetails(result)}
+ {result.type === 'company' && renderCompanyDetails(result)}
+
+
+ {/* Rating */}
+ {result.rating && result.rating > 0 && (
+
+
+ {result.rating.toFixed(1)}
+ {result.data?.review_count && (
+
+ ({result.data.review_count})
+
+ )}
+
+ )}
+
+
+
+
+
+ ))}
+
+
+ {/* Load More Button */}
+ {hasMore && (
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/search/SearchFilters.tsx b/src/components/search/SearchFilters.tsx
new file mode 100644
index 00000000..cc3df390
--- /dev/null
+++ b/src/components/search/SearchFilters.tsx
@@ -0,0 +1,387 @@
+import { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Slider } from '@/components/ui/slider';
+import { Badge } from '@/components/ui/badge';
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
+import { ChevronDown, Filter, X } from 'lucide-react';
+
+export interface SearchFilters {
+ // Park filters
+ parkType?: string;
+ country?: string;
+ stateProvince?: string;
+ status?: string;
+ openingYearMin?: number;
+ openingYearMax?: number;
+ ratingMin?: number;
+ ratingMax?: number;
+ rideCountMin?: number;
+ rideCountMax?: number;
+
+ // Ride filters
+ rideCategory?: string;
+ rideType?: string;
+ manufacturer?: string;
+ heightMin?: number;
+ heightMax?: number;
+ speedMin?: number;
+ speedMax?: number;
+ intensityLevel?: string;
+
+ // Company filters
+ companyType?: string;
+ foundedYearMin?: number;
+ foundedYearMax?: number;
+ headquarters?: string;
+}
+
+interface SearchFiltersProps {
+ filters: SearchFilters;
+ onFiltersChange: (filters: SearchFilters) => void;
+ activeTab: string;
+}
+
+export function SearchFiltersComponent({ filters, onFiltersChange, activeTab }: SearchFiltersProps) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const updateFilter = (key: keyof SearchFilters, value: any) => {
+ onFiltersChange({ ...filters, [key]: value });
+ };
+
+ const clearFilters = () => {
+ onFiltersChange({});
+ };
+
+ const hasActiveFilters = Object.values(filters).some(value => value !== undefined && value !== '');
+
+ const currentYear = new Date().getFullYear();
+
+ return (
+
+
+
+ setIsOpen(!isOpen)}>
+
+
+ Filters
+ {hasActiveFilters && (
+
+ Active
+
+ )}
+
+
+ {hasActiveFilters && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ {/* Park Filters */}
+ {(activeTab === 'all' || activeTab === 'park') && (
+
+
Park Filters
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {filters.ratingMin || 0} - {filters.ratingMax || 5}
+
+
+
{
+ updateFilter('ratingMin', min);
+ updateFilter('ratingMax', max);
+ }}
+ min={0}
+ max={5}
+ step={0.1}
+ className="w-full"
+ />
+
+
+
+
+
+
+ {filters.rideCountMin || 0} - {filters.rideCountMax || 100}+
+
+
+
{
+ updateFilter('rideCountMin', min);
+ updateFilter('rideCountMax', max);
+ }}
+ min={0}
+ max={100}
+ step={1}
+ className="w-full"
+ />
+
+
+
+ )}
+
+ {/* Ride Filters */}
+ {(activeTab === 'all' || activeTab === 'ride') && (
+
+
Ride Filters
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {filters.heightMin || 0}m - {filters.heightMax || 200}m
+
+
+
{
+ updateFilter('heightMin', min);
+ updateFilter('heightMax', max);
+ }}
+ min={0}
+ max={200}
+ step={1}
+ className="w-full"
+ />
+
+
+
+
+
+
+ {filters.speedMin || 0} - {filters.speedMax || 200}+ km/h
+
+
+
{
+ updateFilter('speedMin', min);
+ updateFilter('speedMax', max);
+ }}
+ min={0}
+ max={200}
+ step={5}
+ className="w-full"
+ />
+
+
+
+ )}
+
+ {/* Company Filters */}
+ {(activeTab === 'all' || activeTab === 'company') && (
+
+
Company Filters
+
+
+
+
+
+
+
+
+
+ updateFilter('headquarters', e.target.value || undefined)}
+ />
+
+
+
+
+
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/search/SearchSortOptions.tsx b/src/components/search/SearchSortOptions.tsx
new file mode 100644
index 00000000..54dc4652
--- /dev/null
+++ b/src/components/search/SearchSortOptions.tsx
@@ -0,0 +1,99 @@
+import { SortAsc, SortDesc, ArrowUpDown } from 'lucide-react';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Button } from '@/components/ui/button';
+
+export interface SortOption {
+ field: string;
+ direction: 'asc' | 'desc';
+}
+
+interface SearchSortOptionsProps {
+ sort: SortOption;
+ onSortChange: (sort: SortOption) => void;
+ activeTab: string;
+}
+
+export function SearchSortOptions({ sort, onSortChange, activeTab }: SearchSortOptionsProps) {
+ const getSortOptions = () => {
+ const commonOptions = [
+ { value: 'relevance', label: 'Relevance' },
+ { value: 'name', label: 'Name' },
+ { value: 'rating', label: 'Rating' },
+ { value: 'reviews', label: 'Review Count' },
+ ];
+
+ const parkOptions = [
+ ...commonOptions,
+ { value: 'rides', label: 'Ride Count' },
+ { value: 'coasters', label: 'Coaster Count' },
+ { value: 'opening', label: 'Opening Date' },
+ ];
+
+ const rideOptions = [
+ ...commonOptions,
+ { value: 'height', label: 'Height' },
+ { value: 'speed', label: 'Speed' },
+ { value: 'opening', label: 'Opening Date' },
+ { value: 'intensity', label: 'Intensity' },
+ ];
+
+ const companyOptions = [
+ ...commonOptions.filter(opt => opt.value !== 'rating'), // Companies might not have ratings
+ { value: 'founded', label: 'Founded Year' },
+ { value: 'parks', label: 'Parks Count' },
+ { value: 'rides', label: 'Rides Count' },
+ ];
+
+ switch (activeTab) {
+ case 'park':
+ return parkOptions;
+ case 'ride':
+ return rideOptions;
+ case 'company':
+ return companyOptions;
+ default:
+ return commonOptions;
+ }
+ };
+
+ const toggleDirection = () => {
+ onSortChange({
+ ...sort,
+ direction: sort.direction === 'asc' ? 'desc' : 'asc'
+ });
+ };
+
+ return (
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/Search.tsx b/src/pages/Search.tsx
index 65e740cd..97c05558 100644
--- a/src/pages/Search.tsx
+++ b/src/pages/Search.tsx
@@ -1,12 +1,13 @@
import { useState, useEffect } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { Header } from '@/components/layout/Header';
-import { Card, CardContent } from '@/components/ui/card';
-import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import { Search, MapPin, Zap, Star, Castle, FerrisWheel, Factory } from 'lucide-react';
+import { Search, Filter, SlidersHorizontal } from 'lucide-react';
import { AutocompleteSearch } from '@/components/search/AutocompleteSearch';
+import { SearchFiltersComponent, SearchFilters } from '@/components/search/SearchFilters';
+import { SearchSortOptions, SortOption } from '@/components/search/SearchSortOptions';
+import { EnhancedSearchResults } from '@/components/search/EnhancedSearchResults';
import { useSearch, SearchResult } from '@/hooks/useSearch';
export default function SearchPage() {
@@ -15,6 +16,10 @@ export default function SearchPage() {
const initialQuery = searchParams.get('q') || '';
const [activeTab, setActiveTab] = useState('all');
+ const [filters, setFilters] = useState({});
+ const [sort, setSort] = useState({ field: 'relevance', direction: 'desc' });
+ const [showFilters, setShowFilters] = useState(false);
+
const {
query,
setQuery,
@@ -32,9 +37,59 @@ export default function SearchPage() {
}
}, [initialQuery, setQuery]);
- const filteredResults = results.filter(result =>
- activeTab === 'all' || result.type === activeTab
- );
+ // Filter and sort results
+ const filteredAndSortedResults = (() => {
+ let filtered = results.filter(result =>
+ activeTab === 'all' || result.type === activeTab
+ );
+
+ // Apply filters
+ if (filters.country) {
+ filtered = filtered.filter(result =>
+ result.subtitle?.toLowerCase().includes(filters.country!.toLowerCase())
+ );
+ }
+
+ if (filters.stateProvince) {
+ filtered = filtered.filter(result =>
+ result.subtitle?.toLowerCase().includes(filters.stateProvince!.toLowerCase())
+ );
+ }
+
+ if (filters.ratingMin !== undefined || filters.ratingMax !== undefined) {
+ filtered = filtered.filter(result => {
+ if (!result.rating) return false;
+ const rating = result.rating;
+ const min = filters.ratingMin ?? 0;
+ const max = filters.ratingMax ?? 5;
+ return rating >= min && rating <= max;
+ });
+ }
+
+ // Sort results
+ filtered.sort((a, b) => {
+ const direction = sort.direction === 'asc' ? 1 : -1;
+
+ switch (sort.field) {
+ case 'name':
+ return direction * a.title.localeCompare(b.title);
+ case 'rating':
+ return direction * ((b.rating || 0) - (a.rating || 0));
+ case 'reviews':
+ return direction * (((b.data as any)?.review_count || 0) - ((a.data as any)?.review_count || 0));
+ case 'rides':
+ return direction * (((b.data as any)?.ride_count || 0) - ((a.data as any)?.ride_count || 0));
+ case 'opening':
+ const aDate = (a.data as any)?.opening_date ? new Date((a.data as any).opening_date).getTime() : 0;
+ const bDate = (b.data as any)?.opening_date ? new Date((b.data as any).opening_date).getTime() : 0;
+ return direction * (bDate - aDate);
+ default: // relevance
+ return 0; // Keep original order for relevance
+ }
+ });
+
+ return filtered;
+ })();
const resultCounts = {
all: results.length,
@@ -43,42 +98,6 @@ export default function SearchPage() {
company: results.filter(r => r.type === 'company').length
};
- const handleResultClick = (result: SearchResult) => {
- switch (result.type) {
- case 'park':
- navigate(`/parks/${result.slug || result.id}`);
- break;
- case 'ride':
- navigate(`/rides/${result.id}`);
- break;
- case 'company':
- navigate(`/companies/${result.id}`);
- break;
- }
- };
-
- const getTypeIcon = (type: string) => {
- switch (type) {
- case 'park': return ;
- case 'ride': return ;
- case 'company': return ;
- default: return ;
- }
- };
-
- const getTypeColor = (type: string) => {
- switch (type) {
- case 'park':
- return 'bg-primary/10 text-primary border-primary/20';
- case 'ride':
- return 'bg-secondary/10 text-secondary border-secondary/20';
- case 'company':
- return 'bg-accent/10 text-accent border-accent/20';
- default:
- return 'bg-muted text-muted-foreground';
- }
- };
-
return (
@@ -88,12 +107,12 @@ export default function SearchPage() {
-
Search Results
+ Search
{query && (
- Results for "{query}"
+ Showing {filteredAndSortedResults.length} results for "{query}"
)}
@@ -112,109 +131,95 @@ export default function SearchPage() {
- {/* Results */}
+ {/* Results Section */}
{query && (
- <>
- {/* Filter Tabs */}
-
-
-
- All ({resultCounts.all})
-
-
- Parks ({resultCounts.park})
-
-
- Rides ({resultCounts.ride})
-
-
- Companies ({resultCounts.company})
-
-
-
+
+ {/* Tabs and Controls */}
+
+
+
+
+ All ({resultCounts.all})
+
+
+ Parks ({resultCounts.park})
+
+
+ Rides ({resultCounts.ride})
+
+
+ Companies ({resultCounts.company})
+
+
+
- {/* Loading State */}
- {loading && (
-
- )}
-
- {/* Results Grid */}
- {!loading && filteredResults.length > 0 && (
-
- {filteredResults.map((result) => (
-
handleResultClick(result)}
- className="group cursor-pointer hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 border-border/50 hover:border-primary/50"
- >
-
-
-
{getTypeIcon(result.type)}
-
-
-
- {result.title}
-
-
- {result.type}
-
-
-
-
- {result.subtitle}
-
-
-
-
- {result.type === 'park' && (
-
-
-
- )}
- {result.type === 'ride' && (
-
-
-
- )}
-
-
- {result.rating && result.rating > 0 && (
-
-
- {result.rating.toFixed(1)}
-
- )}
-
-
-
-
-
- ))}
-
- )}
-
- {/* No Results */}
- {!loading && query && filteredResults.length === 0 && (
-
-
-
No results found
-
- Try searching for something else or adjust your search terms
-
-
+
+ {/* Layout Grid */}
+
+ {/* Filters Sidebar */}
+ {showFilters && (
+
+
+
+ )}
+
+ {/* Results */}
+
+ {!loading && filteredAndSortedResults.length === 0 && query && (
+
+
+
No results found
+
+ Try adjusting your search terms or filters
+
+
+ {
+ setQuery('');
+ navigate('/search');
+ }}
+ variant="outline"
+ >
+ Clear search
+
+ setFilters({})}
+ variant="outline"
+ >
+ Clear filters
+
+
+
+ )}
+
+
+
+
+
)}
{/* Initial State */}