mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 10:11:13 -05:00
242 lines
8.4 KiB
TypeScript
242 lines
8.4 KiB
TypeScript
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 {parkData.opening_date.split('-')[0]}</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>
|
|
);
|
|
} |