Spaces:
Runtime error
Runtime error
| 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; |