mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 01:31:12 -05:00
feat: Implement comprehensive search page
This commit is contained in:
@@ -18,6 +18,7 @@ import ParkOwners from "./pages/ParkOwners";
|
|||||||
import Auth from "./pages/Auth";
|
import Auth from "./pages/Auth";
|
||||||
import Profile from "./pages/Profile";
|
import Profile from "./pages/Profile";
|
||||||
import UserSettings from "./pages/UserSettings";
|
import UserSettings from "./pages/UserSettings";
|
||||||
|
import Search from "./pages/Search";
|
||||||
import NotFound from "./pages/NotFound";
|
import NotFound from "./pages/NotFound";
|
||||||
import Terms from "./pages/Terms";
|
import Terms from "./pages/Terms";
|
||||||
import Privacy from "./pages/Privacy";
|
import Privacy from "./pages/Privacy";
|
||||||
@@ -43,6 +44,7 @@ function AppContent() {
|
|||||||
<Route path="/parks/:slug" element={<ParkDetail />} />
|
<Route path="/parks/:slug" element={<ParkDetail />} />
|
||||||
<Route path="/parks/:parkSlug/rides/:rideSlug" element={<RideDetail />} />
|
<Route path="/parks/:parkSlug/rides/:rideSlug" element={<RideDetail />} />
|
||||||
<Route path="/rides" element={<Rides />} />
|
<Route path="/rides" element={<Rides />} />
|
||||||
|
<Route path="/search" element={<Search />} />
|
||||||
<Route path="/manufacturers" element={<Manufacturers />} />
|
<Route path="/manufacturers" element={<Manufacturers />} />
|
||||||
<Route path="/designers" element={<Designers />} />
|
<Route path="/designers" element={<Designers />} />
|
||||||
<Route path="/owners" element={<ParkOwners />} />
|
<Route path="/owners" element={<ParkOwners />} />
|
||||||
|
|||||||
242
src/components/search/EnhancedSearchResults.tsx
Normal file
242
src/components/search/EnhancedSearchResults.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
387
src/components/search/SearchFilters.tsx
Normal file
387
src/components/search/SearchFilters.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
99
src/components/search/SearchSortOptions.tsx
Normal file
99
src/components/search/SearchSortOptions.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
import { Header } from '@/components/layout/Header';
|
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 { Button } from '@/components/ui/button';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
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 { 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';
|
import { useSearch, SearchResult } from '@/hooks/useSearch';
|
||||||
|
|
||||||
export default function SearchPage() {
|
export default function SearchPage() {
|
||||||
@@ -15,6 +16,10 @@ export default function SearchPage() {
|
|||||||
const initialQuery = searchParams.get('q') || '';
|
const initialQuery = searchParams.get('q') || '';
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState('all');
|
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 {
|
const {
|
||||||
query,
|
query,
|
||||||
setQuery,
|
setQuery,
|
||||||
@@ -32,9 +37,59 @@ export default function SearchPage() {
|
|||||||
}
|
}
|
||||||
}, [initialQuery, setQuery]);
|
}, [initialQuery, setQuery]);
|
||||||
|
|
||||||
const filteredResults = results.filter(result =>
|
// Filter and sort results
|
||||||
activeTab === 'all' || result.type === activeTab
|
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 = {
|
const resultCounts = {
|
||||||
all: results.length,
|
all: results.length,
|
||||||
@@ -43,42 +98,6 @@ export default function SearchPage() {
|
|||||||
company: results.filter(r => r.type === 'company').length
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<Header />
|
<Header />
|
||||||
@@ -88,12 +107,12 @@ export default function SearchPage() {
|
|||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<Search className="w-8 h-8 text-primary" />
|
<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>
|
</div>
|
||||||
|
|
||||||
{query && (
|
{query && (
|
||||||
<p className="text-lg text-muted-foreground mb-6">
|
<p className="text-lg text-muted-foreground mb-6">
|
||||||
Results for "{query}"
|
Showing {filteredAndSortedResults.length} results for "{query}"
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -112,109 +131,95 @@ export default function SearchPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Results */}
|
{/* Results Section */}
|
||||||
{query && (
|
{query && (
|
||||||
<>
|
<div className="space-y-6">
|
||||||
{/* Filter Tabs */}
|
{/* Tabs and Controls */}
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-6">
|
<div className="flex flex-col lg:flex-row gap-4 items-start lg:items-center justify-between">
|
||||||
<TabsList>
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
<TabsTrigger value="all">
|
<TabsList className="grid w-full grid-cols-4 lg:w-auto">
|
||||||
All ({resultCounts.all})
|
<TabsTrigger value="all">
|
||||||
</TabsTrigger>
|
All ({resultCounts.all})
|
||||||
<TabsTrigger value="park">
|
</TabsTrigger>
|
||||||
Parks ({resultCounts.park})
|
<TabsTrigger value="park">
|
||||||
</TabsTrigger>
|
Parks ({resultCounts.park})
|
||||||
<TabsTrigger value="ride">
|
</TabsTrigger>
|
||||||
Rides ({resultCounts.ride})
|
<TabsTrigger value="ride">
|
||||||
</TabsTrigger>
|
Rides ({resultCounts.ride})
|
||||||
<TabsTrigger value="company">
|
</TabsTrigger>
|
||||||
Companies ({resultCounts.company})
|
<TabsTrigger value="company">
|
||||||
</TabsTrigger>
|
Companies ({resultCounts.company})
|
||||||
</TabsList>
|
</TabsTrigger>
|
||||||
</Tabs>
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
{/* Loading State */}
|
<div className="flex items-center gap-2 w-full lg:w-auto">
|
||||||
{loading && (
|
<Button
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* No Results */}
|
|
||||||
{!loading && query && filteredResults.length === 0 && (
|
|
||||||
<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
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setQuery('');
|
|
||||||
navigate('/search');
|
|
||||||
}}
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
Clear search
|
<SlidersHorizontal className="w-4 h-4" />
|
||||||
|
Filters
|
||||||
</Button>
|
</Button>
|
||||||
|
<SearchSortOptions
|
||||||
|
sort={sort}
|
||||||
|
onSortChange={setSort}
|
||||||
|
activeTab={activeTab}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</>
|
|
||||||
|
{/* 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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 adjusting your search terms or filters
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2 justify-center">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setQuery('');
|
||||||
|
navigate('/search');
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
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 */}
|
{/* Initial State */}
|
||||||
|
|||||||
Reference in New Issue
Block a user