// components/CustomInstantSearchResults.tsx
import React, { useEffect, useRef } from 'react';
import { Search, Loader, Zap } from 'lucide-react';
import { useAlgoliaSearch } from '../hooks/useAlgoliaSearch';
import { useGSAPAnimations } from '../hooks/useGSAPAnimations';
import SearchResultItem from './SearchResultItem';
import type { PokemonData } from '../types/pokemon';
interface CustomInstantSearchResultsProps {
query: string;
onPokemonSelect: (pokemon: PokemonData, sourceImageRef?: HTMLImageElement) => void;
onLoadingChange?: (isLoading: boolean) => void;
onErrorChange?: (hasError: boolean, errorMessage: string) => void;
}
const CustomInstantSearchResults: React.FC<CustomInstantSearchResultsProps> = ({
query,
onPokemonSelect,
onLoadingChange,
onErrorChange
}) => {
const { results, isLoading, error, hasSearched, search } = useAlgoliaSearch();
const previousLoading = useRef(isLoading);
const previousError = useRef(error);
const cardRefs = useRef<HTMLElement[]>([]);
const { animateCardsEntrance, createCardHoverAnimation } = useGSAPAnimations();
// Trigger search when query changes
useEffect(() => {
search(query);
}, [query, search]);
// Notify parent of loading state changes (only when actually changed)
useEffect(() => {
if (previousLoading.current !== isLoading) {
previousLoading.current = isLoading;
onLoadingChange?.(isLoading);
}
}, [isLoading, onLoadingChange]);
// Notify parent of error state changes (only when actually changed)
useEffect(() => {
if (previousError.current !== error) {
previousError.current = error;
if (error) {
onErrorChange?.(true, error);
} else {
onErrorChange?.(false, '');
}
}
}, [error, onErrorChange]);
// Animate cards when results change
useEffect(() => {
if (results && results.hits.length > 0 && cardRefs.current.length > 0) {
// Animate cards entering with stagger effect
animateCardsEntrance(cardRefs.current);
// Set up hover animations for each card
const cleanupFunctions = cardRefs.current.map(createCardHoverAnimation);
return () => {
cleanupFunctions.forEach(cleanup => cleanup());
};
}
}, [results, animateCardsEntrance, createCardHoverAnimation]);
// Error state
if (error) {
return (
<div className="text-center py-8">
<div className="text-red-400 mb-4">
<Search size={48} className="mx-auto opacity-50" />
</div>
<h3 className="text-lg font-semibold text-red-300 mb-2">Search Error</h3>
<p className="text-red-400 mb-4 text-sm">
{error}
</p>
<div className="text-xs text-gray-500 bg-gray-800/50 p-3 rounded-lg max-w-sm mx-auto">
<p className="mb-2">The Pokémon index may not be available.</p>
<p>Try switching to Demo mode or AI Analysis mode.</p>
</div>
</div>
);
}
// Empty query state
if (!query.trim()) {
return (
<div className="text-center py-8">
<Search className="mx-auto text-gray-400 mb-4" size={32} />
<p className="text-gray-400 text-sm">
Start typing a Pokémon name to see instant results
</p>
</div>
);
}
// Loading state
if (isLoading) {
return (
<div className="text-center py-8">
<Loader className="mx-auto text-purple-400 animate-spin mb-4" size={32} />
<p className="text-gray-300 text-sm">Searching Pokémon...</p>
</div>
);
}
// No results state
if (hasSearched && (!results || results.hits.length === 0)) {
return (
<div className="text-center py-8">
<div className="text-gray-400 mb-4">
<Search size={32} className="mx-auto opacity-50" />
</div>
<h3 className="text-lg font-semibold text-gray-300 mb-2">No Pokémon found</h3>
<p className="text-gray-400 text-sm">
Try a different name or check your spelling
</p>
<div className="mt-4 text-xs text-gray-500">
<p>Searching for: <span className="font-mono bg-gray-800 px-2 py-1 rounded">"{query}"</span></p>
</div>
</div>
);
}
// Results state
if (results && results.hits.length > 0) {
return (
<div className="space-y-4">
{/* Results Header */}
<div className="flex items-center justify-between text-sm text-gray-400">
<span>
Found <span className="text-purple-400 font-semibold">{results.nbHits}</span> Pokémon
{results.query && (
<span> for "<span className="text-white font-medium">{results.query}</span>"</span>
)}
</span>
<span className="flex items-center space-x-1">
<Zap size={14} className="text-yellow-400" />
<span>{results.processingTimeMS}ms</span>
</span>
</div>
{/* Results Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{results.hits.map((pokemon, index) => {
// Extract highlighted name if available
const highlightedName = (pokemon as PokemonData & { _highlightResult?: { name?: { value?: string } } })._highlightResult?.name?.value;
return (
<SearchResultItem
key={pokemon.objectID || pokemon.name}
pokemon={pokemon}
onSelect={onPokemonSelect}
highlightedName={highlightedName}
ref={el => {
if (el) cardRefs.current[index] = el;
}}
/>
);
})}
</div>
{/* Show more results indicator */}
{results.nbHits > results.hits.length && (
<div className="text-center py-4 text-gray-400 text-xs">
Showing {results.hits.length} of {results.nbHits} results
</div>
)}
{/* Performance info */}
<div className="text-center pt-2">
<div className="inline-flex items-center space-x-2 text-xs text-gray-500 bg-gray-800/30 px-3 py-1 rounded-full">
<Zap size={12} className="text-yellow-400" />
<span>Custom search • {results.processingTimeMS}ms</span>
</div>
</div>
</div>
);
}
// Fallback state (shouldn't normally reach here)
return (
<div className="text-center py-8">
<Search className="mx-auto text-gray-400 mb-4" size={32} />
<p className="text-gray-400 text-sm">
Ready to search...
</p>
</div>
);
};
export default CustomInstantSearchResults;