'use client' import * as React from 'react' import { Suspense } from 'react' import { useSearchParams, useRouter } from 'next/navigation' import { Search, Star, Filter, SlidersHorizontal, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from '@/components/ui/sheet' import { Skeleton } from '@/components/ui/skeleton' import { ScrollArea } from '@/components/ui/scroll-area' interface SearchResult { repository_id: number repository_name: string repository_url: string description: string | null stars: number | null language: string | null topics: string | null awesome_list_name: string | null awesome_list_category: string | null snippet: string | null } interface SearchResponse { results: SearchResult[] total: number page: number pageSize: number totalPages: number } interface StatsResponse { languages: { name: string; count: number }[] categories: { name: string; count: number }[] } function SearchPageContent() { const searchParams = useSearchParams() const router = useRouter() const [query, setQuery] = React.useState(searchParams.get('q') || '') const [results, setResults] = React.useState(null) const [stats, setStats] = React.useState(null) const [loading, setLoading] = React.useState(false) const [filters, setFilters] = React.useState({ language: searchParams.get('language') || '', category: searchParams.get('category') || '', minStars: searchParams.get('minStars') || '', sortBy: (searchParams.get('sortBy') as 'relevance' | 'stars' | 'recent') || 'relevance' }) // Fetch stats for filters React.useEffect(() => { fetch('/api/stats') .then(res => res.json()) .then(data => setStats(data)) .catch(err => console.error('Failed to fetch stats:', err)) }, []) // Perform search const performSearch = React.useCallback((searchQuery: string, page = 1) => { if (!searchQuery.trim()) { setResults(null) return } setLoading(true) const params = new URLSearchParams({ q: searchQuery, page: page.toString(), ...Object.fromEntries( Object.entries(filters).filter(([_, v]) => v !== '') ) }) fetch(`/api/search?${params}`) .then(res => res.json()) .then(data => { // Check if response is an error if (data.error || !data.results) { console.error('Search API error:', data.error || 'Invalid response') setResults(null) return } setResults(data) // Update URL router.push(`/search?${params}`) }) .catch(err => { console.error('Search failed:', err) setResults(null) }) .finally(() => setLoading(false)) }, [filters, router]) // Search on query change (debounced) React.useEffect(() => { const initialQuery = searchParams.get('q') if (initialQuery) { setQuery(initialQuery) performSearch(initialQuery, parseInt(searchParams.get('page') || '1')) } }, []) // Only on mount const handleSearch = (e: React.FormEvent) => { e.preventDefault() performSearch(query) } const handleFilterChange = (key: string, value: string) => { setFilters(prev => ({ ...prev, [key]: value })) } // Strip markdown and HTML from descriptions const stripMarkdown = (text: string | null): string => { if (!text) return '' return text .replace(/<[^>]*>/g, '') // Remove HTML tags .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Convert [text](url) to text .replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1') // Convert ![alt](url) to alt text .replace(/[*_~`#]/g, '') // Remove markdown formatting chars .replace(/\s+/g, ' ') // Normalize whitespace .trim() } // Generate page numbers for pagination const getPageNumbers = (current: number, total: number): (number | string)[] => { const delta = 2 const range: (number | string)[] = [] const rangeWithDots: (number | string)[] = [] let l: number | undefined for (let i = 1; i <= total; i++) { if (i === 1 || i === total || (i >= current - delta && i <= current + delta)) { range.push(i) } } for (const i of range) { if (l !== undefined && typeof i === 'number') { if (i - l === 2) { rangeWithDots.push(l + 1) } else if (i - l !== 1) { rangeWithDots.push('...') } } rangeWithDots.push(i) if (typeof i === 'number') { l = i } } return rangeWithDots } React.useEffect(() => { if (query) { performSearch(query) } }, [filters.sortBy, filters.language, filters.category, filters.minStars]) return (
{/* Header */}

Search Awesome

{/* Search Form */}
setQuery(e.target.value)} className="pl-10 text-base" autoFocus />
{/* Quick Filters */}
{/* Mobile Filter Sheet */} Filters Refine your search results
{/* Results */}
{loading && (
{[...Array(5)].map((_, i) => (
))}
)} {!loading && results && results.total !== undefined && ( <>
Found {results.total.toLocaleString()} results {query && <> for "{query}"}
{results.results.map((result) => (

{result.repository_name}

{result.stars !== null && (
{result.stars.toLocaleString()}
)}
{result.description && (

{stripMarkdown(result.description)}

)} {result.snippet && (
)}
{result.language && ( {result.language} )} {result.awesome_list_category && ( {result.awesome_list_category} )} {result.awesome_list_name && ( {result.awesome_list_name} )}
))}
{/* Pagination */} {results.totalPages > 1 && (
{getPageNumbers(results.page, results.totalPages).map((pageNum, idx) => pageNum === '...' ? ( ... ) : ( ) )}
)} )} {!loading && !results && query && (

Enter a search query to find awesome repositories

)}
) } export default function SearchPage() { return ( Loading...
}> ) }