'use client' import { logger } from '@thedaviddias/logging' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useAnalyticsEvents } from '@/components/analytics-tracker' import { EmptyState } from '@/components/empty-state' import { LLMGrid } from '@/components/llm/llm-grid' import { WebsitesSearchControls } from '@/components/websites-search-controls' import { useFavoritesFilter } from '@/hooks/use-favorites-filter' import type { WebsiteMetadata } from '@/lib/content-loader' import { getRoute } from '@/lib/routes' interface WebsitesListWithSearchProps { initialWebsites: WebsiteMetadata[] emptyTitle?: string emptyDescription?: string initialShowFavoritesOnly?: boolean totalCount?: number } /** * Client component that handles searching and sorting on the client side * Initial data is server-side sorted, then client can filter and re-sort */ export function WebsitesListWithSearch({ initialWebsites, emptyTitle = 'No websites found', emptyDescription = 'There are no websites available. Try checking back later or submit a new website.', initialShowFavoritesOnly = false, totalCount }: WebsitesListWithSearchProps) { const [sortBy, setSortBy] = useState<'name' | 'latest'>('latest') const [searchQuery, setSearchQuery] = useState('') const [showFavoritesOnly, setShowFavoritesOnly] = useState(initialShowFavoritesOnly) const [isClient, setIsClient] = useState(false) const [isLoading, setIsLoading] = useState(true) const [allWebsites, setAllWebsites] = useState(initialWebsites) const [isLoadingMore, setIsLoadingMore] = useState(false) const [hasMoreWebsites, setHasMoreWebsites] = useState( totalCount ? initialWebsites.length < totalCount : false ) const [isSearching, setIsSearching] = useState(false) const [isSearchPending, setIsSearchPending] = useState(false) const [searchResults, setSearchResults] = useState([]) const [searchTotalCount, setSearchTotalCount] = useState(0) const isLoadingRef = useRef(false) const { trackSearch, trackSortChange } = useAnalyticsEvents() const { favoriteWebsites, hasFavorites } = useFavoritesFilter(allWebsites) /** * Search all websites using server-side search * Searches across all 887+ websites regardless of what's loaded on the page */ const searchAllWebsites = useCallback(async (query: string) => { if (!query.trim()) { setSearchResults([]) setSearchTotalCount(0) setIsSearchPending(false) return } setIsSearchPending(true) setIsSearching(true) try { const response = await fetch( `/api/websites/search?q=${encodeURIComponent(query.trim())}&limit=100` ) if (response.ok) { const data = await response.json() setSearchResults(data.websites) setSearchTotalCount(data.totalCount) } } catch (error) { logger.error(error instanceof Error ? error : new Error(String(error)), { data: error, tags: { type: 'component', component: 'websites-list-with-search' } }) setSearchResults([]) setSearchTotalCount(0) } finally { setIsSearching(false) setIsSearchPending(false) } }, []) /** * Load more websites from the API when user clicks Load More button * Loads 48 websites at a time for optimal performance * Follows Load More UX pattern for better performance and user control */ const loadMoreWebsites = useCallback(async () => { if (isLoadingRef.current || isLoadingMore || !hasMoreWebsites || !totalCount) return isLoadingRef.current = true setIsLoadingMore(true) try { const response = await fetch(`/api/websites/paginated?offset=${allWebsites.length}&limit=48`) if (response.ok) { const data = await response.json() setAllWebsites(prev => [...prev, ...data.websites]) setHasMoreWebsites(data.hasMore && data.websites.length > 0) } } catch (error) { logger.error(error instanceof Error ? error : new Error(String(error)), { data: error, tags: { type: 'component', component: 'websites-list-with-search' } }) } finally { setIsLoadingMore(false) isLoadingRef.current = false } }, [allWebsites.length, hasMoreWebsites, totalCount, isLoadingMore]) useEffect(() => { requestAnimationFrame(() => { const savedSortBy = localStorage.getItem('websites-sort-by') if (savedSortBy !== null) { const parsedSortBy = JSON.parse(savedSortBy) if (parsedSortBy === 'name' || parsedSortBy === 'latest') { setSortBy(parsedSortBy) } } setIsClient(true) setIsLoading(false) }) }, []) useEffect(() => { if (isClient) { localStorage.setItem('websites-sort-by', JSON.stringify(sortBy)) } }, [sortBy, isClient]) useEffect(() => { if (searchQuery.trim()) { setIsSearchPending(true) } else { setIsSearchPending(false) setSearchResults([]) setSearchTotalCount(0) } const timeoutId = setTimeout(() => { if (searchQuery.trim()) { searchAllWebsites(searchQuery) } }, 300) // 300ms debounce return () => clearTimeout(timeoutId) }, [searchQuery, searchAllWebsites]) const filteredAndSortedWebsites = useMemo(() => { if (searchQuery.trim()) { let websites = [...searchResults] if (showFavoritesOnly) { const favoriteSlugs = new Set(favoriteWebsites.map(w => w.slug)) websites = websites.filter(website => favoriteSlugs.has(website.slug)) } if (sortBy === 'latest') { return websites.sort((a, b) => { const dateA = new Date(a.publishedAt).getTime() const dateB = new Date(b.publishedAt).getTime() return dateB - dateA }) } else { return websites.sort((a, b) => a.name.localeCompare(b.name)) } } let websites = showFavoritesOnly ? [...favoriteWebsites] : [...allWebsites] if (sortBy === 'latest') { return websites.sort((a, b) => { const dateA = new Date(a.publishedAt).getTime() const dateB = new Date(b.publishedAt).getTime() return dateB - dateA }) } else { return websites.sort((a, b) => a.name.localeCompare(b.name)) } }, [allWebsites, favoriteWebsites, showFavoritesOnly, sortBy, searchQuery, searchResults]) if (initialWebsites.length === 0) { return ( ) } return (
{/* Search and Sort Controls */} {filteredAndSortedWebsites.length === 0 ? ( searchQuery.trim() && (isSearchPending || isSearching) ? (

{isSearching ? `Searching all ${totalCount || 0} websites for "${searchQuery}"...` : `Preparing search for "${searchQuery}"...`}

) : searchQuery.trim() && !isSearchPending && !isSearching ? ( setSearchQuery('')} /> ) : ( ) ) : (

Websites

{searchQuery && !isSearching && (

Showing {filteredAndSortedWebsites.length} of {searchTotalCount} result {searchTotalCount !== 1 ? 's' : ''} for "{searchQuery}"

)}
{!searchQuery.trim() && !isSearching && (
{hasMoreWebsites ? ( ) : (

You've reached the end! Showing all {allWebsites.length} websites.

)} {/* Progress indicator */}

Showing {allWebsites.length} of {totalCount || 0} websites

)}
)}
) }