'use client' import { useState, useMemo, useEffect } from 'react' import { motion } from 'framer-motion' import Link from 'next/link' import { Plus, Loader2, CheckCircle, AlertCircle, X, ArrowUpDown, Activity, WifiOff, RefreshCw, } from 'lucide-react' import Header from '@/components/Header' import Footer from '@/components/Footer' import { SearchBar, CategoryFilter, SiteGrid } from '@/components/directory' import { KNOWN_SITES, CATEGORIES, CategoryId, SiteEntry } from '@/data/sites' type SortOption = 'name-asc' | 'name-desc' | 'category' | 'type' interface HealthSummary { online: number offline: number checking: number total: number } export default function DirectoryPage() { const [searchQuery, setSearchQuery] = useState('') const [selectedCategory, setSelectedCategory] = useState('all') const [showSuggestModal, setShowSuggestModal] = useState(false) const [sortBy, setSortBy] = useState('name-asc') const [healthSummary, setHealthSummary] = useState({ online: 0, offline: 0, checking: KNOWN_SITES.length, total: KNOWN_SITES.length, }) const [isLoadingHealth, setIsLoadingHealth] = useState(false) // Fetch health summary on mount useEffect(() => { fetchHealthSummary() }, []) const fetchHealthSummary = async () => { setIsLoadingHealth(true) try { const response = await fetch('/api/sites/health') if (response.ok) { const data = await response.json() setHealthSummary({ online: data.online, offline: data.offline + data.errors, checking: 0, total: data.totalSites, }) } } catch (error) { console.error('Failed to fetch health summary:', error) } finally { setIsLoadingHealth(false) } } // Calculate category counts const categoryCounts = useMemo(() => { const counts: Record = { all: KNOWN_SITES.length, ai: 0, 'developer-tools': 0, documentation: 0, cloud: 0, other: 0, } KNOWN_SITES.forEach((site) => { counts[site.category]++ }) return counts }, []) // Filter and sort sites const filteredSites = useMemo(() => { let sites = KNOWN_SITES.filter((site) => { const matchesCategory = selectedCategory === 'all' || site.category === selectedCategory const matchesSearch = searchQuery === '' || site.name.toLowerCase().includes(searchQuery.toLowerCase()) || site.description.toLowerCase().includes(searchQuery.toLowerCase()) || site.url.toLowerCase().includes(searchQuery.toLowerCase()) return matchesCategory && matchesSearch }) // Sort sites switch (sortBy) { case 'name-asc': sites = [...sites].sort((a, b) => a.name.localeCompare(b.name)) break case 'name-desc': sites = [...sites].sort((a, b) => b.name.localeCompare(a.name)) break case 'category': sites = [...sites].sort((a, b) => a.category.localeCompare(b.category)) break case 'type': sites = [...sites].sort((a, b) => { if (a.llmsTxtType === 'full' && b.llmsTxtType !== 'full') return -1 if (a.llmsTxtType !== 'full' && b.llmsTxtType === 'full') return 1 return 0 }) break } return sites }, [searchQuery, selectedCategory, sortBy]) return (
{/* Page header */}

Site Directory

Discover sites that support llms.txt and extract their documentation with one click.

{/* Health Status Banner */}
Live Status
{healthSummary.online} online {healthSummary.offline} offline {healthSummary.checking > 0 && ( {healthSummary.checking} checking )}
View Dashboard
{/* Search and filters */}
{/* Sort dropdown */}
setShowSuggestModal(true)} className="flex items-center gap-2 px-4 py-3 bg-neutral-900 text-neutral-300 border border-neutral-800 rounded-xl hover:bg-neutral-800 hover:text-white transition-all whitespace-nowrap" > Suggest a site
{/* Results count */} Showing {filteredSites.length} of {KNOWN_SITES.length} sites {/* Site grid */} {/* CTA section */}

Know a site with llms.txt?

Help us grow the directory by suggesting sites that support the llms.txt standard.

{/* Suggest site modal */} {showSuggestModal && ( setShowSuggestModal(false)} /> )}
) } interface SuggestSiteModalProps { onClose: () => void } interface VerificationPreview { status: 'online' | 'offline' | 'error' llmsTxtUrl?: string llmsFullTxtUrl?: string size?: number type?: 'full' | 'standard' } function SuggestSiteModal({ onClose }: SuggestSiteModalProps) { const [url, setUrl] = useState('') const [name, setName] = useState('') const [description, setDescription] = useState('') const [status, setStatus] = useState<'idle' | 'verifying' | 'verified' | 'loading' | 'success' | 'error'>('idle') const [errorMessage, setErrorMessage] = useState('') const [verification, setVerification] = useState(null) // Auto-verify when URL changes const handleUrlChange = async (newUrl: string) => { setUrl(newUrl) setVerification(null) setErrorMessage('') if (!newUrl || !newUrl.startsWith('http')) return try { new URL(newUrl) // Validate URL } catch { return } setStatus('verifying') try { const response = await fetch(`/api/sites/verify?url=${encodeURIComponent(newUrl)}`) if (response.ok) { const result = await response.json() setVerification({ status: result.status, llmsTxtUrl: result.llmsTxt?.url, llmsFullTxtUrl: result.llmsFullTxt?.url, size: result.llmsFullTxt?.size || result.llmsTxt?.size, type: result.llmsFullTxt?.type || result.llmsTxt?.type, }) setStatus(result.status === 'online' ? 'verified' : 'idle') if (result.status !== 'online') { setErrorMessage('No llms.txt found at this URL. The site may not support the llms.txt standard.') } } } catch { setStatus('idle') } } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setStatus('loading') setErrorMessage('') try { const response = await fetch('/api/sites/suggest', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url, name, description, verification: verification, }), }) const data = await response.json() if (!response.ok) { throw new Error(data.error || 'Failed to submit suggestion') } setStatus('success') } catch (err) { setStatus('error') setErrorMessage(err instanceof Error ? err.message : 'Something went wrong') } } return ( e.target === e.currentTarget && onClose()} >

Suggest a Site

{status === 'success' ? (

Thank you!

Your suggestion has been submitted for review.

) : (
handleUrlChange(e.target.value)} placeholder="https://docs.example.com" required className="w-full px-4 py-2.5 bg-neutral-800 border border-neutral-700 rounded-lg text-white placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-white/20 focus:border-neutral-600" /> {status === 'verifying' && (
)} {status === 'verified' && (
)}
{/* Verification result */} {verification && verification.status === 'online' && (
llms.txt found!
{verification.llmsFullTxtUrl && (

✓ llms-full.txt available

)} {verification.llmsTxtUrl && !verification.llmsFullTxtUrl && (

✓ llms.txt available

)} {verification.size && (

Size: {(verification.size / 1024).toFixed(1)} KB

)} {verification.type && (

Type: {verification.type === 'full' ? 'Full documentation' : 'Standard'}

)}
)}
setName(e.target.value)} placeholder="Example Docs" required className="w-full px-4 py-2.5 bg-neutral-800 border border-neutral-700 rounded-lg text-white placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-white/20 focus:border-neutral-600" />