mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 09:31:12 -05:00
271 lines
9.4 KiB
TypeScript
271 lines
9.4 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
|
import { Header } from '@/components/layout/Header';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
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';
|
|
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
|
|
|
export default function SearchPage() {
|
|
const [searchParams] = useSearchParams();
|
|
const navigate = useNavigate();
|
|
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,
|
|
results,
|
|
loading,
|
|
search
|
|
} = useSearch({
|
|
types: ['park', 'ride', 'company'],
|
|
limit: 50
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (initialQuery) {
|
|
setQuery(initialQuery);
|
|
}
|
|
}, [initialQuery, setQuery]);
|
|
|
|
// 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;
|
|
});
|
|
}
|
|
|
|
// Type-safe helpers for sorting
|
|
const getReviewCount = (data: unknown): number => {
|
|
if (data && typeof data === 'object' && 'review_count' in data) {
|
|
const count = (data as { review_count?: number }).review_count;
|
|
return typeof count === 'number' ? count : 0;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
const getRideCount = (data: unknown): number => {
|
|
if (data && typeof data === 'object' && 'ride_count' in data) {
|
|
const count = (data as { ride_count?: number }).ride_count;
|
|
return typeof count === 'number' ? count : 0;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
const getOpeningDate = (data: unknown): number => {
|
|
if (data && typeof data === 'object' && 'opening_date' in data) {
|
|
const dateStr = (data as { opening_date?: string }).opening_date;
|
|
return dateStr ? new Date(dateStr).getTime() : 0;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
// 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 * (getReviewCount(b.data) - getReviewCount(a.data));
|
|
case 'rides':
|
|
return direction * (getRideCount(b.data) - getRideCount(a.data));
|
|
case 'opening':
|
|
return direction * (getOpeningDate(b.data) - getOpeningDate(a.data));
|
|
default: // relevance
|
|
return 0; // Keep original order for relevance
|
|
}
|
|
});
|
|
|
|
return filtered;
|
|
})();
|
|
|
|
const resultCounts = {
|
|
all: results.length,
|
|
park: results.filter(r => r.type === 'park').length,
|
|
ride: results.filter(r => r.type === 'ride').length,
|
|
company: results.filter(r => r.type === 'company').length
|
|
};
|
|
|
|
useOpenGraph({
|
|
title: query ? `Search: "${query}" - ThrillWiki` : 'Search - ThrillWiki',
|
|
description: query
|
|
? `Found ${filteredAndSortedResults.length} results for "${query}"`
|
|
: 'Search theme parks, rides, and more on ThrillWiki',
|
|
type: 'website',
|
|
enabled: !loading
|
|
});
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background">
|
|
<Header />
|
|
|
|
<main className="container mx-auto px-4 py-8">
|
|
{/* Search Header */}
|
|
<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</h1>
|
|
</div>
|
|
|
|
{query && (
|
|
<p className="text-lg text-muted-foreground mb-6">
|
|
Showing {filteredAndSortedResults.length} results for "{query}"
|
|
</p>
|
|
)}
|
|
|
|
{/* Search Bar */}
|
|
<div className="max-w-2xl">
|
|
<AutocompleteSearch
|
|
placeholder="Search parks, rides, or companies..."
|
|
types={['park', 'ride', 'company']}
|
|
limit={8}
|
|
onSearch={(newQuery) => {
|
|
const params = new URLSearchParams();
|
|
params.set('q', newQuery);
|
|
navigate(`/search?${params.toString()}`, { replace: true });
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Results Section */}
|
|
{query && (
|
|
<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>
|
|
|
|
<div className="flex items-center gap-2 w-full lg:w-auto">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowFilters(!showFilters)}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<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 */}
|
|
{!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">Start your search</h3>
|
|
<p className="text-muted-foreground">
|
|
Search for theme parks, rides, or companies to get started
|
|
</p>
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
} |