Skip to main content
Glama

DevDocs MCP Server

by cyberagiinc
SubdomainList.tsx11.9 kB
'use client' import React, { useState, useEffect } from 'react' import { Button, ScrollArea } from "@/components/ui" import { DiscoveredPage } from "@/lib/types" import { Globe, Loader2, CheckCircle2, AlertCircle, Link as LinkIcon, ChevronDown, ChevronRight } from 'lucide-react' interface SubdomainListProps { subdomains: DiscoveredPage[] onCrawlSelected?: (selectedUrls: string[]) => void // Made optional isProcessing: boolean } export default function SubdomainList({ subdomains, /* onCrawlSelected, */ isProcessing }: SubdomainListProps) { // Commented out unused prop const [selectedPages, setSelectedPages] = useState<Set<string>>(new Set()) const [expandedPages, setExpandedPages] = useState<Set<string>>(new Set()) // Reset selection when subdomains change useEffect(() => { setSelectedPages(new Set()) setExpandedPages(new Set()) }, [subdomains]) const togglePage = (url: string, page?: DiscoveredPage, isInternalLink: boolean = false) => { const newSelected = new Set(selectedPages) if (isInternalLink) { // Handle internal link selection if (newSelected.has(url)) { newSelected.delete(url) } else { newSelected.add(url) } } else { // Handle parent URL selection if (newSelected.has(url)) { // When deselecting a parent URL, remove it and its internal links newSelected.delete(url) if (page?.internalLinks) { page.internalLinks.forEach(link => newSelected.delete(link.href)) } } else { // When selecting a parent URL, add it and its internal links newSelected.add(url) if (page?.internalLinks) { page.internalLinks.forEach(link => newSelected.add(link.href)) setExpandedPages(prev => new Set([...prev, url])) } } } setSelectedPages(newSelected) } const toggleExpand = (url: string) => { const newExpanded = new Set(expandedPages) if (newExpanded.has(url)) { newExpanded.delete(url) } else { newExpanded.add(url) } setExpandedPages(newExpanded) } const toggleAll = () => { const allUrls = new Set<string>() const allSelected = subdomains.every(page => { const hasAllLinks = page.internalLinks ? page.internalLinks.every(link => selectedPages.has(link.href)) : selectedPages.has(page.url) return hasAllLinks && selectedPages.has(page.url) }) if (allSelected) { setSelectedPages(new Set()) } else { subdomains.forEach(page => { allUrls.add(page.url) if (page.internalLinks) { page.internalLinks.forEach(link => allUrls.add(link.href)) } }) setSelectedPages(allUrls) } } const getTotalUrls = () => { let total = 0 subdomains.forEach(page => { total++ // Count the main page if (page.internalLinks) { total += page.internalLinks.length // Count internal links } }) return total } const getStatusIcon = (status: string) => { switch (status) { case 'crawled': return <CheckCircle2 className="w-4 h-4 text-green-400" /> case 'error': return <AlertCircle className="w-4 h-4 text-red-400" /> default: return <Loader2 className="w-4 h-4 text-blue-400 animate-spin" /> } } const getStatusStyle = (status: string) => { switch (status) { case 'crawled': return 'bg-green-500/10 text-green-400 border-green-500/20' case 'error': return 'bg-red-500/10 text-red-400 border-red-500/20' default: return 'bg-blue-500/10 text-blue-400 border-blue-500/20' } } return ( <div className="space-y-4 animate-in fade-in duration-500"> {/* Header */} <div className="flex justify-between items-center bg-gray-800/50 backdrop-blur-sm rounded-xl p-4 border border-gray-700"> <div className="flex items-center gap-3"> <Globe className="w-5 h-5 text-purple-400" /> <h2 className="text-xl font-semibold text-purple-400">Discovered Pages</h2> <div className="flex items-center gap-3"> <span className="px-2 py-1 rounded-lg bg-purple-500/10 text-purple-400 text-sm"> {subdomains.length} pages </span> {selectedPages.size > 0 && selectedPages.size !== getTotalUrls() && ( <span className="px-2 py-1 rounded-lg bg-purple-600/20 text-purple-400 text-sm font-medium"> {selectedPages.size} selected </span> )} </div> </div> {/* Button removed as functionality moved to CrawlStatusMonitor */} </div> {/* Table Container */} <div className="rounded-xl border border-gray-700 overflow-hidden bg-gray-900/50 backdrop-blur-sm"> <ScrollArea className="h-[600px]"> <table className="w-full"> <thead className="bg-gray-800/50 sticky top-0"> <tr> <th className="px-4 py-3 text-left text-gray-400 font-medium w-[140px]"> <div className="flex items-center gap-2"> <input type="checkbox" className="rounded border-gray-600 bg-gray-800 text-purple-500 focus:ring-purple-500" checked={selectedPages.size === getTotalUrls() && getTotalUrls() > 0} onChange={toggleAll} /> <span className="text-sm"> {selectedPages.size === getTotalUrls() ? "Unselect All" : "Select All"} </span> </div> </th> <th className="px-4 py-3 text-left text-gray-400 font-medium">URL</th> <th className="px-4 py-3 text-left text-gray-400 font-medium">Title</th> <th className="px-4 py-3 text-left text-gray-400 font-medium">Internal Links</th> <th className="px-4 py-3 text-left text-gray-400 font-medium w-[120px]">Status</th> </tr> </thead> <tbody className="divide-y divide-gray-700/50"> {subdomains.length === 0 ? ( <tr> <td colSpan={5} className="px-4 py-8 text-center text-gray-400"> <div className="flex flex-col items-center gap-2"> <Globe className="w-8 h-8 text-gray-500" /> <p>No pages discovered yet. Enter a URL to start.</p> </div> </td> </tr> ) : ( subdomains.map((page) => ( <React.Fragment key={page.url}> <tr className="transition-colors hover:bg-gray-800/30"> <td className="px-4 py-3"> <div className="flex items-center"> <input type="checkbox" className="rounded border-gray-600 bg-gray-800 text-purple-500 focus:ring-purple-500" checked={selectedPages.has(page.url)} onChange={() => togglePage(page.url, page)} aria-label={`Select ${page.title || 'page'}`} /> </div> </td> <td className="px-4 py-3"> <div className="flex items-center gap-2"> <LinkIcon className="w-4 h-4 text-gray-500 flex-shrink-0" /> <span className="font-mono text-sm text-gray-300 break-all"> {page.url} </span> </div> </td> <td className="px-4 py-3 text-gray-300"> {page.title || 'Untitled'} </td> <td className="px-4 py-3"> <div className="flex items-center gap-2"> <button onClick={() => toggleExpand(page.url)} className="p-1 hover:bg-gray-700 rounded" disabled={!page.internalLinks?.length} > {expandedPages.has(page.url) ? ( <ChevronDown className="w-4 h-4 text-gray-400" /> ) : ( <ChevronRight className="w-4 h-4 text-gray-400" /> )} </button> <span className="text-gray-400"> {page.internalLinks?.length || 0} links </span> </div> </td> <td className="px-4 py-3"> <div className={` inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-medium border ${getStatusStyle(page.status)} `}> {getStatusIcon(page.status)} <span>{page.status}</span> </div> </td> </tr> {/* Internal Links */} {expandedPages.has(page.url) && page.internalLinks?.map((link) => ( <tr key={link.href} className="bg-gray-800/20 border-t border-gray-700/30" > <td className="px-4 py-3"> <div className="flex items-center pl-4"> <input type="checkbox" className="rounded border-gray-600 bg-gray-800 text-purple-500 focus:ring-purple-500" checked={selectedPages.has(link.href)} onChange={() => togglePage(link.href, undefined, true)} aria-label={`Select ${link.text || 'link'}`} /> </div> </td> <td className="px-4 py-3" colSpan={3}> <div className="flex items-center gap-2 pl-4"> <LinkIcon className="w-3 h-3 text-gray-500 flex-shrink-0" /> <div className="flex flex-col"> <span className="font-mono text-sm text-gray-400 break-all"> {link.href} </span> {link.text && ( <span className="text-sm text-gray-500 mt-1"> {link.text} </span> )} </div> </div> </td> <td className="px-4 py-3"> <div className={` inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-medium border ${getStatusStyle(link.status || 'pending')} `}> {getStatusIcon(link.status || 'pending')} <span>{link.status || 'pending'}</span> </div> </td> </tr> ))} </React.Fragment> )) )} </tbody> </table> </ScrollArea> </div> </div> ) }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cyberagiinc/DevDocs'

If you have feedback or need assistance with the MCP directory API, please join our Discord server