feat: Implement comprehensive search page

This commit is contained in:
gpt-engineer-app[bot]
2025-09-29 15:45:56 +00:00
parent 9ca1455ab6
commit f799cdd563
5 changed files with 876 additions and 141 deletions

View File

@@ -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() {
<Route path="/parks/:slug" element={<ParkDetail />} />
<Route path="/parks/:parkSlug/rides/:rideSlug" element={<RideDetail />} />
<Route path="/rides" element={<Rides />} />
<Route path="/search" element={<Search />} />
<Route path="/manufacturers" element={<Manufacturers />} />
<Route path="/designers" element={<Designers />} />
<Route path="/owners" element={<ParkOwners />} />

View File

@@ -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 <MapPin className={size} />;
case 'ride': return <Zap className={size} />;
case 'company': return <Factory className={size} />;
default: return <MapPin className={size} />;
}
};
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 (
<div className="flex flex-wrap gap-2 mt-3">
{parkData?.ride_count && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Zap className="w-3 h-3" />
<span>{parkData.ride_count} rides</span>
</div>
)}
{parkData?.opening_date && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Calendar className="w-3 h-3" />
<span>Opened {new Date(parkData.opening_date).getFullYear()}</span>
</div>
)}
{parkData?.status && (
<Badge variant="outline" className="text-xs">
{parkData.status.replace('_', ' ')}
</Badge>
)}
</div>
);
};
const renderRideDetails = (result: SearchResult) => {
if (result.type !== 'ride') return null;
const rideData = result.data as any; // Type assertion for ride-specific properties
return (
<div className="flex flex-wrap gap-2 mt-3">
{rideData?.category && (
<Badge variant="outline" className="text-xs">
{rideData.category.replace('_', ' ')}
</Badge>
)}
{rideData?.max_height_meters && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Ruler className="w-3 h-3" />
<span>{rideData.max_height_meters}m</span>
</div>
)}
{rideData?.max_speed_kmh && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Gauge className="w-3 h-3" />
<span>{rideData.max_speed_kmh} km/h</span>
</div>
)}
{rideData?.intensity_level && (
<Badge variant="outline" className="text-xs">
{rideData.intensity_level}
</Badge>
)}
</div>
);
};
const renderCompanyDetails = (result: SearchResult) => {
if (result.type !== 'company') return null;
const companyData = result.data as any; // Type assertion for company-specific properties
return (
<div className="flex flex-wrap gap-2 mt-3">
{companyData?.company_type && (
<Badge variant="outline" className="text-xs">
{companyData.company_type.replace('_', ' ')}
</Badge>
)}
{companyData?.founded_year && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Calendar className="w-3 h-3" />
<span>Founded {companyData.founded_year}</span>
</div>
)}
{companyData?.headquarters_location && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Building className="w-3 h-3" />
<span>{companyData.headquarters_location}</span>
</div>
)}
</div>
);
};
if (loading && results.length === 0) {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
if (results.length === 0) {
return null;
}
return (
<div className="space-y-4">
<div className="grid gap-4">
{results.map((result) => (
<Card
key={result.id}
onClick={() => handleResultClick(result)}
className="group cursor-pointer hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 border-border/50 hover:border-primary/50"
>
<CardContent className="p-6">
<div className="flex gap-4">
{/* Image placeholder or actual image */}
<div className="flex-shrink-0">
{result.image ? (
<img
src={result.image}
alt={result.title}
className="w-16 h-16 object-cover rounded-lg"
/>
) : (
<div className="w-16 h-16 bg-muted rounded-lg flex items-center justify-center">
{getTypeIcon(result.type, 'w-8 h-8')}
</div>
)}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<h3 className="font-semibold text-lg group-hover:text-primary transition-colors truncate">
{result.title}
</h3>
<Badge variant="outline" className={`text-xs ${getTypeColor(result.type)}`}>
{result.type}
</Badge>
</div>
<p className="text-muted-foreground text-sm mb-2">
{result.subtitle}
</p>
{/* Type-specific details */}
{result.type === 'park' && renderParkDetails(result)}
{result.type === 'ride' && renderRideDetails(result)}
{result.type === 'company' && renderCompanyDetails(result)}
</div>
{/* Rating */}
{result.rating && result.rating > 0 && (
<div className="flex items-center gap-1 flex-shrink-0">
<Star className="w-4 h-4 fill-yellow-400 text-yellow-400" />
<span className="text-sm font-medium">{result.rating.toFixed(1)}</span>
{result.data?.review_count && (
<span className="text-xs text-muted-foreground">
({result.data.review_count})
</span>
)}
</div>
)}
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
{/* Load More Button */}
{hasMore && (
<div className="flex justify-center py-4">
<Button
onClick={onLoadMore}
variant="outline"
disabled={loading}
className="min-w-32"
>
{loading ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary"></div>
) : (
'Load More'
)}
</Button>
</div>
)}
</div>
);
}

View File

@@ -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 (
<Card className="w-full">
<CollapsibleTrigger asChild>
<CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
<div className="flex items-center justify-between" onClick={() => setIsOpen(!isOpen)}>
<div className="flex items-center gap-2">
<Filter className="w-5 h-5" />
<CardTitle className="text-lg">Filters</CardTitle>
{hasActiveFilters && (
<Badge variant="secondary" className="ml-2">
Active
</Badge>
)}
</div>
<div className="flex items-center gap-2">
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
clearFilters();
}}
className="text-xs"
>
<X className="w-4 h-4 mr-1" />
Clear
</Button>
)}
<ChevronDown className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</div>
</div>
</CardHeader>
</CollapsibleTrigger>
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleContent>
<CardContent className="space-y-6">
{/* Park Filters */}
{(activeTab === 'all' || activeTab === 'park') && (
<div className="space-y-4">
<h4 className="font-semibold text-primary">Park Filters</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="parkType">Park Type</Label>
<Select
value={filters.parkType || ''}
onValueChange={(value) => updateFilter('parkType', value || undefined)}
>
<SelectTrigger>
<SelectValue placeholder="All types" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All types</SelectItem>
<SelectItem value="theme_park">Theme Park</SelectItem>
<SelectItem value="amusement_park">Amusement Park</SelectItem>
<SelectItem value="water_park">Water Park</SelectItem>
<SelectItem value="family_entertainment_center">Family Entertainment Center</SelectItem>
<SelectItem value="indoor_park">Indoor Park</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select
value={filters.status || ''}
onValueChange={(value) => updateFilter('status', value || undefined)}
>
<SelectTrigger>
<SelectValue placeholder="All statuses" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All statuses</SelectItem>
<SelectItem value="operating">Operating</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
<SelectItem value="under_construction">Under Construction</SelectItem>
<SelectItem value="seasonal">Seasonal</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Country</Label>
<Input
placeholder="Enter country"
value={filters.country || ''}
onChange={(e) => updateFilter('country', e.target.value || undefined)}
/>
</div>
<div className="space-y-2">
<Label>State/Province</Label>
<Input
placeholder="Enter state/province"
value={filters.stateProvince || ''}
onChange={(e) => updateFilter('stateProvince', e.target.value || undefined)}
/>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label>Opening Year Range</Label>
<div className="grid grid-cols-2 gap-2">
<Input
type="number"
placeholder="From"
min="1800"
max={currentYear}
value={filters.openingYearMin || ''}
onChange={(e) => updateFilter('openingYearMin', e.target.value ? parseInt(e.target.value) : undefined)}
/>
<Input
type="number"
placeholder="To"
min="1800"
max={currentYear}
value={filters.openingYearMax || ''}
onChange={(e) => updateFilter('openingYearMax', e.target.value ? parseInt(e.target.value) : undefined)}
/>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Rating Range</Label>
<Badge variant="outline">
{filters.ratingMin || 0} - {filters.ratingMax || 5}
</Badge>
</div>
<Slider
value={[filters.ratingMin || 0, filters.ratingMax || 5]}
onValueChange={([min, max]) => {
updateFilter('ratingMin', min);
updateFilter('ratingMax', max);
}}
min={0}
max={5}
step={0.1}
className="w-full"
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Number of Rides</Label>
<Badge variant="outline">
{filters.rideCountMin || 0} - {filters.rideCountMax || 100}+
</Badge>
</div>
<Slider
value={[filters.rideCountMin || 0, filters.rideCountMax || 100]}
onValueChange={([min, max]) => {
updateFilter('rideCountMin', min);
updateFilter('rideCountMax', max);
}}
min={0}
max={100}
step={1}
className="w-full"
/>
</div>
</div>
</div>
)}
{/* Ride Filters */}
{(activeTab === 'all' || activeTab === 'ride') && (
<div className="space-y-4">
<h4 className="font-semibold text-secondary">Ride Filters</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="rideCategory">Category</Label>
<Select
value={filters.rideCategory || ''}
onValueChange={(value) => updateFilter('rideCategory', value || undefined)}
>
<SelectTrigger>
<SelectValue placeholder="All categories" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All categories</SelectItem>
<SelectItem value="roller_coaster">Roller Coaster</SelectItem>
<SelectItem value="flat_ride">Flat Ride</SelectItem>
<SelectItem value="water_ride">Water Ride</SelectItem>
<SelectItem value="dark_ride">Dark Ride</SelectItem>
<SelectItem value="family_ride">Family Ride</SelectItem>
<SelectItem value="thrill_ride">Thrill Ride</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="intensityLevel">Intensity Level</Label>
<Select
value={filters.intensityLevel || ''}
onValueChange={(value) => updateFilter('intensityLevel', value || undefined)}
>
<SelectTrigger>
<SelectValue placeholder="All levels" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All levels</SelectItem>
<SelectItem value="mild">Mild</SelectItem>
<SelectItem value="moderate">Moderate</SelectItem>
<SelectItem value="intense">Intense</SelectItem>
<SelectItem value="extreme">Extreme</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Height Range (meters)</Label>
<Badge variant="outline">
{filters.heightMin || 0}m - {filters.heightMax || 200}m
</Badge>
</div>
<Slider
value={[filters.heightMin || 0, filters.heightMax || 200]}
onValueChange={([min, max]) => {
updateFilter('heightMin', min);
updateFilter('heightMax', max);
}}
min={0}
max={200}
step={1}
className="w-full"
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Speed Range (km/h)</Label>
<Badge variant="outline">
{filters.speedMin || 0} - {filters.speedMax || 200}+ km/h
</Badge>
</div>
<Slider
value={[filters.speedMin || 0, filters.speedMax || 200]}
onValueChange={([min, max]) => {
updateFilter('speedMin', min);
updateFilter('speedMax', max);
}}
min={0}
max={200}
step={5}
className="w-full"
/>
</div>
</div>
</div>
)}
{/* Company Filters */}
{(activeTab === 'all' || activeTab === 'company') && (
<div className="space-y-4">
<h4 className="font-semibold text-accent">Company Filters</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="companyType">Company Type</Label>
<Select
value={filters.companyType || ''}
onValueChange={(value) => updateFilter('companyType', value || undefined)}
>
<SelectTrigger>
<SelectValue placeholder="All types" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All types</SelectItem>
<SelectItem value="manufacturer">Manufacturer</SelectItem>
<SelectItem value="operator">Operator</SelectItem>
<SelectItem value="designer">Designer</SelectItem>
<SelectItem value="contractor">Contractor</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Headquarters</Label>
<Input
placeholder="Enter country/location"
value={filters.headquarters || ''}
onChange={(e) => updateFilter('headquarters', e.target.value || undefined)}
/>
</div>
</div>
<div className="space-y-2">
<Label>Founded Year Range</Label>
<div className="grid grid-cols-2 gap-2">
<Input
type="number"
placeholder="From"
min="1800"
max={currentYear}
value={filters.foundedYearMin || ''}
onChange={(e) => updateFilter('foundedYearMin', e.target.value ? parseInt(e.target.value) : undefined)}
/>
<Input
type="number"
placeholder="To"
min="1800"
max={currentYear}
value={filters.foundedYearMax || ''}
onChange={(e) => updateFilter('foundedYearMax', e.target.value ? parseInt(e.target.value) : undefined)}
/>
</div>
</div>
</div>
)}
</CardContent>
</CollapsibleContent>
</Collapsible>
</Card>
);
}

