wiki-project / src /components /SearchInterface.tsx
Nagi15's picture
Add codebase
fcb5a67
import React, { useState, useEffect } from 'react';
import { Search, Filter, ExternalLink, Clock, FileText, Loader2, BookOpen, Eye } from 'lucide-react';
import { WikimediaAPI, WIKIMEDIA_PROJECTS } from '../utils/wikimedia-api';
import { SearchResult } from '../types';
interface SearchInterfaceProps {
onViewArticle?: (title: string, project: string, content: string) => void;
}
const SearchInterface: React.FC<SearchInterfaceProps> = ({ onViewArticle }) => {
const [query, setQuery] = useState('');
const [selectedProject, setSelectedProject] = useState('wikipedia');
const [results, setResults] = useState<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [showFilters, setShowFilters] = useState(false);
const [loadingContent, setLoadingContent] = useState<string | null>(null);
const handleSearch = async (searchQuery: string = query) => {
if (!searchQuery.trim()) return;
setLoading(true);
try {
const searchResults = await WikimediaAPI.search(searchQuery, selectedProject, 20);
setResults(searchResults);
} catch (error) {
console.error('Search failed:', error);
setResults([]);
} finally {
setLoading(false);
}
};
const handleViewInWikistro = async (result: SearchResult) => {
setLoadingContent(result.pageid.toString());
try {
const content = await WikimediaAPI.getPageContent(result.title, result.project);
if (onViewArticle) {
onViewArticle(result.title, result.project, content);
}
} catch (error) {
console.error('Failed to load article content:', error);
} finally {
setLoadingContent(null);
}
};
const formatDate = (timestamp: string) => {
return new Date(timestamp).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
const truncateSnippet = (snippet: string, maxLength: number = 200) => {
const cleanSnippet = snippet.replace(/<[^>]*>/g, '');
return cleanSnippet.length > maxLength
? cleanSnippet.substring(0, maxLength) + '...'
: cleanSnippet;
};
useEffect(() => {
const delayedSearch = setTimeout(() => {
if (query.trim()) {
handleSearch(query);
}
}, 500);
return () => clearTimeout(delayedSearch);
}, [query, selectedProject]);
const currentProject = WIKIMEDIA_PROJECTS.find(p => p.id === selectedProject);
return (
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search across Wikimedia projects..."
className="w-full pl-12 pr-4 py-4 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent text-lg"
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
<div className="flex gap-2">
<button
onClick={() => setShowFilters(!showFilters)}
className="flex items-center px-4 py-4 border border-gray-300 rounded-xl hover:bg-gray-50 transition-colors"
>
<Filter className="w-5 h-5 mr-2" />
Filters
</button>
<button
onClick={() => handleSearch()}
disabled={loading}
className="flex items-center px-6 py-4 bg-primary-600 text-white rounded-xl hover:bg-primary-700 transition-colors disabled:opacity-50"
>
{loading ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<Search className="w-5 h-5" />
)}
</button>
</div>
</div>
{showFilters && (
<div className="mt-4 p-4 bg-gray-50 rounded-xl">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
{WIKIMEDIA_PROJECTS.map((project) => (
<button
key={project.id}
onClick={() => setSelectedProject(project.id)}
className={`p-3 rounded-lg text-sm font-medium transition-all ${
selectedProject === project.id
? 'bg-primary-600 text-white shadow-md'
: 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-200'
}`}
>
{project.name}
</button>
))}
</div>
</div>
)}
</div>
{currentProject && (
<div className="mb-6">
<div className="flex items-center space-x-3 p-4 bg-white rounded-xl border border-gray-200">
<div className={`w-3 h-3 rounded-full ${currentProject.color}`} />
<div>
<h3 className="font-semibold text-gray-900">{currentProject.name}</h3>
<p className="text-sm text-gray-600">{currentProject.description}</p>
</div>
</div>
</div>
)}
{loading && (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-primary-600" />
<span className="ml-3 text-gray-600">Searching {currentProject?.name}...</span>
</div>
)}
{!loading && results.length === 0 && query && (
<div className="text-center py-12">
<FileText className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No results found</h3>
<p className="text-gray-600">Try adjusting your search terms or selecting a different project.</p>
</div>
)}
{!loading && results.length > 0 && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900">
Search Results ({results.length})
</h2>
</div>
<div className="grid gap-4">
{results.map((result) => (
<div
key={result.pageid}
className="bg-white border border-gray-200 rounded-xl p-6 hover:shadow-md transition-all duration-200 hover:border-primary-200"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 mb-2 hover:text-primary-600">
<a
href={result.url}
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
{result.title}
</a>
</h3>
<p className="text-gray-600 mb-3 leading-relaxed">
{truncateSnippet(result.snippet)}
</p>
<div className="flex items-center space-x-6 text-sm text-gray-500 mb-4">
<div className="flex items-center space-x-1">
<Clock className="w-4 h-4" />
<span>{formatDate(result.timestamp)}</span>
</div>
<div className="flex items-center space-x-1">
<FileText className="w-4 h-4" />
<span>{Math.round(result.size / 1024)}KB</span>
</div>
<div className="flex items-center space-x-1">
<BookOpen className="w-4 h-4" />
<span className="capitalize">{result.project}</span>
</div>
</div>
<div className="flex items-center space-x-3">
<button
onClick={() => handleViewInWikistro(result)}
disabled={loadingContent === result.pageid.toString()}
className="flex items-center space-x-2 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:opacity-50"
>
{loadingContent === result.pageid.toString() ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
<span>Loading...</span>
</>
) : (
<>
<Eye className="w-4 h-4" />
<span>View in Wikistro</span>
</>
)}
</button>
<a
href={result.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center space-x-2 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
<ExternalLink className="w-4 h-4" />
<span>Open Original</span>
</a>
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
};
export default SearchInterface;