Files
thrilltrack-explorer/src/components/search/SearchResults.tsx
gpt-engineer-app[bot] fc7c2d5adc Refactor park detail address display
Implement the plan to refactor the address display in the park detail page. This includes updating the sidebar address to show the street address on its own line, followed by city, state, and postal code on the next line, and the country on a separate line. This change aims to create a more compact and natural address format.
2025-11-06 14:03:58 +00:00

204 lines
6.8 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
import { useGlobalSearch } from '@/hooks/search/useGlobalSearch';
import { formatLocationShort } from '@/lib/locationFormatter';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { MapPin, Star, Search as SearchIcon, Castle, FerrisWheel, Waves, Theater, Factory } from 'lucide-react';
import { Park, Ride, Company } from '@/types/database';
import { supabase } from '@/lib/supabaseClient';
import { useNavigate } from 'react-router-dom';
import { getErrorMessage } from '@/lib/errorHandler';
import { logger } from '@/lib/logger';
interface SearchResultsProps {
query: string;
onClose: () => void;
}
type SearchResult = {
type: 'park' | 'ride' | 'company';
data: Park | Ride | Company;
};
export function SearchResults({ query, onClose }: SearchResultsProps) {
const navigate = useNavigate();
// Debounce search query
const debouncedQuery = useDebouncedValue(query, 300);
// Use global search hook with caching
const { data, isLoading: loading } = useGlobalSearch(debouncedQuery);
// Flatten results
const results: SearchResult[] = [
...(data?.parks || []).map(park => ({ type: 'park' as const, data: park })),
...(data?.rides || []).map(ride => ({ type: 'ride' as const, data: ride })),
...(data?.companies || []).map(company => ({ type: 'company' as const, data: company })),
];
const handleResultClick = (result: SearchResult) => {
onClose();
switch (result.type) {
case 'park':
navigate(`/parks/${(result.data as Park).slug}`);
break;
case 'ride':
const ride = result.data as Ride;
if (ride.park && typeof ride.park === 'object' && 'slug' in ride.park) {
navigate(`/parks/${ride.park.slug}/rides/${ride.slug}`);
}
break;
case 'company':
// Navigate to manufacturer page when implemented
break;
}
};
const getResultIcon = (result: SearchResult) => {
switch (result.type) {
case 'park':
const park = result.data as Park;
switch (park.park_type) {
case 'theme_park': return <Castle className="w-5 h-5" />;
case 'amusement_park': return <FerrisWheel className="w-5 h-5" />;
case 'water_park': return <Waves className="w-5 h-5" />;
default: return <FerrisWheel className="w-5 h-5" />;
}
case 'ride':
const ride = result.data as Ride;
switch (ride.category) {
case 'roller_coaster': return <FerrisWheel className="w-5 h-5" />;
case 'water_ride': return <Waves className="w-5 h-5" />;
case 'dark_ride': return <Theater className="w-5 h-5" />;
default: return <FerrisWheel className="w-5 h-5" />;
}
case 'company':
return <Factory className="w-5 h-5" />;
}
};
const getResultTitle = (result: SearchResult) => {
return result.data.name;
};
const getResultSubtitle = (result: SearchResult) => {
switch (result.type) {
case 'park':
const park = result.data as Park;
return park.location ? formatLocationShort(park.location) : 'Theme Park';
case 'ride':
const ride = result.data as Ride;
return ride.park && typeof ride.park === 'object' && 'name' in ride.park
? `at ${ride.park.name}`
: 'Ride';
case 'company':
const company = result.data as Company;
return company.company_type.replace('_', ' ');
}
};
const getResultRating = (result: SearchResult) => {
if (result.type === 'park' || result.type === 'ride') {
const data = result.data as Park | Ride;
return (data.average_rating != null && data.average_rating > 0) ? data.average_rating : null;
}
return null;
};
if (query.length < 2) {
return (
<div className="p-6 text-center">
<SearchIcon className="w-12 h-12 text-muted-foreground mx-auto mb-3" />
<p className="text-muted-foreground">
Start typing to search parks, rides, and manufacturers...
</p>
</div>
);
}
if (loading) {
return (
<div className="p-6">
<div className="animate-pulse space-y-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-16 bg-muted rounded"></div>
))}
</div>
</div>
);
}
if (results.length === 0) {
return (
<div className="p-6 text-center">
<SearchIcon className="w-12 h-12 text-muted-foreground mx-auto mb-3" />
<p className="text-muted-foreground">
No results found for "{query}"
</p>
<p className="text-sm text-muted-foreground mt-1">
Try searching for park names, ride names, or locations
</p>
</div>
);
}
return (
<div className="max-h-96 overflow-y-auto">
<div className="p-2 space-y-1">
{results.map((result, index) => (
<Card
key={`${result.type}-${result.data.id}-${index}`}
className="cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => handleResultClick(result)}
>
<CardContent className="p-3">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center">{getResultIcon(result)}</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium text-sm truncate">
{getResultTitle(result)}
</h4>
<Badge variant="secondary" className="text-xs">
{result.type}
</Badge>
</div>
<p className="text-xs text-muted-foreground truncate">
{getResultSubtitle(result)}
</p>
</div>
{getResultRating(result) && (
<div className="flex items-center gap-1">
<Star className="w-3 h-3 fill-yellow-400 text-yellow-400" />
<span className="text-xs font-medium">
{getResultRating(result)?.toFixed(1)}
</span>
</div>
)}
</div>
</CardContent>
</Card>
))}
</div>
{results.length > 0 && (
<div className="p-3 border-t">
<Button
variant="outline"
size="sm"
className="w-full"
onClick={() => {
onClose();
navigate(`/search?q=${encodeURIComponent(query)}`);
}}
>
View all results for "{query}"
</Button>
</div>
)}
</div>
);
}