View File

@@ -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 (
<div className="flex items-center gap-2">
<Select
value={sort.field}
onValueChange={(field) => onSortChange({ ...sort, field })}
>
<SelectTrigger className="w-48 bg-muted/50 border-border/50">
<ArrowUpDown className="w-4 h-4 mr-2" />
<SelectValue />
</SelectTrigger>
<SelectContent>
{getSortOptions().map(option => (
<SelectItem key={option.value} value={option.value}>
Sort by {option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
size="icon"
onClick={toggleDirection}
className="shrink-0 bg-muted/50 border-border/50"
>
{sort.direction === 'asc' ? (
<SortAsc className="w-4 h-4" />
) : (
<SortDesc className="w-4 h-4" />
)}
</Button>
</div>
);
}

View File

@@ -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<SearchFilters>({});
const [sort, setSort] = useState<SortOption>({ field: 'relevance', direction: 'desc' });
const [showFilters, setShowFilters] = useState(false);
const {
query,
setQuery,
@@ -32,10 +37,60 @@ export default function SearchPage() {
}
}, [initialQuery, setQuery]);
const filteredResults = results.filter(result =>
// 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,
park: results.filter(r => r.type === 'park').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 <Castle className="w-8 h-8" />;
case 'ride': return <FerrisWheel className="w-8 h-8" />;
case 'company': return <Factory className="w-8 h-8" />;
default: return <Search className="w-8 h-8" />;
}
};
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 (
<div className="min-h-screen bg-background">
<Header />
@@ -88,12 +107,12 @@ export default function SearchPage() {
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<Search className="w-8 h-8 text-primary" />
<h1 className="text-4xl font-bold">Search Results</h1>
<h1 className="text-4xl font-bold">Search</h1>
</div>
{query && (
<p className="text-lg text-muted-foreground mb-6">
Results for "{query}"
Showing {filteredAndSortedResults.length} results for "{query}"
</p>
)}
@@ -112,12 +131,13 @@ export default function SearchPage() {
</div>
</div>
{/* Results */}
{/* Results Section */}
{query && (
<>
{/* Filter Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-6">
<TabsList>
<div className="space-y-6">
{/* Tabs and Controls */}
<div className="flex flex-col lg:flex-row gap-4 items-start lg:items-center justify-between">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-4 lg:w-auto">
<TabsTrigger value="all">
All ({resultCounts.all})
</TabsTrigger>
@@ -133,76 +153,46 @@ export default function SearchPage() {
</TabsList>
</Tabs>
{/* Loading State */}
{loading && (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
)}
{/* Results Grid */}
{!loading && filteredResults.length > 0 && (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredResults.map((result) => (
<Card
key={result.id}
onClick={() => handleResultClick(result)}
className="group cursor-pointer hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 border-border/50 hover:border-primary/50"
<div className="flex items-center gap-2 w-full lg:w-auto">
<Button
variant="outline"
onClick={() => setShowFilters(!showFilters)}
className="flex items-center gap-2"
>
<CardContent className="p-6">
<div className="flex items-start gap-4">
<div className="flex items-center justify-center">{getTypeIcon(result.type)}</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<h3 className="font-semibold text-lg group-hover:text-primary transition-colors truncate">
{result.title}
</h3>
<Badge variant="outline" className={`text-xs ${getTypeColor(result.type)}`}>
{result.type}
</Badge>
<SlidersHorizontal className="w-4 h-4" />
Filters
</Button>
<SearchSortOptions
sort={sort}
onSortChange={setSort}
activeTab={activeTab}
/>
</div>
</div>
<p className="text-muted-foreground text-sm mb-3 truncate">
{result.subtitle}
</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{result.type === 'park' && (
<div className="flex items-center gap-1">
<MapPin className="w-3 h-3 text-muted-foreground" />
</div>
)}
{result.type === 'ride' && (
<div className="flex items-center gap-1">
<Zap className="w-3 h-3 text-secondary" />
</div>
)}
</div>
{result.rating && result.rating > 0 && (
<div className="flex items-center gap-1">
<Star className="w-3 h-3 fill-yellow-400 text-yellow-400" />
<span className="text-sm font-medium">{result.rating.toFixed(1)}</span>
</div>
)}
</div>
</div>
</div>
</CardContent>
</Card>
))}
{/* Layout Grid */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Filters Sidebar */}
{showFilters && (
<div className="lg:col-span-1">
<SearchFiltersComponent
filters={filters}
onFiltersChange={setFilters}
activeTab={activeTab}
/>
</div>
)}
{/* No Results */}
{!loading && query && filteredResults.length === 0 && (
{/* Results */}
<div className={`${showFilters ? 'lg:col-span-3' : 'lg:col-span-4'}`}>
{!loading && filteredAndSortedResults.length === 0 && query && (
<div className="text-center py-12">
<Search className="w-16 h-16 mb-4 opacity-50 mx-auto" />
<h3 className="text-xl font-semibold mb-2">No results found</h3>
<p className="text-muted-foreground mb-4">
Try searching for something else or adjust your search terms
Try adjusting your search terms or filters
</p>
<div className="flex gap-2 justify-center">
<Button
onClick={() => {
setQuery('');
@@ -212,9 +202,24 @@ export default function SearchPage() {
>
Clear search
</Button>
<Button
onClick={() => setFilters({})}
variant="outline"
>
Clear filters
</Button>
</div>
</div>
)}
</>
<EnhancedSearchResults
results={filteredAndSortedResults}
loading={loading}
hasMore={false}
/>
</div>
</div>
</div>
)}
{/* Initial State */}