mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 08:51:16 -05:00
feat: Implement comprehensive search page
This commit is contained in:
@@ -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,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 <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,109 +131,95 @@ export default function SearchPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{/* Results Section */}
|
||||
{query && (
|
||||
<>
|
||||
{/* Filter Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="all">
|
||||
All ({resultCounts.all})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="park">
|
||||
Parks ({resultCounts.park})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="ride">
|
||||
Rides ({resultCounts.ride})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="company">
|
||||
Companies ({resultCounts.company})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<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>
|
||||
<TabsTrigger value="park">
|
||||
Parks ({resultCounts.park})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="ride">
|
||||
Rides ({resultCounts.ride})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="company">
|
||||
Companies ({resultCounts.company})
|
||||
</TabsTrigger>
|
||||
</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"
|
||||
>
|
||||
<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');
|
||||
}}
|
||||
<div className="flex items-center gap-2 w-full lg:w-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
Clear search
|
||||
<SlidersHorizontal className="w-4 h-4" />
|
||||
Filters
|
||||
</Button>
|
||||
<SearchSortOptions
|
||||
sort={sort}
|
||||
onSortChange={setSort}
|
||||
activeTab={activeTab}
|
||||
/>
|
||||
</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 */}
|
||||
|
||||
Reference in New Issue
Block a user